bulk fix support for specified diagnostics

Change-Id: Ifa98da5466b0efb1b88cfa17d06b6b4e1d4ba8d3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/255340
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
diff --git a/pkg/analysis_server/doc/api.html b/pkg/analysis_server/doc/api.html
index 7ce4cd6..7e52568 100644
--- a/pkg/analysis_server/doc/api.html
+++ b/pkg/analysis_server/doc/api.html
@@ -110,7 +110,7 @@
 <body>
 <h1>Analysis Server API Specification</h1>
 <h1 style="color:#999999">Version
-  1.33.1
+  1.33.2
 </h1>
 <p>
   This document contains a specification of the API provided by the
diff --git a/pkg/analysis_server/lib/protocol/protocol_constants.dart b/pkg/analysis_server/lib/protocol/protocol_constants.dart
index 4867f3d..086d0c8 100644
--- a/pkg/analysis_server/lib/protocol/protocol_constants.dart
+++ b/pkg/analysis_server/lib/protocol/protocol_constants.dart
@@ -8,7 +8,7 @@
 
 // ignore_for_file: constant_identifier_names
 
-const String PROTOCOL_VERSION = '1.33.1';
+const String PROTOCOL_VERSION = '1.33.2';
 
 const String ANALYSIS_NOTIFICATION_ANALYZED_FILES = 'analysis.analyzedFiles';
 const String ANALYSIS_NOTIFICATION_ANALYZED_FILES_DIRECTORIES = 'directories';
@@ -192,6 +192,7 @@
 const String DIAGNOSTIC_RESPONSE_GET_DIAGNOSTICS_CONTEXTS = 'contexts';
 const String DIAGNOSTIC_RESPONSE_GET_SERVER_PORT_PORT = 'port';
 const String EDIT_REQUEST_BULK_FIXES = 'edit.bulkFixes';
+const String EDIT_REQUEST_BULK_FIXES_CODES = 'codes';
 const String EDIT_REQUEST_BULK_FIXES_INCLUDED = 'included';
 const String EDIT_REQUEST_BULK_FIXES_IN_TEST_MODE = 'inTestMode';
 const String EDIT_REQUEST_FORMAT = 'edit.format';
diff --git a/pkg/analysis_server/lib/protocol/protocol_generated.dart b/pkg/analysis_server/lib/protocol/protocol_generated.dart
index 8b1d885..6748c45 100644
--- a/pkg/analysis_server/lib/protocol/protocol_generated.dart
+++ b/pkg/analysis_server/lib/protocol/protocol_generated.dart
@@ -5982,6 +5982,7 @@
 /// {
 ///   "included": List<FilePath>
 ///   "inTestMode": optional bool
+///   "codes": optional List<String>
 /// }
 ///
 /// Clients may not extend, implement or mix-in this class.
@@ -6004,7 +6005,10 @@
   /// If this field is omitted the flag defaults to false.
   bool? inTestMode;
 
-  EditBulkFixesParams(this.included, {this.inTestMode});
+  /// A list of diagnostic codes to be fixed.
+  List<String>? codes;
+
+  EditBulkFixesParams(this.included, {this.inTestMode, this.codes});
 
   factory EditBulkFixesParams.fromJson(
       JsonDecoder jsonDecoder, String jsonPath, Object? json) {
@@ -6022,7 +6026,13 @@
         inTestMode =
             jsonDecoder.decodeBool('$jsonPath.inTestMode', json['inTestMode']);
       }
-      return EditBulkFixesParams(included, inTestMode: inTestMode);
+      List<String>? codes;
+      if (json.containsKey('codes')) {
+        codes = jsonDecoder.decodeList(
+            '$jsonPath.codes', json['codes'], jsonDecoder.decodeString);
+      }
+      return EditBulkFixesParams(included,
+          inTestMode: inTestMode, codes: codes);
     } else {
       throw jsonDecoder.mismatch(jsonPath, 'edit.bulkFixes params', json);
     }
@@ -6041,6 +6051,10 @@
     if (inTestMode != null) {
       result['inTestMode'] = inTestMode;
     }
+    var codes = this.codes;
+    if (codes != null) {
+      result['codes'] = codes;
+    }
     return result;
   }
 
@@ -6057,7 +6071,8 @@
     if (other is EditBulkFixesParams) {
       return listEqual(
               included, other.included, (String a, String b) => a == b) &&
-          inTestMode == other.inTestMode;
+          inTestMode == other.inTestMode &&
+          listEqual(codes, other.codes, (String a, String b) => a == b);
     }
     return false;
   }
@@ -6066,6 +6081,7 @@
   int get hashCode => Object.hash(
         Object.hashAll(included),
         inTestMode,
+        Object.hashAll(codes ?? []),
       );
 }
 
diff --git a/pkg/analysis_server/lib/src/handler/legacy/edit_bulk_fixes.dart b/pkg/analysis_server/lib/src/handler/legacy/edit_bulk_fixes.dart
index 1ddaedd..5246a7f 100644
--- a/pkg/analysis_server/lib/src/handler/legacy/edit_bulk_fixes.dart
+++ b/pkg/analysis_server/lib/src/handler/legacy/edit_bulk_fixes.dart
@@ -38,7 +38,7 @@
       var workspace = DartChangeWorkspace(
           collection.contexts.map((c) => c.currentSession).toList());
       var processor = BulkFixProcessor(server.instrumentationService, workspace,
-          useConfigFiles: params.inTestMode ?? false);
+          useConfigFiles: params.inTestMode ?? false, codes: params.codes);
 
       var changeBuilder = await processor.fixErrors(collection.contexts);
 
diff --git a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
index 372d262..ae8e9de 100644
--- a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
+++ b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
@@ -139,6 +139,9 @@
   /// the transforms.
   final bool useConfigFiles;
 
+  /// An optional list of diagnostic codes to fix.
+  final List<String>? codes;
+
   /// The change builder used to build the changes required to fix the
   /// diagnostics.
   ChangeBuilder builder;
@@ -149,8 +152,9 @@
   /// Initialize a newly created processor to create fixes for diagnostics in
   /// libraries in the [workspace].
   BulkFixProcessor(this.instrumentationService, this.workspace,
-      {this.useConfigFiles = false})
-      : builder = ChangeBuilder(workspace: workspace);
+      {this.useConfigFiles = false, List<String>? codes})
+      : builder = ChangeBuilder(workspace: workspace),
+        codes = codes?.map((e) => e.toLowerCase()).toList();
 
   List<BulkFix> get fixDetails {
     var details = <BulkFix>[];
@@ -221,8 +225,14 @@
     Iterable<AnalysisError> filteredErrors(ResolvedUnitResult result) sync* {
       var errors = result.errors.toList();
       errors.sort((a, b) => a.offset.compareTo(b.offset));
-      // Only fix errors not filtered out in analysis options.
+      final codes = this.codes;
+      // Only fix errors specified in the `codes` list (if defined) and not
+      // filtered out in analysis options.
       for (var error in errors) {
+        if (codes != null &&
+            !codes.contains(error.errorCode.name.toLowerCase())) {
+          continue;
+        }
         var processor = ErrorProcessor.getProcessor(analysisOptions, error);
         if (processor == null || processor.severity != null) {
           yield error;
diff --git a/pkg/analysis_server/test/edit/bulk_fixes_test.dart b/pkg/analysis_server/test/edit/bulk_fixes_test.dart
index d3c47c2..ef67835 100644
--- a/pkg/analysis_server/test/edit/bulk_fixes_test.dart
+++ b/pkg/analysis_server/test/edit/bulk_fixes_test.dart
@@ -14,49 +14,136 @@
 
 void main() {
   defineReflectiveSuite(() {
-    defineReflectiveTests(BulkFixesTest);
+    defineReflectiveTests(BulkFixesFromOptionsTest);
+    defineReflectiveTests(BulkFixesFromCodesTest);
   });
 }
 
 @reflectiveTest
-class BulkFixesTest extends PubPackageAnalysisServerTest {
-  void assertContains(List<BulkFix> details,
-      {required String path, required String code, required int count}) {
-    for (var detail in details) {
-      if (detail.path == path) {
-        for (var fix in detail.fixes) {
-          if (fix.code == code) {
-            expect(fix.occurrences, count);
-            return;
-          }
-        }
-      }
-    }
-    fail('No match found for: $path:$code->$count in $details');
+class BulkFixesFromCodesTest extends BulkFixesTest {
+  Future<void> test_hint_checkWithNull() async {
+    addDiagnosticCode('TYPE_CHECK_WITH_NULL');
+    addTestFile('''
+void f(p, q) {
+  p is Null;
+  q is Null;
+}
+''');
+
+    await assertEditEquals(testFile, '''
+void f(p, q) {
+  p == null;
+  q == null;
+}
+''');
   }
 
-  Future<void> assertEditEquals(File file, String expectedSource) async {
-    await waitForTasksFinished();
-    var edits = await _getBulkEdits();
-    expect(edits, hasLength(1));
-    var editedSource =
-        SourceEdit.applySequence(file.readAsStringSync(), edits[0].edits);
-    expect(editedSource, expectedSource);
+  Future<void> test_hint_checkWithNull_notSpecified() async {
+    addDiagnosticCode('unnecessary_new');
+    addTestFile('''
+void f(p, q) {
+  p is Null;
+  q is Null;
+}
+''');
+
+    await assertNoEdits();
   }
 
-  Future<void> assertNoEdits() async {
-    await waitForTasksFinished();
-    var edits = await _getBulkEdits();
-    expect(edits, isEmpty);
+  Future<void> test_hint_unusedImport() async {
+    addDiagnosticCode('unused_import');
+
+    newFile('$testPackageLibPath/a.dart', '');
+
+    addTestFile('''
+import 'a.dart';
+''');
+
+    var details = await _getBulkFixDetails();
+    expect(details, hasLength(1));
+    var fixes = details.first.fixes;
+    expect(fixes, hasLength(1));
+    var fix = fixes.first;
+    expect(fix.code, 'unused_import');
+    expect(fix.occurrences, 1);
   }
 
-  @override
-  Future<void> setUp() async {
-    super.setUp();
-    registerLintRules();
-    await setRoots(included: [workspaceRootPath], excluded: []);
+  Future<void> test_hint_unusedImport_notSpecified() async {
+    addDiagnosticCode('unnecessary_new');
+
+    newFile('$testPackageLibPath/a.dart', '');
+
+    addTestFile('''
+import 'a.dart';
+
+class A {
+  A f() => new A();
+}
+''');
+
+    var details = await _getBulkFixDetails();
+    expect(details, isEmpty);
   }
 
+  Future<void> test_lint_unnecessaryNew() async {
+    newAnalysisOptionsYamlFile(testPackageRootPath, '''
+linter:
+  rules:
+    - annotate_overrides
+    - unnecessary_new
+''');
+    addDiagnosticCode('unnecessary_new');
+
+    addTestFile('''
+class A {
+  A f() => new A();
+}
+
+class B extends A {
+  A f() => new B();
+}
+''');
+
+    var details = await _getBulkFixDetails();
+    expect(details, hasLength(1));
+    var fixes = details.first.fixes;
+    expect(fixes, hasLength(1));
+    var fix = fixes.first;
+    expect(fix.code, 'unnecessary_new');
+    expect(fix.occurrences, 2);
+  }
+
+  Future<void> test_lint_unnecessaryNew_ignoreCase() async {
+    newAnalysisOptionsYamlFile(testPackageRootPath, '''
+linter:
+  rules:
+    - annotate_overrides
+    - unnecessary_new
+''');
+    addDiagnosticCode('UNNECESSARY_NEW');
+
+    addTestFile('''
+class A {
+  A f() => new A();
+}
+
+class B extends A {
+  A f() => new B();
+}
+''');
+
+    var details = await _getBulkFixDetails();
+    expect(details, hasLength(1));
+    var fixes = details.first.fixes;
+    expect(fixes, hasLength(1));
+    var fix = fixes.first;
+    expect(fix.code, 'unnecessary_new');
+    expect(fix.occurrences, 2);
+  }
+}
+
+@reflectiveTest
+class BulkFixesFromOptionsTest extends BulkFixesTest {
   Future<void> test_annotateOverrides_excludedFile() async {
     newAnalysisOptionsYamlFile(testPackageRootPath, '''
 analyzer:
@@ -250,6 +337,52 @@
 ''');
     await assertNoEdits();
   }
+}
+
+abstract class BulkFixesTest extends PubPackageAnalysisServerTest {
+  List<String>? codes;
+
+  void addDiagnosticCode(String code) {
+    codes ??= <String>[];
+    codes!.add(code);
+  }
+
+  void assertContains(List<BulkFix> details,
+      {required String path, required String code, required int count}) {
+    for (var detail in details) {
+      if (detail.path == path) {
+        for (var fix in detail.fixes) {
+          if (fix.code == code) {
+            expect(fix.occurrences, count);
+            return;
+          }
+        }
+      }
+    }
+    fail('No match found for: $path:$code->$count in $details');
+  }
+
+  Future<void> assertEditEquals(File file, String expectedSource) async {
+    await waitForTasksFinished();
+    var edits = await _getBulkEdits();
+    expect(edits, hasLength(1));
+    var editedSource =
+        SourceEdit.applySequence(file.readAsStringSync(), edits[0].edits);
+    expect(editedSource, expectedSource);
+  }
+
+  Future<void> assertNoEdits() async {
+    await waitForTasksFinished();
+    var edits = await _getBulkEdits();
+    expect(edits, isEmpty);
+  }
+
+  @override
+  Future<void> setUp() async {
+    super.setUp();
+    registerLintRules();
+    await setRoots(included: [workspaceRootPath], excluded: []);
+  }
 
   Future<List<SourceFileEdit>> _getBulkEdits() async {
     var result = await _getBulkFixes();
@@ -262,7 +395,8 @@
   }
 
   Future<EditBulkFixesResult> _getBulkFixes() async {
-    var request = EditBulkFixesParams([workspaceRoot.path]).toRequest('0');
+    var request =
+        EditBulkFixesParams([workspaceRoot.path], codes: codes).toRequest('0');
     var response = await handleSuccessfulRequest(request);
     return EditBulkFixesResult.fromResponse(response);
   }
diff --git a/pkg/analysis_server/test/integration/support/integration_test_methods.dart b/pkg/analysis_server/test/integration/support/integration_test_methods.dart
index cbc2ec6..7910c35 100644
--- a/pkg/analysis_server/test/integration/support/integration_test_methods.dart
+++ b/pkg/analysis_server/test/integration/support/integration_test_methods.dart
@@ -1735,6 +1735,10 @@
   ///
   ///   If this field is omitted the flag defaults to false.
   ///
+  /// codes: List<String> (optional)
+  ///
+  ///   A list of diagnostic codes to be fixed.
+  ///
   /// Returns
   ///
   /// edits: List<SourceFileEdit>
@@ -1746,8 +1750,10 @@
   ///   Details that summarize the fixes associated with the recommended
   ///   changes.
   Future<EditBulkFixesResult> sendEditBulkFixes(List<String> included,
-      {bool? inTestMode}) async {
-    var params = EditBulkFixesParams(included, inTestMode: inTestMode).toJson();
+      {bool? inTestMode, List<String>? codes}) async {
+    var params =
+        EditBulkFixesParams(included, inTestMode: inTestMode, codes: codes)
+            .toJson();
     var result = await server.send('edit.bulkFixes', params);
     var decoder = ResponseDecoder(null);
     return EditBulkFixesResult.fromJson(decoder, 'result', result);
diff --git a/pkg/analysis_server/test/integration/support/protocol_matchers.dart b/pkg/analysis_server/test/integration/support/protocol_matchers.dart
index 6998661..c8b6c26 100644
--- a/pkg/analysis_server/test/integration/support/protocol_matchers.dart
+++ b/pkg/analysis_server/test/integration/support/protocol_matchers.dart
@@ -2305,10 +2305,11 @@
 /// {
 ///   "included": List<FilePath>
 ///   "inTestMode": optional bool
+///   "codes": optional List<String>
 /// }
 final Matcher isEditBulkFixesParams = LazyMatcher(() => MatchesJsonObject(
     'edit.bulkFixes params', {'included': isListOf(isFilePath)},
-    optionalFields: {'inTestMode': isBool}));
+    optionalFields: {'inTestMode': isBool, 'codes': isListOf(isString)}));
 
 /// edit.bulkFixes result
 ///
diff --git a/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java b/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java
index 63daf3e..89c2e07 100644
--- a/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java
+++ b/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java
@@ -517,8 +517,9 @@
    *        difference is that in test mode the fix processor will look for a configuration file that
    *        can modify the content of the data file used to compute the fixes when data-driven fixes
    *        are being considered. If this field is omitted the flag defaults to false.
+   * @param codes A list of diagnostic codes to be fixed.
    */
-  public void edit_bulkFixes(List<String> included, boolean inTestMode, BulkFixesConsumer consumer);
+  public void edit_bulkFixes(List<String> included, boolean inTestMode, List<String> codes, BulkFixesConsumer consumer);
 
   /**
    * {@code edit.format}
diff --git a/pkg/analysis_server/tool/spec/spec_input.html b/pkg/analysis_server/tool/spec/spec_input.html
index 0c14fe6..3bb1feb 100644
--- a/pkg/analysis_server/tool/spec/spec_input.html
+++ b/pkg/analysis_server/tool/spec/spec_input.html
@@ -7,7 +7,7 @@
 <body>
 <h1>Analysis Server API Specification</h1>
 <h1 style="color:#999999">Version
-  <version>1.33.1</version>
+  <version>1.33.2</version>
 </h1>
 <p>
   This document contains a specification of the API provided by the
@@ -2436,6 +2436,14 @@
           If this field is omitted the flag defaults to <tt>false</tt>.
         </p>
       </field>
+      <field name="codes" optional="true">
+        <list>
+          <ref>String</ref>
+        </list>
+        <p>
+          A list of diagnostic codes to be fixed.
+        </p>
+      </field>
     </params>
     <result>
       <field name="edits">
diff --git a/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart b/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
index 4867f3d..086d0c8 100644
--- a/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
+++ b/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
@@ -8,7 +8,7 @@
 
 // ignore_for_file: constant_identifier_names
 
-const String PROTOCOL_VERSION = '1.33.1';
+const String PROTOCOL_VERSION = '1.33.2';
 
 const String ANALYSIS_NOTIFICATION_ANALYZED_FILES = 'analysis.analyzedFiles';
 const String ANALYSIS_NOTIFICATION_ANALYZED_FILES_DIRECTORIES = 'directories';
@@ -192,6 +192,7 @@
 const String DIAGNOSTIC_RESPONSE_GET_DIAGNOSTICS_CONTEXTS = 'contexts';
 const String DIAGNOSTIC_RESPONSE_GET_SERVER_PORT_PORT = 'port';
 const String EDIT_REQUEST_BULK_FIXES = 'edit.bulkFixes';
+const String EDIT_REQUEST_BULK_FIXES_CODES = 'codes';
 const String EDIT_REQUEST_BULK_FIXES_INCLUDED = 'included';
 const String EDIT_REQUEST_BULK_FIXES_IN_TEST_MODE = 'inTestMode';
 const String EDIT_REQUEST_FORMAT = 'edit.format';
diff --git a/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart b/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart
index 7d49b78..94a247f 100644
--- a/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart
+++ b/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart
@@ -5982,6 +5982,7 @@
 /// {
 ///   "included": List<FilePath>
 ///   "inTestMode": optional bool
+///   "codes": optional List<String>
 /// }
 ///
 /// Clients may not extend, implement or mix-in this class.
@@ -6004,7 +6005,10 @@
   /// If this field is omitted the flag defaults to false.
   bool? inTestMode;
 
-  EditBulkFixesParams(this.included, {this.inTestMode});
+  /// A list of diagnostic codes to be fixed.
+  List<String>? codes;
+
+  EditBulkFixesParams(this.included, {this.inTestMode, this.codes});
 
   factory EditBulkFixesParams.fromJson(
       JsonDecoder jsonDecoder, String jsonPath, Object? json) {
@@ -6022,7 +6026,13 @@
         inTestMode =
             jsonDecoder.decodeBool('$jsonPath.inTestMode', json['inTestMode']);
       }
-      return EditBulkFixesParams(included, inTestMode: inTestMode);
+      List<String>? codes;
+      if (json.containsKey('codes')) {
+        codes = jsonDecoder.decodeList(
+            '$jsonPath.codes', json['codes'], jsonDecoder.decodeString);
+      }
+      return EditBulkFixesParams(included,
+          inTestMode: inTestMode, codes: codes);
     } else {
       throw jsonDecoder.mismatch(jsonPath, 'edit.bulkFixes params', json);
     }
@@ -6041,6 +6051,10 @@
     if (inTestMode != null) {
       result['inTestMode'] = inTestMode;
     }
+    var codes = this.codes;
+    if (codes != null) {
+      result['codes'] = codes;
+    }
     return result;
   }
 
@@ -6057,7 +6071,8 @@
     if (other is EditBulkFixesParams) {
       return listEqual(
               included, other.included, (String a, String b) => a == b) &&
-          inTestMode == other.inTestMode;
+          inTestMode == other.inTestMode &&
+          listEqual(codes, other.codes, (String a, String b) => a == b);
     }
     return false;
   }
@@ -6066,6 +6081,7 @@
   int get hashCode => Object.hash(
         Object.hashAll(included),
         inTestMode,
+        Object.hashAll(codes ?? []),
       );
 }