Migration: rework the ExpressionChecks data structure.

We no longer need to keep pointers to the nullability nodes directly
in the ExpressionChecks data structure; we merely need to record which
edges, if unsatisfied, would require a check.

This allowed the tests using assertNullCheck to be clarified--they now
simply verify that the appropriate edge exists, and then pass that
edge to assertNullCheck to verify that the ExpressionChecks object
points to the appropriate edge.

Change-Id: Iaaee51d1f23ca6f86a2fbf0a15ded6a844b2811b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/106383
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/nnbd_migration/lib/src/expression_checks.dart b/pkg/nnbd_migration/lib/src/expression_checks.dart
index 58bae89..fd87c85 100644
--- a/pkg/nnbd_migration/lib/src/expression_checks.dart
+++ b/pkg/nnbd_migration/lib/src/expression_checks.dart
@@ -11,38 +11,51 @@
 /// set of runtime checks that might need to be performed on the value of an
 /// expression.
 ///
-/// TODO(paulberry): the only check we support now is [nullCheck], which checks
-/// that the expression is not null.  We need to add other checks, e.g. to check
-/// that a List<int?> is actually a List<int>.
+/// TODO(paulberry): we don't currently have any way of distinguishing checks
+/// based on the nullability of the type itself (which can be checked by adding
+/// a trailing `!`) from checks based on type parameters (which will have to be
+/// checked using an `as` expression).
 class ExpressionChecks extends PotentialModification implements EdgeOrigin {
   /// Source offset where a trailing `!` might need to be inserted.
   final int offset;
 
-  /// Nullability node indicating whether the expression's value is nullable.
-  final NullabilityNode valueNode;
+  /// List of all nullability edges that are related to this potential check.
+  ///
+  /// TODO(paulberry): update this data structure to keep track of all the ways
+  /// in which edges can be related to an [ExpressionChecks], including:
+  ///
+  /// - An edge which, if unsatisfied, indicates that the expression needs to be
+  ///   null-checked.
+  /// - An edge which, if unsatisfied, indicates that a type parameter of the
+  ///   expression needs to be checked for nullability (e.g. by the migration
+  ///   engine inserting a test like `as List<int>?`)
+  /// - An edge which, if unsatisfied, indicates that a return type of the
+  ///   expression needs to be checked for nullability (e.g. by the migration
+  ///   engine inserting a test like `as int Function(...)?`)
+  /// - An edge which, if unsatisfied, indicates that a parameter type of the
+  ///   expression needs to be checked for nullability (e.g. by the migration
+  ///   engine inserting a test like `as void Function(int?)?`)
+  ///
+  /// ...and so on.
+  final List<NullabilityEdge> edges = [];
 
-  /// Nullability node indicating whether the expression's context requires a
-  /// nullable value.
-  final NullabilityNode contextNode;
-
-  /// Nullability nodes guarding execution of the expression.  If any of the
-  /// nodes in this list turns out to be non-nullable, the expression is dead
-  /// code and will be removed by the migration tool.
-  final List<NullabilityNode> guards;
-
-  ExpressionChecks(this.offset, this.valueNode, this.contextNode,
-      Iterable<NullabilityNode> guards)
-      : guards = guards.toList();
+  ExpressionChecks(this.offset);
 
   @override
   bool get isEmpty {
-    for (var guard in guards) {
-      if (!guard.isNullable) return true;
+    for (var edge in edges) {
+      if (!edge.isSatisfied) return false;
     }
-    return !valueNode.isNullable || contextNode.isNullable;
+    return true;
   }
 
   @override
-  Iterable<SourceEdit> get modifications =>
-      isEmpty ? [] : [SourceEdit(offset, 0, '!')];
+  Iterable<SourceEdit> get modifications {
+    // TODO(paulberry): this assumes that the check that needs to be done is for
+    // the nullability of the type itself (in which case all we need is a simple
+    // null check).  Need to support checks that will have to be addressed by
+    // adding an `as` expression, e.g. `as List<int>?` to verify that a list is
+    // reified to contain only non-null ints.
+    return isEmpty ? [] : [SourceEdit(offset, 0, '!')];
+  }
 }
diff --git a/pkg/nnbd_migration/lib/src/graph_builder.dart b/pkg/nnbd_migration/lib/src/graph_builder.dart
index 08e7138..358601b 100644
--- a/pkg/nnbd_migration/lib/src/graph_builder.dart
+++ b/pkg/nnbd_migration/lib/src/graph_builder.dart
@@ -655,8 +655,10 @@
   void _checkAssignment(DecoratedType destinationType, DecoratedType sourceType,
       ExpressionChecks expressionChecks,
       {@required bool hard}) {
-    _graph.connect(sourceType.node, destinationType.node, expressionChecks,
+    var edge = _graph.connect(
+        sourceType.node, destinationType.node, expressionChecks,
         guards: _guards, hard: hard);
+    expressionChecks?.edges?.add(edge);
     // TODO(paulberry): generalize this.
     if ((_isSimple(sourceType) || destinationType.type.isObject) &&
         _isSimple(destinationType)) {
@@ -702,8 +704,7 @@
     }
     ExpressionChecks expressionChecks;
     if (canInsertChecks) {
-      expressionChecks = ExpressionChecks(
-          expression.end, sourceType.node, destinationType.node, _guards);
+      expressionChecks = ExpressionChecks(expression.end);
       _variables.recordExpressionChecks(_source, expression, expressionChecks);
     }
     _checkAssignment(destinationType, sourceType, expressionChecks,
diff --git a/pkg/nnbd_migration/test/migration_visitor_test.dart b/pkg/nnbd_migration/test/migration_visitor_test.dart
index c20e7f8..6633808 100644
--- a/pkg/nnbd_migration/test/migration_visitor_test.dart
+++ b/pkg/nnbd_migration/test/migration_visitor_test.dart
@@ -56,26 +56,13 @@
     }
   }
 
-  /// Verifies that a null check will occur under the proper circumstances.
+  /// Verifies that a null check will occur when the given edge is unsatisfied.
   ///
   /// [expressionChecks] is the object tracking whether or not a null check is
-  /// needed.  [valueNode] is the node representing the possibly-nullable value
-  /// that is the source of the assignment or use.  [contextNode] is the node
-  /// representing the possibly-nullable value that is the destination of the
-  /// assignment (if the value is being assigned), or `null` if the value is
-  /// being used in a circumstance where `null` is not permitted.  [guards] is
-  /// a list of nullability nodes for which there are enclosing if statements
-  /// checking that the corresponding values are non-null.
+  /// needed.
   void assertNullCheck(
-      ExpressionChecks expressionChecks, NullabilityNode valueNode,
-      {NullabilityNode contextNode, List<NullabilityNode> guards = const []}) {
-    expect(expressionChecks.valueNode, same(valueNode));
-    if (contextNode == null) {
-      expect(expressionChecks.contextNode, same(never));
-    } else {
-      expect(expressionChecks.contextNode, same(contextNode));
-    }
-    expect(expressionChecks.guards, guards);
+      ExpressionChecks expressionChecks, NullabilityEdge expectedEdge) {
+    expect(expressionChecks.edges, contains(expectedEdge));
   }
 
   /// Gets the [ExpressionChecks] associated with the expression whose text
@@ -139,8 +126,8 @@
   c.x = i;
 }
 ''');
-    assertNullCheck(
-        checkExpression('c.x'), decoratedTypeAnnotation('C c').node);
+    assertNullCheck(checkExpression('c.x'),
+        assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true));
   }
 
   test_assignmentExpression_field_target_check_cascaded() async {
@@ -152,8 +139,8 @@
   c..x = i;
 }
 ''');
-    assertNullCheck(
-        checkExpression('c..x'), decoratedTypeAnnotation('C c').node);
+    assertNullCheck(checkExpression('c..x'),
+        assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true));
   }
 
   test_assignmentExpression_indexExpression_index() async {
@@ -191,7 +178,8 @@
   c[i] = j;
 }
 ''');
-    assertNullCheck(checkExpression('c['), decoratedTypeAnnotation('C c').node);
+    assertNullCheck(checkExpression('c['),
+        assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true));
   }
 
   test_assignmentExpression_indexExpression_value() async {
@@ -268,8 +256,8 @@
   c.s = i;
 }
 ''');
-    assertNullCheck(
-        checkExpression('c.s'), decoratedTypeAnnotation('C c').node);
+    assertNullCheck(checkExpression('c.s'),
+        assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true));
   }
 
   @failingTest
@@ -311,8 +299,8 @@
 int f(int i, int j) => i + j;
 ''');
 
-    assertNullCheck(
-        checkExpression('i +'), decoratedTypeAnnotation('int i').node);
+    assertNullCheck(checkExpression('i +'),
+        assertEdge(decoratedTypeAnnotation('int i').node, never, hard: true));
   }
 
   test_binaryExpression_add_left_check_custom() async {
@@ -323,8 +311,8 @@
 Int f(Int i, Int j) => i + j;
 ''');
 
-    assertNullCheck(
-        checkExpression('i +'), decoratedTypeAnnotation('Int i').node);
+    assertNullCheck(checkExpression('i +'),
+        assertEdge(decoratedTypeAnnotation('Int i').node, never, hard: true));
   }
 
   test_binaryExpression_add_result_custom() async {
@@ -335,9 +323,11 @@
 Int f(Int i, Int j) => (i + j);
 ''');
 
-    assertNullCheck(checkExpression('(i + j)'),
-        decoratedTypeAnnotation('Int operator+').node,
-        contextNode: decoratedTypeAnnotation('Int f').node);
+    assertNullCheck(
+        checkExpression('(i + j)'),
+        assertEdge(decoratedTypeAnnotation('Int operator+').node,
+            decoratedTypeAnnotation('Int f').node,
+            hard: false));
   }
 
   test_binaryExpression_add_result_not_null() async {
@@ -353,8 +343,8 @@
 int f(int i, int j) => i + j;
 ''');
 
-    assertNullCheck(
-        checkExpression('j;'), decoratedTypeAnnotation('int j').node);
+    assertNullCheck(checkExpression('j;'),
+        assertEdge(decoratedTypeAnnotation('int j').node, never, hard: true));
   }
 
   test_binaryExpression_add_right_check_custom() async {
@@ -366,8 +356,10 @@
 ''');
 
     assertNullCheck(
-        checkExpression('j/*check*/'), decoratedTypeAnnotation('Int j').node,
-        contextNode: decoratedTypeAnnotation('Int other').node);
+        checkExpression('j/*check*/'),
+        assertEdge(decoratedTypeAnnotation('Int j').node,
+            decoratedTypeAnnotation('Int other').node,
+            hard: true));
   }
 
   test_binaryExpression_equal() async {
@@ -408,7 +400,7 @@
 
     var nullable_b = decoratedTypeAnnotation('bool b').node;
     var check_b = checkExpression('b ?');
-    assertNullCheck(check_b, nullable_b);
+    assertNullCheck(check_b, assertEdge(nullable_b, never, hard: true));
   }
 
   test_conditionalExpression_general() async {
@@ -423,8 +415,8 @@
     var nullable_conditional = decoratedExpressionType('(b ?').node;
     assertConditional(nullable_conditional, nullable_i, nullable_j);
     var nullable_return = decoratedTypeAnnotation('int f').node;
-    assertNullCheck(checkExpression('(b ? i : j)'), nullable_conditional,
-        contextNode: nullable_return);
+    assertNullCheck(checkExpression('(b ? i : j)'),
+        assertEdge(nullable_conditional, nullable_return, hard: false));
   }
 
   test_conditionalExpression_left_non_null() async {
@@ -496,8 +488,10 @@
 ''');
 
     assertNullCheck(
-        checkExpression('i/*3*/'), decoratedTypeAnnotation('int/*2*/').node,
-        contextNode: decoratedTypeAnnotation('int/*1*/').node);
+        checkExpression('i/*3*/'),
+        assertEdge(decoratedTypeAnnotation('int/*2*/').node,
+            decoratedTypeAnnotation('int/*1*/').node,
+            hard: true));
   }
 
   test_functionDeclaration_parameter_named_default_notNull() async {
@@ -585,7 +579,7 @@
     var int_1 = decoratedTypeAnnotation('int/*1*/');
     var int_2 = decoratedTypeAnnotation('int/*2*/');
     var i_3 = checkExpression('i/*3*/');
-    assertNullCheck(i_3, int_2.node, contextNode: int_1.node);
+    assertNullCheck(i_3, assertEdge(int_2.node, int_1.node, hard: true));
     assertEdge(int_2.node, int_1.node, hard: true);
   }
 
@@ -598,8 +592,8 @@
 ''');
     var nullable_i = decoratedTypeAnnotation('int i').node;
     var nullable_j = decoratedTypeAnnotation('int j').node;
-    assertNullCheck(checkExpression('j/*check*/'), nullable_j,
-        contextNode: nullable_i);
+    assertNullCheck(checkExpression('j/*check*/'),
+        assertEdge(nullable_j, nullable_i, hard: true));
   }
 
   test_functionInvocation_parameter_named_missing() async {
@@ -638,8 +632,8 @@
 }
 ''');
 
-    assertNullCheck(checkExpression('null'), always,
-        contextNode: decoratedTypeAnnotation('int').node);
+    assertNullCheck(checkExpression('null'),
+        assertEdge(always, decoratedTypeAnnotation('int').node, hard: false));
   }
 
   test_functionInvocation_return() async {
@@ -651,8 +645,10 @@
 ''');
 
     assertNullCheck(
-        checkExpression('(f())'), decoratedTypeAnnotation('int/*1*/').node,
-        contextNode: decoratedTypeAnnotation('int/*2*/').node);
+        checkExpression('(f())'),
+        assertEdge(decoratedTypeAnnotation('int/*1*/').node,
+            decoratedTypeAnnotation('int/*2*/').node,
+            hard: false));
   }
 
   test_if_condition() async {
@@ -662,8 +658,8 @@
 }
 ''');
 
-    assertNullCheck(
-        checkExpression('b) {}'), decoratedTypeAnnotation('bool b').node);
+    assertNullCheck(checkExpression('b) {}'),
+        assertEdge(decoratedTypeAnnotation('bool b').node, never, hard: true));
   }
 
   test_if_conditional_control_flow_after() async {
@@ -708,10 +704,12 @@
     var nullable_j = decoratedTypeAnnotation('int j').node;
     var nullable_k = decoratedTypeAnnotation('int k').node;
     var nullable_return = decoratedTypeAnnotation('int f').node;
-    assertNullCheck(checkExpression('j/*check*/'), nullable_j,
-        contextNode: nullable_return, guards: [nullable_i]);
-    assertNullCheck(checkExpression('k/*check*/'), nullable_k,
-        contextNode: nullable_return);
+    assertNullCheck(
+        checkExpression('j/*check*/'),
+        assertEdge(nullable_j, nullable_return,
+            guards: [nullable_i], hard: false));
+    assertNullCheck(checkExpression('k/*check*/'),
+        assertEdge(nullable_k, nullable_return, hard: false));
     var discard = statementDiscard('if (i == null)');
     expect(discard.trueGuard, same(nullable_i));
     expect(discard.falseGuard, null);
@@ -732,10 +730,10 @@
     var nullable_i = decoratedTypeAnnotation('int i').node;
     var nullable_j = decoratedTypeAnnotation('int j').node;
     var nullable_return = decoratedTypeAnnotation('int f').node;
-    assertNullCheck(checkExpression('i/*check*/'), nullable_i,
-        contextNode: nullable_return);
-    assertNullCheck(checkExpression('j/*check*/'), nullable_j,
-        contextNode: nullable_return);
+    assertNullCheck(checkExpression('i/*check*/'),
+        assertEdge(nullable_i, nullable_return, hard: false));
+    assertNullCheck(checkExpression('j/*check*/'),
+        assertEdge(nullable_j, nullable_return, hard: false));
   }
 
   test_if_without_else() async {
@@ -750,8 +748,8 @@
 
     var nullable_i = decoratedTypeAnnotation('int i').node;
     var nullable_return = decoratedTypeAnnotation('int f').node;
-    assertNullCheck(checkExpression('i/*check*/'), nullable_i,
-        contextNode: nullable_return);
+    assertNullCheck(checkExpression('i/*check*/'),
+        assertEdge(nullable_i, nullable_return, hard: false));
   }
 
   test_indexExpression_index() async {
@@ -797,7 +795,8 @@
 }
 int f(C c) => c[0];
 ''');
-    assertNullCheck(checkExpression('c['), decoratedTypeAnnotation('C c').node);
+    assertNullCheck(checkExpression('c['),
+        assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true));
   }
 
   test_indexExpression_target_check_cascaded() async {
@@ -807,8 +806,8 @@
 }
 C f(C c) => c..[0];
 ''');
-    assertNullCheck(
-        checkExpression('c..['), decoratedTypeAnnotation('C c').node);
+    assertNullCheck(checkExpression('c..['),
+        assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true));
   }
 
   test_indexExpression_target_demonstrates_non_null_intent() async {
@@ -991,11 +990,11 @@
     var nullable_t = decoratedTypeAnnotation('T t').node;
     var check_i = checkExpression('i/*check*/');
     var nullable_c_t_or_nullable_t =
-        check_i.contextNode as NullabilityNodeForSubstitution;
+        check_i.edges.single.destinationNode as NullabilityNodeForSubstitution;
     expect(nullable_c_t_or_nullable_t.innerNode, same(nullable_c_t));
     expect(nullable_c_t_or_nullable_t.outerNode, same(nullable_t));
-    assertNullCheck(check_i, nullable_i,
-        contextNode: nullable_c_t_or_nullable_t);
+    assertNullCheck(check_i,
+        assertEdge(nullable_i, nullable_c_t_or_nullable_t, hard: true));
   }
 
   test_methodInvocation_parameter_generic() async {
@@ -1010,9 +1009,11 @@
     assertEdge(decoratedTypeAnnotation('int/*3*/').node,
         decoratedTypeAnnotation('int/*1*/').node,
         hard: false);
-    assertNullCheck(checkExpression('c/*check*/'),
-        decoratedTypeAnnotation('C<int/*3*/>/*4*/').node,
-        contextNode: decoratedTypeAnnotation('C<int/*1*/>/*2*/').node);
+    assertNullCheck(
+        checkExpression('c/*check*/'),
+        assertEdge(decoratedTypeAnnotation('C<int/*3*/>/*4*/').node,
+            decoratedTypeAnnotation('C<int/*1*/>/*2*/').node,
+            hard: true));
   }
 
   test_methodInvocation_parameter_named() async {
@@ -1026,8 +1027,8 @@
 ''');
     var nullable_i = decoratedTypeAnnotation('int i').node;
     var nullable_j = decoratedTypeAnnotation('int j').node;
-    assertNullCheck(checkExpression('j/*check*/'), nullable_j,
-        contextNode: nullable_i);
+    assertNullCheck(checkExpression('j/*check*/'),
+        assertEdge(nullable_j, nullable_i, hard: true));
   }
 
   test_methodInvocation_return_type() async {
@@ -1066,8 +1067,8 @@
 }
 ''');
 
-    assertNullCheck(
-        checkExpression('c.m'), decoratedTypeAnnotation('C c').node);
+    assertNullCheck(checkExpression('c.m'),
+        assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true));
   }
 
   test_methodInvocation_target_check_cascaded() async {
@@ -1080,8 +1081,8 @@
 }
 ''');
 
-    assertNullCheck(
-        checkExpression('c..m'), decoratedTypeAnnotation('C c').node);
+    assertNullCheck(checkExpression('c..m'),
+        assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true));
   }
 
   test_methodInvocation_target_demonstrates_non_null_intent() async {
@@ -1123,8 +1124,8 @@
 }
 ''');
 
-    assertNullCheck(checkExpression('(null)'), always,
-        contextNode: decoratedTypeAnnotation('int').node);
+    assertNullCheck(checkExpression('(null)'),
+        assertEdge(always, decoratedTypeAnnotation('int').node, hard: false));
   }
 
   test_prefixedIdentifier_field_type() async {
@@ -1161,8 +1162,8 @@
 }
 ''');
 
-    assertNullCheck(
-        checkExpression('c.x'), decoratedTypeAnnotation('C c').node);
+    assertNullCheck(checkExpression('c.x'),
+        assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true));
   }
 
   test_prefixedIdentifier_target_demonstrates_non_null_intent() async {
@@ -1187,7 +1188,7 @@
 
     var nullable_b = decoratedTypeAnnotation('bool b').node;
     var check_b = checkExpression('b;');
-    assertNullCheck(check_b, nullable_b);
+    assertNullCheck(check_b, assertEdge(nullable_b, never, hard: true));
 
     var return_f = decoratedTypeAnnotation('bool f').node;
     assertEdge(never, return_f, hard: false);
@@ -1229,8 +1230,9 @@
 }
 ''');
 
-    assertNullCheck(
-        checkExpression('c).x'), decoratedTypeAnnotation('C c').node);
+    // TODO(paulberry): this is wrong.  It should be a hard edge.
+    assertNullCheck(checkExpression('c).x'),
+        assertEdge(decoratedTypeAnnotation('C c').node, never, hard: false));
   }
 
   test_return_implicit_null() async {
@@ -1251,8 +1253,8 @@
 }
 ''');
 
-    assertNullCheck(checkExpression('null'), always,
-        contextNode: decoratedTypeAnnotation('int').node);
+    assertNullCheck(checkExpression('null'),
+        assertEdge(always, decoratedTypeAnnotation('int').node, hard: false));
   }
 
   test_return_null_generic() async {
@@ -1265,7 +1267,8 @@
 ''');
     var tNode = decoratedTypeAnnotation('T f').node;
     assertEdge(always, tNode, hard: false);
-    assertNullCheck(checkExpression('null'), always, contextNode: tNode);
+    assertNullCheck(
+        checkExpression('null'), assertEdge(always, tNode, hard: false));
   }
 
   test_simpleIdentifier_local() async {
@@ -1413,8 +1416,9 @@
     return testUnit;
   }
 
-  void assertEdge(NullabilityNode source, NullabilityNode destination,
-      {@required bool hard}) {
+  NullabilityEdge assertEdge(
+      NullabilityNode source, NullabilityNode destination,
+      {@required bool hard, List<NullabilityNode> guards}) {
     var edges = getEdges(source, destination);
     if (edges.length == 0) {
       fail('Expected edge $source -> $destination, found none');
@@ -1423,6 +1427,10 @@
     } else {
       var edge = edges[0];
       expect(edge.hard, hard);
+      if (guards != null) {
+        expect(edge.guards, unorderedEquals(guards));
+      }
+      return edge;
     }
   }