Initial new union of Dart Analysis Fixes, the initial fix is to remove all unused imports in a file.

Change-Id: I259e796b2603234955a1950b2c886e6f08a697bc
Reviewed-on: https://dart-review.googlesource.com/56220
Commit-Queue: Jaime Wren <jwren@google.com>
Reviewed-by: Jaime Wren <jwren@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/plugin/edit/fix/fix_core.dart b/pkg/analysis_server/lib/plugin/edit/fix/fix_core.dart
index e0af23c..031d864 100644
--- a/pkg/analysis_server/lib/plugin/edit/fix/fix_core.dart
+++ b/pkg/analysis_server/lib/plugin/edit/fix/fix_core.dart
@@ -72,6 +72,12 @@
   AnalysisError get error;
 
   /**
+   * All of the errors in the file. This is used to compute additional fixes
+   * such "Fix all instances in file."
+   */
+  List<AnalysisError> get errors;
+
+  /**
    * The [ResourceProvider] to access files and folders.
    */
   ResourceProvider get resourceProvider;
diff --git a/pkg/analysis_server/lib/src/edit/edit_domain.dart b/pkg/analysis_server/lib/src/edit/edit_domain.dart
index 1af301c..86e8dce 100644
--- a/pkg/analysis_server/lib/src/edit/edit_domain.dart
+++ b/pkg/analysis_server/lib/src/edit/edit_domain.dart
@@ -526,12 +526,18 @@
       CompilationUnit unit = result.unit;
       LineInfo lineInfo = result.lineInfo;
       int requestLine = lineInfo.getLocation(offset).lineNumber;
+      List<engine.AnalysisError> errorsCopy = new List.from(result.errors);
       for (engine.AnalysisError error in result.errors) {
         int errorLine = lineInfo.getLocation(error.offset).lineNumber;
         if (errorLine == requestLine) {
           AstProvider astProvider = new AstProviderForDriver(driver);
           DartFixContext context = new _DartFixContextImpl(
-              server.resourceProvider, result.driver, astProvider, unit, error);
+              server.resourceProvider,
+              result.driver,
+              astProvider,
+              unit,
+              error,
+              errorsCopy);
           List<Fix> fixes =
               await new DefaultFixContributor().internalComputeFixes(context);
           if (fixes.isNotEmpty) {
@@ -679,8 +685,11 @@
   @override
   final engine.AnalysisError error;
 
+  @override
+  final List<engine.AnalysisError> errors;
+
   _DartFixContextImpl(this.resourceProvider, this.analysisDriver,
-      this.astProvider, this.unit, this.error);
+      this.astProvider, this.unit, this.error, this.errors);
 
   @override
   GetTopLevelDeclarations get getTopLevelDeclarations =>
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index cdcdc36..9603adb 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -212,8 +212,9 @@
       const FixKind('REMOVE_UNUSED_CATCH', 50, "Remove unused 'catch' clause");
   static const REMOVE_UNUSED_CATCH_STACK = const FixKind(
       'REMOVE_UNUSED_CATCH_STACK', 50, "Remove unused stack trace variable");
-  static const REMOVE_UNUSED_IMPORT =
-      const FixKind('REMOVE_UNUSED_IMPORT', 50, "Remove unused import");
+  static const REMOVE_UNUSED_IMPORT = const FixKind(
+      'REMOVE_UNUSED_IMPORT', 50, "Remove unused import",
+      appliedTogetherMessage: "Remove all unused imports in this file");
   static const RENAME_TO_CAMEL_CASE =
       const FixKind('RENAME_TO_CAMEL_CASE', 50, "Rename to '{0}'");
   static const REPLACE_BOOLEAN_WITH_BOOL = const FixKind(
@@ -266,10 +267,15 @@
   @override
   final AnalysisError error;
 
-  FixContextImpl(this.resourceProvider, this.analysisDriver, this.error);
+  @override
+  final List<AnalysisError> errors;
+
+  FixContextImpl(
+      this.resourceProvider, this.analysisDriver, this.error, this.errors);
 
   FixContextImpl.from(FixContext other)
       : resourceProvider = other.resourceProvider,
         analysisDriver = other.analysisDriver,
-        error = other.error;
+        error = other.error,
+        errors = other.errors;
 }
diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
index 3df2168..da9de25 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -84,11 +84,97 @@
     try {
       FixProcessor processor = new FixProcessor(context);
       List<Fix> fixes = await processor.compute();
-      return fixes;
+      List<Fix> fixAllFixes = await _computeFixAllFixes(context, fixes);
+      return new List.from(fixes)..addAll(fixAllFixes);
     } on CancelCorrectionException {
       return Fix.EMPTY_LIST;
     }
   }
+
+  Future<List<Fix>> _computeFixAllFixes(
+      DartFixContext context, List<Fix> fixes) async {
+    final AnalysisError analysisError = context.error;
+    final List<AnalysisError> allAnalysisErrors = context.errors;
+
+    // Validate inputs:
+    // - return if no fixes
+    // - return if no other analysis errors
+    if (fixes.isEmpty || allAnalysisErrors.length < 2) {
+      return Fix.EMPTY_LIST;
+    }
+
+    // Remove any analysis errors that don't have the expected error code name
+    allAnalysisErrors
+        .removeWhere((e) => analysisError.errorCode.name != e.errorCode.name);
+    if (allAnalysisErrors.length < 2) {
+      return Fix.EMPTY_LIST;
+    }
+
+    // A map between each FixKind and the List of associated fixes
+    final HashMap<FixKind, List<Fix>> map = new HashMap();
+
+    // Populate the HashMap by looping through all AnalysisErrors, creating a
+    // new FixProcessor to compute the other fixes that can be applied with this
+    // one.
+    // For each fix, put the fix into the HashMap.
+    for (int i = 0; i < allAnalysisErrors.length; i++) {
+      final FixContext fixContextI = new FixContextImpl(
+          context.resourceProvider,
+          context.analysisDriver,
+          allAnalysisErrors[i],
+          allAnalysisErrors);
+      final DartFixContextImpl dartFixContextI = new DartFixContextImpl(
+          fixContextI, context.astProvider, context.unit);
+      final FixProcessor processorI = new FixProcessor(dartFixContextI);
+      final List<Fix> fixesListI = await processorI.compute();
+      for (Fix f in fixesListI) {
+        if (!map.containsKey(f.kind)) {
+          map[f.kind] = new List<Fix>()..add(f);
+        } else {
+          map[f.kind].add(f);
+        }
+      }
+    }
+
+    // For each FixKind in the HashMap, union each list together, then return
+    // the set of unioned Fixes.
+    final List<Fix> result = new List<Fix>();
+    map.forEach((FixKind kind, List<Fix> fixesListJ) {
+      if (fixesListJ.first.kind.canBeAppliedTogether()) {
+        Fix unionFix = _unionFixList(fixesListJ);
+        if (unionFix != null) {
+          result.add(unionFix);
+        }
+      }
+    });
+    return result;
+  }
+
+  Fix _unionFixList(List<Fix> fixList) {
+    if (fixList == null || fixList.isEmpty) {
+      return null;
+    } else if (fixList.length == 1) {
+      return fixList[0];
+    }
+    final SourceChange sourceChange =
+        new SourceChange(fixList[0].kind.appliedTogetherMessage);
+    sourceChange.edits = new List.from(fixList[0].change.edits);
+    final List<SourceEdit> edits = new List<SourceEdit>();
+    edits.addAll(fixList[0].change.edits[0].edits);
+    sourceChange.linkedEditGroups =
+        new List.from(fixList[0].change.linkedEditGroups);
+    for (int i = 1; i < fixList.length; i++) {
+      edits.addAll(fixList[i].change.edits[0].edits);
+      sourceChange.linkedEditGroups..addAll(fixList[i].change.linkedEditGroups);
+    }
+    // Sort the list of SourceEdits so that when the edits are applied, they
+    // are applied from the end of the file to the top of the file.
+    edits.sort((s1, s2) => s2.offset - s1.offset);
+
+    sourceChange.edits[0].edits = edits;
+
+    return new Fix(fixList[0].kind, sourceChange);
+  }
 }
 
 /**
diff --git a/pkg/analysis_server/test/services/correction/fix_test.dart b/pkg/analysis_server/test/services/correction/fix_test.dart
index 62b0d84..b0d8f2c 100644
--- a/pkg/analysis_server/test/services/correction/fix_test.dart
+++ b/pkg/analysis_server/test/services/correction/fix_test.dart
@@ -100,6 +100,27 @@
     await _assertNoFix(kind, error);
   }
 
+  assertHasFixAllFix(ErrorCode errorCode, FixKind kind, String expected,
+      {String target}) async {
+    AnalysisError error = await _findErrorToFixOfType(errorCode);
+    fix = await _assertHasFix(kind, error, hasFixAllFix: true);
+    change = fix.change;
+
+    // apply to "file"
+    List<SourceFileEdit> fileEdits = change.edits;
+    expect(fileEdits, hasLength(1));
+
+    String fileContent = testCode;
+    if (target != null) {
+      expect(fileEdits.first.file, convertPath(target));
+      fileContent = getFile(target).readAsStringSync();
+    }
+
+    resultCode = SourceEdit.applySequence(fileContent, change.edits[0].edits);
+    // verify
+    expect(resultCode, expected);
+  }
+
   List<LinkedEditSuggestion> expectedSuggestions(
       LinkedEditSuggestionKind kind, List<String> values) {
     return values.map((value) {
@@ -115,14 +136,22 @@
   /**
    * Computes fixes and verifies that there is a fix of the given kind.
    */
-  Future<Fix> _assertHasFix(FixKind kind, AnalysisError error) async {
-    List<Fix> fixes = await _computeFixes(error);
-    for (Fix fix in fixes) {
-      if (fix.kind == kind) {
-        return fix;
-      }
+  Future<Fix> _assertHasFix(FixKind kind, AnalysisError error,
+      {bool hasFixAllFix: false}) async {
+    if (hasFixAllFix && !kind.canBeAppliedTogether()) {
+      fail(
+          'Expected to find and return fix-all FixKind for $kind, but kind.canBeAppliedTogether is ${kind.canBeAppliedTogether}');
     }
-    fail('Expected to find fix $kind in\n${fixes.join('\n')}');
+    List<Fix> fixes = await _computeFixes(error);
+    Fix firstMatchingFix = fixes.firstWhere((fix) => fix.kind == kind);
+    Fix lastMatchingFix = fixes.lastWhere((fix) => fix.kind == kind);
+    if (firstMatchingFix == null) {
+      fail('Expected to find fix $kind in\n${fixes.join('\n')}');
+    } else if (lastMatchingFix == null &&
+        !lastMatchingFix.kind.canBeAppliedTogether()) {
+      fail('Expected to find fix $kind in\n${fixes.join('\n')}');
+    }
+    return !hasFixAllFix ? firstMatchingFix : lastMatchingFix;
   }
 
   void _assertLinkedGroup(LinkedEditGroup group, List<String> expectedStrings,
@@ -151,8 +180,13 @@
    * Computes fixes for the given [error] in [testUnit].
    */
   Future<List<Fix>> _computeFixes(AnalysisError error) async {
-    DartFixContext fixContext = new _DartFixContextImpl(resourceProvider,
-        driver, new AstProviderForDriver(driver), testUnit, error);
+    DartFixContext fixContext = new _DartFixContextImpl(
+        resourceProvider,
+        driver,
+        new AstProviderForDriver(driver),
+        testUnit,
+        error,
+        await _computeErrors());
     return await new DefaultFixContributor().internalComputeFixes(fixContext);
   }
 
@@ -189,6 +223,14 @@
     return errors[0];
   }
 
+  Future<AnalysisError> _findErrorToFixOfType(ErrorCode errorCode) async {
+    List<AnalysisError> errors = await _computeErrors();
+    if (errorFilter != null) {
+      errors = errors.where(errorFilter).toList();
+    }
+    return errors.firstWhere((error) => errorCode == error.errorCode);
+  }
+
   List<Position> _findResultPositions(List<String> searchStrings) {
     List<Position> positions = <Position>[];
     for (String search in searchStrings) {
@@ -5119,6 +5161,51 @@
 ''');
   }
 
+  test_removeUnusedImport_all() async {
+    await resolveTestUnit('''
+import 'dart:math';
+import 'dart:math';
+import 'dart:math';
+main() {
+}
+''');
+    await assertHasFixAllFix(
+        HintCode.UNUSED_IMPORT, DartFixKind.REMOVE_UNUSED_IMPORT, '''
+main() {
+}
+''');
+  }
+
+  test_removeUnusedImport_all_diverseImports() async {
+    // TODO (jwren) add an additional test to verify the used, unused, used,
+    // unused ... permutations.
+    await resolveTestUnit('''
+import 'dart:math';
+import 'dart:math';
+import 'dart:async';
+main() {
+}
+''');
+    await assertHasFixAllFix(
+        HintCode.UNUSED_IMPORT, DartFixKind.REMOVE_UNUSED_IMPORT, '''
+main() {
+}
+''');
+  }
+
+  test_removeUnusedImport_all_singleLine() async {
+    await resolveTestUnit('''
+import 'dart:math'; import 'dart:math'; import 'dart:math';
+main() {
+}
+''');
+    await assertHasFixAllFix(
+        HintCode.UNUSED_IMPORT, DartFixKind.REMOVE_UNUSED_IMPORT, '''
+main() {
+}
+''');
+  }
+
   test_replaceVarWithDynamic() async {
     errorFilter = (AnalysisError error) {
       return error.errorCode == ParserErrorCode.VAR_AS_TYPE_NAME;
@@ -7923,8 +8010,11 @@
   @override
   final AnalysisError error;
 
+  @override
+  final List<AnalysisError> errors;
+
   _DartFixContextImpl(this.resourceProvider, this.analysisDriver,
-      this.astProvider, this.unit, this.error);
+      this.astProvider, this.unit, this.error, this.errors);
 
   @override
   GetTopLevelDeclarations get getTopLevelDeclarations =>
diff --git a/pkg/analyzer_plugin/lib/utilities/fixes/fixes.dart b/pkg/analyzer_plugin/lib/utilities/fixes/fixes.dart
index 865d169..3fc7fde7 100644
--- a/pkg/analyzer_plugin/lib/utilities/fixes/fixes.dart
+++ b/pkg/analyzer_plugin/lib/utilities/fixes/fixes.dart
@@ -139,11 +139,30 @@
   final String message;
 
   /**
-   * Initialize a newly created kind of fix to have the given [name],
-   * [priority] and [message].
+   * A human-readable description of the changes that will be applied by this
+   * kind of 'applied together' fix.
    */
-  const FixKind(this.name, this.priority, this.message);
+  final String appliedTogetherMessage;
+
+  /**
+   * The change can be made with other fixes of this [FixKind].
+   */
+  bool canBeAppliedTogether() => appliedTogetherMessage != null;
+
+  /**
+   * Initialize a newly created kind of fix to have the given [name],
+   * [priority], [message], and optionally [canBeAppliedTogether] and
+   * [appliedTogetherMessage].
+   */
+  const FixKind(this.name, this.priority, this.message,
+      {this.appliedTogetherMessage: null});
 
   @override
   String toString() => name;
+
+  @override
+  bool operator ==(o) => o is FixKind && o.name == name;
+
+  @override
+  int get hashCode => name.hashCode;
 }