Improvements for flow analysis.
Support for ConditionalExpression.
Check for potential mutations in closures.
Un-promote on assignment.
Support for 'while' statement.
Support for 'do-while' statement.
R=paulberry@google.com
Change-Id: Ic9e33a08057dc3519efec759702c52ddada728ed
Reviewed-on: https://dart-review.googlesource.com/c/89763
Reviewed-by: Paul Berry <paulberry@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analyzer/lib/dart/element/type.dart b/pkg/analyzer/lib/dart/element/type.dart
index f2ddef8..f52cc53 100644
--- a/pkg/analyzer/lib/dart/element/type.dart
+++ b/pkg/analyzer/lib/dart/element/type.dart
@@ -69,6 +69,12 @@
bool get isDartAsyncFutureOr;
/**
+ * Return `true` if this type represents the type 'bool' defined in the
+ * dart:core library.
+ */
+ bool get isDartCoreBool;
+
+ /**
* Return `true` if this type represents the type 'Function' defined in the
* dart:core library.
*/
diff --git a/pkg/analyzer/lib/src/dart/element/type.dart b/pkg/analyzer/lib/src/dart/element/type.dart
index 6b7d311..4ea8023 100644
--- a/pkg/analyzer/lib/src/dart/element/type.dart
+++ b/pkg/analyzer/lib/src/dart/element/type.dart
@@ -1496,6 +1496,15 @@
}
@override
+ bool get isDartCoreBool {
+ ClassElement element = this.element;
+ if (element == null) {
+ return false;
+ }
+ return element.name == "bool" && element.library.isDartCore;
+ }
+
+ @override
bool get isDartCoreFunction {
ClassElement element = this.element;
if (element == null) {
@@ -2735,6 +2744,9 @@
bool get isDartAsyncFutureOr => false;
@override
+ bool get isDartCoreBool => false;
+
+ @override
bool get isDartCoreFunction => false;
@override
diff --git a/pkg/analyzer/lib/src/dart/resolver/flow_analysis.dart b/pkg/analyzer/lib/src/dart/resolver/flow_analysis.dart
index e0940d2..5867f95 100644
--- a/pkg/analyzer/lib/src/dart/resolver/flow_analysis.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/flow_analysis.dart
@@ -12,9 +12,21 @@
/// 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].
@@ -26,7 +38,7 @@
/// The state when [_condition] evaluates to `false`.
_State _conditionFalse;
- FlowAnalysis(this.typeSystem) {
+ FlowAnalysis(this.typeSystem, this.functionBody) {
_current = _State(false, _ElementSet.empty, const {});
}
@@ -37,12 +49,103 @@
}
}
+ 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 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() {
@@ -78,15 +181,18 @@
}
void isExpression_end(
- IsExpression isExpression, LocalElement element, DartType type) {
- // TODO(scheglov) check for mutations
+ IsExpression isExpression, VariableElement variable, DartType type) {
+ if (functionBody.isPotentiallyMutatedInClosure(variable)) {
+ return;
+ }
+
_condition = isExpression;
if (isExpression.notOperator == null) {
- _conditionTrue = _current.promote(typeSystem, element, type);
+ _conditionTrue = _current.promote(typeSystem, variable, type);
_conditionFalse = _current;
} else {
_conditionTrue = _current;
- _conditionFalse = _current.promote(typeSystem, element, type);
+ _conditionFalse = _current.promote(typeSystem, variable, type);
}
}
@@ -158,10 +264,10 @@
_current = falseLeft;
}
- /// Retrieves the type that [element] is promoted to, if [element] is
- /// currently promoted. Otherwise returns `null`.
- DartType promotedType(LocalElement element) {
- return _current.promoted[element];
+ /// 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.
@@ -188,8 +294,34 @@
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(
+ WhileStatement node, 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(LocalVariableElement variable) {
+ void write(VariableElement variable) {
_current = _current.write(variable);
}
@@ -207,6 +339,38 @@
}
}
+/// 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._(
@@ -279,11 +443,11 @@
}
class _State {
- static final identity = _State(false, _ElementSet.empty, null);
+ static final identity = _State(false, _ElementSet.empty, const {});
final bool reachable;
final _ElementSet notAssigned;
- final Map<LocalElement, DartType> promoted;
+ final Map<VariableElement, DartType> promoted;
_State(this.reachable, this.notAssigned, this.promoted);
@@ -316,39 +480,48 @@
return _State(newReachable, newNotAssigned, newPromoted);
}
- _State promote(TypeSystem typeSystem, LocalElement element, DartType type) {
- var previousType = promoted[element];
- if (previousType == null) {
- if (element is LocalVariableElement) {
- previousType = element.type;
- } else if (element is ParameterElement) {
- previousType = element.type;
- } else {
- throw StateError('Unexpected type: (${element.runtimeType}) $element');
- }
- }
+ _State promote(
+ TypeSystem typeSystem, VariableElement variable, DartType type) {
+ var previousType = promoted[variable];
+ previousType ??= variable.type;
if (typeSystem.isSubtypeOf(type, previousType) && type != previousType) {
- var newPromoted = <LocalElement, DartType>{}..addAll(promoted);
- newPromoted[element] = type;
+ var newPromoted = <VariableElement, DartType>{}..addAll(promoted);
+ newPromoted[variable] = type;
return _State(reachable, notAssigned, newPromoted);
}
return this;
}
- _State write(LocalVariableElement variable) {
- var newNotAssigned = notAssigned.remove(variable);
- if (identical(newNotAssigned, notAssigned)) return this;
- return _State(reachable, newNotAssigned, promoted);
+ _State removePromotedAll(Set<VariableElement> variables) {
+ var newPromoted = _removePromotedAll(promoted, variables);
+
+ if (identical(newPromoted, promoted)) return this;
+
+ return _State(reachable, notAssigned, newPromoted);
}
- Map<LocalElement, DartType> _combinePromoted(TypeSystem typeSystem,
- Map<LocalElement, DartType> a, Map<LocalElement, DartType> b) {
+ _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 = <LocalElement, DartType>{};
+ var result = <VariableElement, DartType>{};
var alwaysA = true;
var alwaysB = true;
for (var element in a.keys) {
@@ -376,4 +549,34 @@
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;
+ }
}
diff --git a/pkg/analyzer/test/src/dart/resolution/flow_analysis_test.dart b/pkg/analyzer/test/src/dart/resolution/flow_analysis_test.dart
index d185aa6..7873fce 100644
--- a/pkg/analyzer/test/src/dart/resolution/flow_analysis_test.dart
+++ b/pkg/analyzer/test/src/dart/resolution/flow_analysis_test.dart
@@ -7,6 +7,7 @@
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/dart/element/type_system.dart';
import 'package:analyzer/src/dart/resolver/flow_analysis.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
@@ -22,7 +23,7 @@
@reflectiveTest
class DefiniteAssignmentFlowTest extends DriverResolutionTest {
- FlowAnalysis flow;
+ final List<LocalVariableElement> readBeforeWritten = [];
/// Assert that only local variables with the given names are marked as read
/// before being written. All the other local variables are implicitly
@@ -33,7 +34,7 @@
.where((i) => i != null)
.map((name) => findElement.localVar(name))
.toList();
- expect(flow.readBeforeWritten, unorderedEquals(expected));
+ expect(readBeforeWritten, unorderedEquals(expected));
}
test_binaryExpression_logicalAnd_left() async {
@@ -102,6 +103,184 @@
assertReadBeforeWritten();
}
+ test_conditional_both() async {
+ await trackCode(r'''
+f(bool v) {
+ int v;
+ b ? (v = 1) : (v = 2);
+ v;
+}
+''');
+ assertReadBeforeWritten();
+ }
+
+ test_conditional_else() async {
+ await trackCode(r'''
+f(bool v) {
+ int v;
+ b ? 1 : (v = 2);
+ v;
+}
+''');
+ assertReadBeforeWritten('v');
+ }
+
+ test_conditional_then() async {
+ await trackCode(r'''
+f(bool v) {
+ int v;
+ b ? (v = 1) : 2;
+ v;
+}
+''');
+ assertReadBeforeWritten('v');
+ }
+
+ test_doWhile_break_afterAssignment() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v;
+ do {
+ v = 0;
+ v;
+ if (b) break;
+ } while (b);
+ v;
+}
+''');
+ assertReadBeforeWritten();
+ }
+
+ test_doWhile_break_beforeAssignment() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v;
+ do {
+ if (b) break;
+ v = 0;
+ } while (b);
+ v;
+}
+''');
+ assertReadBeforeWritten('v');
+ }
+
+ test_doWhile_breakOuterFromInner() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v1, v2, v3;
+ L1: do {
+ do {
+ v1 = 0;
+ if (b) break L1;
+ v2 = 0;
+ v3 = 0;
+ } while (b);
+ v2;
+ } while (b);
+ v1;
+ v3;
+}
+''');
+ assertReadBeforeWritten('v3');
+ }
+
+ test_doWhile_condition() async {
+ await trackCode(r'''
+void f() {
+ int v1, v2;
+ do {
+ v1; // assigned in the condition, but not yet
+ } while ((v1 = 0) + (v2 = 0) >= 0);
+ v2;
+}
+''');
+ assertReadBeforeWritten('v1');
+ }
+
+ test_doWhile_condition_break() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v;
+ do {
+ if (b) break;
+ } while ((v = 0) >= 0);
+ v;
+}
+''');
+ assertReadBeforeWritten('v');
+ }
+
+ test_doWhile_condition_break_continue() async {
+ await trackCode(r'''
+void f(bool b1, b2) {
+ int v1, v2, v3, v4, v5, v6;
+ do {
+ v1 = 0; // visible outside, visible to the condition
+ if (b1) break;
+ v2 = 0; // not visible outside, visible to the condition
+ v3 = 0; // not visible outside, visible to the condition
+ if (b2) continue;
+ v4 = 0; // not visible
+ v5 = 0; // not visible
+ } while ((v6 = v1 + v2 + v4) == 0); // has break => v6 is not visible outside
+ v1;
+ v3;
+ v5;
+ v6;
+}
+''');
+ assertReadBeforeWritten('v3', 'v4', 'v5', 'v6');
+ }
+
+ test_doWhile_condition_continue() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v1, v2, v3, v4;
+ do {
+ v1 = 0; // visible outside, visible to the condition
+ if (b) continue;
+ v2 = 0; // not visible
+ v3 = 0; // not visible
+ } while ((v4 = v1 + v2) == 0); // no break => v4 visible outside
+ v1;
+ v3;
+ v4;
+}
+''');
+ assertReadBeforeWritten('v2', 'v3');
+ }
+
+ test_doWhile_continue_beforeAssignment() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v;
+ do {
+ if (b) continue;
+ v = 0;
+ } while (b);
+ v;
+}
+''');
+ assertReadBeforeWritten('v');
+ }
+
+ test_doWhile_true_assignInBreak() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v;
+ do {
+ if (b) {
+ v = 0;
+ break;
+ }
+ } while (true);
+ v;
+}
+''');
+ assertReadBeforeWritten();
+ }
+
test_if_condition() async {
await trackCode(r'''
main() {
@@ -118,7 +297,6 @@
}
test_if_condition_false() async {
- // new test
await trackCode(r'''
void f() {
int v;
@@ -134,7 +312,6 @@
}
test_if_condition_logicalAnd_else() async {
- // new test
await trackCode(r'''
void f(bool b, int i) {
int v;
@@ -148,7 +325,6 @@
}
test_if_condition_logicalAnd_then() async {
- // new test
await trackCode(r'''
void f(bool b, int i) {
int v;
@@ -161,7 +337,6 @@
}
test_if_condition_logicalOr_else() async {
- // new test
await trackCode(r'''
void f(bool b, int i) {
int v;
@@ -175,7 +350,6 @@
}
test_if_condition_logicalOr_then() async {
- // new test
await trackCode(r'''
void f(bool b, int i) {
int v;
@@ -189,7 +363,6 @@
}
test_if_condition_notFalse() async {
- // new test
await trackCode(r'''
void f() {
int v;
@@ -203,7 +376,6 @@
}
test_if_condition_notTrue() async {
- // new test
await trackCode(r'''
void f() {
int v;
@@ -219,7 +391,6 @@
}
test_if_condition_true() async {
- // new test
await trackCode(r'''
void f() {
int v;
@@ -292,28 +463,205 @@
assertReadBeforeWritten('v');
}
+ test_while_condition() async {
+ await trackCode(r'''
+void f() {
+ int v;
+ while ((v = 0) >= 0) {
+ v;
+ }
+ v;
+}
+''');
+ assertReadBeforeWritten();
+ }
+
+ test_while_condition_notTrue() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v1, v2;
+ while (b) {
+ v1 = 0;
+ v2 = 0;
+ v1;
+ }
+ v2;
+}
+''');
+ assertReadBeforeWritten('v2');
+ }
+
+ test_while_true_break_afterAssignment() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v1, v2;
+ while (true) {
+ v1 = 0;
+ v1;
+ if (b) break;
+ v1;
+ v2 = 0;
+ v2;
+ }
+ v1;
+}
+''');
+ assertReadBeforeWritten();
+ }
+
+ test_while_true_break_beforeAssignment() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v1, v2;
+ while (true) {
+ if (b) break;
+ v1 = 0;
+ v2 = 0;
+ v2;
+ }
+ v1;
+}
+''');
+ assertReadBeforeWritten('v1');
+ }
+
+ test_while_true_break_if() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v;
+ while (true) {
+ if (b) {
+ v = 0;
+ break;
+ } else {
+ v = 0;
+ break;
+ }
+ v;
+ }
+ v;
+}
+''');
+ assertReadBeforeWritten();
+ }
+
+ test_while_true_break_if2() async {
+ await trackCode(r'''
+void f(bool b) {
+ var v;
+ while (true) {
+ if (b) {
+ break;
+ } else {
+ v = 0;
+ }
+ v;
+ }
+}
+''');
+ assertReadBeforeWritten();
+ }
+
+ test_while_true_break_if3() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v1, v2;
+ while (true) {
+ if (b) {
+ v1 = 0;
+ v2 = 0;
+ if (b) break;
+ } else {
+ if (b) break;
+ v1 = 0;
+ v2 = 0;
+ }
+ v1;
+ }
+ v2;
+}
+''');
+ assertReadBeforeWritten('v2');
+ }
+
+ test_while_true_breakOuterFromInner() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v1, v2, v3;
+ L1: while (true) {
+ L2: while (true) {
+ v1 = 0;
+ if (b) break L1;
+ v2 = 0;
+ v3 = 0;
+ if (b) break L2;
+ }
+ v2;
+ }
+ v1;
+ v3;
+}
+''');
+ assertReadBeforeWritten('v3');
+ }
+
+ test_while_true_continue() async {
+ await trackCode(r'''
+void f(bool b) {
+ int v;
+ while (true) {
+ if (b) continue;
+ v = 0;
+ }
+ v;
+}
+''');
+ assertReadBeforeWritten();
+ }
+
+ test_while_true_noBreak() async {
+ await trackCode(r'''
+void f() {
+ int v;
+ while (true) {
+ // No assignment, but no break.
+ // So, we don't exit the loop.
+ // So, all variables are assigned.
+ }
+ v;
+}
+''');
+ assertReadBeforeWritten();
+ }
+
/// Resolve the given [code] and track assignments in the unit.
Future<void> trackCode(String code) async {
addTestFile(code);
await resolveTestFile();
- var typeSystem = result.unit.declaredElement.context.typeSystem;
- flow = FlowAnalysis(typeSystem);
+ var unit = result.unit;
- var visitor = _AstVisitor(flow, {});
- result.unit.accept(visitor);
+ var loopAssignedVariables = LoopAssignedVariables();
+ unit.accept(_LoopAssignedVariablesVisitor(loopAssignedVariables));
+
+ var typeSystem = unit.declaredElement.context.typeSystem;
+ unit.accept(_AstVisitor(
+ typeSystem,
+ loopAssignedVariables,
+ {},
+ readBeforeWritten,
+ ));
}
}
@reflectiveTest
class TypePromotionFlowTest extends DriverResolutionTest {
- Map<AstNode, DartType> promotedTypes = {};
- FlowAnalysis flow;
+ final Map<AstNode, DartType> promotedTypes = {};
void assertNotPromoted(String search) {
var node = findNode.simple(search);
var actualType = promotedTypes[node];
- expect(actualType, isNull);
+ expect(actualType, isNull, reason: search);
}
void assertPromoted(String search, String expectedType) {
@@ -325,8 +673,119 @@
assertElementTypeString(actualType, expectedType);
}
+ test_assignment() async {
+ await trackCode(r'''
+f(Object x) {
+ if (x is String) {
+ x = 42;
+ x; // 1
+ }
+}
+''');
+ assertNotPromoted('x; // 1');
+ }
+
+ test_conditional() async {
+ await trackCode(r'''
+f(bool b, Object x) {
+ b ? ((x is num) || (throw 1)) : ((x is int) || (throw 2));
+ x; // 1
+}
+''');
+ assertPromoted('x; // 1', 'num');
+ }
+
+ test_do_condition_isNotType() async {
+ await trackCode(r'''
+void f(Object x) {
+ do {
+ x; // 1
+ x = '';
+ } while (x is! String)
+ x; // 2
+}
+''');
+ assertNotPromoted('x; // 1');
+ assertPromoted('x; // 2', 'String');
+ }
+
+ test_do_condition_isType() async {
+ await trackCode(r'''
+void f(Object x) {
+ do {
+ x; // 1
+ } while (x is String)
+ x; // 2
+}
+''');
+ assertNotPromoted('x; // 1');
+ assertNotPromoted('x; // 2');
+ }
+
+ test_do_outerIsType() async {
+ await trackCode(r'''
+void f(bool b, Object x) {
+ if (x is String) {
+ do {
+ x; // 1
+ } while (b);
+ x; // 2
+ }
+}
+''');
+ assertPromoted('x; // 1', 'String');
+ assertPromoted('x; // 2', 'String');
+ }
+
+ test_do_outerIsType_loopAssigned_body() async {
+ await trackCode(r'''
+void f(bool b, Object x) {
+ if (x is String) {
+ do {
+ x; // 1
+ x = x.length;
+ } while (b);
+ x; // 2
+ }
+}
+''');
+ assertNotPromoted('x; // 1');
+ assertNotPromoted('x; // 2');
+ }
+
+ test_do_outerIsType_loopAssigned_condition() async {
+ await trackCode(r'''
+void f(bool b, Object x) {
+ if (x is String) {
+ do {
+ x; // 1
+ x = x.length;
+ } while (x != 0);
+ x; // 2
+ }
+}
+''');
+ assertNotPromoted('x != 0');
+ assertNotPromoted('x; // 1');
+ assertNotPromoted('x; // 2');
+ }
+
+ test_do_outerIsType_loopAssigned_condition2() async {
+ await trackCode(r'''
+void f(bool b, Object x) {
+ if (x is String) {
+ do {
+ x; // 1
+ } while ((x = 1) != 0);
+ x; // 2
+ }
+}
+''');
+ assertNotPromoted('x; // 1');
+ assertNotPromoted('x; // 2');
+ }
+
test_if_combine_empty() async {
- // new test
await trackCode(r'''
main(bool b, Object v) {
if (b) {
@@ -340,8 +799,39 @@
assertNotPromoted('v; // 3');
}
+ test_if_conditional_isNotType() async {
+ await trackCode(r'''
+f(bool b, Object v) {
+ if (b ? (v is! int) : (v is! num)) {
+ v; // 1
+ } else {
+ v; // 2
+ }
+ v; // 3
+}
+''');
+ assertNotPromoted('v; // 1');
+ assertPromoted('v; // 2', 'num');
+ assertNotPromoted('v; // 3');
+ }
+
+ test_if_conditional_isType() async {
+ await trackCode(r'''
+f(bool b, Object v) {
+ if (b ? (v is int) : (v is num)) {
+ v; // 1
+ } else {
+ v; // 2
+ }
+ v; // 3
+}
+''');
+ assertPromoted('v; // 1', 'num');
+ assertNotPromoted('v; // 2');
+ assertNotPromoted('v; // 3');
+ }
+
test_if_isNotType() async {
- // new test
await trackCode(r'''
main(v) {
if (v is! String) {
@@ -358,7 +848,6 @@
}
test_if_isNotType_return() async {
- // new test
await trackCode(r'''
main(v) {
if (v is! String) return;
@@ -368,6 +857,16 @@
assertPromoted('v; // ref', 'String');
}
+ test_if_isNotType_throw() async {
+ await trackCode(r'''
+main(v) {
+ if (v is! String) throw 42;
+ v; // ref
+}
+''');
+ assertPromoted('v; // ref', 'String');
+ }
+
test_if_isType() async {
await trackCode(r'''
main(v) {
@@ -396,7 +895,6 @@
}
test_if_logicalNot_isType() async {
- // new test
await trackCode(r'''
main(v) {
if (!(v is String)) {
@@ -413,7 +911,6 @@
}
test_logicalOr_throw() async {
- // new test
await trackCode(r'''
main(v) {
v is String || (throw 42);
@@ -423,36 +920,151 @@
assertPromoted('v; // ref', 'String');
}
+ test_potentiallyMutatedInClosure() async {
+ await trackCode(r'''
+f(Object x) {
+ localFunction() {
+ x = 42;
+ }
+
+ if (x is String) {
+ localFunction();
+ x; // 1
+ }
+}
+''');
+ assertNotPromoted('x; // 1');
+ }
+
+ test_potentiallyMutatedInScope() async {
+ await trackCode(r'''
+f(Object x) {
+ if (x is String) {
+ x; // 1
+ }
+
+ x = 42;
+}
+''');
+ assertPromoted('x; // 1', 'String');
+ }
+
+ test_while_condition_false() async {
+ await trackCode(r'''
+void f(Object x) {
+ while (x is! String) {
+ x; // 1
+ }
+ x; // 2
+}
+''');
+ assertNotPromoted('x; // 1');
+ assertPromoted('x; // 2', 'String');
+ }
+
+ test_while_condition_true() async {
+ await trackCode(r'''
+void f(Object x) {
+ while (x is String) {
+ x; // 1
+ }
+ x; // 2
+}
+''');
+ assertPromoted('x; // 1', 'String');
+ assertNotPromoted('x; // 2');
+ }
+
+ test_while_outerIsType() async {
+ await trackCode(r'''
+void f(bool b, Object x) {
+ if (x is String) {
+ while (b) {
+ x; // 1
+ }
+ x; // 2
+ }
+}
+''');
+ assertPromoted('x; // 1', 'String');
+ assertPromoted('x; // 2', 'String');
+ }
+
+ test_while_outerIsType_loopAssigned_body() async {
+ await trackCode(r'''
+void f(bool b, Object x) {
+ if (x is String) {
+ while (b) {
+ x; // 1
+ x = x.length;
+ }
+ x; // 2
+ }
+}
+''');
+ assertNotPromoted('x; // 1');
+ assertNotPromoted('x; // 2');
+ }
+
+ test_while_outerIsType_loopAssigned_condition() async {
+ await trackCode(r'''
+void f(bool b, Object x) {
+ if (x is String) {
+ while (x != 0) {
+ x; // 1
+ x = x.length;
+ }
+ x; // 2
+ }
+}
+''');
+ assertNotPromoted('x != 0');
+ assertNotPromoted('x; // 1');
+ assertNotPromoted('x; // 2');
+ }
+
/// Resolve the given [code] and track assignments in the unit.
Future<void> trackCode(String code) async {
addTestFile(code);
await resolveTestFile();
- var typeSystem = result.unit.declaredElement.context.typeSystem;
- flow = FlowAnalysis(typeSystem);
+ var unit = result.unit;
- var visitor = _AstVisitor(flow, promotedTypes);
- result.unit.accept(visitor);
+ var loopAssignedVariables = LoopAssignedVariables();
+ unit.accept(_LoopAssignedVariablesVisitor(loopAssignedVariables));
+
+ var typeSystem = unit.declaredElement.context.typeSystem;
+ unit.accept(_AstVisitor(
+ typeSystem,
+ loopAssignedVariables,
+ promotedTypes,
+ [],
+ ));
}
}
/// [AstVisitor] that drives the [flow] in the way we expect the resolver
/// will do in production.
class _AstVisitor extends RecursiveAstVisitor<void> {
- final FlowAnalysis flow;
+ final TypeSystem typeSystem;
+ final LoopAssignedVariables loopAssignedVariables;
final Map<AstNode, DartType> promotedTypes;
+ final List<LocalVariableElement> readBeforeWritten;
- _AstVisitor(this.flow, this.promotedTypes);
+ FlowAnalysis flow;
+
+ _AstVisitor(this.typeSystem, this.loopAssignedVariables, this.promotedTypes,
+ this.readBeforeWritten);
@override
void visitAssignmentExpression(AssignmentExpression node) {
var left = node.leftHandSide;
var right = node.rightHandSide;
- LocalVariableElement localElement;
+ VariableElement localElement;
if (left is SimpleIdentifier) {
var element = left.staticElement;
- if (element is LocalVariableElement) {
+ if (element is VariableElement) {
localElement = element;
}
}
@@ -515,8 +1127,16 @@
@override
void visitBlockFunctionBody(BlockFunctionBody node) {
+ var isFlowOwner = flow == null;
+ flow ??= FlowAnalysis(typeSystem, node);
+
super.visitBlockFunctionBody(node);
- flow.verifyStackEmpty();
+
+ if (isFlowOwner) {
+ readBeforeWritten.addAll(flow.readBeforeWritten);
+ flow.verifyStackEmpty();
+ flow = null;
+ }
}
@override
@@ -531,9 +1151,49 @@
}
@override
- void visitExpressionFunctionBody(ExpressionFunctionBody node) {
- super.visitExpressionFunctionBody(node);
- flow.verifyStackEmpty();
+ void visitBreakStatement(BreakStatement node) {
+ var target = _getLabelTarget(node, node.label?.staticElement);
+ flow.handleBreak(target);
+ super.visitBreakStatement(node);
+ }
+
+ @override
+ void visitConditionalExpression(ConditionalExpression node) {
+ var condition = node.condition;
+ var thenExpression = node.thenExpression;
+ var elseExpression = node.elseExpression;
+
+ condition.accept(this);
+
+ flow.conditional_thenBegin(node);
+ thenExpression.accept(this);
+ var isBool = thenExpression.staticType.isDartCoreBool;
+
+ flow.conditional_elseBegin(node, isBool);
+ elseExpression.accept(this);
+
+ flow.conditional_end(node, isBool);
+ }
+
+ @override
+ void visitContinueStatement(ContinueStatement node) {
+ var target = _getLabelTarget(node, node.label?.staticElement);
+ flow.handleContinue(target);
+ super.visitContinueStatement(node);
+ }
+
+ @override
+ void visitDoStatement(DoStatement node) {
+ var body = node.body;
+ var condition = node.condition;
+
+ flow.doStatement_bodyBegin(node, loopAssignedVariables[node]);
+ body.accept(this);
+
+ flow.doStatement_conditionBegin();
+ condition.accept(this);
+
+ flow.doStatement_end(node);
}
@override
@@ -563,7 +1223,7 @@
if (expression is SimpleIdentifier) {
var element = expression.staticElement;
- if (element is LocalElement) {
+ if (element is VariableElement) {
flow.isExpression_end(node, element, typeAnnotation.type);
}
}
@@ -598,7 +1258,7 @@
flow.read(element);
}
- var promotedType = flow.promotedType(element);
+ var promotedType = flow?.promotedType(element);
if (promotedType != null) {
promotedTypes[node] = promotedType;
}
@@ -626,6 +1286,54 @@
super.visitVariableDeclarationStatement(node);
}
+ @override
+ void visitWhileStatement(WhileStatement node) {
+ var condition = node.condition;
+ var body = node.body;
+
+ flow.whileStatement_conditionBegin(node, loopAssignedVariables[node]);
+ condition.accept(this);
+
+ flow.whileStatement_bodyBegin(node);
+ body.accept(this);
+
+ flow.whileStatement_end();
+ }
+
+ /// This code has OK performance for tests, but think if there is something
+ /// better when using in production.
+ AstNode _getLabelTarget(AstNode node, LabelElement element) {
+ for (; node != null; node = node.parent) {
+ if (node is DoStatement ||
+ node is ForEachStatement ||
+ node is ForStatement ||
+ node is SwitchStatement ||
+ node is WhileStatement) {
+ if (element == null) {
+ return node;
+ }
+ var parent = node.parent;
+ if (parent is LabeledStatement) {
+ for (var nodeLabel in parent.labels) {
+ if (identical(nodeLabel.label.staticElement, element)) {
+ return node;
+ }
+ }
+ }
+ }
+ if (element != null && node is SwitchStatement) {
+ for (var member in node.members) {
+ for (var nodeLabel in member.labels) {
+ if (identical(nodeLabel.label.staticElement, element)) {
+ return node;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
static bool _isFalseLiteral(AstNode node) {
return node is BooleanLiteral && !node.value;
}
@@ -634,3 +1342,37 @@
return node is BooleanLiteral && node.value;
}
}
+
+class _LoopAssignedVariablesVisitor extends RecursiveAstVisitor<void> {
+ final LoopAssignedVariables loopAssignedVariables;
+
+ _LoopAssignedVariablesVisitor(this.loopAssignedVariables);
+
+ @override
+ void visitAssignmentExpression(AssignmentExpression node) {
+ var left = node.leftHandSide;
+
+ super.visitAssignmentExpression(node);
+
+ if (left is SimpleIdentifier) {
+ var element = left.staticElement;
+ if (element is VariableElement) {
+ loopAssignedVariables.write(element);
+ }
+ }
+ }
+
+ @override
+ void visitDoStatement(DoStatement node) {
+ loopAssignedVariables.beginLoop();
+ super.visitDoStatement(node);
+ loopAssignedVariables.endLoop(node);
+ }
+
+ @override
+ void visitWhileStatement(WhileStatement node) {
+ loopAssignedVariables.beginLoop();
+ super.visitWhileStatement(node);
+ loopAssignedVariables.endLoop(node);
+ }
+}