blob: 628955398de9c61525f634f13f8ff3e4fcb60904 [file] [log] [blame]
// Copyright (c) 2018, 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:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/edit/fix/dartfix_info.dart';
import 'package:analysis_server/src/edit/fix/dartfix_listener.dart';
import 'package:analysis_server/src/edit/fix/dartfix_registrar.dart';
import 'package:analysis_server/src/edit/fix/fix_code_task.dart';
import 'package:analysis_server/src/edit/fix/fix_error_task.dart';
import 'package:analysis_server/src/edit/fix/fix_lint_task.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/generated/engine.dart' show AnalysisOptionsImpl;
import 'package:collection/collection.dart';
class EditDartFix
with FixCodeProcessor, FixErrorProcessor, FixLintProcessor
implements DartFixRegistrar {
final AnalysisServer server;
final Request request;
final pkgFolders = <Folder>[];
final fixFolders = <Folder>[];
final fixFiles = <File>[];
DartFixListener listener;
EditDartFix(this.server, this.request) : listener = DartFixListener(server);
Future<Response> compute() async {
final params = EditDartfixParams.fromRequest(request);
// Determine the fixes to be applied
final fixInfo = <DartFixInfo>[];
if (params.includePedanticFixes == true) {
for (var fix in allFixes) {
if (fix.isPedantic && !fixInfo.contains(fix)) {
fixInfo.add(fix);
}
}
}
var includedFixes = params.includedFixes;
if (includedFixes != null) {
for (var key in includedFixes) {
var info = allFixes.firstWhereOrNull((i) => i.key == key);
if (info != null) {
fixInfo.add(info);
} else {
return Response.invalidParameter(
request, 'includedFixes', 'Unknown fix: $key');
}
}
}
var excludedFixes = params.excludedFixes;
if (excludedFixes != null) {
for (var key in excludedFixes) {
var info = allFixes.firstWhereOrNull((i) => i.key == key);
if (info != null) {
fixInfo.remove(info);
} else {
return Response.invalidParameter(
request, 'excludedFixes', 'Unknown fix: $key');
}
}
}
for (var info in fixInfo) {
info.setup(this, listener, params);
}
// Validate each included file and directory.
final resourceProvider = server.resourceProvider;
final contextManager = server.contextManager;
// Discard any existing analysis so that the linters set below will be
// used to generate errors that can then be fixed.
// TODO(danrubel): Rework to use a different approach if this command
// will be used from within the IDE.
contextManager.refresh();
for (var filePath in params.included) {
if (!server.isValidFilePath(filePath)) {
return Response.invalidFilePathFormat(request, filePath);
}
var analysisContext = contextManager.getContextFor(filePath);
if (analysisContext == null) {
return Response.fileNotAnalyzed(request, filePath);
}
var res = resourceProvider.getResource(filePath);
if (!res.exists) {
return Response.fileNotAnalyzed(request, filePath);
}
// Set the linters used during analysis. If this command is used from
// within an IDE, then this will cause the lint results to change.
// TODO(danrubel): Rework to use a different approach if this command
// will be used from within the IDE.
var driver = analysisContext.driver;
var analysisOptions = driver.analysisOptions as AnalysisOptionsImpl;
analysisOptions.lint = true;
analysisOptions.lintRules = linters;
var pkgFolder = analysisContext.contextRoot.root;
if (!pkgFolders.contains(pkgFolder)) {
pkgFolders.add(pkgFolder);
}
if (res is Folder) {
fixFolders.add(res);
} else {
fixFiles.add(res as File);
}
}
String? changedPath;
contextManager.driverMap.values.forEach((driver) {
// Setup a listener to remember the resource that changed during analysis
// so it can be reported if there is an InconsistentAnalysisException.
driver.onCurrentSessionAboutToBeDiscarded = (String? path) {
changedPath = path;
};
});
bool hasErrors;
try {
hasErrors = await runAllTasks();
} on InconsistentAnalysisException catch (_) {
// If a resource changed, report the problem without suggesting fixes
var changedMessage = changedPath != null
? 'resource changed during analysis: $changedPath'
: 'multiple resources changed during analysis.';
return EditDartfixResult(
[DartFixSuggestion('Analysis canceled because $changedMessage')],
listener.otherSuggestions,
false, // We may have errors, but we do not know, and it doesn't matter.
listener.sourceChange.edits,
details: listener.details,
).toResponse(request.id);
}
return EditDartfixResult(
listener.suggestions,
listener.otherSuggestions,
hasErrors,
listener.sourceChange.edits,
details: listener.details,
).toResponse(request.id);
}
Folder? findPkgFolder(Folder start) {
for (var folder in start.withAncestors) {
if (folder.getChild('analysis_options.yaml').exists ||
folder.getChild('pubspec.yaml').exists) {
return folder;
}
}
return null;
}
Set<String> getPathsToProcess() {
final contextManager = server.contextManager;
final resourceProvider = server.resourceProvider;
final resources = <Resource>[];
for (var rootPath in contextManager.includedPaths) {
resources.add(resourceProvider.getResource(rootPath));
}
var pathsToProcess = <String>{};
while (resources.isNotEmpty) {
var res = resources.removeLast();
if (res is Folder) {
for (var child in res.getChildren()) {
if (!child.shortName.startsWith('.') &&
server.isAnalyzed(child.path)) {
resources.add(child);
}
}
continue;
}
if (!isIncluded(res.path)) {
continue;
}
pathsToProcess.add(res.path);
}
return pathsToProcess;
}
/// Return `true` if the path in within the set of `included` files
/// or is within an `included` directory.
bool isIncluded(String filePath) {
for (var file in fixFiles) {
if (file.path == filePath) {
return true;
}
}
for (var folder in fixFolders) {
if (folder.contains(filePath)) {
return true;
}
}
return false;
}
/// Call the supplied [process] function to process each compilation unit.
Future processResources(
Future<void> Function(ResolvedUnitResult result) process) async {
final pathsToProcess = getPathsToProcess();
var pathsProcessed = <String>{};
for (var path in pathsToProcess) {
if (pathsProcessed.contains(path)) continue;
var driver = server.getAnalysisDriver(path);
if (driver != null) {
var result = await driver.getResolvedLibrary2(path);
if (result is ResolvedLibraryResult) {
for (var unit in result.units!) {
if (pathsToProcess.contains(unit.path) &&
!pathsProcessed.contains(unit.path)) {
await process(unit);
pathsProcessed.add(unit.path!);
}
}
break;
}
}
}
for (var path in pathsToProcess.difference(pathsProcessed)) {
var result = await server.getResolvedUnit(path);
if (result == null || result.unit == null) {
continue;
}
await process(result);
}
}
Future<bool> runAllTasks() async {
// Process each package
for (var pkgFolder in pkgFolders) {
await processPackage(pkgFolder);
}
var hasErrors = false;
// Process each source file.
try {
await processResources((ResolvedUnitResult result) async {
if (await processErrors(result)) {
hasErrors = true;
}
if (numPhases > 0) {
await processCodeTasks(0, result);
}
});
for (var phase = 1; phase < numPhases; phase++) {
await processResources((ResolvedUnitResult result) async {
await processCodeTasks(phase, result);
});
}
await finishCodeTasks();
} finally {
server.contextManager.driverMap.values
.forEach((d) => d.onCurrentSessionAboutToBeDiscarded = null);
}
return hasErrors;
}
}