blob: ee9347675bfddc49b113fe2133072b64bd4ddfd2 [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 'dart:async';
import 'dart:io' show File, Platform;
import 'package:analysis_server_client/handler/connection_handler.dart';
import 'package:analysis_server_client/handler/notification_handler.dart';
import 'package:analysis_server_client/listener/server_listener.dart';
import 'package:analysis_server_client/protocol.dart';
import 'package:analysis_server_client/server.dart';
import 'package:cli_util/cli_logging.dart';
import 'package:dartfix/handler/analysis_complete_handler.dart';
import 'package:dartfix/listener/bad_message_listener.dart';
import 'package:dartfix/src/context.dart';
import 'package:dartfix/src/options.dart';
import 'package:dartfix/src/util.dart';
import 'package:path/path.dart' as path;
import 'package:pub_semver/pub_semver.dart';
class Driver {
Context context;
_Handler handler;
Logger logger;
Server server;
bool force;
bool overwrite;
List<String> targets;
EditDartfixResult result;
Ansi get ansi => logger.ansi;
Future applyFixes() async {
showDescriptions('Recommended changes that cannot be automatically applied',
result.otherSuggestions);
showDetails(result.details);
if (result.edits.isEmpty) {
logger.stdout('');
logger.stdout(result.otherSuggestions.isNotEmpty
? 'None of the recommended changes can be automatically applied.'
: 'No recommended changes.');
return;
}
logger.stdout('');
logger.stdout(ansi.emphasized('Files to be changed:'));
for (SourceFileEdit fileEdit in result.edits) {
logger.stdout(' ${_relativePath(fileEdit.file)}');
}
if (checkIfChangesShouldBeApplied(result)) {
for (SourceFileEdit fileEdit in result.edits) {
final file = File(fileEdit.file);
String code = file.existsSync() ? file.readAsStringSync() : '';
code = SourceEdit.applySequence(code, fileEdit.edits);
await file.writeAsString(code);
}
logger.stdout(ansi.emphasized('Changes applied.'));
}
}
bool checkIfChangesShouldBeApplied(EditDartfixResult result) {
logger.stdout('');
if (result.hasErrors) {
logger.stdout('WARNING: The analyzed source contains errors'
' that may affect the accuracy of these changes.');
logger.stdout('');
if (!force) {
logger.stdout('Rerun with --$forceOption to apply these changes.');
return false;
}
} else if (!overwrite && !force) {
logger.stdout('Rerun with --$overwriteOption to apply these changes.');
return false;
}
return true;
}
/// Check if the specified options is supported by the version of analysis
/// server being run and return `true` if they are.
/// Display an error message and return `false` if not.
bool checkIfSelectedOptionsAreSupported(Options options) {
if (handler.serverProtocolVersion.compareTo(Version(1, 27, 2)) >= 0) {
return true;
}
if (options.pedanticFixes) {
_unsupportedOption(pedanticOption);
return false;
}
if (handler.serverProtocolVersion.compareTo(Version(1, 22, 2)) >= 0) {
return true;
}
if (options.excludeFixes.isNotEmpty) {
_unsupportedOption(excludeFixOption);
return false;
}
if (options.includeFixes.isNotEmpty) {
_unsupportedOption(includeFixOption);
return false;
}
if (options.requiredFixes) {
_unsupportedOption(requiredOption);
return false;
}
if (options.showHelp) {
return false;
}
return true;
}
Future<EditDartfixResult> requestFixes(
Options options, {
Progress progress,
}) async {
logger.trace('Requesting fixes');
Future isAnalysisComplete = handler.analysisComplete();
final params = EditDartfixParams(options.targets);
if (options.excludeFixes.isNotEmpty) {
params.excludedFixes = options.excludeFixes;
}
if (options.includeFixes.isNotEmpty) {
params.includedFixes = options.includeFixes;
}
if (options.requiredFixes) {
params.includeRequiredFixes = true;
}
if (options.pedanticFixes) {
params.includePedanticFixes = true;
}
String dir = options.outputDir;
if (dir != null) {
if (!path.isAbsolute(dir)) {
dir = path.absolute(dir);
}
dir = path.canonicalize(dir);
params.outputDir = dir;
}
Map<String, dynamic> json =
await server.send(EDIT_REQUEST_DARTFIX, params.toJson());
// TODO(danrubel): This is imprecise signal for determining when all
// analysis error notifications have been received. Consider adding a new
// notification indicating that the server is idle (all requests processed,
// all analysis complete, all notifications sent).
await isAnalysisComplete;
progress.finish(showTiming: true);
ResponseDecoder decoder = ResponseDecoder(null);
return EditDartfixResult.fromJson(decoder, 'result', json);
}
void showDescriptions(String title, List<DartFixSuggestion> suggestions) {
if (suggestions.isNotEmpty) {
logger.stdout('');
logger.stdout(ansi.emphasized('$title:'));
List<DartFixSuggestion> sorted = List.from(suggestions)
..sort(compareSuggestions);
for (DartFixSuggestion suggestion in sorted) {
final msg = StringBuffer();
msg.write(' ${toSentenceFragment(suggestion.description)}');
final loc = suggestion.location;
if (loc != null) {
msg.write(' • ${_relativePath(loc.file)}');
msg.write(' • ${loc.startLine}:${loc.startColumn}');
}
logger.stdout(msg.toString());
}
}
}
void showDetails(List<String> details) {
if (details == null || details.isEmpty) {
return;
}
logger.stdout('''
Analysis Details:
''');
for (String detail in details) {
logger.stdout('''
• $detail
''');
}
}
void showFix(DartFix fix) {
logger.stdout('''
• ${ansi.emphasized(fix.name)}''');
if (fix.description != null) {
for (String line in _indentAndWrapDescription(fix.description)) {
logger.stdout(line);
}
}
}
Future<EditGetDartfixInfoResult> showFixes({Progress progress}) async {
Map<String, dynamic> json = await server.send(
EDIT_REQUEST_GET_DARTFIX_INFO, EditGetDartfixInfoParams().toJson());
progress?.finish(showTiming: true);
ResponseDecoder decoder = ResponseDecoder(null);
final result = EditGetDartfixInfoResult.fromJson(decoder, 'result', json);
final fixes = List<DartFix>.from(result.fixes)
..sort((f1, f2) => f1.name.compareTo(f2.name));
logger.stdout('''
The following fixes are automatically applied unless at least one --$includeFixOption option is specified
(and --$requiredOption is not specified). They may be individually disabled using --$excludeFixOption.''');
fixes.where((fix) => fix.isRequired).forEach(showFix);
logger.stdout('''
These fixes are NOT automatically applied, but may be enabled using --$includeFixOption:''');
fixes.where((fix) => !fix.isRequired).toList()
..sort(compareFixes)
..forEach(showFix);
return result;
}
Future start(
List<String> args, {
Context testContext,
Logger testLogger,
}) async {
final Options options = Options.parse(args, testContext, testLogger);
force = options.force;
overwrite = options.overwrite;
targets = options.targets;
context = testContext ?? options.context;
logger = testLogger ?? options.logger;
server = Server(listener: _Listener(logger));
handler = _Handler(this);
// Start showing progress before we start the analysis server.
Progress progress;
if (options.showHelp) {
progress = logger.progress('${ansi.emphasized('Listing fixes')}');
} else {
progress = logger.progress('${ansi.emphasized('Calculating fixes')}');
}
if (!await startServer(options)) {
context.exit(16);
}
if (!checkIfSelectedOptionsAreSupported(options)) {
await server.stop();
context.exit(1);
}
if (options.showHelp) {
try {
await showFixes(progress: progress);
} finally {
await server.stop();
}
context.exit(0);
}
Future serverStopped;
try {
await startServerAnalysis(options);
result = await requestFixes(options, progress: progress);
serverStopped = server.stop();
await applyFixes();
await serverStopped;
} finally {
// If we didn't already try to stop the server, then stop it now.
if (serverStopped == null) {
await server.stop();
}
}
}
Future<bool> startServer(Options options) async {
// Automatically run analysis server from source
// if this command line tool is being run from source within the SDK repo.
String serverPath = options.serverSnapshot ?? findServerPath();
if (options.verbose) {
logger.trace('''
Dart SDK version ${Platform.version}
${Platform.resolvedExecutable}
dartfix
${Platform.script.toFilePath()}
analysis server
$serverPath
''');
}
await server.start(
clientId: 'dartfix',
clientVersion: 'unspecified',
sdkPath: options.sdkPath,
serverPath: serverPath,
);
server.listenToOutput(notificationProcessor: handler.handleEvent);
return handler.serverConnected(timeLimit: const Duration(seconds: 30));
}
Future<Progress> startServerAnalysis(
Options options, {
Progress progress,
}) async {
logger.trace('');
logger.trace('Setup analysis');
await server.send(SERVER_REQUEST_SET_SUBSCRIPTIONS,
ServerSetSubscriptionsParams([ServerService.STATUS]).toJson());
await server.send(
ANALYSIS_REQUEST_SET_ANALYSIS_ROOTS,
AnalysisSetAnalysisRootsParams(
options.targets,
const [],
).toJson());
return progress;
}
List<String> _indentAndWrapDescription(String description) =>
description.split('\n').map((line) => ' $line').toList();
String _relativePath(String filePath) {
for (String target in targets) {
if (filePath.startsWith(target)) {
return filePath.substring(target.length + 1);
}
}
return filePath;
}
void _unsupportedOption(String option) {
final version = handler.serverProtocolVersion.toString();
logger.stderr('''
The --$option option is not supported by analysis server version $version.
Please upgrade to a newer version of the Dart SDK to use this option.''');
}
}
class _Handler
with NotificationHandler, ConnectionHandler, AnalysisCompleteHandler {
final Driver driver;
final Logger logger;
final Server server;
Version serverProtocolVersion;
_Handler(this.driver)
: logger = driver.logger,
server = driver.server;
@override
bool checkServerProtocolVersion(Version version) {
serverProtocolVersion = version;
return super.checkServerProtocolVersion(version);
}
@override
void onAnalysisErrors(AnalysisErrorsParams params) {
List<AnalysisError> errors = params.errors;
bool foundAtLeastOneError = false;
for (AnalysisError error in errors) {
if (shouldShowError(error)) {
if (!foundAtLeastOneError) {
foundAtLeastOneError = true;
logger.stdout('${driver._relativePath(params.file)}:');
}
Location loc = error.location;
logger.stdout(' ${toSentenceFragment(error.message)}'
' • ${loc.startLine}:${loc.startColumn}');
}
}
}
@override
void onFailedToConnect() {
logger.stderr('Failed to connect to server');
}
@override
void onProtocolNotSupported(Version version) {
logger.stderr('Expected protocol version $PROTOCOL_VERSION,'
' but found $version');
final expectedVersion = Version.parse(PROTOCOL_VERSION);
if (version > expectedVersion) {
logger.stdout('''
This version of dartfix is incompatible with the current Dart SDK.
Try installing a newer version of dartfix by running
pub global activate dartfix
''');
} else {
logger.stdout('''
This version of dartfix is too new to be used with the current Dart SDK.
Try upgrading the Dart SDK to a newer version
or installing an older version of dartfix using
pub global activate dartfix <version>
''');
}
}
@override
void onServerError(ServerErrorParams params) {
if (params.isFatal) {
logger.stderr('Fatal Server Error: ${params.message}');
} else {
logger.stderr('Server Error: ${params.message}');
}
if (params.stackTrace != null) {
logger.stderr(params.stackTrace);
}
super.onServerError(params);
}
}
class _Listener with ServerListener, BadMessageListener {
final Logger logger;
final bool verbose;
_Listener(this.logger) : verbose = logger.isVerbose;
@override
void log(String prefix, String details) {
if (verbose) {
logger.trace('$prefix $details');
}
}
}