Migrator: Accept 'late final' hint; same as 'late' hint

`late final` is not functionally different from `late`, as far
as the migrator is concerned; it is just an indication of late
in the face of missing initialization.

Fixes https://github.com/dart-lang/sdk/issues/41655

Change-Id: I1b0efd3de3b5c7064784ebd1e86c7d12436b1319
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/167203
Reviewed-by: Paul Berry <paulberry@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/nnbd_migration/lib/nnbd_migration.dart b/pkg/nnbd_migration/lib/nnbd_migration.dart
index 787b235..b222f91 100644
--- a/pkg/nnbd_migration/lib/nnbd_migration.dart
+++ b/pkg/nnbd_migration/lib/nnbd_migration.dart
@@ -33,6 +33,12 @@
       appliedMessage: 'Added a late keyword, due to assignment in `setUp`',
       kind: NullabilityFixKind.addLateDueToTestSetup);
 
+  /// A variable declaration needs to be marked as "late" and "final" due to the
+  /// presence of a `/*late final*/` hint.
+  static const addLateFinalDueToHint = NullabilityFixDescription._(
+      appliedMessage: 'Added late and final keywords, due to a hint',
+      kind: NullabilityFixKind.addLateFinalDueToHint);
+
   /// An expression's value needs to be null-checked.
   static const checkExpression = NullabilityFixDescription._(
     appliedMessage: 'Added a non-null assertion to nullable expression',
@@ -242,6 +248,7 @@
   addLate,
   addLateDueToHint,
   addLateDueToTestSetup,
+  addLateFinalDueToHint,
   addRequired,
   addType,
   checkExpression,
diff --git a/pkg/nnbd_migration/lib/src/fix_aggregator.dart b/pkg/nnbd_migration/lib/src/fix_aggregator.dart
index 47d8aa5..cc0a510 100644
--- a/pkg/nnbd_migration/lib/src/fix_aggregator.dart
+++ b/pkg/nnbd_migration/lib/src/fix_aggregator.dart
@@ -1017,9 +1017,11 @@
     innerPlans.addAll(aggregator.innerPlansForNode(node));
     var plan = aggregator.planner.passThrough(node, innerPlans: innerPlans);
     if (lateHint != null) {
+      var description = lateHint.kind == HintCommentKind.late_
+          ? NullabilityFixDescription.addLateDueToHint
+          : NullabilityFixDescription.addLateFinalDueToHint;
       plan = aggregator.planner.acceptLateHint(plan, lateHint,
-          info: AtomicEditInfo(NullabilityFixDescription.addLateDueToHint, {},
-              hintComment: lateHint));
+          info: AtomicEditInfo(description, {}, hintComment: lateHint));
     }
     return plan;
   }
diff --git a/pkg/nnbd_migration/lib/src/front_end/info_builder.dart b/pkg/nnbd_migration/lib/src/front_end/info_builder.dart
index c16f646..d8b4f69 100644
--- a/pkg/nnbd_migration/lib/src/front_end/info_builder.dart
+++ b/pkg/nnbd_migration/lib/src/front_end/info_builder.dart
@@ -144,6 +144,9 @@
       case NullabilityFixKind.addLateDueToHint:
         edits.add(_removeHint('Remove /*late*/ hint'));
         break;
+      case NullabilityFixKind.addLateFinalDueToHint:
+        edits.add(_removeHint('Remove /*late final*/ hint'));
+        break;
       case NullabilityFixKind.addRequired:
         // TODO(brianwilkerson) This doesn't verify that the meta package has
         //  been imported.
diff --git a/pkg/nnbd_migration/lib/src/front_end/migration_summary.dart b/pkg/nnbd_migration/lib/src/front_end/migration_summary.dart
index 1a90c12..bc4322f 100644
--- a/pkg/nnbd_migration/lib/src/front_end/migration_summary.dart
+++ b/pkg/nnbd_migration/lib/src/front_end/migration_summary.dart
@@ -69,6 +69,9 @@
       case NullabilityFixKind.addLateDueToTestSetup:
         return 'addLateDueToTestSetup';
         break;
+      case NullabilityFixKind.addLateFinalDueToHint:
+        return 'addLateFinalDueToHint';
+        break;
       case NullabilityFixKind.addRequired:
         return 'addRequired';
         break;
diff --git a/pkg/nnbd_migration/lib/src/front_end/unit_renderer.dart b/pkg/nnbd_migration/lib/src/front_end/unit_renderer.dart
index fbd5c8d..31e1bde 100644
--- a/pkg/nnbd_migration/lib/src/front_end/unit_renderer.dart
+++ b/pkg/nnbd_migration/lib/src/front_end/unit_renderer.dart
@@ -41,6 +41,7 @@
     NullabilityFixKind.addLate,
     NullabilityFixKind.addLateDueToTestSetup,
     NullabilityFixKind.addLateDueToHint,
+    NullabilityFixKind.addLateFinalDueToHint,
     NullabilityFixKind.checkExpressionDueToHint,
     NullabilityFixKind.makeTypeNullableDueToHint,
     NullabilityFixKind.removeLanguageVersionComment
@@ -265,6 +266,8 @@
         return '$count late hint$s converted to late keyword$s';
       case NullabilityFixKind.addLateDueToTestSetup:
         return '$count late keyword$s added, due to assignment in `setUp`';
+      case NullabilityFixKind.addLateFinalDueToHint:
+        return '$count late final hint$s converted to late and final keywords';
       case NullabilityFixKind.addRequired:
         return '$count required keyword$s added';
       case NullabilityFixKind.addType:
diff --git a/pkg/nnbd_migration/lib/src/node_builder.dart b/pkg/nnbd_migration/lib/src/node_builder.dart
index dd52503..0bdb629 100644
--- a/pkg/nnbd_migration/lib/src/node_builder.dart
+++ b/pkg/nnbd_migration/lib/src/node_builder.dart
@@ -633,6 +633,9 @@
     if (hint != null && hint.kind == HintCommentKind.late_) {
       _variables.recordLateHint(source, node, hint);
     }
+    if (hint != null && hint.kind == HintCommentKind.lateFinal) {
+      _variables.recordLateHint(source, node, hint);
+    }
     for (var variable in node.variables) {
       variable.metadata.accept(this);
       var declaredElement = variable.declaredElement;
diff --git a/pkg/nnbd_migration/lib/src/utilities/hint_utils.dart b/pkg/nnbd_migration/lib/src/utilities/hint_utils.dart
index 020cdf4..471149c 100644
--- a/pkg/nnbd_migration/lib/src/utilities/hint_utils.dart
+++ b/pkg/nnbd_migration/lib/src/utilities/hint_utils.dart
@@ -43,11 +43,11 @@
     var lexeme = commentToken.lexeme;
     if (lexeme.startsWith('/*') &&
         lexeme.endsWith('*/') &&
-        lexeme.length > 'late'.length) {
+        lexeme.length >= '/*late*/'.length) {
       var commentText =
           lexeme.substring('/*'.length, lexeme.length - '*/'.length).trim();
+      var commentOffset = commentToken.offset;
       if (commentText == 'late') {
-        var commentOffset = commentToken.offset;
         var lateOffset = commentOffset + commentToken.lexeme.indexOf('late');
         return HintComment(
             HintCommentKind.late_,
@@ -57,6 +57,16 @@
             lateOffset + 'late'.length,
             commentToken.end,
             token.offset);
+      } else if (commentText == 'late final') {
+        var lateOffset = commentOffset + commentToken.lexeme.indexOf('late');
+        return HintComment(
+            HintCommentKind.lateFinal,
+            commentOffset,
+            commentOffset,
+            lateOffset,
+            lateOffset + 'late final'.length,
+            commentToken.end,
+            token.offset);
       }
     }
   }
@@ -196,4 +206,8 @@
   /// The comment `/*late*/`, which indicates that the variable declaration
   /// should be late.
   late_,
+
+  /// The comment `/*late final*/`, which indicates that the variable
+  /// declaration should be late and final.
+  lateFinal,
 }
diff --git a/pkg/nnbd_migration/test/api_test.dart b/pkg/nnbd_migration/test/api_test.dart
index 2140235..e137b2a 100644
--- a/pkg/nnbd_migration/test/api_test.dart
+++ b/pkg/nnbd_migration/test/api_test.dart
@@ -4070,6 +4070,28 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_late_final_hint_instance_field_without_constructor() async {
+    var content = '''
+class C {
+  /*late final*/ int x;
+  f() {
+    x = 1;
+  }
+  int g() => x;
+}
+''';
+    var expected = '''
+class C {
+  late final int x;
+  f() {
+    x = 1;
+  }
+  int g() => x;
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   Future<void> test_late_hint_local_variable() async {
     var content = '''
 int f(bool b1, bool b2) {
@@ -4098,6 +4120,34 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_late_final_hint_local_variable() async {
+    var content = '''
+int f(bool b1, bool b2) {
+  /*late final*/ int x;
+  if (b1) {
+    x = 1;
+  }
+  if (b2) {
+    return x;
+  }
+  return 0;
+}
+''';
+    var expected = '''
+int f(bool b1, bool b2) {
+  late final int x;
+  if (b1) {
+    x = 1;
+  }
+  if (b2) {
+    return x;
+  }
+  return 0;
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   Future<void> test_late_hint_static_field() async {
     var content = '''
 class C {
@@ -4138,6 +4188,24 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_late_final_hint_top_level_var() async {
+    var content = '''
+/*late final*/ int x;
+f() {
+  x = 1;
+}
+int g() => x;
+''';
+    var expected = '''
+late final int x;
+f() {
+  x = 1;
+}
+int g() => x;
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   Future<void> test_leave_downcast_from_dynamic_implicit() async {
     var content = 'int f(dynamic n) => n;';
     var expected = 'int f(dynamic n) => n;';
diff --git a/pkg/nnbd_migration/test/front_end/info_builder_test.dart b/pkg/nnbd_migration/test/front_end/info_builder_test.dart
index 8d7ad3c..9fc7d93 100644
--- a/pkg/nnbd_migration/test/front_end/info_builder_test.dart
+++ b/pkg/nnbd_migration/test/front_end/info_builder_test.dart
@@ -319,6 +319,28 @@
             replacement: ''));
   }
 
+  Future<void> test_addLateFinal_dueToHint() async {
+    var content = '/*late final*/ int x = 0;';
+    var migratedContent = '/*late final*/ int  x = 0;';
+    var unit = await buildInfoForSingleTestFile(content,
+        migratedContent: migratedContent);
+    var regions = unit.fixRegions;
+    expect(regions, hasLength(2));
+    var textToRemove = '/*late final*/ ';
+    assertRegionPair(regions, 0,
+        offset1: migratedContent.indexOf('/*'),
+        length1: 2,
+        offset2: migratedContent.indexOf('*/'),
+        length2: 2,
+        explanation: 'Added late and final keywords, due to a hint',
+        kind: NullabilityFixKind.addLateFinalDueToHint,
+        edits: (List<EditDetail> edits) => assertEdit(
+            edit: edits.single,
+            offset: content.indexOf(textToRemove),
+            length: textToRemove.length,
+            replacement: ''));
+  }
+
   Future<void> test_addLate_dueToTestSetup() async {
     addTestCorePackage();
     var content = '''
diff --git a/pkg/nnbd_migration/test/front_end/unit_renderer_test.dart b/pkg/nnbd_migration/test/front_end/unit_renderer_test.dart
index 2604c89..e7f297a 100644
--- a/pkg/nnbd_migration/test/front_end/unit_renderer_test.dart
+++ b/pkg/nnbd_migration/test/front_end/unit_renderer_test.dart
@@ -151,6 +151,16 @@
         unorderedEquals(['1 late hint converted to late keyword']));
   }
 
+  Future<void> test_editList_countsHintAcceptanceSingly_lateFinal() async {
+    await buildInfoForSingleTestFile('/*late final*/ int x = 0;',
+        migratedContent: '/*late final*/ int  x = 0;');
+    var output = renderUnits()[0];
+    expect(
+        output.edits.keys,
+        unorderedEquals(
+            ['1 late final hint converted to late and final keywords']));
+  }
+
   Future<void> test_editList_pluralHeader() async {
     await buildInfoForSingleTestFile('''
 int a = null;
diff --git a/pkg/nnbd_migration/test/node_builder_test.dart b/pkg/nnbd_migration/test/node_builder_test.dart
index 24737b9..b85ac69 100644
--- a/pkg/nnbd_migration/test/node_builder_test.dart
+++ b/pkg/nnbd_migration/test/node_builder_test.dart
@@ -2054,6 +2054,14 @@
         isNotNull);
   }
 
+  Future<void> test_variableDeclaration_late_final_hint_simple() async {
+    await analyze('/*late final*/ int i;');
+    expect(
+        variables.getLateHint(
+            testSource, findNode.variableDeclarationList('int i')),
+        isNotNull);
+  }
+
   Future<void> test_variableDeclaration_late_hint_with_spaces() async {
     await analyze('/* late */ int i;');
     expect(