diff --git a/pkg/analysis_server/lib/protocol/protocol_generated.dart b/pkg/analysis_server/lib/protocol/protocol_generated.dart index 947615a643c..0d1bf0ca169 100644 --- a/pkg/analysis_server/lib/protocol/protocol_generated.dart +++ b/pkg/analysis_server/lib/protocol/protocol_generated.dart @@ -6180,7 +6180,7 @@ class EditDartfixParams implements RequestParams { /** * A list of the files and directories for which edits should be suggested. * - * If a request is made with path that is invalid, e.g. is not absolute and + * If a request is made with a path that is invalid, e.g. is not absolute and * normalized, an error of type INVALID_FILE_PATH_FORMAT will be generated. * If a request is made for a file which does not exist, or which is not * currently subject to analysis (e.g. because it is not associated with any @@ -6192,7 +6192,7 @@ class EditDartfixParams implements RequestParams { /** * A list of the files and directories for which edits should be suggested. * - * If a request is made with path that is invalid, e.g. is not absolute and + * If a request is made with a path that is invalid, e.g. is not absolute and * normalized, an error of type INVALID_FILE_PATH_FORMAT will be generated. * If a request is made for a file which does not exist, or which is not * currently subject to analysis (e.g. because it is not associated with any diff --git a/pkg/analysis_server/lib/src/edit/edit_dartfix.dart b/pkg/analysis_server/lib/src/edit/edit_dartfix.dart index 336cedde79e..0aa72a1d67e 100644 --- a/pkg/analysis_server/lib/src/edit/edit_dartfix.dart +++ b/pkg/analysis_server/lib/src/edit/edit_dartfix.dart @@ -1,10 +1,14 @@ -// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// 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'; @@ -14,6 +18,8 @@ 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/base/source.dart'; import 'package:front_end/src/scanner/token.dart'; import 'package:source_span/src/span.dart'; @@ -24,8 +30,23 @@ class EditDartFix { final fixFolders = []; final fixFiles = []; + List descriptions; + SourceChange sourceChange; + EditDartFix(this.server, this.request); + void addResult(String description, [SourceChange change]) { + assert(description != null && description.isNotEmpty); + descriptions.add(description); + if (change != null) { + for (SourceFileEdit fileEdit in change.edits) { + for (SourceEdit sourceEdit in fileEdit.edits) { + sourceChange.addEdit(fileEdit.file, fileEdit.fileStamp, sourceEdit); + } + } + } + } + Future compute() async { final params = new EditDartfixParams.fromRequest(request); @@ -53,7 +74,7 @@ class EditDartFix { final LintRule preferMixin = Registry.ruleRegistry['prefer_mixin']; if (preferMixin == null) { return new Response.serverError( - request, 'Missing PreferMixin lint', null); + request, 'Missing prefer_mixin lint', null); } final preferMixinFix = new PreferMixinFix(this); preferMixin.reporter = preferMixinFix; @@ -123,11 +144,13 @@ class EditDartFix { } // Reporting - final descriptions = []; + descriptions = []; + sourceChange = new SourceChange('dartfix'); for (LinterFix fix in fixes) { - fix.updateResponse(descriptions); + await fix.applyFix(); } - return new EditDartfixResult(descriptions, []).toResponse(request.id); + return new EditDartfixResult(descriptions, sourceChange.edits) + .toResponse(request.id); } /// Return `true` if the path in within the set of `included` files @@ -149,6 +172,29 @@ class EditDartFix { } } +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; @@ -198,7 +244,7 @@ abstract class LinterFix implements ErrorReporter { // ignored } - void updateResponse(List descriptions); + void applyFix(); } class PreferMixinFix extends LinterFix { @@ -212,18 +258,44 @@ class PreferMixinFix extends LinterFix { TypeName type = node; Element element = type.name.staticElement; String path = element.source?.fullName; - if (dartFix.isIncluded(path)) { - // Only report classes that are `included` + if (path != null && dartFix.isIncluded(path)) { classesToConvert.add(element); } } @override - void updateResponse(List descriptions) { - final sorted = classesToConvert.toList() - ..sort((c1, c2) => c1.name.compareTo(c2.name)); - for (Element elem in sorted) { - descriptions.add('Convert class to mixin: ${elem.name}'); + 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 assists = + await processor.compute(DartAssistKind.CONVERT_CLASS_TO_MIXIN); + if (assists.isNotEmpty) { + for (Assist assist in assists) { + dartFix.addResult( + '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.addResult( + 'Could not automatically convert ${elem.name} to a mixin' + ' because the class contains a constructor.'); + } + } } } } diff --git a/pkg/analysis_server/lib/src/services/correction/assist_internal.dart b/pkg/analysis_server/lib/src/services/correction/assist_internal.dart index 2dad5df1bbd..5f083c57667 100644 --- a/pkg/analysis_server/lib/src/services/correction/assist_internal.dart +++ b/pkg/analysis_server/lib/src/services/correction/assist_internal.dart @@ -109,7 +109,7 @@ class AssistProcessor { return _typeProvider; } - Future> compute() async { + Future> compute([AssistKind assistKind]) async { // TODO(brianwilkerson) Determine whether this await is necessary. await null; try { @@ -123,6 +123,13 @@ class AssistProcessor { return assists; } + // Calculate only specific assists for edit.dartFix + if (assistKind == DartAssistKind.CONVERT_CLASS_TO_MIXIN) { + await _addProposal_convertClassToMixin(); + return assists; + } + + // Calculate all assists await _addProposal_addTypeAnnotation_DeclaredIdentifier(); await _addProposal_addTypeAnnotation_SimpleFormalParameter(); await _addProposal_addTypeAnnotation_VariableDeclaration(); diff --git a/pkg/analysis_server/test/integration/support/integration_test_methods.dart b/pkg/analysis_server/test/integration/support/integration_test_methods.dart index 9d91f7514ff..ae805f47707 100644 --- a/pkg/analysis_server/test/integration/support/integration_test_methods.dart +++ b/pkg/analysis_server/test/integration/support/integration_test_methods.dart @@ -1506,12 +1506,13 @@ abstract class IntegrationTestMixin { * * A list of the files and directories for which edits should be suggested. * - * If a request is made with path that is invalid, e.g. is not absolute and - * normalized, an error of type INVALID_FILE_PATH_FORMAT will be generated. - * If a request is made for a file which does not exist, or which is not - * currently subject to analysis (e.g. because it is not associated with - * any analysis root specified to analysis.setAnalysisRoots), an error of - * type FILE_NOT_ANALYZED will be generated. + * If a request is made with a path that is invalid, e.g. is not absolute + * and normalized, an error of type INVALID_FILE_PATH_FORMAT will be + * generated. If a request is made for a file which does not exist, or + * which is not currently subject to analysis (e.g. because it is not + * associated with any analysis root specified to + * analysis.setAnalysisRoots), an error of type FILE_NOT_ANALYZED will be + * generated. * * Returns * diff --git a/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java b/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java index b01778110d1..ed29103b55d 100644 --- a/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java +++ b/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java @@ -413,11 +413,11 @@ public interface AnalysisServer { * if a change in a specified source requires it. * * @param included A list of the files and directories for which edits should be suggested. If a - * request is made with path that is invalid, e.g. is not absolute and normalized, an error - * of type INVALID_FILE_PATH_FORMAT will be generated. If a request is made for a file - * which does not exist, or which is not currently subject to analysis (e.g. because it is - * not associated with any analysis root specified to analysis.setAnalysisRoots), an error - * of type FILE_NOT_ANALYZED will be generated. + * request is made with a path that is invalid, e.g. is not absolute and normalized, an + * error of type INVALID_FILE_PATH_FORMAT will be generated. If a request is made for a + * file which does not exist, or which is not currently subject to analysis (e.g. because + * it is not associated with any analysis root specified to analysis.setAnalysisRoots), an + * error of type FILE_NOT_ANALYZED will be generated. */ public void edit_dartfix(List included, DartfixConsumer consumer); diff --git a/pkg/analysis_server/tool/spec/spec_input.html b/pkg/analysis_server/tool/spec/spec_input.html index 7469fc5e767..c854f7baa5e 100644 --- a/pkg/analysis_server/tool/spec/spec_input.html +++ b/pkg/analysis_server/tool/spec/spec_input.html @@ -1949,7 +1949,7 @@ A list of the files and directories for which edits should be suggested.

- If a request is made with path that is invalid, e.g. is not absolute and normalized, + If a request is made with a path that is invalid, e.g. is not absolute and normalized, an error of type INVALID_FILE_PATH_FORMAT will be generated. If a request is made for a file which does not exist, or which is not currently subject to analysis (e.g. because it is not associated with any analysis root specified to analysis.setAnalysisRoots), diff --git a/pkg/analyzer_cli/lib/src/fix/driver.dart b/pkg/analyzer_cli/lib/src/fix/driver.dart index 1e022a72ad2..0a03c13961c 100644 --- a/pkg/analyzer_cli/lib/src/fix/driver.dart +++ b/pkg/analyzer_cli/lib/src/fix/driver.dart @@ -18,9 +18,9 @@ class Driver { Completer serverConnected; Completer analysisComplete; - bool processAnalysisErrors; - int errorCount; bool verbose; + static const progressThreshold = 10; + int progressCount = progressThreshold; Future start(List args) async { final options = Options.parse(args); @@ -31,11 +31,12 @@ class Driver { } verbose = options.verbose; + EditDartfixResult result; await startServer(options); bool normalShutdown = false; try { - await performAnalysis(options); - await requestFixes(options); + await setupAnalysis(options); + result = await requestFixes(options); normalShutdown = true; } finally { try { @@ -46,6 +47,9 @@ class Driver { } } } + if (result != null) { + applyFixes(result); + } } Future startServer(Options options) async { @@ -63,11 +67,7 @@ class Driver { }); } - Future performAnalysis(Options options) async { - analysisComplete = new Completer(); - processAnalysisErrors = true; - errorCount = 0; - outSink.writeln('Analyzing...'); + Future setupAnalysis(Options options) async { verboseOut('Setup analysis'); await server.send(SERVER_REQUEST_SET_SUBSCRIPTIONS, new ServerSetSubscriptionsParams([ServerService.STATUS]).toJson()); @@ -77,29 +77,19 @@ class Driver { options.analysisRoots, const [], ).toJson()); - await analysisComplete.future; - analysisComplete = null; - processAnalysisErrors = false; - verboseOut('Analysis complete.'); - if (errorCount > 0) { - // TODO: Ask "Do you want to continue given # of errors?" - } } - Future requestFixes(Options options) async { - outSink.writeln('Calculating fixes...'); + Future requestFixes(Options options) async { + outSink.write('Calculating fixes...'); + verboseOut(''); + analysisComplete = new Completer(); Map json = await server.send(EDIT_REQUEST_DARTFIX, new EditDartfixParams(options.analysisRoots).toJson()); + await analysisComplete.future; + analysisComplete = null; + resetProgress(); ResponseDecoder decoder = new ResponseDecoder(null); - final result = EditDartfixResult.fromJson(decoder, 'result', json); - if (result.description.isNotEmpty) { - outSink.writeln('Recommended changes:'); - for (String line in result.description) { - outSink.writeln(line); - } - } else { - outSink.writeln('No recommended changes.'); - } + return EditDartfixResult.fromJson(decoder, 'result', json); } Future stopServer(Server server) async { @@ -114,6 +104,31 @@ class Driver { }); } + Future applyFixes(EditDartfixResult result) async { + // TODO(danrubel): Add flag to dartfix result indicating whether there + // were any errors detected that would compromise the suggested edits. + // If this flag is true, then the changes should not be applied + // or the user should be asked if they want to apply the changes. + if (!result.description.isNotEmpty) { + outSink.writeln('No recommended changes.'); + return; + } + outSink.writeln('Recommended changes:'); + List sorted = new List.from(result.description)..sort(); + for (String line in sorted) { + outSink.writeln(line); + } + // TODO(danrubel): Apply the fixes rather than displaying them. + outSink.writeln('=== Source edits:'); + for (SourceFileEdit fileEdit in result.fixes) { + outSink.writeln(fileEdit.file); + for (SourceEdit sourceEdit in fileEdit.edits) { + outSink.writeln( + ' ${sourceEdit.offset} ${sourceEdit.length} ${sourceEdit.replacement}'); + } + } + } + /** * Dispatch the notification named [event], and containing parameters * [params], to the appropriate stream. @@ -144,10 +159,8 @@ class Driver { // decoder, 'params', params)); // break; case ANALYSIS_NOTIFICATION_ERRORS: - if (processAnalysisErrors) { - onAnalysisErrors( - new AnalysisErrorsParams.fromJson(decoder, 'params', params)); - } + onAnalysisErrors( + new AnalysisErrorsParams.fromJson(decoder, 'params', params)); break; // case ANALYSIS_NOTIFICATION_FLUSH_RESULTS: // outOfTestExpect(params, isAnalysisFlushResultsParams); @@ -223,15 +236,15 @@ class Driver { void onAnalysisErrors(AnalysisErrorsParams params) { List errors = params.errors; if (errors.isNotEmpty) { + resetProgress(); outSink.writeln(params.file); for (AnalysisError error in errors) { - if (error.severity == AnalysisErrorSeverity.ERROR) { - ++errorCount; - } Location loc = error.location; outSink.writeln(' ${error.message}' ' at ${loc.startLine}:${loc.startColumn}'); } + } else { + showProgress(); } } @@ -260,6 +273,20 @@ class Driver { } } + void resetProgress() { + if (!verbose && progressCount >= progressThreshold) { + outSink.writeln(); + } + progressCount = 0; + } + + void showProgress() { + if (!verbose && progressCount % progressThreshold == 0) { + outSink.write('.'); + } + ++progressCount; + } + void verboseOut(String message) { if (verbose) { outSink.writeln(message);