blob: 5376ad9e2429ad48ee7d3117979d5a53633597d0 [file] [log] [blame]
// Copyright (c) 2020, 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:_fe_analyzer_shared/src/scanner/errors.dart';
import 'package:analysis_server/protocol/protocol_generated.dart'
hide AnalysisOptions;
import 'package:analysis_server/src/lsp/error_or.dart';
import 'package:analysis_server/src/lsp/source_edits.dart';
import 'package:analysis_server/src/services/correction/change_workspace.dart';
import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart';
import 'package:analysis_server/src/services/correction/dart/data_driven.dart';
import 'package:analysis_server/src/services/correction/dart/organize_imports.dart';
import 'package:analysis_server/src/services/correction/dart/remove_unused_import.dart';
import 'package:analysis_server/src/services/correction/fix/pubspec/fix_generator.dart';
import 'package:analysis_server/src/services/correction/fix_processor.dart';
import 'package:analysis_server/src/services/correction/organize_imports.dart';
import 'package:analysis_server/src/services/linter/lint_names.dart';
import 'package:analysis_server_plugin/edit/fix/dart_fix_context.dart';
import 'package:analysis_server_plugin/edit/fix/fix.dart';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/exception/exception.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/instrumentation/service.dart';
import 'package:analyzer/source/error_processor.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer/src/clients/build_resolvers/build_resolvers.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/dart/error/syntactic_errors.g.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/lint/linter.dart';
import 'package:analyzer/src/lint/linter_visitor.dart';
import 'package:analyzer/src/lint/registry.dart';
import 'package:analyzer/src/pubspec/pubspec_warning_code.dart';
import 'package:analyzer/src/pubspec/validators/missing_dependency_validator.dart';
import 'package:analyzer/src/source/source_resource.dart';
import 'package:analyzer/src/string_source.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer/src/utilities/cancellation.dart';
import 'package:analyzer/src/utilities/extensions/analysis_session.dart';
import 'package:analyzer/src/utilities/extensions/string.dart';
import 'package:analyzer/src/workspace/pub.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart'
show SourceFileEdit;
import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/change_builder/conflicting_edit_exception.dart';
import 'package:yaml/yaml.dart';
/// A fix producer that produces changes that will fix multiple diagnostics in
/// one or more files.
///
/// Each diagnostic should have a single fix (correction producer) associated
/// with it except in cases where at most one of the given producers will ever
/// produce a fix.
///
/// The correction producers that are associated with the diagnostics should not
/// produce changes that alter the semantics of the code.
class BulkFixProcessor {
/// A list of lint codes that can be run on parsed code. These lints will all
/// be run when the `--syntactic-fixes` flag is specified.
static const List<String> syntacticLintCodes = [
LintNames.prefer_generic_function_type_aliases,
LintNames.slash_for_doc_comments,
LintNames.unnecessary_const,
LintNames.unnecessary_new,
LintNames.unnecessary_string_escapes,
LintNames.use_function_type_syntax_for_parameters,
];
/// A map from an error code to a list of generators used to create multiple
/// correction producers used to build fixes for those diagnostics. The
/// generators used for lint rules are in the [lintMultiProducerMap].
///
/// The expectation is that only one of the correction producers will produce
/// a change for a given fix. If more than one change is produced the result
/// will almost certainly be invalid code.
static const Map<ErrorCode, List<MultiProducerGenerator>>
nonLintMultiProducerMap = {
CompileTimeErrorCode.ARGUMENT_TYPE_NOT_ASSIGNABLE: [
DataDriven.new,
],
CompileTimeErrorCode.CAST_TO_NON_TYPE: [
DataDriven.new,
],
CompileTimeErrorCode.EXTENDS_NON_CLASS: [
DataDriven.new,
],
// TODO(brianwilkerson): The following fix fails if an invocation of the
// function is the argument that needs to be removed.
// CompileTimeErrorCode.EXTRA_POSITIONAL_ARGUMENTS: [
// DataDriven.newInstance,
// ],
// TODO(brianwilkerson): The following fix fails if an invocation of the
// function is the argument that needs to be updated.
// CompileTimeErrorCode.EXTRA_POSITIONAL_ARGUMENTS_COULD_BE_NAMED: [
// DataDriven.newInstance,
// ],
CompileTimeErrorCode.IMPLEMENTS_NON_CLASS: [
DataDriven.new,
],
CompileTimeErrorCode.INVALID_OVERRIDE: [
DataDriven.new,
],
CompileTimeErrorCode.INVALID_OVERRIDE_SETTER: [
DataDriven.new,
],
CompileTimeErrorCode.MISSING_REQUIRED_ARGUMENT: [
DataDriven.new,
],
CompileTimeErrorCode.MIXIN_OF_NON_CLASS: [
DataDriven.new,
],
CompileTimeErrorCode.NEW_WITH_UNDEFINED_CONSTRUCTOR_DEFAULT: [
DataDriven.new,
],
CompileTimeErrorCode.NON_TYPE_AS_TYPE_ARGUMENT: [
DataDriven.new,
],
CompileTimeErrorCode.NOT_ENOUGH_POSITIONAL_ARGUMENTS_NAME_PLURAL: [
DataDriven.new,
],
CompileTimeErrorCode.NOT_ENOUGH_POSITIONAL_ARGUMENTS_NAME_SINGULAR: [
DataDriven.new,
],
CompileTimeErrorCode.NOT_ENOUGH_POSITIONAL_ARGUMENTS_PLURAL: [
DataDriven.new,
],
CompileTimeErrorCode.NOT_ENOUGH_POSITIONAL_ARGUMENTS_SINGULAR: [
DataDriven.new,
],
CompileTimeErrorCode.UNDEFINED_CLASS: [
DataDriven.new,
],
CompileTimeErrorCode.UNDEFINED_EXTENSION_GETTER: [
DataDriven.new,
],
CompileTimeErrorCode.UNDEFINED_FUNCTION: [
DataDriven.new,
],
CompileTimeErrorCode.UNDEFINED_GETTER: [
DataDriven.new,
],
CompileTimeErrorCode.UNDEFINED_IDENTIFIER: [
DataDriven.new,
],
CompileTimeErrorCode.UNDEFINED_METHOD: [
DataDriven.new,
],
CompileTimeErrorCode.UNDEFINED_NAMED_PARAMETER: [
DataDriven.new,
],
CompileTimeErrorCode.UNDEFINED_SETTER: [
DataDriven.new,
],
CompileTimeErrorCode.WRONG_NUMBER_OF_TYPE_ARGUMENTS: [
DataDriven.new,
],
CompileTimeErrorCode.WRONG_NUMBER_OF_TYPE_ARGUMENTS_CONSTRUCTOR: [
DataDriven.new,
],
CompileTimeErrorCode.WRONG_NUMBER_OF_TYPE_ARGUMENTS_EXTENSION: [
DataDriven.new,
],
CompileTimeErrorCode.WRONG_NUMBER_OF_TYPE_ARGUMENTS_METHOD: [
DataDriven.new,
],
HintCode.DEPRECATED_MEMBER_USE: [
DataDriven.new,
],
HintCode.DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE: [
DataDriven.new,
],
HintCode.DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE_WITH_MESSAGE: [
DataDriven.new,
],
HintCode.DEPRECATED_MEMBER_USE_WITH_MESSAGE: [
DataDriven.new,
],
WarningCode.DEPRECATED_EXPORT_USE: [
DataDriven.new,
],
WarningCode.OVERRIDE_ON_NON_OVERRIDING_METHOD: [
DataDriven.new,
],
};
static final Set<String> _errorCodes =
errorCodeValues.map((ErrorCode code) => code.name.toLowerCase()).toSet();
static final Set<String> _lintCodes =
Registry.ruleRegistry.rules.map((rule) => rule.name).toSet();
/// The service used to report errors when building fixes.
final InstrumentationService instrumentationService;
/// Information about the workspace containing the libraries in which changes
/// will be produced.
final DartChangeWorkspace workspace;
/// 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;
/// A map associating libraries to fixes with change counts.
final ChangeMap changeMap = ChangeMap();
/// A token used to signal that the caller is no longer interested in the
/// results and processing can end early (in which case any results may be
/// invalid).
final CancellationToken? cancellationToken;
/// Initialize a newly created processor to create fixes for diagnostics in
/// libraries in the [workspace].
BulkFixProcessor(
this.instrumentationService,
this.workspace, {
List<String>? codes,
this.cancellationToken,
}) : builder = ChangeBuilder(workspace: workspace),
codes = codes?.map((e) => e.toLowerCase()).toList();
List<BulkFix> get fixDetails {
var details = <BulkFix>[];
for (var change in changeMap.libraryMap.entries) {
var fixes = <BulkFixDetail>[];
for (var codeEntry in change.value.entries) {
fixes.add(BulkFixDetail(codeEntry.key, codeEntry.value));
}
details.add(BulkFix(change.key, fixes));
}
return details;
}
bool get isCancelled => cancellationToken?.isCancellationRequested ?? false;
/// Return a [BulkFixRequestResult] that includes a change builder that has
/// been used to create fixes for the diagnostics in the libraries in the
/// given [contexts].
Future<BulkFixRequestResult> fixErrors(List<AnalysisContext> contexts) =>
_computeFixes(contexts);
/// Return a change builder that has been used to create fixes for the
/// diagnostics in [file] in the given [context].
Future<ChangeBuilder> fixErrorsForFile(OperationPerformanceImpl performance,
AnalysisContext context, String path,
{required bool autoTriggered}) async {
var pathContext = context.contextRoot.resourceProvider.pathContext;
if (file_paths.isDart(pathContext, path) &&
!file_paths.isGenerated(path) &&
!file_paths.isMacroGenerated(path)) {
var library = await performance.runAsync(
'getResolvedLibrary',
(_) => context.currentSession.getResolvedContainingLibrary(path),
);
var unit = library?.unitWithPath(path);
if (!isCancelled && library != null && unit != null) {
await _fixErrorsInLibraryUnit(unit, library,
autoTriggered: autoTriggered);
}
}
return builder;
}
/// Return a [BulkFixRequestResult] that includes a change builder that has
/// been used to create fixes for the diagnostics in the libraries in the
/// given [contexts].
Future<BulkFixRequestResult> fixErrorsUsingParsedResult(
List<AnalysisContext> contexts) =>
_computeFixesUsingParsedResult(contexts);
/// Return a [PubspecFixRequestResult] that includes edits to the pubspec
/// files in the given [contexts].
Future<PubspecFixRequestResult> fixPubspec(List<AnalysisContext> contexts) =>
_computeChangesToPubspec(contexts);
/// Return a [BulkFixRequestResult] that includes a change builder that has
/// been used to format the dart files in the given [contexts].
Future<BulkFixRequestResult> formatCode(List<AnalysisContext> contexts) =>
_formatCode(contexts);
/// Checks whether any diagnostics are bulk fixable.
///
/// This is faster than calling [fixErrors] if the only requirement is to
/// know that there are fixes, because it stops processing when the first
/// fixable diagnostic is found.
Future<bool> hasFixes(List<AnalysisContext> analysisContexts) async {
await _computeFixes(analysisContexts, stopAfterFirst: true);
return changeMap.hasFixes;
}
/// Return a [BulkFixRequestResult] that includes a change builder that has
/// been used to organize the directives in the dart files in the given
/// [contexts].
Future<BulkFixRequestResult> organizeDirectives(
List<AnalysisContext> contexts) =>
_organizeDirectives(contexts);
Future<void> _applyProducer(
CorrectionProducerContext context, CorrectionProducer producer) async {
producer.configure(context);
try {
var localBuilder = builder.copy() as ChangeBuilderImpl;
// Set a description of the change for this fix for the duration of
// computer which will be passed down to the individual changes.
localBuilder.currentChangeDescription = producer.fixKind?.message;
var fixKind = producer.fixKind;
await producer.compute(localBuilder);
assert(
!(producer.canBeAppliedToFile || producer.canBeAppliedInBulk) ||
producer.fixKind == fixKind,
'Producers use in bulk fixes must not modify FixKind during computation. '
'$producer changed from $fixKind to ${producer.fixKind}.',
);
localBuilder.currentChangeDescription = null;
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.
}
}
Future<void> _bulkApply(List<ProducerGenerator> generators, String codeName,
CorrectionProducerContext context) async {
for (var generator in generators) {
var producer = generator();
var shouldFix = (context.dartFixContext?.autoTriggered ?? false)
? producer.canBeAppliedAutomatically
: producer.canBeAppliedInBulk;
if (shouldFix) {
await _generateFix(context, producer, codeName);
if (isCancelled) {
return;
}
}
}
}
Future<PubspecFixRequestResult> _computeChangesToPubspec(
List<AnalysisContext> contexts) async {
var fixes = <SourceFileEdit>[];
var details = <BulkFix>[];
for (var context in contexts) {
var workspace = context.contextRoot.workspace;
if (workspace is! PackageConfigWorkspace) {
continue;
}
var pathContext = context.contextRoot.resourceProvider.pathContext;
var resourceProvider = workspace.provider;
var packageToDeps = <PubPackage, _PubspecDeps>{};
for (var path in context.contextRoot.analyzedFiles()) {
if (!file_paths.isDart(pathContext, path) ||
file_paths.isGenerated(path) ||
file_paths.isMacroGenerated(path)) {
continue;
}
var package = workspace.findPackageFor(path);
if (package is! PubPackage) {
continue;
}
var libPath =
resourceProvider.getFolder(package.root).getChild('lib').path;
var binPath =
resourceProvider.getFolder(package.root).getChild('bin').path;
bool isPublic(String path, PubPackage package) {
if (path.startsWith(libPath) || path.startsWith(binPath)) {
return true;
}
return false;
}
var pubspecDeps =
packageToDeps.putIfAbsent(package, () => _PubspecDeps());
// Get the list of imports used in the files.
var result = context.currentSession.getParsedLibrary(path);
if (result is! ParsedLibraryResult) {
return PubspecFixRequestResult(fixes, details);
}
for (var unit in result.units) {
var directives = unit.unit.directives;
for (var directive in directives) {
var uri =
(directive is ImportDirective) ? directive.uri.stringValue : '';
if (uri!.startsWith('package:')) {
var name = Uri.parse(uri).pathSegments.first;
if (isPublic(path, package)) {
pubspecDeps.packages.add(name);
} else {
pubspecDeps.devPackages.add(name);
}
}
}
}
}
// Iterate over packages in the workspace, compute changes to pubspec.
for (var package in packageToDeps.keys) {
var pubspecDeps = packageToDeps[package]!;
var pubspecFile = package.pubspecFile;
var result = await _runPubspecValidatorAndFixGenerator(
FileSource(pubspecFile),
pubspecDeps.packages,
pubspecDeps.devPackages,
context.contextRoot.resourceProvider);
if (result.isNotEmpty) {
for (var fix in result) {
fixes.addAll(fix.change.edits);
}
details.add(BulkFix(pubspecFile.path,
[BulkFixDetail(PubspecWarningCode.MISSING_DEPENDENCY.name, 1)]));
}
}
}
return PubspecFixRequestResult(fixes, details);
}
/// Implementation for [fixErrors] and [hasFixes].
///
/// Return a [BulkFixRequestResult] that includes a change builder that has
/// been used to create fixes for the diagnostics in the libraries in the
/// given [contexts].
///
/// As an optimization for [hasFixes], if [stopAfterFirst] is `true`,
/// processing will stop early once a fixable diagnostic is found and the
/// results will contain at least that fix, but otherwise be incomplete.
Future<BulkFixRequestResult> _computeFixes(
List<AnalysisContext> contexts, {
bool stopAfterFirst = false,
}) async {
// Ensure specified codes are defined.
final codes = this.codes;
if (codes != null) {
var undefinedCodes = <String>[];
for (var code in codes) {
if (!_errorCodes.contains(code) && !_lintCodes.contains(code)) {
undefinedCodes.add(code);
}
}
if (undefinedCodes.isNotEmpty) {
var count = undefinedCodes.length;
var diagnosticCodes = undefinedCodes.quotedAndCommaSeparatedWithAnd;
return BulkFixRequestResult.error('The '
'${'diagnostic'.pluralized(count)} $diagnosticCodes ${count.isAre} '
'not defined by the analyzer.');
}
}
for (var context in contexts) {
var pathContext = context.contextRoot.resourceProvider.pathContext;
for (var path in context.contextRoot.analyzedFiles()) {
if (!file_paths.isDart(pathContext, path) ||
file_paths.isGenerated(path) ||
file_paths.isMacroGenerated(path)) {
continue;
}
if (!await _hasFixableErrors(context, path)) {
continue;
}
var library = await context.currentSession.getResolvedLibrary(path);
if (isCancelled) {
break;
}
if (library is ResolvedLibraryResult) {
await _fixErrorsInLibrary(library, stopAfterFirst: stopAfterFirst);
if (isCancelled || (stopAfterFirst && changeMap.hasFixes)) {
break;
}
}
}
}
return BulkFixRequestResult(builder);
}
Future<BulkFixRequestResult> _computeFixesUsingParsedResult(
List<AnalysisContext> contexts, {
bool stopAfterFirst = false,
}) async {
for (var context in contexts) {
var pathContext = context.contextRoot.resourceProvider.pathContext;
for (var path in context.contextRoot.analyzedFiles()) {
if (!file_paths.isDart(pathContext, path) ||
file_paths.isGenerated(path) ||
file_paths.isMacroGenerated(path)) {
continue;
}
if (!await _hasFixableErrors(context, path)) {
continue;
}
var result = context.currentSession.getParsedLibrary(path);
if (isCancelled) {
break;
}
if (result is ParsedLibraryResult) {
var allUnits = result.units
.map((parsedUnit) =>
LinterContextUnit(parsedUnit.content, parsedUnit.unit))
.toList();
var errorListener = RecordingErrorListener();
for (var linterUnit in allUnits) {
var errorReporter = ErrorReporter(
errorListener,
StringSource(linterUnit.content, null),
);
_computeLints(
linterUnit,
allUnits,
errorReporter,
);
}
await _fixErrorsInParsedLibrary(result, errorListener.errors,
stopAfterFirst: stopAfterFirst);
if (isCancelled || (stopAfterFirst && changeMap.hasFixes)) {
break;
}
}
}
}
return BulkFixRequestResult(builder);
}
void _computeLints(LinterContextUnit currentUnit,
List<LinterContextUnit> allUnits, ErrorReporter errorReporter) {
var unit = currentUnit.unit;
var nodeRegistry = NodeLintRegistry(false);
var context = LinterContextParsedImpl(allUnits, currentUnit);
var lintRules = syntacticLintCodes
.map((name) => Registry.ruleRegistry.getRule(name))
.nonNulls
.toList();
for (var linter in lintRules) {
linter.reporter = errorReporter;
linter.registerNodeProcessors(nodeRegistry, context);
}
// Run lints that handle specific node types.
unit.accept(
LinterVisitor(
nodeRegistry,
LinterExceptionHandler(
propagateExceptions: false,
).logException,
),
);
}
/// Filters errors to only those that are in [codes] and are not filtered out
/// in analysis_options.
Iterable<AnalysisError> _filterErrors(AnalysisOptions analysisOptions,
List<AnalysisError> originalErrors) sync* {
var errors = originalErrors.toList();
errors.sort((a, b) => a.offset.compareTo(b.offset));
final codes = this.codes;
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;
}
}
}
/// Use the change [builder] to create fixes for the diagnostics in the
/// library associated with the analysis [result].
Future<void> _fixErrorsInLibrary(ResolvedLibraryResult result,
{bool stopAfterFirst = false, bool autoTriggered = false}) async {
for (var unitResult in result.units) {
await _fixErrorsInLibraryUnit(unitResult, result,
stopAfterFirst: stopAfterFirst, autoTriggered: autoTriggered);
}
}
/// Use the change [builder] to create fixes for the diagnostics in
/// [unit].
Future<void> _fixErrorsInLibraryUnit(
ResolvedUnitResult unit, ResolvedLibraryResult library,
{bool stopAfterFirst = false, bool autoTriggered = false}) async {
var analysisOptions =
unit.session.analysisContext.getAnalysisOptionsForFile(unit.file);
DartFixContext fixContext(
AnalysisError diagnostic, {
required bool autoTriggered,
}) {
return DartFixContext(
instrumentationService: instrumentationService,
workspace: workspace,
resolvedResult: unit,
error: diagnostic,
autoTriggered: autoTriggered,
);
}
CorrectionProducerContext<ResolvedUnitResult>? correctionContext(
AnalysisError diagnostic) {
var context = fixContext(diagnostic, autoTriggered: autoTriggered);
return CorrectionProducerContext.createResolved(
applyingBulkFixes: true,
dartFixContext: context,
diagnostic: diagnostic,
resolvedResult: unit,
selectionOffset: diagnostic.offset,
selectionLength: diagnostic.length,
);
}
//
// Attempt to apply the fixes that aren't related to directives.
//
for (var error in _filterErrors(analysisOptions, unit.errors)) {
var context = fixContext(error, autoTriggered: autoTriggered);
await _fixSingleError(context, unit, error);
if (isCancelled || (stopAfterFirst && changeMap.hasFixes)) {
return;
}
}
// Only if this unit is the defining unit, we don't have other fixes and
// we were not auto-triggered should be continue with fixes for directives.
if (unit != library.units.first ||
autoTriggered ||
builder.hasEditsFor(unit.path)) {
return;
}
AnalysisError? directivesOrderingError;
var unusedImportErrors = <AnalysisError>[];
for (var error in _filterErrors(analysisOptions, unit.errors)) {
var errorCode = error.errorCode;
if (errorCode is LintCode) {
var lintName = errorCode.name;
if (lintName == LintNames.directives_ordering) {
directivesOrderingError = error;
break;
}
} else if (errorCode == WarningCode.DUPLICATE_IMPORT ||
errorCode == HintCode.UNNECESSARY_IMPORT ||
errorCode == WarningCode.UNUSED_IMPORT) {
unusedImportErrors.add(error);
}
}
if (directivesOrderingError != null) {
// `OrganizeImports` will also remove some of the unused imports, so we
// apply it first.
var context = correctionContext(directivesOrderingError);
if (context != null) {
await _generateFix(
context, OrganizeImports(), directivesOrderingError.errorCode.name);
if (isCancelled || (stopAfterFirst && changeMap.hasFixes)) {
return;
}
}
} else {
for (var error in unusedImportErrors) {
var context = correctionContext(error);
if (context != null) {
await _generateFix(
context, RemoveUnusedImport(), error.errorCode.name);
if (isCancelled || (stopAfterFirst && changeMap.hasFixes)) {
return;
}
}
}
}
}
Future<void> _fixErrorsInParsedLibrary(
ParsedLibraryResult result, List<AnalysisError> errors,
{required bool stopAfterFirst}) async {
for (var unitResult in result.units) {
var analysisOptions = result.session.analysisContext
.getAnalysisOptionsForFile(unitResult.file);
for (var error in _filterErrors(analysisOptions, errors)) {
await _fixSingleParseError(unitResult, error);
if (isCancelled || (stopAfterFirst && changeMap.hasFixes)) {
return;
}
}
}
}
/// Uses the change [builder] and the [fixContext] to create a fix for the
/// given [diagnostic] in the compilation unit associated with the analysis
/// [result].
Future<void> _fixSingleError(
DartFixContext fixContext,
ResolvedUnitResult result,
AnalysisError diagnostic,
) async {
var context = CorrectionProducerContext.createResolved(
applyingBulkFixes: true,
dartFixContext: fixContext,
diagnostic: diagnostic,
resolvedResult: result,
selectionOffset: diagnostic.offset,
selectionLength: diagnostic.length,
);
if (context == null) {
return;
}
var errorCode = diagnostic.errorCode;
var codeName = errorCode.name;
try {
if (errorCode is LintCode) {
var generators = FixProcessor.lintProducerMap[codeName] ?? [];
await _bulkApply(generators, codeName, context);
if (isCancelled) {
return;
}
var multiGenerators = FixProcessor.lintMultiProducerMap[codeName];
if (multiGenerators != null) {
for (var multiGenerator in multiGenerators) {
var multiProducer = multiGenerator();
multiProducer.configure(context);
for (var producer in await multiProducer.producers) {
await _generateFix(context, producer, codeName);
}
}
}
} else {
var generators = FixProcessor.nonLintProducerMap[errorCode] ?? [];
await _bulkApply(generators, codeName, context);
if (isCancelled) {
return;
}
var multiGenerators = nonLintMultiProducerMap[errorCode];
if (multiGenerators != null) {
for (var multiGenerator in multiGenerators) {
var multiProducer = multiGenerator();
multiProducer.configure(context);
for (var producer in await multiProducer.producers) {
await _generateFix(context, producer, codeName);
if (isCancelled) {
return;
}
}
}
}
}
} catch (e, s) {
throw CaughtException.withMessage(
'Exception generating fix for $codeName in ${result.path}', e, s);
}
}
/// Uses the change [builder] to create a fix for the given [diagnostic] in
/// the compilation unit associated with the analysis [result].
Future<void> _fixSingleParseError(
ParsedUnitResult result,
AnalysisError diagnostic,
) async {
var context = CorrectionProducerContext.createParsed(
applyingBulkFixes: true,
diagnostic: diagnostic,
resolvedResult: result,
selectionOffset: diagnostic.offset,
selectionLength: diagnostic.length,
);
var errorCode = diagnostic.errorCode;
var codeName = errorCode.name;
try {
if (errorCode is LintCode) {
var generators = FixProcessor.parseLintProducerMap[codeName] ?? [];
await _bulkApply(generators, codeName, context);
if (isCancelled) {
return;
}
}
} catch (e, s) {
throw CaughtException.withMessage(
'Exception generating fix for $codeName in ${result.path}', e, s);
}
}
Future<BulkFixRequestResult> _formatCode(
List<AnalysisContext> contexts) async {
for (var context in contexts) {
for (var path in context.contextRoot.analyzedFiles()) {
var pathContext = context.contextRoot.resourceProvider.pathContext;
if (!file_paths.isDart(pathContext, path) ||
file_paths.isGenerated(path) ||
file_paths.isMacroGenerated(path)) {
continue;
}
var result =
context.currentSession.getParsedUnit(path) as ParsedUnitResult;
if (result.errors.isNotEmpty) {
continue;
}
var formatResult = generateEditsForFormatting(result, null);
await formatResult.mapResult((formatResult) async {
var edits = formatResult ?? [];
if (edits.isNotEmpty) {
await builder.addDartFileEdit(path, (builder) {
for (var edit in edits) {
var lineInfo = result.lineInfo;
var startOffset =
lineInfo.getOffsetOfLine(edit.range.start.line) +
edit.range.start.character;
var endOffset = lineInfo.getOffsetOfLine(edit.range.end.line) +
edit.range.end.character;
builder.addSimpleReplacement(
SourceRange(startOffset, endOffset - startOffset),
edit.newText);
}
});
}
// TODO(dantup): Consider an async ifResult to avoid needing to return
// an ErrorOr?
return success(null);
});
}
}
return BulkFixRequestResult(builder);
}
Future<void> _generateFix(CorrectionProducerContext context,
CorrectionProducer producer, String code) async {
int computeChangeHash() => (builder as ChangeBuilderImpl).changeHash;
var oldHash = computeChangeHash();
await _applyProducer(context, producer);
var newHash = computeChangeHash();
if (newHash != oldHash) {
changeMap.add(context.path, code.toLowerCase());
}
}
/// Returns whether [path] has any errors that might be fixable.
Future<bool> _hasFixableErrors(AnalysisContext context, String path) async {
var errorsResult = await context.currentSession.getErrors(path);
if (errorsResult is! ErrorsResult) {
return false;
}
var analysisOptions = errorsResult.session.analysisContext
.getAnalysisOptionsForFile(errorsResult.file);
var filteredErrors = _filterErrors(analysisOptions, errorsResult.errors);
return filteredErrors.any(_isFixableError);
}
/// Returns whether [error] is something that might be fixable.
bool _isFixableError(AnalysisError error) {
var errorCode = error.errorCode;
// Special cases that can be bulk fixed by this class but not by
// FixProcessor.
if (errorCode == WarningCode.DUPLICATE_IMPORT ||
errorCode == HintCode.UNNECESSARY_IMPORT ||
errorCode == WarningCode.UNUSED_IMPORT ||
(errorCode is LintCode &&
errorCode.name == LintNames.directives_ordering)) {
return true;
}
return FixProcessor.canBulkFix(errorCode);
}
Future<BulkFixRequestResult> _organizeDirectives(
List<AnalysisContext> contexts) async {
for (var context in contexts) {
for (var path in context.contextRoot.analyzedFiles()) {
var pathContext = context.contextRoot.resourceProvider.pathContext;
if (!file_paths.isDart(pathContext, path) ||
file_paths.isGenerated(path) ||
file_paths.isMacroGenerated(path)) {
continue;
}
var result =
context.currentSession.getParsedUnit(path) as ParsedUnitResult;
var code = result.content;
var errors = result.errors;
// check if there are scan/parse errors in the file
var hasParseErrors = errors.any((error) =>
error.errorCode is ScannerErrorCode ||
error.errorCode is ParserErrorCode);
if (hasParseErrors) {
// cannot process files with parse errors
continue;
}
// do organize
var sorter = ImportOrganizer(code, result.unit, errors);
var edits = sorter.organize();
await builder.addDartFileEdit(path, (builder) {
for (var edit in edits) {
builder.addSimpleReplacement(
SourceRange(edit.offset, edit.length), edit.replacement);
}
});
}
}
return BulkFixRequestResult(builder);
}
Future<List<Fix>> _runPubspecValidatorAndFixGenerator(
Source pubspec,
Set<String> usedDeps,
Set<String> usedDevDeps,
ResourceProvider resourceProvider) async {
String contents = pubspec.contents.data;
YamlNode node = loadYamlNode(contents);
if (node is! YamlMap) {
// The file is empty.
node = YamlMap();
}
var errors = MissingDependencyValidator(node, pubspec, resourceProvider)
.validate(usedDeps, usedDevDeps);
if (errors.isNotEmpty) {
var generator =
PubspecFixGenerator(resourceProvider, errors[0], contents, node);
return await generator.computeFixes();
}
return [];
}
}
class BulkFixRequestResult {
final ChangeBuilder? builder;
final String? errorMessage;
BulkFixRequestResult(this.builder) : errorMessage = null;
BulkFixRequestResult.error(this.errorMessage) : builder = null;
}
/// Maps changes to library paths.
class ChangeMap {
/// Map of paths to maps of codes to counts.
final Map<String, Map<String, int>> libraryMap = {};
/// Whether or not there are any available fixes.
bool get hasFixes => libraryMap.isNotEmpty;
/// Add an entry for the given [code] in the given [libraryPath].
void add(String libraryPath, String code) {
var changes = libraryMap.putIfAbsent(libraryPath, () => {});
changes.update(code, (value) => value + 1, ifAbsent: () => 1);
}
}
/// Calls [BulkFixProcessor] iteratively to apply multiple rounds of changes.
///
/// Temporarily modifies overlays in [resourceProvider] while computing fixes
/// so the caller must ensure that no other requests are modifying them.
class IterativeBulkFixProcessor {
/// The maximum number of passes to make.
///
/// This should match what "dart fix" does (`FixCommand.maxPasses` in
/// `pkg/dartdev/lib/src/commands/fix.dart`).
static const maxPasses = 4;
final InstrumentationService instrumentationService;
final AnalysisContext context;
final void Function(SourceFileEdit) applyTemporaryOverlayEdits;
final Future<void> Function() applyOverlays;
int _passesWithEdits = 0;
/// A token used to signal that the caller is no longer interested in the
/// results and processing can end early (in which case any results may be
/// invalid).
final CancellationToken? cancellationToken;
IterativeBulkFixProcessor({
required this.instrumentationService,
required this.context,
required this.applyTemporaryOverlayEdits,
required this.applyOverlays,
this.cancellationToken,
});
bool get isCancelled => cancellationToken?.isCancellationRequested ?? false;
/// The number of passes that produced edits.
int get passesWithEdits => _passesWithEdits;
Future<List<SourceFileEdit>> fixErrorsForFile(
OperationPerformanceImpl performance,
String path, {
required bool autoTriggered,
}) async {
return performance.runAsync('IterativeBulkFixProcessor.fixErrorsForFile',
(performance) async {
var changes = <SourceFileEdit>[];
_passesWithEdits = 0;
for (var i = 0; i < maxPasses; i++) {
var workspace = DartChangeWorkspace([context.currentSession]);
var processor = BulkFixProcessor(instrumentationService, workspace,
cancellationToken: cancellationToken);
var builder = await performance.runAsync(
'BulkFixProcessor.fixErrorsForFile pass $i',
(performance) => processor.fixErrorsForFile(
performance, context, path,
autoTriggered: autoTriggered),
);
if (isCancelled) {
return [];
}
var change = builder.sourceChange;
// If this pass made no changes, we don't need to do anything more.
if (change.edits.isEmpty) {
break;
}
// Record these changes in the results.
changes.addAll(change.edits);
_passesWithEdits++;
// Also apply them to the overlay provider so the next iteration can
// use them.
await performance.runAsync('Apply edits from pass $i', (_) async {
for (var fileEdit in change.edits) {
applyTemporaryOverlayEdits(fileEdit);
}
await applyOverlays();
});
if (isCancelled) {
return [];
}
}
return changes;
});
}
}
class PubspecFixRequestResult {
final List<SourceFileEdit> edits;
final List<BulkFix> details;
PubspecFixRequestResult(this.edits, this.details);
}
class _PubspecDeps {
final Set<String> packages = <String>{};
final Set<String> devPackages = <String>{};
}
extension on int {
String get isAre => this == 1 ? 'is' : 'are';
}