Migration: prevent AngularDart view/content fields from being marked late.

Change-Id: I525a160c90f2bff8851c175ff34edf684538a927
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/230301
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/nnbd_migration/lib/src/node_builder.dart b/pkg/nnbd_migration/lib/src/node_builder.dart
index 2dad8d1..0f1e8f4 100644
--- a/pkg/nnbd_migration/lib/src/node_builder.dart
+++ b/pkg/nnbd_migration/lib/src/node_builder.dart
@@ -476,7 +476,7 @@
         var type = decoratedType.positionalParameters![0];
         _variables!.recordDecoratedElementType(declaredElement.variable, type,
             soft: true);
-        if (_hasAngularChildAnnotation(node.metadata)) {
+        if (_getAngularAnnotation(node.metadata) == _AngularAnnotation.child) {
           _graph.makeNullable(
               type!.node!, AngularAnnotationOrigin(source, node));
         }
@@ -686,9 +686,18 @@
       _variables!.recordDecoratedElementType(declaredElement, type);
       variable.initializer?.accept(this);
       if (parent is FieldDeclaration) {
-        if (_hasAngularChildAnnotation(parent.metadata)) {
-          _graph.makeNullable(
-              type.node!, AngularAnnotationOrigin(source, node));
+        var angularAnnotation = _getAngularAnnotation(parent.metadata);
+        if (angularAnnotation != null) {
+          switch (angularAnnotation) {
+            case _AngularAnnotation.child:
+              _graph.makeNullable(
+                  type.node!, AngularAnnotationOrigin(source, node));
+              break;
+            case _AngularAnnotation.children:
+              _graph.preventLate(
+                  type.node!, AngularAnnotationOrigin(source, node));
+              break;
+          }
         }
       }
     }
@@ -712,6 +721,26 @@
     );
   }
 
+  /// Determines if the given [metadata] contains a reference to one of the
+  /// Angular annotations that we have special behaviors for.  If it does,
+  /// returns an enumerated value describing the type of annotation.
+  _AngularAnnotation? _getAngularAnnotation(NodeList<Annotation> metadata) {
+    for (var annotation in metadata) {
+      var element = annotation.element;
+      if (element is ConstructorElement) {
+        var name = element.enclosingElement.name;
+        if (_isAngularUri(element.librarySource.uri)) {
+          if (name == 'ViewChild' || name == 'ContentChild') {
+            return _AngularAnnotation.child;
+          } else if (name == 'ViewChildren' || name == 'ContentChildren') {
+            return _AngularAnnotation.children;
+          }
+        }
+      }
+    }
+    return null;
+  }
+
   /// Common handling of function and method declarations.
   DecoratedType _handleExecutableDeclaration(
       AstNode node,
@@ -938,23 +967,6 @@
         .recordDecoratedDirectSupertypes(declaredElement, decoratedSupertypes);
   }
 
-  /// Determines if the given [metadata] contains a reference to one of the
-  /// Angular annotations `ViewChild` or `ContentChild`, either of which implies
-  /// nullability of the underlying property.
-  bool _hasAngularChildAnnotation(NodeList<Annotation> metadata) {
-    for (var annotation in metadata) {
-      var element = annotation.element;
-      if (element is ConstructorElement) {
-        var name = element.enclosingElement.name;
-        if ((name == 'ViewChild' || name == 'ContentChild') &&
-            _isAngularUri(element.librarySource.uri)) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
   /// Determines whether the given [uri] comes from the Angular package.
   bool _isAngularUri(Uri uri) {
     if (uri.scheme != 'package') return false;
@@ -993,3 +1005,15 @@
     throw UnimplementedError(buffer.toString());
   }
 }
+
+/// Enum describing the kinds of annotations supplied by the angular package for
+/// which we have special migration behaviors.
+enum _AngularAnnotation {
+  /// Either the `@ViewChild` or `@ContentChild` annotation.  Fields with these
+  /// annotations should always be nullable and should never be late.
+  child,
+
+  /// Either the `@ViewChildren` or `@ContentChildren` annotation.  Fields with
+  /// these annotations should never be late.
+  children,
+}
diff --git a/pkg/nnbd_migration/lib/src/nullability_node.dart b/pkg/nnbd_migration/lib/src/nullability_node.dart
index 8030833..f730d53 100644
--- a/pkg/nnbd_migration/lib/src/nullability_node.dart
+++ b/pkg/nnbd_migration/lib/src/nullability_node.dart
@@ -337,6 +337,12 @@
     _pathsBeingMigrated.add(source.fullName);
   }
 
+  /// Creates a graph edge that will try to prevent the given [node] from being
+  /// marked `late`.
+  void preventLate(NullabilityNode node, EdgeOrigin origin) {
+    connect(node, always, origin);
+  }
+
   /// Determines the nullability of each node in the graph by propagating
   /// nullability information from one node to another.
   PropagationResult propagate() {
diff --git a/pkg/nnbd_migration/test/abstract_context.dart b/pkg/nnbd_migration/test/abstract_context.dart
index 3d5267f..51c7632 100644
--- a/pkg/nnbd_migration/test/abstract_context.dart
+++ b/pkg/nnbd_migration/test/abstract_context.dart
@@ -60,12 +60,18 @@
 class ContentChild {
   const ContentChild(Object selector, {Object? read});
 }
+class ContentChildren {
+  const ContentChildren(Object selector, {Object? read});
+}
 class Optional {
   const Optional();
 }
 class ViewChild {
   const ViewChild(Object selector, {Object? read});
 }
+class ViewChildren {
+  const ViewChildren(Object selector, {Object? read});
+}
 ''');
     if (internalUris) {
       addPackageFile('angular', 'angular.dart', '''
diff --git a/pkg/nnbd_migration/test/api_test.dart b/pkg/nnbd_migration/test/api_test.dart
index 6e5e609..2301e5b 100644
--- a/pkg/nnbd_migration/test/api_test.dart
+++ b/pkg/nnbd_migration/test/api_test.dart
@@ -387,6 +387,88 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_angular_contentChild_field_not_late() async {
+    addAngularPackage();
+    var content = '''
+import 'dart:html';
+import 'package:angular/angular.dart';
+
+class MyComponent {
+  @ContentChild('foo')
+  Element bar;
+  Element baz;
+
+  f(Element e) {
+    bar = e;
+    baz = e;
+  }
+  g() => bar.id;
+  h() => baz.id;
+}
+''';
+    // `late` heuristics are disabled for `bar` since it's marked with
+    // `ContentChild`.  But they do apply to `baz`.
+    var expected = '''
+import 'dart:html';
+import 'package:angular/angular.dart';
+
+class MyComponent {
+  @ContentChild('foo')
+  Element? bar;
+  late Element baz;
+
+  f(Element e) {
+    bar = e;
+    baz = e;
+  }
+  g() => bar!.id;
+  h() => baz.id;
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
+  Future<void> test_angular_contentChildren_field_not_late() async {
+    addAngularPackage();
+    var content = '''
+import 'dart:html';
+import 'package:angular/angular.dart';
+
+class myComponent {
+  @ContentChildren('foo')
+  Element bar;
+  Element baz;
+
+  f(Element e) {
+    bar = e;
+    baz = e;
+  }
+  g() => bar.id;
+  h() => baz.id;
+}
+''';
+    // `late` heuristics are disabled for `bar` since it's marked with
+    // `ContentChildren`.  But they do apply to `baz`.
+    var expected = '''
+import 'dart:html';
+import 'package:angular/angular.dart';
+
+class myComponent {
+  @ContentChildren('foo')
+  Element? bar;
+  late Element baz;
+
+  f(Element e) {
+    bar = e;
+    baz = e;
+  }
+  g() => bar!.id;
+  h() => baz.id;
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   Future<void> test_angular_optional_constructor_param() async {
     addAngularPackage();
     var content = '''
@@ -508,6 +590,47 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_angular_viewChild_field_not_late() async {
+    addAngularPackage();
+    var content = '''
+import 'dart:html';
+import 'package:angular/angular.dart';
+
+class MyComponent {
+  @ViewChild('foo')
+  Element bar;
+  Element baz;
+
+  f(Element e) {
+    bar = e;
+    baz = e;
+  }
+  g() => bar.id;
+  h() => baz.id;
+}
+''';
+    // `late` heuristics are disabled for `bar` since it's marked with
+    // `ViewChild`.  But they do apply to `baz`.
+    var expected = '''
+import 'dart:html';
+import 'package:angular/angular.dart';
+
+class MyComponent {
+  @ViewChild('foo')
+  Element? bar;
+  late Element baz;
+
+  f(Element e) {
+    bar = e;
+    baz = e;
+  }
+  g() => bar!.id;
+  h() => baz.id;
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   Future<void> test_angular_viewChild_setter() async {
     addAngularPackage();
     var content = '''
@@ -531,6 +654,47 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_angular_viewChildren_field_not_late() async {
+    addAngularPackage();
+    var content = '''
+import 'dart:html';
+import 'package:angular/angular.dart';
+
+class myComponent {
+  @ViewChildren('foo')
+  Element bar;
+  Element baz;
+
+  f(Element e) {
+    bar = e;
+    baz = e;
+  }
+  g() => bar.id;
+  h() => baz.id;
+}
+''';
+    // `late` heuristics are disabled for `bar` since it's marked with
+    // `ViewChildren`.  But they do apply to `baz`.
+    var expected = '''
+import 'dart:html';
+import 'package:angular/angular.dart';
+
+class myComponent {
+  @ViewChildren('foo')
+  Element? bar;
+  late Element baz;
+
+  f(Element e) {
+    bar = e;
+    baz = e;
+  }
+  g() => bar!.id;
+  h() => baz.id;
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   Future<void> test_argumentError_checkNotNull_implies_non_null_intent() async {
     var content = '''
 void f(int i) {