fix (all) in file processor

Change-Id: I092d99ab8dd7e16fe351cf475a7c935a2546d67c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/183700
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
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 97be45b..55d892e 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart
@@ -271,6 +271,117 @@
   }
 }
 
+/// Computer for Dart "fix all in file" fixes.
+class FixInFileProcessor {
+  final DartFixContext context;
+  FixInFileProcessor(this.context);
+
+  Future<List<Fix>> compute() async {
+    var error = context.error;
+    var errors = context.resolveResult.errors.toList();
+    // Remove any analysis errors that don't have the expected error code name
+    errors.removeWhere((e) => error.errorCode.name != e.errorCode.name);
+
+    if (errors.length < 2) {
+      return const <Fix>[];
+    }
+
+    var instrumentationService = context.instrumentationService;
+    var workspace = context.workspace;
+    var resolveResult = context.resolveResult;
+
+    var generators = _getGenerators(
+        error.errorCode,
+        CorrectionProducerContext(
+          dartFixContext: context,
+          diagnostic: error,
+          resolvedResult: resolveResult,
+          selectionOffset: context.error.offset,
+          selectionLength: context.error.length,
+          workspace: workspace,
+        ));
+
+    var fixes = <Fix>[];
+    for (var generator in generators) {
+      var builder = ChangeBuilder(workspace: workspace);
+      for (var error in errors) {
+        var fixContext = DartFixContextImpl(
+          instrumentationService,
+          workspace,
+          resolveResult,
+          error,
+          (name) => [],
+        );
+        builder = await _fixError(fixContext, builder, generator(), error);
+      }
+      var sourceChange = builder.sourceChange;
+      if (sourceChange.edits.isNotEmpty) {
+        var fixKind = generator().fixKind;
+        // todo (pq): with a table redesign we can bail earlier.
+        if (fixKind.canBeAppliedTogether()) {
+          sourceChange.message = fixKind.appliedTogetherMessage;
+          fixes.add(Fix(fixKind, sourceChange));
+        }
+      }
+    }
+    return fixes;
+  }
+
+  Future<ChangeBuilder> _fixError(
+      DartFixContext fixContext,
+      ChangeBuilder builder,
+      CorrectionProducer producer,
+      AnalysisError diagnostic) async {
+    var context = CorrectionProducerContext(
+      applyingBulkFixes: true,
+      dartFixContext: fixContext,
+      diagnostic: diagnostic,
+      resolvedResult: fixContext.resolveResult,
+      selectionOffset: diagnostic.offset,
+      selectionLength: diagnostic.length,
+      workspace: fixContext.workspace,
+    );
+
+    var setupSuccess = context.setupCompute();
+    if (!setupSuccess) {
+      return null;
+    }
+
+    producer.configure(context);
+
+    try {
+      var localBuilder = builder.copy();
+      await producer.compute(localBuilder);
+      builder = localBuilder;
+    } on ConflictingEditException {
+      // If a conflicting edit was added in [compute], then the [localBuilder]
+      // is discarded and we revert to the previous state of the builder.
+    }
+
+    return builder;
+  }
+
+  List<ProducerGenerator> _getGenerators(
+      ErrorCode errorCode, CorrectionProducerContext context) {
+    var producers = <ProducerGenerator>[];
+    if (errorCode is LintCode) {
+      var generators = FixProcessor.lintProducerMap[errorCode.name];
+      if (generators != null) {
+        producers.addAll(generators);
+      }
+    } else {
+      var generators = FixProcessor.nonLintProducerMap[errorCode];
+      if (generators != null) {
+        if (generators != null) {
+          producers.addAll(generators);
+        }
+      }
+      // todo (pq): consider support for multiGenerators
+    }
+    return producers;
+  }
+}
+
 /// The computer for Dart fixes.
 class FixProcessor extends BaseProcessor {
   /// A map from the names of lint rules to a list of generators used to create
diff --git a/pkg/analysis_server/test/src/services/correction/fix/fix_in_file_test.dart b/pkg/analysis_server/test/src/services/correction/fix/fix_in_file_test.dart
new file mode 100644
index 0000000..a73400b
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/fix_in_file_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'fix_processor.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    // defineReflectiveTests(MultiFixInFileTest);
+    defineReflectiveTests(SingleFixInFileTest);
+  });
+}
+
+//// todo (pq): update w/ a FixKind that we're sure we want to support as a file fix
+// @reflectiveTest
+// class MultiFixInFileTest extends FixInFileProcessorTest
+//     with WithNullSafetyMixin {
+//   Future<void> test_nullable() async {
+//     await resolveTestCode('''
+// class C {
+//   String? s;
+//   String? s2;
+//   C({String this.s, String this.s2});
+// }
+// ''');
+//
+//     var fixes = await getFixes();
+//     expect(fixes, hasLength(2));
+//
+//     var addRequired =
+//         fixes.where((f) => f.kind == DartFixKind.ADD_REQUIRED).first;
+//     assertProduces(addRequired, '''
+// class C {
+//   String? s;
+//   String? s2;
+//   C({required String this.s, required String this.s2});
+// }
+// ''');
+//
+//     var makeNullable =
+//         fixes.where((f) => f.kind == DartFixKind.MAKE_VARIABLE_NULLABLE).first;
+//     assertProduces(makeNullable, '''
+// class C {
+//   String? s;
+//   String? s2;
+//   C({String? this.s, String? this.s2});
+// }
+// ''');
+//   }
+// }
+
+@reflectiveTest
+class SingleFixInFileTest extends FixInFileProcessorTest {
+  Future<void> test_isNull() async {
+    await resolveTestCode('''
+bool f(p, q) {
+  return p is Null && q is Null;
+}
+''');
+
+    var fixes = await getFixes();
+    expect(fixes, hasLength(1));
+    assertProduces(fixes.first, '''
+bool f(p, q) {
+  return p == null && q == null;
+}
+''');
+  }
+}
+
+/// todo (pq): add negative tests
diff --git a/pkg/analysis_server/test/src/services/correction/fix/fix_processor.dart b/pkg/analysis_server/test/src/services/correction/fix/fix_processor.dart
index 70b6a66..ac90224 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/fix_processor.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/fix_processor.dart
@@ -25,6 +25,131 @@
 
 export 'package:analyzer/src/test_utilities/package_config_file_builder.dart';
 
+abstract class BaseFixProcessorTest extends AbstractSingleUnitTest {
+  /// The errors in the file for which fixes are being computed.
+  List<AnalysisError> _errors;
+
+  /// The source change associated with the fix that was found, or `null` if
+  /// neither [assertHasFix] nor [assertHasFixAllFix] has been invoked.
+  SourceChange change;
+
+  /// The result of applying the [change] to the file content, or `null` if
+  /// neither [assertHasFix] nor [assertHasFixAllFix] has been invoked.
+  String resultCode;
+
+  /// The workspace in which fixes contributor operates.
+  ChangeWorkspace get workspace {
+    return DartChangeWorkspace([session]);
+  }
+
+  Future<List<AnalysisError>> _computeErrors() async {
+    if (_errors == null) {
+      if (testAnalysisResult != null) {
+        _errors = testAnalysisResult.errors;
+      }
+      if (_errors == null) {
+        var result = await session.getResolvedUnit(testFile);
+        _errors = result.errors;
+      }
+    }
+    return _errors;
+  }
+
+  /// Find the error that is to be fixed by computing the errors in the file,
+  /// using the [errorFilter] to filter out errors that should be ignored, and
+  /// expecting that there is a single remaining error. The error filter should
+  /// return `true` if the error should not be ignored.
+  Future<AnalysisError> _findErrorToFix(
+      bool Function(AnalysisError) errorFilter,
+      {int length}) async {
+    var errors = await _computeErrors();
+    if (errorFilter != null) {
+      if (errors.length == 1) {
+        fail('Unnecessary error filter');
+      }
+      errors = errors.where(errorFilter).toList();
+    }
+    if (errors.isEmpty) {
+      fail('Expected one error, found: none');
+    } else if (errors.length > 1) {
+      var buffer = StringBuffer();
+      buffer.writeln('Expected one error, found:');
+      for (var error in errors) {
+        buffer.writeln('  $error [${error.errorCode}]');
+      }
+      fail(buffer.toString());
+    }
+    return errors[0];
+  }
+
+  Future<AnalysisError> _findErrorToFixOfType(ErrorCode errorCode) async {
+    var errors = await _computeErrors();
+    for (var error in errors) {
+      if (error.errorCode == errorCode) {
+        return error;
+      }
+    }
+    return null;
+  }
+}
+
+/// A base class defining support for writing fix-in-file processor tests.
+abstract class FixInFileProcessorTest extends BaseFixProcessorTest {
+  void assertProduces(Fix fix, String expected) {
+    var fileEdits = fix.change.edits;
+    expect(fileEdits, hasLength(1));
+
+    var fileContent = testCode;
+    resultCode = SourceEdit.applySequence(fileContent, fileEdits[0].edits);
+    expect(resultCode, expected);
+  }
+
+  Future<List<Fix>> getFixes() async {
+    var errors = await _computeErrors();
+    expect(errors, isNotEmpty);
+    String errorCode;
+    for (var error in errors) {
+      errorCode ??= error.errorCode.name;
+      if (errorCode != error.errorCode.name) {
+        fail('Expected only errors of one type but found: $errors');
+      }
+    }
+
+    var fixes = await _computeFixes(errors.first);
+    return fixes;
+  }
+
+  @override
+  void setUp() {
+    super.setUp();
+    verifyNoTestUnitErrors = false;
+    useLineEndingsForPlatform = true;
+  }
+
+  /// Computes fixes for the given [error] in [testUnit].
+  Future<List<Fix>> _computeFixes(AnalysisError error) async {
+    var analysisContext = contextFor(testFile);
+
+    var tracker = DeclarationsTracker(MemoryByteStore(), resourceProvider);
+    tracker.addContext(analysisContext);
+
+    var context = DartFixContextImpl(
+      TestInstrumentationService(),
+      workspace,
+      testAnalysisResult,
+      error,
+      (name) {
+        var provider = TopLevelDeclarationsProvider(tracker);
+        provider.doTrackerWork();
+        return provider.get(analysisContext, testFile, name);
+      },
+    );
+
+    var fixes = await FixInFileProcessor(context).compute();
+    return fixes;
+  }
+}
+
 /// A base class defining support for writing fix processor tests that are
 /// specific to fixes associated with lints that use the FixKind.
 abstract class FixProcessorLintTest extends FixProcessorTest {
@@ -60,26 +185,10 @@
 }
 
 /// A base class defining support for writing fix processor tests.
-abstract class FixProcessorTest extends AbstractSingleUnitTest {
-  /// The errors in the file for which fixes are being computed.
-  List<AnalysisError> _errors;
-
-  /// The source change associated with the fix that was found, or `null` if
-  /// neither [assertHasFix] nor [assertHasFixAllFix] has been invoked.
-  SourceChange change;
-
-  /// The result of applying the [change] to the file content, or `null` if
-  /// neither [assertHasFix] nor [assertHasFixAllFix] has been invoked.
-  String resultCode;
-
+abstract class FixProcessorTest extends BaseFixProcessorTest {
   /// Return the kind of fixes being tested by this test class.
   FixKind get kind;
 
-  /// The workspace in which fixes contributor operates.
-  ChangeWorkspace get workspace {
-    return DartChangeWorkspace([session]);
-  }
-
   Future<void> assertHasFix(String expected,
       {bool Function(AnalysisError) errorFilter,
       int length,
@@ -266,19 +375,6 @@
     }
   }
 
-  Future<List<AnalysisError>> _computeErrors() async {
-    if (_errors == null) {
-      if (testAnalysisResult != null) {
-        _errors = testAnalysisResult.errors;
-      }
-      if (_errors == null) {
-        var result = await session.getResolvedUnit(testFile);
-        _errors = result.errors;
-      }
-    }
-    return _errors;
-  }
-
   /// Computes fixes for the given [error] in [testUnit].
   Future<List<Fix>> _computeFixes(AnalysisError error) async {
     var analysisContext = contextFor(testFile);
@@ -300,43 +396,6 @@
     return await DartFixContributor().computeFixes(context);
   }
 
-  /// Find the error that is to be fixed by computing the errors in the file,
-  /// using the [errorFilter] to filter out errors that should be ignored, and
-  /// expecting that there is a single remaining error. The error filter should
-  /// return `true` if the error should not be ignored.
-  Future<AnalysisError> _findErrorToFix(
-      bool Function(AnalysisError) errorFilter,
-      {int length}) async {
-    var errors = await _computeErrors();
-    if (errorFilter != null) {
-      if (errors.length == 1) {
-        fail('Unnecessary error filter');
-      }
-      errors = errors.where(errorFilter).toList();
-    }
-    if (errors.isEmpty) {
-      fail('Expected one error, found: none');
-    } else if (errors.length > 1) {
-      var buffer = StringBuffer();
-      buffer.writeln('Expected one error, found:');
-      for (var error in errors) {
-        buffer.writeln('  $error [${error.errorCode}]');
-      }
-      fail(buffer.toString());
-    }
-    return errors[0];
-  }
-
-  Future<AnalysisError> _findErrorToFixOfType(ErrorCode errorCode) async {
-    var errors = await _computeErrors();
-    for (var error in errors) {
-      if (error.errorCode == errorCode) {
-        return error;
-      }
-    }
-    return null;
-  }
-
   List<Position> _findResultPositions(List<String> searchStrings) {
     var positions = <Position>[];
     for (var search in searchStrings) {
diff --git a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
index 62832b7..41b5cce 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
@@ -81,6 +81,7 @@
 import 'create_setter_test.dart' as create_setter;
 import 'data_driven/test_all.dart' as data_driven;
 import 'extend_class_for_mixin_test.dart' as extend_class_for_mixin;
+import 'fix_in_file_test.dart' as fix_in_file;
 import 'fix_test.dart' as fix;
 import 'import_async_test.dart' as import_async;
 import 'import_library_prefix_test.dart' as import_library_prefix;
@@ -245,6 +246,7 @@
     data_driven.main();
     extend_class_for_mixin.main();
     fix.main();
+    fix_in_file.main();
     import_async.main();
     import_library_prefix.main();
     import_library_project.main();