Add new nullability propagation logic based on the NullabilityGraph.

For now, we verify that the new logic produces the same results as the
old logic.  In follow-up CLs, the old nullability propagation logic
will be removed.

Change-Id: I2004067093e1f5a5cdf51786ed409058707c4a49
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/102384
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/analysis_server/lib/src/nullability/nullability_graph.dart b/pkg/analysis_server/lib/src/nullability/nullability_graph.dart
index e36e92f..598dff1 100644
--- a/pkg/analysis_server/lib/src/nullability/nullability_graph.dart
+++ b/pkg/analysis_server/lib/src/nullability/nullability_graph.dart
@@ -27,6 +27,26 @@
   final _unconditionalUpstream =
       Map<NullabilityNode, List<NullabilityNode>>.identity();
 
+  final _nullableNodes = Set<NullabilityNode>.identity();
+
+  final _nonNullIntentNodes = Set<NullabilityNode>.identity();
+
+  /// Verifies that the conclusions reached by [propagate] match the conclusions
+  /// reached by the old nullability propagation algorithm (using constraint
+  /// variables).
+  void check() {
+    try {
+      var allNodes = _downstream.keys.toSet();
+      allNodes.addAll(_upstream.keys);
+      for (var node in allNodes) {
+        node.check(_nullableNodes.contains(node));
+      }
+    } catch (_) {
+      debugDump();
+      rethrow;
+    }
+  }
+
   /// Records that [sourceNode] is immediately upstream from [destinationNode].
   void connect(NullabilityNode sourceNode, NullabilityNode destinationNode,
       {bool unconditional: false, List<NullabilityNode> guards: const []}) {
@@ -55,7 +75,15 @@
         var suffix = suffixes.isNotEmpty ? ' (${suffixes.join(', ')})' : '';
         return '${edge.destinationNode}$suffix';
       });
-      print('${entry.key} -> ${destinations.join(', ')}');
+      var suffixes = <String>[];
+      if (_nullableNodes.contains(entry.key)) {
+        suffixes.add('nullable');
+      }
+      if (_nonNullIntentNodes.contains(entry.key)) {
+        suffixes.add('non-null intent');
+      }
+      var suffix = suffixes.isNotEmpty ? ' (${suffixes.join(', ')})' : '';
+      print('${entry.key}$suffix -> ${destinations.join(', ')}');
     }
   }
 
@@ -80,10 +108,77 @@
   /// Iterates through all nodes that are "upstream" of [node] (i.e. if
   /// any of the iterated nodes are nullable, then [node] will either have to be
   /// nullable, or null checks will have to be added).
-  //  ///
-  //  /// There is no guarantee of uniqueness of the iterated nodes.
+  ///
+  /// There is no guarantee of uniqueness of the iterated nodes.
   Iterable<NullabilityNode> getUpstreamNodes(NullabilityNode node) =>
       _upstream[node] ?? const [];
+
+  /// Determines the nullability of each node in the graph by propagating
+  /// nullability information from one node to another.
+  void propagate() {
+    _propagateUpstream();
+    _propagateDownstream();
+  }
+
+  /// Propagates nullability downstream.
+  void _propagateDownstream() {
+    var pendingEdges = [_NullabilityEdge(NullabilityNode.always, const [])];
+    var pendingSubstitutions = <NullabilityNodeForSubstitution>[];
+    while (true) {
+      nextEdge:
+      while (pendingEdges.isNotEmpty) {
+        var edge = pendingEdges.removeLast();
+        var node = edge.destinationNode;
+        if (_nonNullIntentNodes.contains(node)) {
+          // Non-null intent nodes are never made nullable; a null check will need
+          // to be added instead.
+          continue;
+        }
+        for (var source in edge.sources) {
+          if (!_nullableNodes.contains(source)) {
+            // Note all sources are nullable, so this edge doesn't apply yet.
+            continue nextEdge;
+          }
+        }
+        if (_nullableNodes.add(node)) {
+          // Was not previously nullable, so we need to propagate.
+          pendingEdges.addAll(_downstream[node] ?? const []);
+          if (node is NullabilityNodeForSubstitution) {
+            pendingSubstitutions.add(node);
+          }
+        }
+      }
+      if (pendingSubstitutions.isEmpty) break;
+      var node = pendingSubstitutions.removeLast();
+      if (_nullableNodes.contains(node.innerNode) ||
+          _nullableNodes.contains(node.outerNode)) {
+        // No further propagation is needed, since some other connection already
+        // propagated nullability to either the inner or outer node.
+        continue;
+      }
+      // Heuristically choose to propagate to the inner node since this seems
+      // to lead to better quality migrations.
+      pendingEdges.add(_NullabilityEdge(node.innerNode, const []));
+    }
+  }
+
+  /// Propagates non-null intent upstream along unconditional control flow
+  /// lines.
+  void _propagateUpstream() {
+    var pendingNodes = [NullabilityNode.never];
+    while (pendingNodes.isNotEmpty) {
+      var node = pendingNodes.removeLast();
+      if (node == NullabilityNode.always) {
+        // The "always" node cannot have non-null intent.
+        continue;
+      }
+      if (_nonNullIntentNodes.add(node)) {
+        // Was not previously in the set of non-null intent nodes, so we need to
+        // propagate.
+        pendingNodes.addAll(getUnconditionalUpstreamNodes(node));
+      }
+    }
+  }
 }
 
 /// Data structure to keep track of the relationship from one [NullabilityNode]
diff --git a/pkg/analysis_server/lib/src/nullability/nullability_node.dart b/pkg/analysis_server/lib/src/nullability/nullability_node.dart
index dbfaceb..b0d963f 100644
--- a/pkg/analysis_server/lib/src/nullability/nullability_node.dart
+++ b/pkg/analysis_server/lib/src/nullability/nullability_node.dart
@@ -116,6 +116,15 @@
 
   String get _debugPrefix;
 
+  /// Verifies that the nullability of this node matches [isNullable].
+  void check(bool isNullable) {
+    if (isNullable != this.isNullable) {
+      throw new StateError(
+          'For $this, new algorithm gives nullability $isNullable; '
+          'old algorithm gives ${this.isNullable}');
+    }
+  }
+
   /// Records the fact that an invocation was made to a function with named
   /// parameters, and the named parameter associated with this node was not
   /// supplied.
diff --git a/pkg/analysis_server/lib/src/nullability/transitional_api.dart b/pkg/analysis_server/lib/src/nullability/transitional_api.dart
index 90c0fc7..ab436bd 100644
--- a/pkg/analysis_server/lib/src/nullability/transitional_api.dart
+++ b/pkg/analysis_server/lib/src/nullability/transitional_api.dart
@@ -170,6 +170,8 @@
 
   Map<Source, List<PotentialModification>> finish() {
     _constraints.applyHeuristics();
+    _graph.propagate();
+    _graph.check();
     return _variables.getPotentialModifications();
   }