DAS: Fix producers that can apply in bulk, but not really across file

This introduces CorrectionApplicability.automaticallyButOncePerFile,
used in just 4 producers:

* AddNullCheck
* MoveDocCommentToLibraryDirective
* RemoveLibraryName
* RemoveUnnecessaryLibraryDirective

Also fixes FixProcessorMapTest to actually test registered fixes.

Change-Id: I2ed2d8d6a030f04ea3fa969988719bb9d4c2f8fb
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/367661
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart b/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart
index 595f694..19a9ff2 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/abstract_producer.dart
@@ -30,9 +30,10 @@
 
 /// How broadly a [CorrectionProducer] can be applied.
 ///
-/// Each value of this enum is cumulative, as the index increases. When a
-/// correctiopn producer has a given applicability, it also can be applied with
-/// each lower-indexed value. For example, a correction producer with an
+/// Each value of this enum is cumulative, as the index increases, except for
+/// [CorrectionApplicability.automaticallyButOncePerFile]. When a correctiopn
+/// producer has a given applicability, it also can be applied with each
+/// lower-indexed value. For example, a correction producer with an
 /// applicability of [CorrectionApplicability.acrossFiles] can also be used to
 /// apply corrections across a single file
 /// ([CorrectionApplicability.singleLocation]) and at a single location
@@ -40,6 +41,9 @@
 /// reflect this property: [CorrectionProducer.canBeAppliedAcrossSingleFile],
 /// [CorrectionProducer.canBeAppliedAcrossFiles],
 /// [CorrectionProducer.canBeAppliedAutomatically].
+///
+/// Note that [CorrectionApplicability.automaticallyButOncePerFile] is the one
+/// value that does not have this cumulative property.
 enum CorrectionApplicability {
   /// Indicates a correction can be applied only at a specific location.
   ///
@@ -78,11 +82,27 @@
   /// A correction with this applicability is also applicable at a specific
   /// location, and across a file, and across multiple files.
   automatically,
+
+  /// Indicates a correction can be applied in multiple files, except only one
+  /// location per file; the correction can be applied even if not chosen
+  /// explicitly as a tool action, and can be applied to potentially incomplete
+  /// code.
+  ///
+  /// A correction with this applicability is also applicable at a specific
+  /// location, and across multiple files.
+  automaticallyButOncePerFile,
 }
 
 /// An object that can compute a correction (fix or assist) in a Dart file.
 abstract class CorrectionProducer<T extends ParsedUnitResult>
     extends _AbstractCorrectionProducer<T> {
+  /// The applicability of this producer.
+  ///
+  /// This property is to be implemented by each subclass, but outside code must
+  /// use other properties to determine a producer's applicability:
+  /// [canBeAppliedAcrossSingleFile], [canBeAppliedAcrossFiles], and
+  /// [canBeAppliedAutomatically].
+  @protected
   CorrectionApplicability get applicability;
 
   /// The arguments that should be used when composing the message for an
@@ -99,7 +119,8 @@
   /// time as applying corrections from other producers.
   bool get canBeAppliedAcrossFiles =>
       applicability == CorrectionApplicability.acrossFiles ||
-      applicability == CorrectionApplicability.automatically;
+      applicability == CorrectionApplicability.automatically ||
+      applicability == CorrectionApplicability.automaticallyButOncePerFile;
 
   /// Whether this producer can be used to apply a correction in multiple
   /// positions simultaneously across a file.
@@ -113,7 +134,8 @@
   /// in bulk across multiple files and/or at the same time as applying
   /// corrections from other producers.
   bool get canBeAppliedAutomatically =>
-      applicability == CorrectionApplicability.automatically;
+      applicability == CorrectionApplicability.automatically ||
+      applicability == CorrectionApplicability.automaticallyButOncePerFile;
 
   /// The length of the source range associated with the error message being
   /// fixed, or `null` if there is no diagnostic.
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/add_null_check.dart b/pkg/analysis_server/lib/src/services/correction/dart/add_null_check.dart
index d195b53..7f8d3d5 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/add_null_check.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/add_null_check.dart
@@ -21,6 +21,12 @@
   final bool skipAssignabilityCheck;
 
   @override
+  // This is a mutable field so it can be changed in `_replaceWithNullCheck`.
+  // TODO(srawlins): This seems to violate a few assert statements around the
+  // package that read:
+  //
+  // > Producers used in bulk fixes must not modify the FixKind during
+  // > computation.
   FixKind fixKind = DartFixKind.ADD_NULL_CHECK;
 
   @override
@@ -32,7 +38,7 @@
 
   AddNullCheck.withoutAssignabilityCheck()
       : skipAssignabilityCheck = true,
-        applicability = CorrectionApplicability.automatically;
+        applicability = CorrectionApplicability.automaticallyButOncePerFile;
 
   @override
   Future<void> compute(ChangeBuilder builder) async {
@@ -221,7 +227,7 @@
     return false;
   }
 
-  /// Replace the null aware [token] with the null check operator.
+  /// Replaces the null aware [token] with the null check operator.
   Future<void> _replaceWithNullCheck(ChangeBuilder builder, Token token) async {
     fixKind = DartFixKind.REPLACE_WITH_NULL_AWARE;
     var lexeme = token.lexeme;
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/move_doc_comment_to_library_directive.dart b/pkg/analysis_server/lib/src/services/correction/dart/move_doc_comment_to_library_directive.dart
index e4ca5f8..02e3b0c 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/move_doc_comment_to_library_directive.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/move_doc_comment_to_library_directive.dart
@@ -16,7 +16,7 @@
 class MoveDocCommentToLibraryDirective extends ResolvedCorrectionProducer {
   @override
   CorrectionApplicability get applicability =>
-      CorrectionApplicability.automatically;
+      CorrectionApplicability.automaticallyButOncePerFile;
 
   @override
   FixKind get fixKind => DartFixKind.MOVE_DOC_COMMENT_TO_LIBRARY_DIRECTIVE;
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/remove_library_name.dart b/pkg/analysis_server/lib/src/services/correction/dart/remove_library_name.dart
index cf019ac..293284d 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/remove_library_name.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/remove_library_name.dart
@@ -12,7 +12,7 @@
 class RemoveLibraryName extends ResolvedCorrectionProducer {
   @override
   CorrectionApplicability get applicability =>
-      CorrectionApplicability.automatically;
+      CorrectionApplicability.automaticallyButOncePerFile;
 
   @override
   FixKind get fixKind => DartFixKind.REMOVE_LIBRARY_NAME;
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/remove_unnecessary_library_directive.dart b/pkg/analysis_server/lib/src/services/correction/dart/remove_unnecessary_library_directive.dart
index 54b4e99..c240e07 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/remove_unnecessary_library_directive.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/remove_unnecessary_library_directive.dart
@@ -12,7 +12,7 @@
 class RemoveUnnecessaryLibraryDirective extends ResolvedCorrectionProducer {
   @override
   CorrectionApplicability get applicability =>
-      CorrectionApplicability.automatically;
+      CorrectionApplicability.automaticallyButOncePerFile;
 
   @override
   FixKind get fixKind => DartFixKind.REMOVE_UNNECESSARY_LIBRARY_DIRECTIVE;
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/replace_with_part_of_uri.dart b/pkg/analysis_server/lib/src/services/correction/dart/replace_with_part_of_uri.dart
index 1549523..ec25bdc 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/replace_with_part_of_uri.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/replace_with_part_of_uri.dart
@@ -14,9 +14,6 @@
   String _uriStr = '';
 
   @override
-  FixKind fixKind = DartFixKind.REPLACE_WITH_PART_OF_URI;
-
-  @override
   CorrectionApplicability get applicability =>
       // TODO(applicability): comment on why.
       CorrectionApplicability.singleLocation;
@@ -25,6 +22,9 @@
   List<String> get fixArguments => [_uriStr];
 
   @override
+  FixKind get fixKind => DartFixKind.REPLACE_WITH_PART_OF_URI;
+
+  @override
   Future<void> compute(ChangeBuilder builder) async {
     var partOfDirective = node;
     if (partOfDirective is! PartOfDirective) {
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index b3830f0..6d98b87 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -1337,26 +1337,6 @@
     DartFixKindPriority.IN_FILE,
     "Remove unnecessary 'const' keywords everywhere in file",
   );
-  static const REMOVE_UNNECESSARY_LATE = FixKind(
-    'dart.fix.remove.unnecessaryLate',
-    DartFixKindPriority.DEFAULT,
-    "Remove unnecessary 'late' keyword",
-  );
-  static const REMOVE_UNNECESSARY_LATE_MULTI = FixKind(
-    'dart.fix.remove.unnecessaryLate.multi',
-    DartFixKindPriority.IN_FILE,
-    "Remove unnecessary 'late' keywords everywhere in file",
-  );
-  static const REMOVE_UNNECESSARY_NEW = FixKind(
-    'dart.fix.remove.unnecessaryNew',
-    DartFixKindPriority.DEFAULT,
-    "Remove unnecessary 'new' keyword",
-  );
-  static const REMOVE_UNNECESSARY_NEW_MULTI = FixKind(
-    'dart.fix.remove.unnecessaryNew.multi',
-    DartFixKindPriority.IN_FILE,
-    "Remove unnecessary 'new' keywords everywhere in file",
-  );
   static const REMOVE_UNNECESSARY_CONTAINER = FixKind(
     'dart.fix.remove.unnecessaryContainer',
     DartFixKindPriority.DEFAULT,
@@ -1367,11 +1347,31 @@
     DartFixKindPriority.IN_FILE,
     "Remove unnecessary 'Container's in file",
   );
+  static const REMOVE_UNNECESSARY_LATE = FixKind(
+    'dart.fix.remove.unnecessaryLate',
+    DartFixKindPriority.DEFAULT,
+    "Remove unnecessary 'late' keyword",
+  );
+  static const REMOVE_UNNECESSARY_LATE_MULTI = FixKind(
+    'dart.fix.remove.unnecessaryLate.multi',
+    DartFixKindPriority.IN_FILE,
+    "Remove unnecessary 'late' keywords everywhere in file",
+  );
   static const REMOVE_UNNECESSARY_LIBRARY_DIRECTIVE = FixKind(
     'dart.fix.remove.unnecessaryLibraryDirective',
     DartFixKindPriority.DEFAULT,
     'Remove unnecessary library directive',
   );
+  static const REMOVE_UNNECESSARY_NEW = FixKind(
+    'dart.fix.remove.unnecessaryNew',
+    DartFixKindPriority.DEFAULT,
+    "Remove unnecessary 'new' keyword",
+  );
+  static const REMOVE_UNNECESSARY_NEW_MULTI = FixKind(
+    'dart.fix.remove.unnecessaryNew.multi',
+    DartFixKindPriority.IN_FILE,
+    "Remove unnecessary 'new' keywords everywhere in file",
+  );
   static const REMOVE_UNNECESSARY_PARENTHESES = FixKind(
     'dart.fix.remove.unnecessaryParentheses',
     DartFixKindPriority.DEFAULT,
diff --git a/pkg/analysis_server/test/src/services/correction/fix/fix_processor_map_test.dart b/pkg/analysis_server/test/src/services/correction/fix/fix_processor_map_test.dart
index 328da67..400b8de 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/fix_processor_map_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/fix_processor_map_test.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart';
+import 'package:analysis_server/src/services/correction/fix_internal.dart';
 import 'package:analysis_server/src/services/correction/fix_processor.dart';
 import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
@@ -23,13 +24,17 @@
     'prefer_inlined_adds',
   ];
 
+  void setUp() {
+    registerBuiltInProducers();
+  }
+
   void test_lintProducerMap() {
-    _assertMap(FixProcessor.lintProducerMap.entries,
-        lintsAllowedToHaveMultipleBulkFixes);
+    _assertMap(
+        FixProcessor.lintProducerMap, lintsAllowedToHaveMultipleBulkFixes);
   }
 
   void test_nonLintProducerMap() {
-    _assertMap(FixProcessor.nonLintProducerMap.entries);
+    _assertMap(FixProcessor.nonLintProducerMap);
   }
 
   void test_registerFixForLint() {
@@ -43,12 +48,12 @@
     FixProcessor.lintProducerMap.remove(lintName);
   }
 
-  void _assertMap<K>(Iterable<MapEntry<K, List<ProducerGenerator>>> entries,
-      [List<String> keysAllowedToHaveMultipleBulkFixes = const []]) {
-    var list = <String>[];
-    for (var entry in entries) {
+  void _assertMap(Map<Object, List<ProducerGenerator>> producerMap,
+      [List<String> codesAllowedToHaveMultipleBulkFixes = const []]) {
+    var unexpectedBulkCodes = <String>[];
+    for (var MapEntry(:key, value: generators) in producerMap.entries) {
       var bulkCount = 0;
-      for (var generator in entry.value) {
+      for (var generator in generators) {
         var producer = generator();
         _assertValidProducer(producer);
         if (producer.canBeAppliedAcrossFiles) {
@@ -56,16 +61,16 @@
         }
       }
       if (bulkCount > 1) {
-        var key = entry.key.toString();
-        if (!keysAllowedToHaveMultipleBulkFixes.contains(key)) {
-          list.add(key);
+        var name = key.toString();
+        if (!codesAllowedToHaveMultipleBulkFixes.contains(name)) {
+          unexpectedBulkCodes.add(name);
         }
       }
     }
-    if (list.isNotEmpty) {
+    if (unexpectedBulkCodes.isNotEmpty) {
       var buffer = StringBuffer();
-      buffer.writeln('Multiple bulk fixes for');
-      for (var code in list) {
+      buffer.writeln('Unexpected multiple bulk fixes for');
+      for (var code in unexpectedBulkCodes) {
         buffer.writeln('- $code');
       }
       fail(buffer.toString());
@@ -77,7 +82,7 @@
     expect(producer.fixKind, isNotNull, reason: '$className.fixKind');
     if (producer.canBeAppliedAcrossSingleFile) {
       expect(producer.multiFixKind, isNotNull,
-          reason: '$className.multiFixKind');
+          reason: '$className.multiFixKind should be non-null');
     }
   }
 }