blob: fc1139a4b93888c9c561a764f458c63d67156742 [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/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: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/driver.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:analyzer/src/generated/source.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 path in params.included) {
if (!server.isValidFilePath(path)) {
return new Response.invalidFilePathFormat(request, path);
}
Resource res = resourceProvider.getResource(path);
if (!res.exists ||
!(contextManager.includedPaths.contains(path) ||
contextManager.isInAnalysisRoot(path))) {
return new Response.fileNotAnalyzed(request, path);
}
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));
}
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 (error.errorCode.type == ErrorType.SYNTACTIC_ERROR) {
hasErrors = true;
break;
}
}
}
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;
}
// Reporting
descriptionOfFixes = <String>[];
otherRecommendations = <String>[];
sourceChange = new SourceChange('dartfix');
for (LinterFix fix in fixes) {
await fix.applyFix();
}
return new EditDartfixResult(descriptionOfFixes, otherRecommendations,
hasErrors, sourceChange.edits)
.toResponse(request.id);
}
/// Return `true` if the path in within the set of `included` files
/// or is within an `included` directory.
bool isIncluded(String path) {
if (path != null) {
for (File file in fixFiles) {
if (file.path == path) {
return true;
}
}
for (Folder folder in fixFolders) {
if (folder.contains(path)) {
return true;
}
}
}
return false;
}
}
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);
@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 reportTypeErrorForNode(
ErrorCode errorCode, AstNode node, List<Object> arguments) {
// ignored
}
void applyFix();
}
class PreferMixinFix extends LinterFix {
final classesToConvert = new Set<Element>();
PreferMixinFix(EditDartFix dartFix) : super(dartFix);
@override
void reportErrorForNode(ErrorCode errorCode, AstNode node,
[List<Object> arguments]) {
TypeName type = node;
Element element = type.name.staticElement;
String path = element.source?.fullName;
if (path != null && dartFix.isIncluded(path)) {
classesToConvert.add(element);
}
}
@override
void applyFix() async {
for (Element elem in classesToConvert) {
await convertClassToMixin(elem);
}
}
void convertClassToMixin(Element elem) async {
String path = elem.source?.fullName;
AnalysisResult result = await dartFix.server.getAnalysisResult(path);
// TODO(danrubel): Verify that class can be converted
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);
if (assists.isNotEmpty) {
for (Assist assist in assists) {
dartFix.addFix(
'Convert class to mixin: ${elem.name}', 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 automatically convert ${elem.name} to a mixin'
' because the class contains a constructor.');
}
}
}
}
}