linter: ubcs: account for when clauses in if-like nodes

Fixes https://github.com/dart-lang/linter/issues/4795

Change-Id: I4cce15ddaf8a669921646fa81dd0e455859e46e3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/332361
Auto-Submit: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
This commit is contained in:
Sam Rawlins 2023-10-26 19:56:58 +00:00 committed by Commit Queue
parent 95c40c924a
commit 716b127a1a
2 changed files with 96 additions and 14 deletions

View file

@ -262,6 +262,10 @@ class AsyncStateVisitor extends SimpleAstVisitor<AsyncState> {
AsyncState? visitCascadeExpression(CascadeExpression node) =>
_asynchronousIfAnyIsAsync([node.target, ...node.cascadeSections]);
@override
AsyncState? visitCaseClause(CaseClause node) =>
node.guardedPattern.accept(this);
@override
AsyncState? visitCatchClause(CatchClause node) =>
node.body.accept(this)?.asynchronousOrNull;
@ -269,7 +273,8 @@ class AsyncStateVisitor extends SimpleAstVisitor<AsyncState> {
@override
AsyncState? visitConditionalExpression(ConditionalExpression node) =>
_visitIfLike(
condition: node.condition,
expression: node.condition,
caseClause: null,
thenBranch: node.thenExpression,
elseBranch: node.elseExpression,
);
@ -367,16 +372,22 @@ class AsyncStateVisitor extends SimpleAstVisitor<AsyncState> {
_asynchronousIfAnyIsAsync(
[node.function, ...node.argumentList.arguments]);
@override
AsyncState? visitGuardedPattern(GuardedPattern node) =>
node.whenClause?.accept(this);
@override
AsyncState? visitIfElement(IfElement node) => _visitIfLike(
condition: node.expression,
expression: node.expression,
caseClause: node.caseClause,
thenBranch: node.thenElement,
elseBranch: node.elseElement,
);
@override
AsyncState? visitIfStatement(IfStatement node) => _visitIfLike(
condition: node.expression,
expression: node.expression,
caseClause: node.caseClause,
thenBranch: node.thenStatement,
elseBranch: node.elseStatement,
);
@ -708,26 +719,54 @@ class AsyncStateVisitor extends SimpleAstVisitor<AsyncState> {
.firstWhereOrNull((state) => state != null);
}
/// Compute the [AsyncState] of an "if-like" node which has a [condition], a
/// [thenBranch], and a possible [elseBranch].
/// Compute the [AsyncState] of an "if-like" node which has a [expression], a
/// possible [caseClause], a [thenBranch], and a possible [elseBranch].
AsyncState? _visitIfLike({
required AstNode condition,
required Expression expression,
required CaseClause? caseClause,
required AstNode thenBranch,
required AstNode? elseBranch,
}) {
if (reference == condition) {
if (reference == expression) {
// The async state of the condition is not affected by the case-clause,
// then-branch, or else-branch.
return null;
}
var conditionMountedCheck = condition.accept(this);
var expressionAsyncState = expression.accept(this);
if (reference == caseClause) {
return switch (expressionAsyncState) {
AsyncState.asynchronous => AsyncState.asynchronous,
AsyncState.mountedCheck => AsyncState.mountedCheck,
_ => null,
};
}
var caseClauseAsyncState = caseClause?.accept(this);
// The condition state is the combined state of `expression` and
// `caseClause`.
var conditionAsyncState =
switch ((expressionAsyncState, caseClauseAsyncState)) {
// If the left is uninteresting, just return the state of the right.
(null, _) => caseClauseAsyncState,
// If the right is uninteresting, just return the state of the left.
(_, null) => expressionAsyncState,
// Anything on the left followed by async on the right is async.
(_, AsyncState.asynchronous) => AsyncState.asynchronous,
// An async state on the left is superseded by the state on the right.
(AsyncState.asynchronous, _) => caseClauseAsyncState,
// Otherwise just use the state on the left.
(AsyncState.mountedCheck, _) => AsyncState.mountedCheck,
(AsyncState.notMountedCheck, _) => AsyncState.notMountedCheck,
};
if (reference == thenBranch) {
return switch (conditionMountedCheck) {
return switch (conditionAsyncState) {
AsyncState.asynchronous => AsyncState.asynchronous,
AsyncState.mountedCheck => AsyncState.mountedCheck,
_ => null,
};
} else if (reference == elseBranch) {
return switch (conditionMountedCheck) {
return switch (conditionAsyncState) {
AsyncState.asynchronous => AsyncState.asynchronous,
AsyncState.notMountedCheck => AsyncState.mountedCheck,
_ => null,
@ -756,16 +795,15 @@ class AsyncStateVisitor extends SimpleAstVisitor<AsyncState> {
return AsyncState.asynchronous;
}
if (conditionMountedCheck == AsyncState.asynchronous) {
if (conditionAsyncState == AsyncState.asynchronous) {
return AsyncState.asynchronous;
}
if (conditionMountedCheck == AsyncState.mountedCheck && elseTerminates) {
if (conditionAsyncState == AsyncState.mountedCheck && elseTerminates) {
return AsyncState.notMountedCheck;
}
if (conditionMountedCheck == AsyncState.notMountedCheck &&
thenTerminates) {
if (conditionAsyncState == AsyncState.notMountedCheck && thenTerminates) {
return AsyncState.notMountedCheck;
}

View file

@ -758,6 +758,22 @@ void foo(BuildContext context) async {
expect(block.asyncStateFor(reference), AsyncState.notMountedCheck);
}
test_ifStatement_referenceInCaseWhen_asyncInCondition() async {
await resolveCode(r'''
import 'package:flutter/widgets.dart';
void foo(BuildContext context, Future<num> value) async {
if (await value case int() when f(context)) {
context /* ref */;
}
}
bool f(BuildContext context) => true;
''');
var ifStatement = findNode.ifStatement('if (');
var reference = findNode.block('context /* ref */');
expect(ifStatement.asyncStateFor(reference), AsyncState.asynchronous);
}
test_ifStatement_referenceInElse_asyncInCondition() async {
await resolveCode(r'''
import 'package:flutter/widgets.dart';
@ -862,6 +878,20 @@ void foo(BuildContext context, bool m) async {
expect(ifStatement.asyncStateFor(reference), AsyncState.mountedCheck);
}
test_ifStatement_referenceInThen_asyncInCaseWhen() async {
await resolveCode(r'''
import 'package:flutter/widgets.dart';
void foo(BuildContext context, Object value) async {
if (value case int() when await Future.value(true)) {
context /* ref */;
}
}
''');
var ifStatement = findNode.ifStatement('if (');
var reference = findNode.block('context /* ref */');
expect(ifStatement.asyncStateFor(reference), AsyncState.asynchronous);
}
test_ifStatement_referenceInThen_asyncInCondition() async {
await resolveCode(r'''
import 'package:flutter/widgets.dart';
@ -932,6 +962,20 @@ void foo(BuildContext context) async {
expect(ifStatement.asyncStateFor(reference), AsyncState.asynchronous);
}
test_ifStatement_referenceInThen_mountedGuardInCaseWhen() async {
await resolveCode(r'''
import 'package:flutter/widgets.dart';
void foo(BuildContext context, Object value) async {
if (value case int() when context.mounted) {
context /* ref */;
}
}
''');
var ifStatement = findNode.ifStatement('if (');
var reference = findNode.block('context /* ref */');
expect(ifStatement.asyncStateFor(reference), AsyncState.mountedCheck);
}
test_ifStatement_referenceInThen_mountedInCondition() async {
await resolveCode(r'''
import 'package:flutter/widgets.dart';