blob: 8821d75524db1efcc6ba11a7276cb0115826661b [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/plugin/edit/assist/assist_core.dart';
import 'package:analysis_server/plugin/edit/assist/assist_dart.dart';
import 'package:analysis_server/plugin/edit/fix/fix_core.dart';
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/services/correction/assist.dart';
import 'package:analysis_server/src/services/correction/assist_internal.dart';
import 'package:analysis_server/src/services/correction/fix.dart';
import 'package:analysis_server/src/services/correction/fix_internal.dart';
import 'package:analyzer/analyzer.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/analysis/ast_provider_driver.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/generated/source.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/services/lint.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart'
show SourceChange, SourceEdit, SourceFileEdit;
import 'package:front_end/src/fasta/fasta_codes.dart';
import 'package:front_end/src/scanner/token.dart';
import 'package:source_span/src/span.dart';
class EditDartFix {
final AnalysisServer server;
final Request request;
final fixFolders = <Folder>[];
final fixFiles = <File>[];
List<String> descriptionOfFixes;
List<String> otherRecommendations;
SourceChange sourceChange;
EditDartFix(this.server, this.request);
void addFix(String description, SourceChange change) {
descriptionOfFixes.add(description);
for (SourceFileEdit fileEdit in change.edits) {
for (SourceEdit sourceEdit in fileEdit.edits) {
sourceChange.addEdit(fileEdit.file, fileEdit.fileStamp, sourceEdit);
}
}
}
void addRecommendation(String recommendation) {
otherRecommendations.add(recommendation);
}
Future<Response> compute() async {
final params = new EditDartfixParams.fromRequest(request);
// Validate each included file and directory.
final resourceProvider = server.resourceProvider;
final contextManager = server.contextManager;
for (String filePath in params.included) {
if (!server.isValidFilePath(filePath)) {
return new Response.invalidFilePathFormat(request, filePath);
}
Resource res = resourceProvider.getResource(filePath);
if (!res.exists ||
!(contextManager.includedPaths.contains(filePath) ||
contextManager.isInAnalysisRoot(filePath))) {
return new Response.fileNotAnalyzed(request, filePath);
}
if (res is Folder) {
fixFolders.add(res);
} else {
fixFiles.add(res);
}
}
// Get the desired lints
final LintRule preferMixin = Registry.ruleRegistry['prefer_mixin'];
if (preferMixin == null) {
return new Response.serverError(
request, 'Missing prefer_mixin lint', null);
}
final preferMixinFix = new PreferMixinFix(this);
preferMixin.reporter = preferMixinFix;
// Setup
final linters = <Linter>[
preferMixin,
];
final fixes = <LinterFix>[
preferMixinFix,
];
final visitors = <AstVisitor>[];
final registry = new NodeLintRegistry(false);
for (Linter linter in linters) {
final visitor = linter.getVisitor();
if (visitor != null) {
visitors.add(visitor);
}
if (linter is NodeLintRule) {
(linter as NodeLintRule).registerNodeProcessors(registry);
}
}
final AstVisitor astVisitor = visitors.isNotEmpty
? new ExceptionHandlingDelegatingAstVisitor(
visitors, ExceptionHandlingDelegatingAstVisitor.logException)
: null;
final AstVisitor linterVisitor = new LinterVisitor(
registry, ExceptionHandlingDelegatingAstVisitor.logException);
// TODO(danrubel): Determine if a lint is configured to run as part of
// standard analysis and use those results if available instead of
// running the lint again.
// Analyze each source file.
final resources = <Resource>[];
for (String rootPath in contextManager.includedPaths) {
resources.add(resourceProvider.getResource(rootPath));
}
descriptionOfFixes = <String>[];
otherRecommendations = <String>[];
sourceChange = new SourceChange('dartfix');
bool hasErrors = false;
while (resources.isNotEmpty) {
Resource res = resources.removeLast();
if (res is Folder) {
for (Resource child in res.getChildren()) {
if (!child.shortName.startsWith('.') &&
contextManager.isInAnalysisRoot(child.path)) {
resources.add(child);
}
}
continue;
}
AnalysisResult result = await server.getAnalysisResult(res.path);
CompilationUnit unit = result?.unit;
if (unit != null) {
if (!hasErrors) {
for (AnalysisError error in result.errors) {
if (!(await fixError(result, error))) {
if (error.errorCode.type == ErrorType.SYNTACTIC_ERROR) {
hasErrors = true;
}
}
}
}
Source source = result.sourceFactory.forUri2(result.uri);
for (Linter linter in linters) {
linter.reporter.source = source;
}
if (astVisitor != null) {
unit.accept(astVisitor);
}
unit.accept(linterVisitor);
}
}
// Cleanup
for (Linter linter in linters) {
linter.reporter = null;
}
// Apply distributed fixes
for (LinterFix fix in fixes) {
await fix.applyFix();
}
return new EditDartfixResult(descriptionOfFixes, otherRecommendations,
hasErrors, sourceChange.edits)
.toResponse(request.id);
}
Future<bool> fixError(AnalysisResult result, AnalysisError error) async {
if (error.errorCode ==
StaticTypeWarningCode.WRONG_NUMBER_OF_TYPE_ARGUMENTS_CONSTRUCTOR) {
// TODO(danrubel): Rather than comparing the error codes individually,
// it would be better if each error code could specify
// whether or not it could be fixed automatically.
// Fall through to calculate and apply the fix
} else {
// This error cannot be automatically fixed
return false;
}
final location = '${locationDescription(result, error.offset)}';
final dartContext = new DartFixContextImpl(
new FixContextImpl(
server.resourceProvider, result.driver, error, result.errors),
new AstProviderForDriver(result.driver),
result.unit);
final processor = new FixProcessor(dartContext);
Fix fix = await processor.computeFix();
if (fix != null) {
addFix('${fix.change.message} in $location', fix.change);
} else {
// TODO(danrubel): Determine why the fix could not be applied
// and report that in the description.
addRecommendation('Could not fix "${error.message}" in $location');
}
return true;
}
/// Return `true` if the path in within the set of `included` files
/// or is within an `included` directory.
bool isIncluded(String filePath) {
if (filePath != null) {
for (File file in fixFiles) {
if (file.path == filePath) {
return true;
}
}
for (Folder folder in fixFolders) {
if (folder.contains(filePath)) {
return true;
}
}
}
return false;
}
/// Return a human readable description of the specified offset and file.
String locationDescription(AnalysisResult result, int offset) {
// TODO(danrubel): Pass the location back to the client along with the
// message indicating what was or was not automatically fixed
// rather than interpreting and integrating the location into the message.
final description = new StringBuffer();
// Determine the relative path
for (Folder folder in fixFolders) {
if (folder.contains(result.path)) {
description.write(server.resourceProvider.pathContext
.relative(result.path, from: folder.path));
break;
}
}
if (description.isEmpty) {
description.write(result.path);
}
// Determine the line and column number
if (offset >= 0) {
final loc = result.unit.lineInfo.getLocation(offset);
description.write(':${loc.lineNumber}');
}
return description.toString();
}
}
class EditDartFixAssistContext implements DartAssistContext {
@override
final AnalysisDriver analysisDriver;
@override
final int selectionLength;
@override
final int selectionOffset;
@override
final Source source;
@override
final CompilationUnit unit;
EditDartFixAssistContext(
EditDartFix dartFix, this.source, this.unit, AstNode node)
: analysisDriver = dartFix.server.getAnalysisDriver(source.fullName),
selectionOffset = node.offset,
selectionLength = 0;
}
abstract class LinterFix implements ErrorReporter {
final EditDartFix dartFix;
@override
Source source;
LinterFix(this.dartFix);
Future<void> applyFix();
@override
void reportError(AnalysisError error) {
// ignored
}
@override
void reportErrorForElement(ErrorCode errorCode, Element element,
[List<Object> arguments]) {
// ignored
}
@override
void reportErrorForNode(ErrorCode errorCode, AstNode node,
[List<Object> arguments]) {
// ignored
}
@override
void reportErrorForOffset(ErrorCode errorCode, int offset, int length,
[List<Object> arguments]) {
// ignored
}
@override
void reportErrorForSpan(ErrorCode errorCode, SourceSpan span,
[List<Object> arguments]) {
// ignored
}
@override
void reportErrorForToken(ErrorCode errorCode, Token token,
[List<Object> arguments]) {
// ignored
}
@override
void reportErrorMessage(
ErrorCode errorCode, int offset, int length, Message message) {
// ignored
}
@override
void reportTypeErrorForNode(
ErrorCode errorCode, AstNode node, List<Object> arguments) {
// ignored
}
}
class PreferMixinFix extends LinterFix {
final classesToConvert = new Set<Element>();
PreferMixinFix(EditDartFix dartFix) : super(dartFix);
@override
Future<void> applyFix() async {
for (Element elem in classesToConvert) {
await convertClassToMixin(elem);
}
}
Future<void> convertClassToMixin(Element elem) async {
AnalysisResult result =
await dartFix.server.getAnalysisResult(elem.source?.fullName);
for (CompilationUnitMember declaration in result.unit.declarations) {
if (declaration is ClassOrMixinDeclaration &&
declaration.name.name == elem.name) {
AssistProcessor processor = new AssistProcessor(
new EditDartFixAssistContext(
dartFix, elem.source, result.unit, declaration.name));
List<Assist> assists = await processor
.computeAssist(DartAssistKind.CONVERT_CLASS_TO_MIXIN);
final location = dartFix.locationDescription(result, elem.nameOffset);
if (assists.isNotEmpty) {
for (Assist assist in assists) {
dartFix.addFix(
'Convert ${elem.displayName} to a mixin in $location',
assist.change);
}
} else {
// TODO(danrubel): If assists is empty, then determine why
// assist could not be performed and report that in the description.
dartFix.addRecommendation(
'Could not convert ${elem.displayName} to a mixin'
' because the class contains a constructor in $location');
}
}
}
}
@override
void reportErrorForNode(ErrorCode errorCode, AstNode node,
[List<Object> arguments]) {
TypeName type = node;
Element element = type.name.staticElement;
String filePath = element.source?.fullName;
if (filePath != null && dartFix.isIncluded(filePath)) {
classesToConvert.add(element);
}
}
}