| // Copyright (c) 2019, 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/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/dart/element/type_system.dart'; |
| |
| class FlowAnalysis { |
| /// The output list of variables that were read before they were written. |
| /// TODO(scheglov) use _ElementSet? |
| final List<LocalVariableElement> readBeforeWritten = []; |
| |
| /// The [TypeSystem] of the enclosing library, used to check subtyping. |
| final TypeSystem typeSystem; |
| |
| /// The enclosing [FunctionBody], used to check for potential mutations. |
| final FunctionBody functionBody; |
| |
| /// The stack of states of variables that are not definitely assigned. |
| final List<_State> _stack = []; |
| |
| /// The mapping from labeled [Statement]s to the index in the [_stack] |
| /// where the first related element is located. The number of elements |
| /// is statement specific. Loops have two elements: `break` and `continue` |
| /// states. |
| final Map<Statement, int> _statementToStackIndex = {}; |
| |
| _State _current; |
| |
| /// The last boolean condition, for [_conditionTrue] and [_conditionFalse]. |
| Expression _condition; |
| |
| /// The state when [_condition] evaluates to `true`. |
| _State _conditionTrue; |
| |
| /// The state when [_condition] evaluates to `false`. |
| _State _conditionFalse; |
| |
| FlowAnalysis(this.typeSystem, this.functionBody) { |
| _current = _State(false, _ElementSet.empty, const {}); |
| } |
| |
| /// Add a new [variable], which might be already [assigned]. |
| void add(LocalVariableElement variable, {bool assigned: false}) { |
| if (!assigned) { |
| _current = _current.add(variable); |
| } |
| } |
| |
| void conditional_elseBegin(ConditionalExpression node, bool isBool) { |
| var afterThen = _current; |
| var falseCondition = _stack.removeLast(); |
| |
| if (isBool) { |
| _conditionalEnd(node.thenExpression); |
| // Tail of the stack: falseThen, trueThen |
| } |
| |
| _stack.add(afterThen); |
| _current = falseCondition; |
| } |
| |
| void conditional_end(ConditionalExpression node, bool isBool) { |
| var afterThen = _stack.removeLast(); |
| var afterElse = _current; |
| |
| if (isBool) { |
| _conditionalEnd(node.elseExpression); |
| // Tail of the stack: falseThen, trueThen, falseElse, trueElse |
| |
| var trueElse = _stack.removeLast(); |
| var falseElse = _stack.removeLast(); |
| |
| var trueThen = _stack.removeLast(); |
| var falseThen = _stack.removeLast(); |
| |
| var trueResult = trueThen.combine(typeSystem, trueElse); |
| var falseResult = falseThen.combine(typeSystem, falseElse); |
| |
| _condition = node; |
| _conditionTrue = trueResult; |
| _conditionFalse = falseResult; |
| } |
| |
| _current = afterThen.combine(typeSystem, afterElse); |
| } |
| |
| void conditional_thenBegin(ConditionalExpression node) { |
| _conditionalEnd(node.condition); |
| // Tail of the stack: falseCondition, trueCondition |
| |
| var trueCondition = _stack.removeLast(); |
| _current = trueCondition; |
| } |
| |
| void doStatement_bodyBegin( |
| DoStatement node, Set<VariableElement> loopAssigned) { |
| _current = _current.removePromotedAll(loopAssigned); |
| |
| _statementToStackIndex[node] = _stack.length; |
| _stack.add(_State.identity); // break |
| _stack.add(_State.identity); // continue |
| } |
| |
| void doStatement_conditionBegin() { |
| // Tail of the stack: break, continue |
| |
| var continueState = _stack.removeLast(); |
| _current = _current.combine(typeSystem, continueState); |
| } |
| |
| void doStatement_end(DoStatement node) { |
| _conditionalEnd(node.condition); |
| // Tail of the stack: break, falseCondition, trueCondition |
| |
| _stack.removeLast(); // trueCondition |
| var falseCondition = _stack.removeLast(); |
| var breakState = _stack.removeLast(); |
| |
| _current = falseCondition.combine(typeSystem, breakState); |
| } |
| |
| void falseLiteral(BooleanLiteral expression) { |
| _condition = expression; |
| _conditionTrue = _State.identity; |
| _conditionFalse = _current; |
| } |
| |
| void forEachStatement_bodyBegin(Set<VariableElement> loopAssigned) { |
| _stack.add(_current); |
| _current = _current.removePromotedAll(loopAssigned); |
| } |
| |
| void forEachStatement_end() { |
| var afterIterable = _stack.removeLast(); |
| _current = _current.combine(typeSystem, afterIterable); |
| } |
| |
| void forStatement_bodyBegin(ForStatement node, Expression condition) { |
| _conditionalEnd(condition); |
| // Tail of the stack: falseCondition, trueCondition |
| |
| var trueCondition = _stack.removeLast(); |
| |
| _statementToStackIndex[node] = _stack.length; |
| _stack.add(_State.identity); // break |
| _stack.add(_State.identity); // continue |
| |
| _current = trueCondition; |
| } |
| |
| void forStatement_conditionBegin(Set<VariableElement> loopAssigned) { |
| _current = _current.removePromotedAll(loopAssigned); |
| } |
| |
| void forStatement_end() { |
| // Tail of the stack: falseCondition, break |
| var breakState = _stack.removeLast(); |
| var falseCondition = _stack.removeLast(); |
| |
| _current = falseCondition.combine(typeSystem, breakState); |
| } |
| |
| void forStatement_updaterBegin() { |
| // Tail of the stack: falseCondition, break, continue |
| var afterBody = _current; |
| var continueState = _stack.removeLast(); |
| |
| _current = afterBody.combine(typeSystem, continueState); |
| } |
| |
| void functionExpression_begin() { |
| _stack.add(_current); |
| |
| Set<VariableElement> notPromoted = null; |
| for (var variable in _current.promoted.keys) { |
| if (functionBody.isPotentiallyMutatedInScope(variable)) { |
| notPromoted ??= Set<VariableElement>.identity(); |
| notPromoted.add(variable); |
| } |
| } |
| |
| if (notPromoted != null) { |
| _current = _current.removePromotedAll(notPromoted); |
| } |
| } |
| |
| void functionExpression_end() { |
| _current = _stack.removeLast(); |
| } |
| |
| void handleBreak(AstNode target) { |
| var breakIndex = _statementToStackIndex[target]; |
| if (breakIndex != null) { |
| _stack[breakIndex] = _stack[breakIndex].combine(typeSystem, _current); |
| } |
| _current = _State.identity; |
| } |
| |
| void handleContinue(AstNode target) { |
| var breakIndex = _statementToStackIndex[target]; |
| if (breakIndex != null) { |
| var continueIndex = breakIndex + 1; |
| _stack[continueIndex] = |
| _stack[continueIndex].combine(typeSystem, _current); |
| } |
| _current = _State.identity; |
| } |
| |
| /// Register the fact that the current state definitely exists, e.g. returns |
| /// from the body, throws an exception, etc. |
| void handleExit() { |
| _current = _State.identity; |
| } |
| |
| void ifNullExpression_end() { |
| var afterLeft = _stack.removeLast(); |
| _current = _current.combine(typeSystem, afterLeft); |
| } |
| |
| void ifNullExpression_rightBegin() { |
| _stack.add(_current); // afterLeft |
| } |
| |
| void ifStatement_elseBegin() { |
| var afterThen = _current; |
| var falseCondition = _stack.removeLast(); |
| _stack.add(afterThen); |
| _current = falseCondition; |
| } |
| |
| void ifStatement_end(bool hasElse) { |
| _State afterThen; |
| _State afterElse; |
| if (hasElse) { |
| afterThen = _stack.removeLast(); |
| afterElse = _current; |
| } else { |
| afterThen = _current; // no `else`, so `then` is still current |
| afterElse = _stack.removeLast(); // `falseCond` is still on the stack |
| } |
| _current = afterThen.combine(typeSystem, afterElse); |
| } |
| |
| void ifStatement_thenBegin(IfStatement ifStatement) { |
| _conditionalEnd(ifStatement.condition); |
| // Tail of the stack: falseCondition, trueCondition |
| |
| var trueCondition = _stack.removeLast(); |
| _current = trueCondition; |
| } |
| |
| void isExpression_end( |
| IsExpression isExpression, VariableElement variable, DartType type) { |
| if (functionBody.isPotentiallyMutatedInClosure(variable)) { |
| return; |
| } |
| |
| _condition = isExpression; |
| if (isExpression.notOperator == null) { |
| _conditionTrue = _current.promote(typeSystem, variable, type); |
| _conditionFalse = _current; |
| } else { |
| _conditionTrue = _current; |
| _conditionFalse = _current.promote(typeSystem, variable, type); |
| } |
| } |
| |
| void logicalAnd_end(BinaryExpression andExpression) { |
| _conditionalEnd(andExpression.rightOperand); |
| // Tail of the stack: falseLeft, trueLeft, falseRight, trueRight |
| |
| var trueRight = _stack.removeLast(); |
| var falseRight = _stack.removeLast(); |
| |
| _stack.removeLast(); // trueLeft is not used |
| var falseLeft = _stack.removeLast(); |
| |
| var trueResult = trueRight; |
| var falseResult = falseLeft.combine(typeSystem, falseRight); |
| var afterResult = trueResult.combine(typeSystem, falseResult); |
| |
| _condition = andExpression; |
| _conditionTrue = trueResult; |
| _conditionFalse = falseResult; |
| |
| _current = afterResult; |
| } |
| |
| void logicalAnd_rightBegin(BinaryExpression andExpression) { |
| _conditionalEnd(andExpression.leftOperand); |
| // Tail of the stack: falseLeft, trueLeft |
| |
| var trueLeft = _stack.last; |
| _current = trueLeft; |
| } |
| |
| void logicalNot_end(PrefixExpression notExpression) { |
| _conditionalEnd(notExpression.operand); |
| var trueExpr = _stack.removeLast(); |
| var falseExpr = _stack.removeLast(); |
| |
| _condition = notExpression; |
| _conditionTrue = falseExpr; |
| _conditionFalse = trueExpr; |
| } |
| |
| void logicalOr_end(BinaryExpression orExpression) { |
| _conditionalEnd(orExpression.rightOperand); |
| // Tail of the stack: falseLeft, trueLeft, falseRight, trueRight |
| |
| var trueRight = _stack.removeLast(); |
| var falseRight = _stack.removeLast(); |
| |
| var trueLeft = _stack.removeLast(); |
| _stack.removeLast(); // falseLeft is not used |
| |
| var trueResult = trueLeft.combine(typeSystem, trueRight); |
| var falseResult = falseRight; |
| var afterResult = trueResult.combine(typeSystem, falseResult); |
| |
| _condition = orExpression; |
| _conditionTrue = trueResult; |
| _conditionFalse = falseResult; |
| |
| _current = afterResult; |
| } |
| |
| void logicalOr_rightBegin(BinaryExpression orExpression) { |
| _conditionalEnd(orExpression.leftOperand); |
| // Tail of the stack: falseLeft, trueLeft |
| |
| var falseLeft = _stack[_stack.length - 2]; |
| _current = falseLeft; |
| } |
| |
| /// Retrieves the type that the [variable] is promoted to, if the [variable] |
| /// is currently promoted. Otherwise returns `null`. |
| DartType promotedType(VariableElement variable) { |
| return _current.promoted[variable]; |
| } |
| |
| /// Register read of the given [variable] in the current state. |
| void read(LocalVariableElement variable) { |
| if (_current.notAssigned.contains(variable)) { |
| // Add to the list of violating variables, if not there yet. |
| for (var i = 0; i < readBeforeWritten.length; ++i) { |
| var violatingVariable = readBeforeWritten[i]; |
| if (identical(violatingVariable, variable)) { |
| return; |
| } |
| } |
| readBeforeWritten.add(variable); |
| } |
| } |
| |
| /// The [notPromoted] set contains all variables that are potentially |
| /// assigned in other cases that might target this with `continue`, so |
| /// these variables might have different types and are "un-promoted" from |
| /// the "afterExpression" state. |
| void switchStatement_beginCase(Set<VariableElement> notPromoted) { |
| _current = _stack.last.removePromotedAll(notPromoted); |
| } |
| |
| void switchStatement_end(SwitchStatement node, bool hasDefault) { |
| // Tail of the stack: break, continue, afterExpression |
| var afterExpression = _current = _stack.removeLast(); |
| _stack.removeLast(); // continue |
| var breakState = _stack.removeLast(); |
| |
| if (hasDefault) { |
| _current = breakState; |
| } else { |
| _current = breakState.combine(typeSystem, afterExpression); |
| } |
| } |
| |
| void switchStatement_expressionEnd(SwitchStatement node) { |
| _statementToStackIndex[node] = _stack.length; |
| _stack.add(_State.identity); // break |
| _stack.add(_State.identity); // continue |
| _stack.add(_current); // afterExpression |
| } |
| |
| void trueLiteral(BooleanLiteral expression) { |
| _condition = expression; |
| _conditionTrue = _current; |
| _conditionFalse = _State.identity; |
| } |
| |
| void tryStatement_bodyBegin() { |
| _stack.add(_current); |
| // Tail of the stack: beforeBody |
| } |
| |
| void tryStatement_bodyEnd(Set<VariableElement> notPromoted) { |
| var beforeBody = _stack.removeLast(); |
| var beforeCatch = beforeBody.removePromotedAll(notPromoted); |
| _stack.add(beforeCatch); |
| _stack.add(_current); // afterBodyAndCatches |
| // Tail of the stack: beforeCatch, afterBodyAndCatches |
| } |
| |
| void tryStatement_catchBegin() { |
| var beforeCatch = _stack[_stack.length - 2]; |
| _current = beforeCatch; |
| } |
| |
| void tryStatement_catchEnd() { |
| var afterBodyAndCatches = _stack.last; |
| _stack.last = afterBodyAndCatches.combine(typeSystem, _current); |
| } |
| |
| void tryStatement_finallyBegin() { |
| var afterBodyAndCatches = _stack.removeLast(); |
| _stack.removeLast(); // beforeCatch |
| |
| _current = afterBodyAndCatches; |
| } |
| |
| void verifyStackEmpty() { |
| assert(_stack.isEmpty); |
| } |
| |
| void whileStatement_bodyBegin(WhileStatement node) { |
| _conditionalEnd(node.condition); |
| // Tail of the stack: falseCondition, trueCondition |
| |
| var trueCondition = _stack.removeLast(); |
| |
| _statementToStackIndex[node] = _stack.length; |
| _stack.add(_State.identity); // break |
| _stack.add(_State.identity); // continue |
| |
| _current = trueCondition; |
| } |
| |
| void whileStatement_conditionBegin(Set<VariableElement> loopAssigned) { |
| _current = _current.removePromotedAll(loopAssigned); |
| } |
| |
| void whileStatement_end() { |
| _stack.removeLast(); // continue |
| var breakState = _stack.removeLast(); |
| var falseCondition = _stack.removeLast(); |
| |
| _current = falseCondition.combine(typeSystem, breakState); |
| } |
| |
| /// Register write of the given [variable] in the current state. |
| void write(VariableElement variable) { |
| _current = _current.write(variable); |
| } |
| |
| void _conditionalEnd(Expression condition) { |
| while (condition is ParenthesizedExpression) { |
| condition = (condition as ParenthesizedExpression).expression; |
| } |
| if (identical(condition, _condition)) { |
| _stack.add(_conditionFalse); |
| _stack.add(_conditionTrue); |
| } else { |
| _stack.add(_current); |
| _stack.add(_current); |
| } |
| } |
| } |
| |
| /// Sets of variables that are potentially assigned in loops. |
| class LoopAssignedVariables { |
| static final emptySet = Set<VariableElement>(); |
| |
| /// Mapping from a loop [AstNode] to the set of variables that are |
| /// potentially assigned in this loop. |
| final Map<AstNode, Set<VariableElement>> _map = {}; |
| |
| /// The stack of nested loops. |
| final List<Set<VariableElement>> _stack = []; |
| |
| /// Return the set of variables that are potentially assigned in the [loop]. |
| Set<VariableElement> operator [](AstNode loop) { |
| return _map[loop] ?? emptySet; |
| } |
| |
| void beginLoop() { |
| var set = Set<VariableElement>.identity(); |
| _stack.add(set); |
| } |
| |
| void endLoop(AstNode loop) { |
| _map[loop] = _stack.removeLast(); |
| } |
| |
| void write(VariableElement variable) { |
| for (var i = 0; i < _stack.length; ++i) { |
| _stack[i].add(variable); |
| } |
| } |
| } |
| |
| /// List based immutable set of elements. |
| class _ElementSet { |
| static final empty = _ElementSet._( |
| List<LocalVariableElement>(0), |
| ); |
| |
| final List<LocalVariableElement> elements; |
| |
| _ElementSet._(this.elements); |
| |
| _ElementSet add(LocalVariableElement addedElement) { |
| if (contains(addedElement)) { |
| return this; |
| } |
| |
| var length = elements.length; |
| var newElements = List<LocalVariableElement>(length + 1); |
| for (var i = 0; i < length; ++i) { |
| newElements[i] = elements[i]; |
| } |
| newElements[length] = addedElement; |
| return _ElementSet._(newElements); |
| } |
| |
| bool contains(LocalVariableElement element) { |
| var length = elements.length; |
| for (var i = 0; i < length; ++i) { |
| if (identical(elements[i], element)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| _ElementSet remove(LocalVariableElement removedElement) { |
| if (!contains(removedElement)) { |
| return this; |
| } |
| |
| var length = elements.length; |
| if (length == 1) { |
| return empty; |
| } |
| |
| var newElements = List<LocalVariableElement>(length - 1); |
| var newIndex = 0; |
| for (var i = 0; i < length; ++i) { |
| var element = elements[i]; |
| if (!identical(element, removedElement)) { |
| newElements[newIndex++] = element; |
| } |
| } |
| |
| return _ElementSet._(newElements); |
| } |
| |
| _ElementSet union(_ElementSet other) { |
| if (other == null || other.elements.isEmpty) { |
| return this; |
| } |
| |
| var result = this; |
| var otherElements = other.elements; |
| for (var i = 0; i < otherElements.length; ++i) { |
| var otherElement = otherElements[i]; |
| result = result.add(otherElement); |
| } |
| return result; |
| } |
| } |
| |
| class _State { |
| static final identity = _State(false, _ElementSet.empty, const {}); |
| |
| final bool reachable; |
| final _ElementSet notAssigned; |
| final Map<VariableElement, DartType> promoted; |
| |
| _State(this.reachable, this.notAssigned, this.promoted); |
| |
| /// Add a new [variable] to track definite assignment. |
| _State add(LocalVariableElement variable) { |
| var newNotAssigned = notAssigned.add(variable); |
| if (identical(newNotAssigned, notAssigned)) return this; |
| return _State(reachable, newNotAssigned, promoted); |
| } |
| |
| _State combine(TypeSystem typeSystem, _State other) { |
| if (identical(this, identity)) return other; |
| if (identical(other, identity)) return this; |
| |
| var newReachable = reachable || other.reachable; |
| var newNotAssigned = notAssigned.union(other.notAssigned); |
| var newPromoted = _combinePromoted(typeSystem, promoted, other.promoted); |
| |
| if (reachable == newReachable && |
| identical(notAssigned, newNotAssigned) && |
| identical(promoted, newPromoted)) { |
| return this; |
| } |
| if (other.reachable == newReachable && |
| identical(other.notAssigned, newNotAssigned) && |
| identical(other.promoted, newPromoted)) { |
| return other; |
| } |
| |
| return _State(newReachable, newNotAssigned, newPromoted); |
| } |
| |
| _State promote( |
| TypeSystem typeSystem, VariableElement variable, DartType type) { |
| var previousType = promoted[variable]; |
| previousType ??= variable.type; |
| |
| if (typeSystem.isSubtypeOf(type, previousType) && type != previousType) { |
| var newPromoted = <VariableElement, DartType>{}..addAll(promoted); |
| newPromoted[variable] = type; |
| return _State(reachable, notAssigned, newPromoted); |
| } |
| |
| return this; |
| } |
| |
| _State removePromotedAll(Set<VariableElement> variables) { |
| var newPromoted = _removePromotedAll(promoted, variables); |
| |
| if (identical(newPromoted, promoted)) return this; |
| |
| return _State(reachable, notAssigned, newPromoted); |
| } |
| |
| _State write(VariableElement variable) { |
| var newNotAssigned = variable is LocalVariableElement |
| ? notAssigned.remove(variable) |
| : notAssigned; |
| var newPromoted = _removePromoted(promoted, variable); |
| |
| if (identical(newNotAssigned, notAssigned) && |
| identical(newPromoted, promoted)) { |
| return this; |
| } |
| |
| return _State(reachable, newNotAssigned, newPromoted); |
| } |
| |
| static Map<VariableElement, DartType> _combinePromoted(TypeSystem typeSystem, |
| Map<VariableElement, DartType> a, Map<VariableElement, DartType> b) { |
| if (identical(a, b)) return a; |
| if (a.isEmpty || b.isEmpty) return const {}; |
| |
| var result = <VariableElement, DartType>{}; |
| var alwaysA = true; |
| var alwaysB = true; |
| for (var element in a.keys) { |
| var aType = a[element]; |
| var bType = b[element]; |
| if (aType != null && bType != null) { |
| if (typeSystem.isSubtypeOf(aType, bType)) { |
| result[element] = bType; |
| alwaysA = false; |
| } else if (typeSystem.isSubtypeOf(bType, aType)) { |
| result[element] = aType; |
| alwaysB = false; |
| } else { |
| alwaysA = false; |
| alwaysB = false; |
| } |
| } else { |
| alwaysA = false; |
| alwaysB = false; |
| } |
| } |
| |
| if (alwaysA) return a; |
| if (alwaysB) return b; |
| if (result.isEmpty) return const {}; |
| return result; |
| } |
| |
| static Map<VariableElement, DartType> _removePromoted( |
| Map<VariableElement, DartType> map, VariableElement variable) { |
| if (map.isEmpty) return const {}; |
| |
| var result = <VariableElement, DartType>{}; |
| for (var key in map.keys) { |
| if (!identical(key, variable)) { |
| result[key] = map[key]; |
| } |
| } |
| |
| if (result.isEmpty) return const {}; |
| return result; |
| } |
| |
| static Map<VariableElement, DartType> _removePromotedAll( |
| Map<VariableElement, DartType> map, Set<VariableElement> variables) { |
| if (map.isEmpty) return const {}; |
| |
| var result = <VariableElement, DartType>{}; |
| for (var key in map.keys) { |
| if (!variables.contains(key)) { |
| result[key] = map[key]; |
| } |
| } |
| |
| if (result.isEmpty) return const {}; |
| return result; |
| } |
| } |