| // Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/token.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| // ignore: implementation_imports |
| import 'package:analyzer/src/lint/linter.dart'; |
| import 'package:collection/collection.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| |
| import '../analyzer.dart'; |
| import '../extensions.dart'; |
| import '../util/flutter_utils.dart'; |
| |
| const _desc = r'Do not use `BuildContext` across asynchronous gaps.'; |
| |
| const _details = r''' |
| **DON'T** use `BuildContext` across asynchronous gaps. |
| |
| Storing `BuildContext` for later usage can easily lead to difficult to diagnose |
| crashes. Asynchronous gaps are implicitly storing `BuildContext` and are some of |
| the easiest to overlook when writing code. |
| |
| When a `BuildContext` is used, a `mounted` property must be checked after an |
| asynchronous gap, depending on how the `BuildContext` is accessed: |
| |
| * When using a `State`'s `context` property, the `State`'s `mounted` property |
| must be checked. |
| * For other `BuildContext` instances (like a local variable or function |
| argument), the `BuildContext`'s `mounted` property must be checked. |
| |
| **BAD:** |
| ```dart |
| void onButtonTapped(BuildContext context) async { |
| await Future.delayed(const Duration(seconds: 1)); |
| Navigator.of(context).pop(); |
| } |
| ``` |
| |
| **GOOD:** |
| ```dart |
| void onButtonTapped(BuildContext context) { |
| Navigator.of(context).pop(); |
| } |
| ``` |
| |
| **GOOD:** |
| ```dart |
| void onButtonTapped(BuildContext context) async { |
| await Future.delayed(const Duration(seconds: 1)); |
| |
| if (!context.mounted) return; |
| Navigator.of(context).pop(); |
| } |
| ``` |
| |
| **GOOD:** |
| ```dart |
| abstract class MyState extends State<MyWidget> { |
| void foo() async { |
| await Future.delayed(const Duration(seconds: 1)); |
| if (!mounted) return; // Checks `this.mounted`, not `context.mounted`. |
| Navigator.of(context).pop(); |
| } |
| } |
| ``` |
| '''; |
| |
| /// An enum whose values describe the state of asynchrony that a certain node |
| /// has in the syntax tree, with respect to another node. |
| /// |
| /// A mounted check is a check of whether a bool-typed identifier, 'mounted', |
| /// is checked to be `true` or `false`, in a position which affects control |
| /// flow. |
| enum AsyncState { |
| /// A value indicating that a node contains an "asynchronous gap" which is |
| /// not definitely guarded with a mounted check. |
| asynchronous, |
| |
| /// A value indicating that a node contains a positive mounted check that can |
| /// guard a certain other node. |
| mountedCheck, |
| |
| /// A value indicating that a node contains a negative mounted check that can |
| /// guard a certain other node. |
| notMountedCheck; |
| |
| AsyncState? get asynchronousOrNull => |
| this == asynchronous ? asynchronous : null; |
| } |
| |
| /// A class that reuses a single [AsyncStateVisitor] to calculate and cache the |
| /// async state between parent and child nodes. |
| class AsyncStateTracker { |
| final _asyncStateVisitor = AsyncStateVisitor(); |
| |
| /// Whether a check on an unrelated 'mounted' property has been seen. |
| bool get hasUnrelatedMountedCheck => |
| _asyncStateVisitor.hasUnrelatedMountedCheck; |
| |
| /// Returns the asynchronous state that exists between `this` and [reference]. |
| /// |
| /// [reference] must be a direct child of `this`, or a sibling of `this` |
| /// in a List of [AstNode]s. |
| AsyncState? asyncStateFor(AstNode reference, Element mountedElement) { |
| _asyncStateVisitor.setReference(reference, mountedElement); |
| var parent = reference.parent; |
| if (parent == null) return null; |
| |
| var state = parent.accept(_asyncStateVisitor); |
| _asyncStateVisitor.cacheState(parent, state); |
| return state; |
| } |
| } |
| |
| /// A visitor whose `visit*` methods return the async state between a given node |
| /// and [_reference]. |
| /// |
| /// The entrypoint for this visitor is [AsyncStateTracker.asyncStateFor]. |
| /// |
| /// Each `visit*` method can return one of three values: |
| /// * `null` means there is no interesting asynchrony between node and |
| /// [_reference]. |
| /// * [AsyncState.asynchronous] means the node contains an asynchronous gap |
| /// which is not guarded with a mounted check. |
| /// * [AsyncState.mountedCheck] means the node guards [_reference] with a |
| /// positive mounted check. |
| /// * [AsyncState.notMountedCheck] means the node guards [_reference] with a |
| /// negative mounted check. |
| /// |
| /// (For all `visit*` methods except the entrypoint call, the value is |
| /// intermediate, and is only used in calculating the value for parent nodes.) |
| /// |
| /// A node that contains a mounted check "guards" [_reference] if control flow |
| /// can only reach [_reference] if 'mounted' is `true`. Such checks can take |
| /// many forms: |
| /// |
| /// * A mounted check in an if-condition can be a simple guard for nodes in the |
| /// if's then-statement or the if's else-statement, depending on the polarity |
| /// of the check. So `if (mounted) { reference; }` has a proper mounted check |
| /// and `if (!mounted) {} else { reference; }` has a proper mounted check. |
| /// * A statement in a series of statements containing a mounted check can guard |
| /// the later statements if control flow definitely exits in the case of a |
| /// `false` value for 'mounted'. So `if (!mounted) { return; } reference;` has |
| /// a proper mounted check. |
| /// * A mounted check in a try-statement can only guard later statements if it |
| /// is found in the `finally` section, as no statements found in the `try` |
| /// section or any `catch` sections are not guaranteed to have run before the |
| /// later statements. |
| /// * etc. |
| /// |
| /// The `visit*` methods generally fall into three categories: |
| /// |
| /// * A node may affect control flow, such that a contained mounted check may |
| /// properly guard [_reference]. See [visitIfStatement] for one of the most |
| /// complicated examples. |
| /// * A node may be one component of a mounted check. An associated `visit*` |
| /// method builds up such a mounted check from inner expressions. For example, |
| /// given `!(context.mounted)`, the notion of a mounted check is built from |
| /// the PrefixedIdentifier, the ParenthesizedExpression, and the |
| /// PrefixExpression (from inside to outside). |
| /// * Otherwise, a node may just contain an asynchronous gap. The vast majority |
| /// of node types fall into this category. Most of these `visit*` methods |
| /// use [AsyncState.asynchronousOrNull] or [_asynchronousIfAnyIsAsync]. |
| class AsyncStateVisitor extends SimpleAstVisitor<AsyncState> { |
| static const mountedName = 'mounted'; |
| |
| late AstNode _reference; |
| |
| /// The `mounted` getter that is appropriate for a mounted check for |
| /// [_reference]. |
| /// |
| /// Generally speaking, this is `State.mounted` when [_reference] refers to |
| /// `State.context`, and this is `BuildContext.mounted` otherwise. |
| late Element _mountedElement; |
| |
| final Map<AstNode, AsyncState?> _stateCache = {}; |
| |
| /// Whether a check on an unrelated 'mounted' property has been seen. |
| bool hasUnrelatedMountedCheck = false; |
| |
| /// Cache the async state between [node] and some reference node. |
| /// |
| /// Caching an async state is only valid when [node] is the parent of the |
| /// reference node, and later visitations are performed using ancestors of the |
| /// reference node as [_reference]. |
| /// That is, if the async state between a parent node and a reference node, |
| /// `R` is `A`, then the async state between any other node and a direct |
| /// child, which is an ancestor of `R`, is also `A`. |
| // TODO(srawlins): Checking the cache in every visit method could improve |
| // performance. Just need to do the legwork. |
| void cacheState(AstNode node, AsyncState? state) { |
| _stateCache[node] = state; |
| } |
| |
| /// Sets [_reference] and [_mountedElement], readying the visitor to accept |
| /// nodes. |
| void setReference(AstNode reference, Element mountedElement) { |
| _reference = reference; |
| _mountedElement = mountedElement; |
| } |
| |
| @override |
| AsyncState? visitAdjacentStrings(AdjacentStrings node) => |
| _asynchronousIfAnyIsAsync(node.strings); |
| |
| @override |
| AsyncState? visitAsExpression(AsExpression node) => |
| node.expression.accept(this)?.asynchronousOrNull; |
| |
| @override |
| AsyncState? visitAssignmentExpression(AssignmentExpression node) => |
| _inOrderAsyncState([ |
| (node: node.leftHandSide, mountedCanGuard: false), |
| (node: node.rightHandSide, mountedCanGuard: true), |
| ]); |
| |
| @override |
| AsyncState? visitAwaitExpression(AwaitExpression node) { |
| if (_stateCache.containsKey(node)) { |
| return _stateCache[node]; |
| } |
| |
| // An expression _inside_ an await is executed before the await, and so is |
| // safe; otherwise asynchronous. |
| return _reference == node.expression ? null : AsyncState.asynchronous; |
| } |
| |
| @override |
| AsyncState? visitBinaryExpression(BinaryExpression node) { |
| if (node.leftOperand == _reference) { |
| return null; |
| } else if (node.rightOperand == _reference) { |
| var leftGuardState = node.leftOperand.accept(this); |
| return switch (leftGuardState) { |
| AsyncState.asynchronous => AsyncState.asynchronous, |
| AsyncState.mountedCheck when node.isAnd => AsyncState.mountedCheck, |
| AsyncState.notMountedCheck when node.isOr => AsyncState.notMountedCheck, |
| _ => null, |
| }; |
| } |
| |
| // `reference` follows `node`, or an ancestor of `node`. |
| |
| if (node.isAnd) { |
| var leftGuardState = node.leftOperand.accept(this); |
| var rightGuardState = node.rightOperand.accept(this); |
| return switch ((leftGuardState, rightGuardState)) { |
| // If the left is uninteresting, just return the state of the right. |
| (null, _) => rightGuardState, |
| // If the right is uninteresting, just return the state of the left. |
| (_, null) => leftGuardState, |
| // 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, _) => rightGuardState, |
| // Otherwise just use the state on the left. |
| (AsyncState.mountedCheck, _) => AsyncState.mountedCheck, |
| (AsyncState.notMountedCheck, _) => AsyncState.notMountedCheck, |
| }; |
| } |
| if (node.isOr) { |
| var leftGuardState = node.leftOperand.accept(this); |
| var rightGuardState = node.rightOperand.accept(this); |
| return switch ((leftGuardState, rightGuardState)) { |
| // Anything on the left followed by async on the right is async. |
| (_, AsyncState.asynchronous) => AsyncState.asynchronous, |
| // Async on the left followed by anything on the right is async. |
| (AsyncState.asynchronous, _) => AsyncState.asynchronous, |
| // A mounted guard only applies if both sides are guarded. |
| (AsyncState.mountedCheck, AsyncState.mountedCheck) => |
| AsyncState.mountedCheck, |
| (_, AsyncState.notMountedCheck) => AsyncState.notMountedCheck, |
| (AsyncState.notMountedCheck, _) => AsyncState.notMountedCheck, |
| // Otherwise it's just uninteresting. |
| (_, _) => null, |
| }; |
| } |
| |
| if (node.isEqual) { |
| var leftGuardState = node.leftOperand.accept(this); |
| var rightGuardState = node.rightOperand.accept(this); |
| if (leftGuardState == AsyncState.asynchronous || |
| rightGuardState == AsyncState.asynchronous) { |
| return AsyncState.asynchronous; |
| } |
| if (leftGuardState == AsyncState.mountedCheck || |
| leftGuardState == AsyncState.notMountedCheck) { |
| var rightConstantValue = node.rightOperand.constantBoolValue; |
| if (rightConstantValue == null) return null; |
| return _constantEquality(leftGuardState, constant: rightConstantValue); |
| } |
| if (rightGuardState == AsyncState.mountedCheck || |
| rightGuardState == AsyncState.notMountedCheck) { |
| var leftConstantValue = node.leftOperand.constantBoolValue; |
| if (leftConstantValue == null) return null; |
| return _constantEquality(rightGuardState, constant: leftConstantValue); |
| } |
| return null; |
| } |
| if (node.isNotEqual) { |
| var leftGuardState = node.leftOperand.accept(this); |
| var rightGuardState = node.rightOperand.accept(this); |
| if (leftGuardState == AsyncState.asynchronous || |
| rightGuardState == AsyncState.asynchronous) { |
| return AsyncState.asynchronous; |
| } |
| if (leftGuardState == AsyncState.mountedCheck || |
| leftGuardState == AsyncState.notMountedCheck) { |
| var rightConstantValue = node.rightOperand.constantBoolValue; |
| if (rightConstantValue == null) return null; |
| return _constantEquality(leftGuardState, constant: !rightConstantValue); |
| } |
| if (rightGuardState == AsyncState.mountedCheck || |
| rightGuardState == AsyncState.notMountedCheck) { |
| var leftConstantValue = node.leftOperand.constantBoolValue; |
| if (leftConstantValue == null) return null; |
| return _constantEquality(rightGuardState, constant: !leftConstantValue); |
| } |
| return null; |
| } else { |
| // Outside of a binary logical operation, a mounted check cannot guard a |
| // later expression, so only check for asynchronous code. |
| return node.leftOperand.accept(this)?.asynchronousOrNull ?? |
| node.rightOperand.accept(this)?.asynchronousOrNull; |
| } |
| } |
| |
| @override |
| AsyncState? visitBlock(Block node) => |
| _visitBlockLike(node.statements, parent: node.parent); |
| |
| @override |
| AsyncState? visitBlockFunctionBody(BlockFunctionBody node) => |
| // Stop visiting when we arrive at a function body. |
| // Awaits and mounted checks inside it don't matter. |
| null; |
| |
| @override |
| 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; |
| |
| @override |
| AsyncState? visitConditionalExpression(ConditionalExpression node) => |
| _visitIfLike( |
| expression: node.condition, |
| caseClause: null, |
| thenBranch: node.thenExpression, |
| elseBranch: node.elseExpression, |
| ); |
| |
| @override |
| AsyncState? visitDoStatement(DoStatement node) { |
| if (node.body == _reference) { |
| // After one loop, an `await` in the condition can affect the body. |
| return node.condition.accept(this)?.asynchronousOrNull; |
| } else if (node.condition == _reference) { |
| return node.body.accept(this)?.asynchronousOrNull; |
| } else { |
| return node.condition.accept(this)?.asynchronousOrNull ?? |
| node.body.accept(this)?.asynchronousOrNull; |
| } |
| } |
| |
| @override |
| AsyncState? visitExpressionFunctionBody(ExpressionFunctionBody node) => |
| // Stop visiting when we arrive at a function body. |
| // Awaits and mounted checks inside it don't matter. |
| null; |
| |
| @override |
| AsyncState? visitExpressionStatement(ExpressionStatement node) => |
| node.expression == _reference |
| ? null |
| : node.expression.accept(this)?.asynchronousOrNull; |
| |
| @override |
| AsyncState? visitExtensionOverride(ExtensionOverride node) => |
| _asynchronousIfAnyIsAsync(node.argumentList.arguments); |
| |
| @override |
| AsyncState? visitForElement(ForElement node) { |
| var forLoopParts = node.forLoopParts; |
| var referenceIsBody = node.body == _reference; |
| return switch (forLoopParts) { |
| ForPartsWithDeclarations() => _inOrderAsyncState([ |
| for (var declaration in forLoopParts.variables.variables) |
| (node: declaration, mountedCanGuard: false), |
| (node: forLoopParts.condition, mountedCanGuard: referenceIsBody), |
| for (var updater in forLoopParts.updaters) |
| (node: updater, mountedCanGuard: false), |
| (node: node.body, mountedCanGuard: false), |
| ]), |
| ForPartsWithExpression() => _inOrderAsyncState([ |
| (node: forLoopParts.initialization, mountedCanGuard: false), |
| (node: forLoopParts.condition, mountedCanGuard: referenceIsBody), |
| for (var updater in forLoopParts.updaters) |
| (node: updater, mountedCanGuard: false), |
| (node: node.body, mountedCanGuard: false), |
| ]), |
| ForEachParts() => _inOrderAsyncState([ |
| (node: forLoopParts.iterable, mountedCanGuard: false), |
| (node: node.body, mountedCanGuard: false), |
| ]), |
| _ => null, |
| }; |
| } |
| |
| @override |
| AsyncState? visitForStatement(ForStatement node) { |
| var forLoopParts = node.forLoopParts; |
| var referenceIsBody = node.body == _reference; |
| return switch (forLoopParts) { |
| ForPartsWithDeclarations() => _inOrderAsyncState([ |
| for (var declaration in forLoopParts.variables.variables) |
| (node: declaration, mountedCanGuard: false), |
| // The body can be guarded by the condition. |
| (node: forLoopParts.condition, mountedCanGuard: referenceIsBody), |
| for (var updater in forLoopParts.updaters) |
| (node: updater, mountedCanGuard: false), |
| (node: node.body, mountedCanGuard: false), |
| ]), |
| ForPartsWithExpression() => _inOrderAsyncState([ |
| (node: forLoopParts.initialization, mountedCanGuard: false), |
| // The body can be guarded by the condition. |
| (node: forLoopParts.condition, mountedCanGuard: referenceIsBody), |
| for (var updater in forLoopParts.updaters) |
| (node: updater, mountedCanGuard: false), |
| (node: node.body, mountedCanGuard: false), |
| ]), |
| ForEachParts() => _inOrderAsyncState([ |
| (node: forLoopParts.iterable, mountedCanGuard: false), |
| (node: node.body, mountedCanGuard: false), |
| ]), |
| _ => null, |
| }; |
| } |
| |
| @override |
| AsyncState? visitFunctionExpressionInvocation( |
| FunctionExpressionInvocation node) => |
| _asynchronousIfAnyIsAsync( |
| [node.function, ...node.argumentList.arguments]); |
| |
| @override |
| AsyncState? visitGuardedPattern(GuardedPattern node) => |
| node.whenClause?.accept(this); |
| |
| @override |
| AsyncState? visitIfElement(IfElement node) => _visitIfLike( |
| expression: node.expression, |
| caseClause: node.caseClause, |
| thenBranch: node.thenElement, |
| elseBranch: node.elseElement, |
| ); |
| |
| @override |
| AsyncState? visitIfStatement(IfStatement node) => _visitIfLike( |
| expression: node.expression, |
| caseClause: node.caseClause, |
| thenBranch: node.thenStatement, |
| elseBranch: node.elseStatement, |
| ); |
| |
| @override |
| AsyncState? visitIndexExpression(IndexExpression node) => |
| _asynchronousIfAnyIsAsync([node.target, node.index]); |
| |
| @override |
| AsyncState? visitInstanceCreationExpression( |
| InstanceCreationExpression node) => |
| _asynchronousIfAnyIsAsync(node.argumentList.arguments); |
| |
| @override |
| AsyncState? visitInterpolationExpression(InterpolationExpression node) => |
| node.expression.accept(this)?.asynchronousOrNull; |
| |
| @override |
| AsyncState? visitIsExpression(IsExpression node) => |
| node.expression.accept(this)?.asynchronousOrNull; |
| |
| @override |
| AsyncState? visitLabeledStatement(LabeledStatement node) => |
| node.statement.accept(this); |
| |
| @override |
| AsyncState? visitListLiteral(ListLiteral node) => |
| _asynchronousIfAnyIsAsync(node.elements); |
| |
| @override |
| AsyncState? visitMapLiteralEntry(MapLiteralEntry node) => |
| _asynchronousIfAnyIsAsync([node.key, node.value]); |
| |
| @override |
| AsyncState? visitMethodInvocation(MethodInvocation node) => |
| _asynchronousIfAnyIsAsync([node.target, ...node.argumentList.arguments]); |
| |
| @override |
| AsyncState? visitNamedExpression(NamedExpression node) => |
| node.expression.accept(this)?.asynchronousOrNull; |
| |
| @override |
| AsyncState? visitParenthesizedExpression(ParenthesizedExpression node) => |
| node.expression.accept(this); |
| |
| @override |
| AsyncState? visitPostfixExpression(PostfixExpression node) => |
| node.operand.accept(this)?.asynchronousOrNull; |
| |
| @override |
| AsyncState? visitPrefixedIdentifier(PrefixedIdentifier node) => |
| _visitIdentifier(node.identifier); |
| |
| @override |
| AsyncState? visitPrefixExpression(PrefixExpression node) { |
| if (node.isNot) { |
| var guardState = node.operand.accept(this); |
| return switch (guardState) { |
| AsyncState.mountedCheck => AsyncState.notMountedCheck, |
| AsyncState.notMountedCheck => AsyncState.mountedCheck, |
| _ => guardState, |
| }; |
| } else { |
| return null; |
| } |
| } |
| |
| @override |
| AsyncState? visitPropertyAccess(PropertyAccess node) { |
| if (node.propertyName.name == mountedName) { |
| return node.target?.accept(this)?.asynchronousOrNull ?? |
| node.propertyName.accept(this); |
| } |
| return node.target?.accept(this)?.asynchronousOrNull; |
| } |
| |
| @override |
| AsyncState? visitRecordLiteral(RecordLiteral node) => |
| _asynchronousIfAnyIsAsync(node.fields); |
| |
| @override |
| AsyncState? visitSetOrMapLiteral(SetOrMapLiteral node) => |
| _asynchronousIfAnyIsAsync(node.elements); |
| |
| @override |
| AsyncState? visitSimpleIdentifier(SimpleIdentifier node) => |
| _visitIdentifier(node); |
| |
| @override |
| AsyncState? visitSpreadElement(SpreadElement node) => |
| node.expression.accept(this)?.asynchronousOrNull; |
| |
| @override |
| AsyncState? visitStringInterpolation(StringInterpolation node) => |
| _asynchronousIfAnyIsAsync(node.elements); |
| |
| @override |
| AsyncState? visitSwitchCase(SwitchCase node) => |
| // TODO(srawlins): Handle when `reference` is in one of the statements. |
| _inOrderAsyncStateGuardable([node.expression, ...node.statements]); |
| |
| @override |
| AsyncState? visitSwitchDefault(SwitchDefault node) => |
| _inOrderAsyncStateGuardable(node.statements); |
| |
| @override |
| AsyncState? visitSwitchExpression(SwitchExpression node) => |
| _asynchronousIfAnyIsAsync([node.expression, ...node.cases]); |
| |
| @override |
| AsyncState? visitSwitchExpressionCase(SwitchExpressionCase node) { |
| if (_reference == node.guardedPattern) { |
| return null; |
| } |
| var whenClauseState = node.guardedPattern.whenClause?.accept(this); |
| if (_reference == node.expression) { |
| if (whenClauseState == AsyncState.asynchronous || |
| whenClauseState == AsyncState.mountedCheck) { |
| return whenClauseState; |
| } |
| return null; |
| } |
| return whenClauseState?.asynchronousOrNull ?? |
| node.expression.accept(this)?.asynchronousOrNull; |
| } |
| |
| @override |
| AsyncState? visitSwitchPatternCase(SwitchPatternCase node) { |
| if (_reference == node.guardedPattern) { |
| return null; |
| } |
| var statementsAsyncState = |
| _visitBlockLike(node.statements, parent: node.parent); |
| if (statementsAsyncState != null) return statementsAsyncState; |
| if (node.statements.contains(_reference)) { |
| // Any when-clause in `node` and any fallthrough when-clauses are handled |
| // in `visitSwitchStatement`. |
| return null; |
| } else { |
| return node.guardedPattern.whenClause?.accept(this)?.asynchronousOrNull; |
| } |
| } |
| |
| @override |
| AsyncState? visitSwitchStatement(SwitchStatement node) { |
| // TODO(srawlins): Check for definite exits in the members. |
| node.expression.accept(this)?.asynchronousOrNull ?? |
| _asynchronousIfAnyIsAsync(node.members); |
| |
| var reference = _reference; |
| if (reference is SwitchMember) { |
| var index = node.members.indexOf(reference); |
| |
| // Control may flow to `node.statements` via this case's `guardedPattern`, |
| // or via fallthrough. Consider fallthrough when-clauses. |
| |
| // Track whether we are iterating in fall-through cases. |
| var checkedCasesFallThrough = true; |
| // Track whether all checked cases have been `AsyncState.mountedCheck` |
| // (only relevant for fall-through cases). |
| var checkedCasesAreAllMountedChecks = true; |
| |
| for (var i = index; i >= 0; i--) { |
| var case_ = node.members[i]; |
| if (case_ is! SwitchPatternCase) { |
| continue; |
| } |
| |
| var whenAsyncState = case_.guardedPattern.whenClause?.accept(this); |
| if (whenAsyncState == AsyncState.asynchronous) { |
| return AsyncState.asynchronous; |
| } |
| if (checkedCasesFallThrough) { |
| var caseIsFallThrough = i == index || case_.statements.isEmpty; |
| |
| if (caseIsFallThrough) { |
| checkedCasesAreAllMountedChecks &= |
| whenAsyncState == AsyncState.mountedCheck; |
| } else { |
| // We have collected whether all of the fallthrough cases have |
| // mounted guards. |
| if (checkedCasesAreAllMountedChecks) { |
| return AsyncState.mountedCheck; |
| } |
| } |
| checkedCasesFallThrough &= caseIsFallThrough; |
| } |
| } |
| |
| if (checkedCasesFallThrough && checkedCasesAreAllMountedChecks) { |
| return AsyncState.mountedCheck; |
| } |
| |
| return null; |
| } else { |
| return node.expression.accept(this)?.asynchronousOrNull ?? |
| _asynchronousIfAnyIsAsync(node.members); |
| } |
| } |
| |
| @override |
| AsyncState? visitTryStatement(TryStatement node) { |
| if (node.body == _reference) { |
| return null; |
| } else if (node.catchClauses.any((clause) => clause == _reference)) { |
| return node.body.accept(this)?.asynchronousOrNull; |
| } else if (node.finallyBlock == _reference) { |
| return _asynchronousIfAnyIsAsync([node.body, ...node.catchClauses]); |
| } |
| |
| // Only statements in the `finally` section of a try-statement can |
| // sufficiently guard statements following the try-statement. |
| return node.finallyBlock?.accept(this) ?? |
| _asynchronousIfAnyIsAsync([node.body, ...node.catchClauses]); |
| } |
| |
| @override |
| AsyncState? visitVariableDeclaration(VariableDeclaration node) => |
| node.initializer?.accept(this)?.asynchronousOrNull; |
| |
| @override |
| AsyncState? visitVariableDeclarationStatement( |
| VariableDeclarationStatement node) => |
| _asynchronousIfAnyIsAsync([ |
| for (var variable in node.variables.variables) variable.initializer, |
| ]); |
| |
| @override |
| AsyncState? visitWhenClause(WhenClause node) => node.expression.accept(this); |
| |
| @override |
| AsyncState? visitWhileStatement(WhileStatement node) => |
| // TODO(srawlins): if the condition is a mounted guard and `reference` is |
| // the body or follows the while. |
| // A while-statement's body is not guaranteed to execute, so no mounted |
| // checks properly guard. |
| node.condition.accept(this)?.asynchronousOrNull ?? |
| node.body.accept(this)?.asynchronousOrNull; |
| |
| @override |
| AsyncState? visitYieldStatement(YieldStatement node) => |
| node.expression.accept(this)?.asynchronousOrNull; |
| |
| /// Returns [AsyncState.asynchronous] if visiting any of [nodes] returns |
| /// [AsyncState.asynchronous], otherwise `null`. |
| /// |
| /// This function does not take mounted checks into account, so it cannot be |
| /// used when [nodes] can affect control flow. |
| AsyncState? _asynchronousIfAnyIsAsync(List<AstNode?> nodes) { |
| var index = nodes.indexOf(_reference); |
| if (index < 0) { |
| return nodes.any((node) => node?.accept(this) == AsyncState.asynchronous) |
| ? AsyncState.asynchronous |
| : null; |
| } else { |
| return nodes |
| .take(index) |
| .any((node) => node?.accept(this) == AsyncState.asynchronous) |
| ? AsyncState.asynchronous |
| : null; |
| } |
| } |
| |
| /// Returns an [AsyncState] representing [state] or its opposite, based on |
| /// equality with [constant]. |
| AsyncState? _constantEquality(AsyncState? state, {required bool constant}) => |
| switch ((state, constant)) { |
| // Representing `context.mounted == true`, etc. |
| (AsyncState.mountedCheck, true) => AsyncState.mountedCheck, |
| (AsyncState.notMountedCheck, true) => AsyncState.notMountedCheck, |
| (AsyncState.mountedCheck, false) => AsyncState.notMountedCheck, |
| (AsyncState.notMountedCheck, false) => AsyncState.mountedCheck, |
| _ => null, |
| }; |
| |
| /// Walks backwards through [nodes] looking for "interesting" async states, |
| /// determining the async state of [nodes], with respect to [_reference]. |
| /// |
| /// [nodes] is a list of records, each with an [AstNode] and a field |
| /// representing whether a mounted check in the node can guard [_reference]. |
| /// |
| /// [nodes] must be in expected execution order. [_reference] can be one of |
| /// [nodes], or can follow [nodes], or can follow an ancestor of [nodes]. |
| /// |
| /// If [_reference] is one of the [nodes], this traversal starts at the node |
| /// that precedes it, rather than at the end of the list. |
| AsyncState? _inOrderAsyncState( |
| List<({AstNode? node, bool mountedCanGuard})> nodes) { |
| if (nodes.isEmpty) return null; |
| if (nodes.first.node == _reference) return null; |
| var referenceIndex = |
| nodes.indexWhere((element) => element.node == _reference); |
| var startingIndex = |
| referenceIndex > 0 ? referenceIndex - 1 : nodes.length - 1; |
| |
| for (var i = startingIndex; i >= 0; i--) { |
| var (:node, :mountedCanGuard) = nodes[i]; |
| if (node == null) continue; |
| var asyncState = node.accept(this); |
| if (asyncState == AsyncState.asynchronous) { |
| return AsyncState.asynchronous; |
| } |
| if (mountedCanGuard && asyncState != null) { |
| // Walking from the last node to the first, as soon as we encounter a |
| // mounted check (positive or negative) or asynchronous code, that's |
| // the state of the whole series. |
| return asyncState; |
| } |
| } |
| return null; |
| } |
| |
| /// A simple wrapper for [_inOrderAsyncState] for [nodes] which can all guard |
| /// [_reference] with a mounted check. |
| AsyncState? _inOrderAsyncStateGuardable(Iterable<AstNode?> nodes) => |
| _inOrderAsyncState([ |
| for (var node in nodes) (node: node, mountedCanGuard: true), |
| ]); |
| |
| /// Compute the [AsyncState] of a "block-like" node which has [statements]. |
| AsyncState? _visitBlockLike(List<Statement> statements, |
| {required AstNode? parent}) { |
| var reference = _reference; |
| if (reference is Statement) { |
| var index = statements.indexOf(reference); |
| if (index >= 0) { |
| var precedingAsyncState = _inOrderAsyncStateGuardable(statements); |
| if (precedingAsyncState != null) return precedingAsyncState; |
| if (parent is DoStatement || |
| parent is ForStatement || |
| parent is WhileStatement) { |
| // Check for asynchrony in the statements that _follow_ [reference], |
| // as they may lead to an async gap before we loop back to |
| // [reference]. |
| return _inOrderAsyncStateGuardable(statements.skip(index + 1)) |
| ?.asynchronousOrNull; |
| } |
| return null; |
| } |
| } |
| |
| // When [reference] is not one of [node.statements], walk through all of |
| // them. |
| return statements.reversed |
| .map((s) => s.accept(this)) |
| .firstWhereOrNull((state) => state != null); |
| } |
| |
| /// The state of [node], accounting for a possible mounted check, or an |
| /// attempted mounted check (using an unrelated element). |
| AsyncState? _visitIdentifier(SimpleIdentifier node) { |
| if (node.name != mountedName) return null; |
| if (node.staticElement?.declaration == _mountedElement) { |
| return AsyncState.mountedCheck; |
| } |
| |
| // This is an attempted mounted check, but it is using the wrong element. |
| hasUnrelatedMountedCheck = true; |
| return null; |
| } |
| |
| /// Compute the [AsyncState] of an "if-like" node which has a [expression], a |
| /// possible [caseClause], a [thenBranch], and a possible [elseBranch]. |
| AsyncState? _visitIfLike({ |
| required Expression expression, |
| required CaseClause? caseClause, |
| required AstNode thenBranch, |
| required AstNode? elseBranch, |
| }) { |
| if (_reference == expression) { |
| // The async state of the condition is not affected by the case-clause, |
| // then-branch, or else-branch. |
| return null; |
| } |
| 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 (conditionAsyncState) { |
| AsyncState.asynchronous => AsyncState.asynchronous, |
| AsyncState.mountedCheck => AsyncState.mountedCheck, |
| _ => null, |
| }; |
| } else if (_reference == elseBranch) { |
| return switch (conditionAsyncState) { |
| AsyncState.asynchronous => AsyncState.asynchronous, |
| AsyncState.notMountedCheck => AsyncState.mountedCheck, |
| _ => null, |
| }; |
| } else { |
| // `reference` is a statement that comes after `node`, or an ancestor of |
| // `node`, in a NodeList. |
| var thenAsyncState = thenBranch.accept(this); |
| var elseAsyncState = elseBranch?.accept(this); |
| var thenTerminates = thenBranch.terminatesControl; |
| var elseTerminates = elseBranch?.terminatesControl ?? false; |
| |
| if (thenAsyncState == AsyncState.notMountedCheck) { |
| if (elseAsyncState == AsyncState.notMountedCheck || elseTerminates) { |
| return AsyncState.notMountedCheck; |
| } |
| } |
| if (elseAsyncState == AsyncState.notMountedCheck && thenTerminates) { |
| return AsyncState.notMountedCheck; |
| } |
| |
| if (thenAsyncState == AsyncState.asynchronous && !thenTerminates) { |
| return AsyncState.asynchronous; |
| } |
| if (elseAsyncState == AsyncState.asynchronous && !elseTerminates) { |
| return AsyncState.asynchronous; |
| } |
| |
| if (conditionAsyncState == AsyncState.asynchronous) { |
| return AsyncState.asynchronous; |
| } |
| |
| if (conditionAsyncState == AsyncState.mountedCheck && elseTerminates) { |
| return AsyncState.notMountedCheck; |
| } |
| |
| if (conditionAsyncState == AsyncState.notMountedCheck && thenTerminates) { |
| return AsyncState.notMountedCheck; |
| } |
| |
| return null; |
| } |
| } |
| } |
| |
| /// Function with callback parameters that should be "protected." |
| /// |
| /// Any callback passed as a [positional] argument or [named] argument to such |
| /// a function must have a mounted guard check for any references to |
| /// BuildContext. |
| class ProtectedFunction { |
| final String library; |
| |
| /// The name of the target type of the function (for instance methods) or the |
| /// defining element (for constructors and static methods). |
| final String? type; |
| |
| /// The name of the function. Can be `null` to represent an unnamed |
| /// constructor. |
| final String? name; |
| |
| /// The list of positional parameters that are protected. |
| final List<int> positional; |
| |
| /// The list of named parameters that are protected. |
| final List<String> named; |
| |
| const ProtectedFunction(this.library, this.type, this.name, |
| {this.positional = const <int>[], this.named = const <String>[]}); |
| } |
| |
| class UseBuildContextSynchronously extends LintRule { |
| static const LintCode asyncUseCode = LintCode( |
| 'use_build_context_synchronously', |
| "Don't use 'BuildContext's across async gaps.", |
| correctionMessage: |
| "Try rewriting the code to not use the 'BuildContext', or guard the " |
| "use with a 'mounted' check.", |
| uniqueName: 'LintCode.use_build_context_synchronously_async_use', |
| ); |
| |
| static const LintCode wrongMountedCode = LintCode( |
| 'use_build_context_synchronously', |
| "Don't use 'BuildContext's across async gaps, guarded by an unrelated " |
| "'mounted' check.", |
| correctionMessage: |
| "Guard a 'State.context' use with a 'mounted' check on the State, and " |
| "other BuildContext use with a 'mounted' check on the BuildContext.", |
| uniqueName: 'LintCode.use_build_context_synchronously_wrong_mounted', |
| ); |
| |
| UseBuildContextSynchronously() |
| : super( |
| name: 'use_build_context_synchronously', |
| description: _desc, |
| details: _details, |
| group: Group.errors, |
| state: State.stable(since: Version(3, 2, 0)), |
| ); |
| |
| @override |
| List<LintCode> get lintCodes => [asyncUseCode, wrongMountedCode]; |
| |
| @override |
| void registerNodeProcessors( |
| NodeLintRegistry registry, LinterContext context) { |
| var unit = context.currentUnit.unit; |
| if (!unit.inTestDir) { |
| var visitor = _Visitor(this); |
| registry.addMethodInvocation(this, visitor); |
| registry.addInstanceCreationExpression(this, visitor); |
| registry.addFunctionExpressionInvocation(this, visitor); |
| registry.addPrefixedIdentifier(this, visitor); |
| } |
| } |
| } |
| |
| class _Visitor extends SimpleAstVisitor { |
| static const mountedName = 'mounted'; |
| |
| static const protectedConstructors = [ |
| // Future constructors. |
| // Protect the unnamed constructor as both `Future()` and `Future.new()`. |
| ProtectedFunction('dart.async', 'Future', null, positional: [0]), |
| ProtectedFunction('dart.async', 'Future', 'new', positional: [0]), |
| ProtectedFunction('dart.async', 'Future', 'delayed', positional: [1]), |
| ProtectedFunction('dart.async', 'Future', 'microtask', positional: [0]), |
| |
| // Stream constructors. |
| ProtectedFunction('dart.async', 'Stream', 'eventTransformed', |
| positional: [1]), |
| ProtectedFunction('dart.async', 'Stream', 'multi', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'periodic', positional: [1]), |
| |
| // StreamController constructors. |
| ProtectedFunction('dart.async', 'StreamController', null, |
| named: ['onListen', 'onPause', 'onResume', 'onCancel']), |
| ProtectedFunction('dart.async', 'StreamController', 'new', |
| named: ['onListen', 'onPause', 'onResume', 'onCancel']), |
| ProtectedFunction('dart.async', 'StreamController', 'broadcast', |
| named: ['onListen', 'onCancel']), |
| ]; |
| |
| static const protectedInstanceMethods = [ |
| // Future instance methods. |
| ProtectedFunction('dart.async', 'Future', 'catchError', |
| positional: [0], named: ['test']), |
| ProtectedFunction('dart.async', 'Future', 'onError', |
| positional: [0], named: ['test']), |
| ProtectedFunction('dart.async', 'Future', 'then', |
| positional: [0], named: ['onError']), |
| ProtectedFunction('dart.async', 'Future', 'timeout', named: ['onTimeout']), |
| ProtectedFunction('dart.async', 'Future', 'whenComplete', positional: [0]), |
| |
| // Stream instance methods. |
| ProtectedFunction('dart.async', 'Stream', 'any', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'asBroadcastStream', |
| named: ['onListen', 'onCancel']), |
| ProtectedFunction('dart.async', 'Stream', 'asyncExpand', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'asyncMap', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'distinct', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'expand', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'firstWhere', |
| positional: [0], named: ['orElse']), |
| ProtectedFunction('dart.async', 'Stream', 'fold', positional: [1]), |
| ProtectedFunction('dart.async', 'Stream', 'forEach', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'handleError', |
| positional: [0], named: ['test']), |
| ProtectedFunction('dart.async', 'Stream', 'lastWhere', |
| positional: [0], named: ['orElse']), |
| ProtectedFunction('dart.async', 'Stream', 'listen', |
| positional: [0], named: ['onError', 'onDone']), |
| ProtectedFunction('dart.async', 'Stream', 'map', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'reduce', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'singleWhere', |
| positional: [0], named: ['orElse']), |
| ProtectedFunction('dart.async', 'Stream', 'skipWhile', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'takeWhile', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'timeout', named: ['onTimeout']), |
| ProtectedFunction('dart.async', 'Stream', 'where', positional: [0]), |
| |
| // StreamSubscription instance methods. |
| ProtectedFunction('dart.async', 'Stream', 'onData', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'onDone', positional: [0]), |
| ProtectedFunction('dart.async', 'Stream', 'onError', positional: [0]), |
| ]; |
| |
| static const protectedStaticMethods = [ |
| // Future static methods. |
| ProtectedFunction('dart.async', 'Future', 'doWhile', positional: [0]), |
| ProtectedFunction('dart.async', 'Future', 'forEach', positional: [1]), |
| ProtectedFunction('dart.async', 'Future', 'wait', named: ['cleanUp']), |
| ]; |
| |
| final LintRule rule; |
| |
| _Visitor(this.rule); |
| |
| void check(Expression node, Element mountedElement) { |
| // Checks each of the statements before `child` for a `mounted` check, and |
| // returns whether it did not find one (and the caller should keep looking). |
| |
| // Walk back and look for an async gap that is not guarded by a mounted |
| // property check. |
| AstNode? child = node; |
| var asyncStateTracker = AsyncStateTracker(); |
| while (child != null && child is! FunctionBody) { |
| var parent = child.parent; |
| if (parent == null) break; |
| |
| var asyncState = asyncStateTracker.asyncStateFor(child, mountedElement); |
| if (asyncState.isGuarded) return; |
| |
| if (asyncState == AsyncState.asynchronous) { |
| var errorCode = asyncStateTracker.hasUnrelatedMountedCheck |
| ? UseBuildContextSynchronously.wrongMountedCode |
| : UseBuildContextSynchronously.asyncUseCode; |
| rule.reportLint(node, errorCode: errorCode); |
| return; |
| } |
| |
| child = parent; |
| } |
| |
| if (child is FunctionBody) { |
| var parent = child.parent; |
| var grandparent = parent?.parent; |
| if (parent is! FunctionExpression) { |
| return; |
| } |
| |
| if (grandparent is NamedExpression) { |
| // Given a FunctionBody in a named argument, like |
| // `future.catchError(test: (_) {...})`, we step up once more to the |
| // argument list. |
| grandparent = grandparent.parent; |
| } |
| if (grandparent is ArgumentList) { |
| if (grandparent.parent case InstanceCreationExpression invocation) { |
| checkConstructorCallback(invocation, parent, node); |
| } |
| |
| if (grandparent.parent case MethodInvocation invocation) { |
| checkMethodCallback(invocation, parent, node); |
| } |
| } |
| } |
| } |
| |
| /// Checks whether [invocation] involves a [callback] argument for a protected |
| /// constructor. |
| /// |
| /// The code inside a callback argument for a protected constructor must not |
| /// contain any references to a `BuildContext` without a guarding mounted |
| /// check. |
| void checkConstructorCallback( |
| InstanceCreationExpression invocation, |
| FunctionExpression callback, |
| Expression errorNode, |
| ) { |
| var staticType = invocation.staticType; |
| if (staticType == null) return; |
| var arguments = invocation.argumentList.arguments; |
| var positionalArguments = |
| arguments.where((a) => a is! NamedExpression).toList(); |
| var namedArguments = arguments.whereType<NamedExpression>().toList(); |
| for (var constructor in protectedConstructors) { |
| if (invocation.constructorName.name?.name == constructor.name && |
| staticType.isSameAs(constructor.type, constructor.library)) { |
| checkPositionalArguments( |
| constructor.positional, positionalArguments, callback, errorNode); |
| checkNamedArguments( |
| constructor.named, namedArguments, callback, errorNode); |
| } |
| } |
| } |
| |
| /// Checks whether [invocation] involves a [callback] argument for a protected |
| /// instance or static method. |
| /// |
| /// The code inside a callback argument for a protected method must not |
| /// contain any references to a `BuildContext` without a guarding mounted |
| /// check. |
| void checkMethodCallback( |
| MethodInvocation invocation, |
| FunctionExpression callback, |
| Expression errorNode, |
| ) { |
| var arguments = invocation.argumentList.arguments; |
| var positionalArguments = |
| arguments.where((a) => a is! NamedExpression).toList(); |
| var namedArguments = arguments.whereType<NamedExpression>().toList(); |
| |
| var target = invocation.realTarget; |
| var targetElement = target is Identifier ? target.staticElement : null; |
| if (targetElement is ClassElement) { |
| // Static function called; `target` is the class. |
| for (var method in protectedStaticMethods) { |
| if (invocation.methodName.name == method.name && |
| targetElement.name == method.type) { |
| checkPositionalArguments( |
| method.positional, positionalArguments, callback, errorNode); |
| checkNamedArguments( |
| method.named, namedArguments, callback, errorNode); |
| } |
| } |
| } else { |
| var staticType = target?.staticType; |
| if (staticType == null) return; |
| for (var method in protectedInstanceMethods) { |
| if (invocation.methodName.name == method.name && |
| staticType.element?.name == method.type) { |
| checkPositionalArguments( |
| method.positional, positionalArguments, callback, errorNode); |
| checkNamedArguments( |
| method.named, namedArguments, callback, errorNode); |
| } |
| } |
| } |
| } |
| |
| /// Checks whether [callback] is one of the [namedArguments] for one of the |
| /// protected argument [names] for a protected function. |
| void checkNamedArguments( |
| List<String> names, |
| List<NamedExpression> namedArguments, |
| Expression callback, |
| Expression errorNode) { |
| for (var named in names) { |
| var argument = |
| namedArguments.firstWhereOrNull((a) => a.name.label.name == named); |
| if (argument == null) continue; |
| if (callback == argument.expression) { |
| rule.reportLint(errorNode, |
| errorCode: UseBuildContextSynchronously.asyncUseCode); |
| } |
| } |
| } |
| |
| /// Checks whether [callback] is one of the [positionalArguments] for one of |
| /// the protected argument [positions] for a protected function. |
| void checkPositionalArguments( |
| List<int> positions, |
| List<Expression> positionalArguments, |
| Expression callback, |
| Expression errorNode) { |
| for (var position in positions) { |
| if (positionalArguments.length > position && |
| callback == positionalArguments[position]) { |
| rule.reportLint(errorNode, |
| errorCode: UseBuildContextSynchronously.asyncUseCode); |
| } |
| } |
| } |
| |
| @override |
| void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { |
| _visitArgumentList(node.argumentList); |
| } |
| |
| @override |
| void visitInstanceCreationExpression(InstanceCreationExpression node) { |
| _visitArgumentList(node.argumentList); |
| } |
| |
| @override |
| void visitMethodInvocation(MethodInvocation node) { |
| if (isBuildContext(node.target?.staticType, skipNullable: true)) { |
| var buildContextElement = node.target?.buildContextTypedElement; |
| if (buildContextElement != null) { |
| var mountedGetter = buildContextElement.associatedMountedGetter; |
| if (mountedGetter != null) { |
| check(node.target!, mountedGetter); |
| } |
| } |
| } |
| _visitArgumentList(node.argumentList); |
| } |
| |
| @override |
| void visitPrefixedIdentifier(PrefixedIdentifier node) { |
| if (node.identifier.name == mountedName) { |
| // Accessing `context.mounted` does not count as a "use" of a |
| // `BuildContext` which needs to be guarded by a mounted check. |
| return; |
| } |
| // Getter access. |
| if (isBuildContext(node.prefix.staticType, skipNullable: true)) { |
| if (node.identifier.name != 'mounted') { |
| var buildContextElement = node.prefix.buildContextTypedElement; |
| if (buildContextElement != null) { |
| var mountedGetter = buildContextElement.associatedMountedGetter; |
| if (mountedGetter != null) { |
| check(node.prefix, mountedGetter); |
| } |
| } |
| } |
| } |
| } |
| |
| void _visitArgumentList(ArgumentList node) { |
| for (var argument in node.arguments) { |
| var buildContextElement = argument.buildContextTypedElement; |
| if (buildContextElement != null) { |
| var mountedGetter = buildContextElement.associatedMountedGetter; |
| if (mountedGetter != null) { |
| check(argument, mountedGetter); |
| } |
| } |
| } |
| } |
| } |
| |
| extension on AsyncState? { |
| bool get isGuarded => |
| this == AsyncState.mountedCheck || this == AsyncState.notMountedCheck; |
| } |
| |
| extension on AstNode { |
| bool get terminatesControl { |
| var self = this; |
| if (self is Block) { |
| return self.statements.isNotEmpty && |
| self.statements.last.terminatesControl; |
| } |
| // TODO(srawlins): Make ExitDetector 100% functional for our needs. The |
| // basic (only?) difference is that it doesn't consider a `break` statement |
| // to be exiting. |
| if (self is ReturnStatement || |
| self is BreakStatement || |
| self is ContinueStatement) { |
| return true; |
| } |
| return accept(ExitDetector()) ?? false; |
| } |
| } |
| |
| extension on PrefixExpression { |
| bool get isNot => operator.type == TokenType.BANG; |
| } |
| |
| extension on BinaryExpression { |
| bool get isAnd => operator.type == TokenType.AMPERSAND_AMPERSAND; |
| bool get isEqual => operator.type == TokenType.EQ_EQ; |
| bool get isNotEqual => operator.type == TokenType.BANG_EQ; |
| bool get isOr => operator.type == TokenType.BAR_BAR; |
| } |
| |
| extension on Expression { |
| /// The element of this expression, if it is typed as a BuildContext. |
| Element? get buildContextTypedElement { |
| var self = this; |
| if (self is NamedExpression) { |
| self = self.expression; |
| } |
| if (self is PropertyAccess) { |
| self = self.propertyName; |
| } |
| |
| if (self is Identifier) { |
| var element = self.staticElement; |
| if (element == null) { |
| return null; |
| } |
| |
| var declaration = element.declaration; |
| // Get the declaration to ensure checks from un-migrated libraries work. |
| var argType = switch (declaration) { |
| ExecutableElement() => declaration.returnType, |
| VariableElement() => declaration.type, |
| _ => null, |
| }; |
| |
| var isGetter = element is PropertyAccessorElement; |
| if (isBuildContext(argType, skipNullable: isGetter)) { |
| return declaration; |
| } |
| } else if (self is ParenthesizedExpression) { |
| return self.expression.buildContextTypedElement; |
| } else if (self is PostfixExpression && |
| self.operator.type == TokenType.BANG) { |
| return self.operand.buildContextTypedElement; |
| } |
| return null; |
| } |
| } |
| |
| extension on Statement { |
| /// Whether this statement terminates control, via a [BreakStatement], a |
| /// [ContinueStatement], or other definite exits, as determined by |
| /// [ExitDetector]. |
| bool get terminatesControl { |
| var self = this; |
| if (self is Block) { |
| var last = self.statements.lastOrNull; |
| return last != null && last.terminatesControl; |
| } |
| // TODO(srawlins): Make ExitDetector 100% functional for our needs. The |
| // basic (only?) difference is that it doesn't consider a `break` statement |
| // to be exiting. |
| if (self is BreakStatement || self is ContinueStatement) { |
| return true; |
| } |
| return accept(ExitDetector()) ?? false; |
| } |
| } |
| |
| extension on Expression { |
| bool? get constantBoolValue => computeConstantValue().value?.toBoolValue(); |
| } |
| |
| @visibleForTesting |
| extension ElementExtension on Element { |
| /// The `mounted` getter which is associated with `this`, if this static |
| /// element is `BuildContext` from Flutter. |
| Element? get associatedMountedGetter { |
| var self = this; |
| |
| if (self is PropertyAccessorElement) { |
| var enclosingElement = self.enclosingElement; |
| if (enclosingElement is InterfaceElement && isState(enclosingElement)) { |
| // The BuildContext object is the field on Flutter's State class. |
| // This object can only be guarded by async gaps with a mounted |
| // check on the State. |
| return enclosingElement.augmented |
| .lookUpGetter(name: 'mounted', library: enclosingElement.library); |
| } |
| } |
| |
| var buildContextElement = switch (self) { |
| ExecutableElement() => self.returnType, |
| VariableElement() => self.type, |
| _ => null, |
| } |
| ?.element; |
| if (buildContextElement is InterfaceElement) { |
| return buildContextElement.augmented |
| .lookUpGetter(name: 'mounted', library: buildContextElement.library); |
| } |
| |
| return null; |
| } |
| } |