Migration: Clean up handling of LUB and substitution nodes.

Previously we created artifical edges from a LUB node's components to
the LUB node; now the nullability propagation algorithm understands
how to propagate nullability downstream through LUB and substitution
nodes, so no artificial edges are needed.

Change-Id: Ib2fef14c3dcd58a013ad8e057fd55597a1deb4fc
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/105402
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/nnbd_migration/lib/src/graph_builder.dart b/pkg/nnbd_migration/lib/src/graph_builder.dart
index 3610a6f..61c5623 100644
--- a/pkg/nnbd_migration/lib/src/graph_builder.dart
+++ b/pkg/nnbd_migration/lib/src/graph_builder.dart
@@ -235,7 +235,7 @@
     var elseType = node.elseExpression.accept(this);
     assert(_isSimple(elseType)); // TODO(paulberry)
     var overallType = DecoratedType(node.staticType,
-        NullabilityNode.forLUB(node, thenType.node, elseType.node, _graph));
+        NullabilityNode.forLUB(node, thenType.node, elseType.node));
     _variables.recordDecoratedExpressionType(node, overallType);
     return overallType;
   }
@@ -616,6 +616,7 @@
   bool _isSimple(DecoratedType type) {
     if (type.type.isBottom) return true;
     if (type.type.isVoid) return true;
+    if (type.type is TypeParameterType) return true;
     if (type.type is! InterfaceType) return false;
     if ((type.type as InterfaceType).typeParameters.isNotEmpty) return false;
     return true;
diff --git a/pkg/nnbd_migration/lib/src/nullability_node.dart b/pkg/nnbd_migration/lib/src/nullability_node.dart
index 453887d..6e4336e 100644
--- a/pkg/nnbd_migration/lib/src/nullability_node.dart
+++ b/pkg/nnbd_migration/lib/src/nullability_node.dart
@@ -60,16 +60,7 @@
     var sources = [sourceNode]..addAll(guards);
     var edge = NullabilityEdge(destinationNode, sources, hard);
     for (var source in sources) {
-      _allSourceNodes.add(source);
-      if (source is NullabilityNodeMutable) {
-        source._downstreamEdges.add(edge);
-      } else if (source == NullabilityNode.always) {
-        _downstreamFromAlways.add(edge);
-      } else {
-        // We don't need to track nodes that are downstream from `never` because
-        // `never` will never be nullable.
-        assert(source == NullabilityNode.never);
-      }
+      _connectDownstream(source, edge);
     }
     if (destinationNode is NullabilityNodeMutable) {
       destinationNode._upstreamEdges.add(edge);
@@ -144,6 +135,24 @@
     _propagateDownstream();
   }
 
+  void _connectDownstream(NullabilityNode source, NullabilityEdge edge) {
+    _allSourceNodes.add(source);
+    if (source is NullabilityNodeMutable) {
+      source._downstreamEdges.add(edge);
+      if (source is _NullabilityNodeCompound) {
+        for (var component in source._components) {
+          _connectDownstream(component, edge);
+        }
+      }
+    } else if (source == NullabilityNode.always) {
+      _downstreamFromAlways.add(edge);
+    } else {
+      // We don't need to track nodes that are downstream from `never` because
+      // `never` will never be nullable.
+      assert(source == NullabilityNode.never);
+    }
+  }
+
   Iterable<NullabilityEdge> _getDownstreamEdges(NullabilityNode node) {
     if (node is NullabilityNodeMutable) {
       return node._downstreamEdges;
@@ -176,7 +185,7 @@
         }
         for (var source in edge.sources) {
           if (!source.isNullable) {
-            // Note all sources are nullable, so this edge doesn't apply yet.
+            // Not all sources are nullable, so this edge doesn't apply yet.
             continue nextEdge;
           }
         }
@@ -264,11 +273,8 @@
   /// [joinNullabilities] callback.  TODO(paulberry): this should become
   /// unnecessary once constraint solving is performed directly using
   /// [NullabilityNode] objects.
-  factory NullabilityNode.forLUB(
-      Expression conditionalExpression,
-      NullabilityNode a,
-      NullabilityNode b,
-      NullabilityGraph graph) = NullabilityNodeForLUB._;
+  factory NullabilityNode.forLUB(Expression conditionalExpression,
+      NullabilityNode a, NullabilityNode b) = NullabilityNodeForLUB._;
 
   /// Creates a [NullabilityNode] representing the nullability of a type
   /// substitution where [outerNode] is the nullability node for the type
@@ -372,17 +378,15 @@
 
 /// Derived class for nullability nodes that arise from the least-upper-bound
 /// implied by a conditional expression.
-class NullabilityNodeForLUB extends NullabilityNodeMutable {
+class NullabilityNodeForLUB extends _NullabilityNodeCompound {
   final NullabilityNode left;
 
   final NullabilityNode right;
 
-  NullabilityNodeForLUB._(
-      Expression expression, this.left, this.right, NullabilityGraph graph)
-      : super._() {
-    graph.connect(left, this);
-    graph.connect(right, this);
-  }
+  NullabilityNodeForLUB._(Expression expression, this.left, this.right);
+
+  @override
+  Iterable<NullabilityNode> get _components => [left, right];
 
   @override
   String get _debugPrefix => 'LUB($left, $right)';
@@ -390,7 +394,7 @@
 
 /// Derived class for nullability nodes that arise from type variable
 /// substitution.
-class NullabilityNodeForSubstitution extends NullabilityNodeMutable {
+class NullabilityNodeForSubstitution extends _NullabilityNodeCompound {
   /// Nullability node representing the inner type of the substitution.
   ///
   /// For example, if this NullabilityNode arose from substituting `int*` for
@@ -405,7 +409,10 @@
   /// `*` in `T*`.
   final NullabilityNode outerNode;
 
-  NullabilityNodeForSubstitution._(this.innerNode, this.outerNode) : super._();
+  NullabilityNodeForSubstitution._(this.innerNode, this.outerNode);
+
+  @override
+  Iterable<NullabilityNode> get _components => [innerNode, outerNode];
 
   @override
   String get _debugPrefix => 'Substituted($innerNode, $outerNode)';
@@ -439,6 +446,15 @@
   bool get isNullable => _state.isNullable;
 }
 
+abstract class _NullabilityNodeCompound extends NullabilityNodeMutable {
+  _NullabilityNodeCompound() : super._();
+
+  @override
+  bool get isNullable => _components.any((c) => c.isNullable);
+
+  Iterable<NullabilityNode> get _components;
+}
+
 class _NullabilityNodeImmutable extends NullabilityNode {
   @override
   final String _debugPrefix;
diff --git a/pkg/nnbd_migration/test/api_test.dart b/pkg/nnbd_migration/test/api_test.dart
index 1419873..cf2d0d1 100644
--- a/pkg/nnbd_migration/test/api_test.dart
+++ b/pkg/nnbd_migration/test/api_test.dart
@@ -316,7 +316,7 @@
     await _checkSingleFileChanges(content, expected);
   }
 
-  test_data_flow_generic_inward() async {
+  test_data_flow_generic_contravariant_inward() async {
     var content = '''
 class C<T> {
   void f(T t) {}
@@ -354,6 +354,38 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  test_data_flow_generic_covariant_outward() async {
+    var content = '''
+class C<T> {
+  T getValue() => null;
+}
+int f(C<int> x) => x.getValue();
+''';
+    var expected = '''
+class C<T> {
+  T? getValue() => null;
+}
+int? f(C<int> x) => x.getValue();
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
+  test_data_flow_generic_covariant_substituted() async {
+    var content = '''
+abstract class C<T> {
+  T getValue();
+}
+int f(C<int/*?*/> x) => x.getValue();
+''';
+    var expected = '''
+abstract class C<T> {
+  T getValue();
+}
+int? f(C<int?/*?*/> x) => x.getValue();
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   test_data_flow_indexed_get_index_value() async {
     var content = '''
 class C {
diff --git a/pkg/nnbd_migration/test/migration_visitor_test.dart b/pkg/nnbd_migration/test/migration_visitor_test.dart
index 1660bb8..8ff7c98 100644
--- a/pkg/nnbd_migration/test/migration_visitor_test.dart
+++ b/pkg/nnbd_migration/test/migration_visitor_test.dart
@@ -1048,6 +1048,19 @@
         contextNode: decoratedTypeAnnotation('int').node);
   }
 
+  test_return_null_generic() async {
+    await analyze('''
+class C<T> {
+  T f() {
+    return null;
+  }
+}
+''');
+    var tNode = decoratedTypeAnnotation('T f').node;
+    assertEdge(always, tNode, hard: false);
+    assertNullCheck(checkExpression('null'), always, contextNode: tNode);
+  }
+
   test_simpleIdentifier_local() async {
     await analyze('''
 main() {