Change representation of ExpressionChecks.

Previously, this class held on to a ConstraintVariable indicating
whether a null check should be inserted.  Now it holds on to
NullabilityNodes indicating whether the source and destination of an
assignment are nullable, and it can infer from this whether a null
check needs to be inserted.

This paves the way for removing the old constraint solving logic in
favor of a more direct nullability propagation approach using
nullability nodes.

Change-Id: I96b95c400ed40bd12f9c9dcbb4c09a69849e51c5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/100885
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart b/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart
index a723c9a..7127e82 100644
--- a/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart
+++ b/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart
@@ -432,7 +432,10 @@
     if (expression != null) {
       checkNotNull = CheckExpression(expression);
       _variables.recordExpressionChecks(
-          _source, expression, ExpressionChecks(checkNotNull));
+          _source,
+          expression,
+          ExpressionChecks(
+              expression.end, sourceType.node, destinationType.node, _guards));
     }
     NullabilityNode.recordAssignment(sourceType.node, destinationType.node,
         checkNotNull, _guards, _constraints, _inConditionalControlFlow);
diff --git a/pkg/analysis_server/lib/src/nullability/expression_checks.dart b/pkg/analysis_server/lib/src/nullability/expression_checks.dart
index 14f47db..bc533d6 100644
--- a/pkg/analysis_server/lib/src/nullability/expression_checks.dart
+++ b/pkg/analysis_server/lib/src/nullability/expression_checks.dart
@@ -2,6 +2,7 @@
 // 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:analysis_server/src/nullability/nullability_node.dart';
 import 'package:analysis_server/src/nullability/transitional_api.dart';
 import 'package:analyzer_plugin/protocol/protocol_common.dart';
 
@@ -13,16 +14,34 @@
 /// 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>.
 class ExpressionChecks extends PotentialModification {
-  /// Constraint variable whose value will be `true` if this expression requires
-  /// a null check.
-  final CheckExpression nullCheck;
+  /// Source offset where a trailing `!` might need to be inserted.
+  final int offset;
 
-  ExpressionChecks(this.nullCheck);
+  /// Nullability node indicating whether the expression's value is nullable.
+  final NullabilityNode valueNode;
+
+  /// 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();
 
   @override
-  bool get isEmpty => !nullCheck.value;
+  bool get isEmpty {
+    for (var guard in guards) {
+      if (!guard.isNullable) return true;
+    }
+    return !valueNode.isNullable || contextNode.isNullable;
+  }
 
   @override
   Iterable<SourceEdit> get modifications =>
-      nullCheck.value ? [SourceEdit(nullCheck.offset, 0, '!')] : [];
+      isEmpty ? [] : [SourceEdit(offset, 0, '!')];
 }
diff --git a/pkg/analysis_server/lib/src/nullability/nullability_node.dart b/pkg/analysis_server/lib/src/nullability/nullability_node.dart
index fd7d82b..f993264 100644
--- a/pkg/analysis_server/lib/src/nullability/nullability_node.dart
+++ b/pkg/analysis_server/lib/src/nullability/nullability_node.dart
@@ -69,10 +69,10 @@
   /// the new nullability node behave consistently with the old nodes.
   /// TODO(paulberry): this should become unnecessary once constraint solving is
   /// performed directly using [NullabilityNode] objects.
-  NullabilityNode.forSubstitution(Constraints constraints,
-      NullabilityNode innerNode, NullabilityNode outerNode)
-      : this._(ConstraintVariable.or(
-            constraints, innerNode?.nullable, outerNode.nullable));
+  factory NullabilityNode.forSubstitution(
+      Constraints constraints,
+      NullabilityNode innerNode,
+      NullabilityNode outerNode) = NullabilityNodeForSubstitution._;
 
   /// Creates a [NullabilityNode] representing the nullability of a type
   /// annotation appearing explicitly in the user's program.
@@ -90,7 +90,7 @@
 
   /// After constraint solving, this getter can be used to query whether the
   /// type associated with this node should be considered nullable.
-  bool get isNullable => nullable.value;
+  bool get isNullable => nullable == null ? false : nullable.value;
 
   /// Indicates whether this node is associated with a named parameter for which
   /// nullability migration needs to decide whether it is optional or required.
@@ -199,3 +199,26 @@
     constraints.record(conditions, consequence);
   }
 }
+
+/// Derived class for nullability nodes that arise from type variable
+/// substitution.
+class NullabilityNodeForSubstitution extends NullabilityNode {
+  /// Nullability node representing the inner type of the substitution.
+  ///
+  /// For example, if this NullabilityNode arose from substituting `int*` for
+  /// `T` in the type `T*`, [innerNode] is the nullability corresponding to the
+  /// `*` in `int*`.
+  final NullabilityNode innerNode;
+
+  /// Nullability node representing the outer type of the substitution.
+  ///
+  /// For example, if this NullabilityNode arose from substituting `int*` for
+  /// `T` in the type `T*`, [innerNode] is the nullability corresponding to the
+  /// `*` in `T*`.
+  final NullabilityNode outerNode;
+
+  NullabilityNodeForSubstitution._(
+      Constraints constraints, this.innerNode, this.outerNode)
+      : super._(ConstraintVariable.or(
+            constraints, innerNode?.nullable, outerNode.nullable));
+}
diff --git a/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart b/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
index 46f94cb..315cea2 100644
--- a/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
+++ b/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
@@ -65,32 +65,17 @@
   /// 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.  [genericNode], if
-  /// present, is the node representing the nullability of a type variables,
-  /// where the assignment is in a context that makes contravariant use of the
-  /// type variable.
+  /// checking that the corresponding values are non-null.
   void assertNullCheck(
       ExpressionChecks expressionChecks, NullabilityNode valueNode,
-      {NullabilityNode contextNode,
-      List<NullabilityNode> guards = const [],
-      NullabilityNode genericNode}) {
-    var conditions = <ConstraintVariable>[];
-    for (var guard in guards) {
-      conditions.add(guard.nullable);
-    }
-    conditions.add(valueNode.nullable);
-    var consequences = <ConstraintVariable>[];
-    if (genericNode != null) {
-      consequences.add(genericNode.nullable);
-    }
-    if (contextNode != null) {
-      consequences.add(contextNode.nullable);
-    }
-    consequences.add(expressionChecks.nullCheck);
-    var consequence = consequences.skip(1).fold(consequences.first, _either);
+      {NullabilityNode contextNode, List<NullabilityNode> guards = const []}) {
+    expect(expressionChecks.valueNode, same(valueNode));
     if (contextNode == null) {
-      assertConstraint(conditions, consequence);
+      expect(expressionChecks.contextNode, same(NullabilityNode.never));
+    } else {
+      expect(expressionChecks.contextNode, same(contextNode));
     }
+    expect(expressionChecks.guards, guards);
   }
 
   /// Gets the [ExpressionChecks] associated with the expression whose text
@@ -636,8 +621,13 @@
     var nullable_i = decoratedTypeAnnotation('int i').node;
     var nullable_c_t = decoratedTypeAnnotation('C<int>').typeArguments[0].node;
     var nullable_t = decoratedTypeAnnotation('T t').node;
-    assertNullCheck(checkExpression('i/*check*/'), nullable_i,
-        genericNode: nullable_c_t, contextNode: nullable_t);
+    var check_i = checkExpression('i/*check*/');
+    var nullable_c_t_or_nullable_t =
+        check_i.contextNode 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);
   }
 
   test_methodInvocation_parameter_generic() async {