| // Copyright (c) 2020, 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' as io; |
| |
| import 'package:analysis_server_client/protocol.dart' hide AnalysisError; |
| import 'package:intl/intl.dart'; |
| import 'package:path/path.dart' as path; |
| |
| import '../analysis_server.dart'; |
| import '../core.dart'; |
| import '../sdk.dart'; |
| import '../utils.dart'; |
| |
| class FixCommand extends DartdevCommand { |
| static const String cmdName = 'fix'; |
| |
| static final NumberFormat _numberFormat = NumberFormat.decimalPattern(); |
| |
| static const String cmdDescription = |
| '''Apply automated fixes to Dart source code. |
| |
| This tool looks for and fixes analysis issues that have associated automated fixes. |
| |
| To use the tool, run either ['dart fix --dry-run'] for a preview of the proposed changes for a project, or ['dart fix --apply'] to apply the changes. |
| |
| [Note:] $disclaimer'''; |
| |
| static const disclaimer = 'The `fix` command is under development and ' |
| 'subject to change before the next stable release. Feedback is welcome - ' |
| 'please file at https://github.com/dart-lang/sdk/issues.'; |
| |
| FixCommand({bool verbose = false}) : super(cmdName, cmdDescription) { |
| argParser.addFlag('dry-run', |
| abbr: 'n', |
| defaultsTo: false, |
| negatable: false, |
| help: 'Preview the proposed changes but make no changes.'); |
| argParser.addFlag( |
| 'apply', |
| defaultsTo: false, |
| negatable: false, |
| help: 'Apply the proposed changes.', |
| ); |
| argParser.addFlag( |
| 'compare-to-golden', |
| defaultsTo: false, |
| negatable: false, |
| help: |
| 'Compare the result of applying fixes to a golden file for testing.', |
| hide: !verbose, |
| ); |
| } |
| |
| @override |
| String get description { |
| if (log != null && log.ansi.useAnsi) { |
| return cmdDescription |
| .replaceAll('[', log.ansi.bold) |
| .replaceAll(']', log.ansi.none); |
| } else { |
| return cmdDescription.replaceAll('[', '').replaceAll(']', ''); |
| } |
| } |
| |
| @override |
| FutureOr<int> run() async { |
| var dryRun = argResults['dry-run']; |
| var testMode = argResults['compare-to-golden']; |
| var apply = argResults['apply']; |
| if (!apply && !dryRun && !testMode) { |
| printUsage(); |
| return 0; |
| } |
| |
| log.stdout('\n${log.ansi.emphasized('Note:')} $disclaimer\n'); |
| |
| var arguments = argResults.rest; |
| var argumentCount = arguments.length; |
| if (argumentCount > 1) { |
| usageException('Only one file or directory is expected.'); |
| } |
| |
| var dir = |
| argumentCount == 0 ? io.Directory.current : io.Directory(arguments[0]); |
| if (!dir.existsSync()) { |
| usageException("Directory doesn't exist: ${dir.path}"); |
| } |
| |
| var modeText = dryRun ? ' (dry run)' : ''; |
| |
| final projectName = path.basename(path.canonicalize(dir.path)); |
| var progress = log.progress( |
| 'Computing fixes in ${log.ansi.emphasized(projectName)}$modeText'); |
| |
| var server = AnalysisServer( |
| io.Directory(sdk.sdkPath), |
| dir, |
| ); |
| |
| await server.start(); |
| |
| EditBulkFixesResult fixes; |
| //ignore: unawaited_futures |
| server.onExit.then((int exitCode) { |
| if (fixes == null && exitCode != 0) { |
| progress?.cancel(); |
| io.exitCode = exitCode; |
| } |
| }); |
| |
| fixes = await server.requestBulkFixes(dir.absolute.path); |
| final List<SourceFileEdit> edits = fixes.edits; |
| |
| await server.shutdown(); |
| |
| progress.finish(showTiming: true); |
| |
| if (testMode) { |
| if (_compareFixes(edits)) { |
| return 1; |
| } |
| } else if (edits.isEmpty) { |
| log.stdout('Nothing to fix!'); |
| } else { |
| var details = fixes.details; |
| details.sort((f1, f2) => path |
| .relative(f1.path, from: dir.path) |
| .compareTo(path.relative(f2.path, from: dir.path))); |
| |
| var fileCount = 0; |
| var fixCount = 0; |
| |
| details.forEach((d) { |
| ++fileCount; |
| d.fixes.forEach((f) { |
| fixCount += f.occurrences; |
| }); |
| }); |
| |
| if (dryRun) { |
| log.stdout(''); |
| log.stdout('${_format(fixCount)} proposed ${_pluralFix(fixCount)} ' |
| 'in ${_format(fileCount)} ${pluralize("file", fileCount)}.'); |
| _printDetails(details, dir); |
| } else { |
| progress = log.progress('Applying fixes'); |
| _applyFixes(edits); |
| progress.finish(showTiming: true); |
| _printDetails(details, dir); |
| log.stdout('${_format(fixCount)} ${_pluralFix(fixCount)} made in ' |
| '${_format(fileCount)} ${pluralize("file", fileCount)}.'); |
| } |
| } |
| |
| return 0; |
| } |
| |
| void _applyFixes(List<SourceFileEdit> edits) { |
| for (var edit in edits) { |
| var fileName = edit.file; |
| var file = io.File(fileName); |
| var code = file.existsSync() ? file.readAsStringSync() : ''; |
| code = SourceEdit.applySequence(code, edit.edits); |
| file.writeAsStringSync(code); |
| } |
| } |
| |
| /// Return `true` if any of the fixes fail to create the same content as is |
| /// found in the golden file. |
| bool _compareFixes(List<SourceFileEdit> edits) { |
| var passCount = 0; |
| var failCount = 0; |
| for (var edit in edits) { |
| var filePath = edit.file; |
| var baseName = path.basename(filePath); |
| var expectFileName = baseName + '.expect'; |
| var expectFilePath = path.join(path.dirname(filePath), expectFileName); |
| try { |
| var originalCode = io.File(filePath).readAsStringSync(); |
| var expectedCode = io.File(expectFilePath).readAsStringSync(); |
| var actualCode = SourceEdit.applySequence(originalCode, edit.edits); |
| if (actualCode != expectedCode) { |
| failCount++; |
| _reportFailure(filePath, actualCode, expectedCode); |
| } else { |
| passCount++; |
| } |
| } on io.FileSystemException { |
| // Ignored for now. |
| } |
| } |
| log.stdout('Passed: $passCount, Failed: $failCount'); |
| return failCount > 0; |
| } |
| |
| String _pluralFix(int count) => count == 1 ? 'fix' : 'fixes'; |
| |
| void _printDetails(List<BulkFix> details, io.Directory workingDir) { |
| log.stdout(''); |
| |
| final bullet = log.ansi.bullet; |
| |
| for (var detail in details) { |
| log.stdout(path.relative(detail.path, from: workingDir.path)); |
| final fixes = detail.fixes.toList(); |
| fixes.sort((a, b) => a.code.compareTo(b.code)); |
| for (var fix in fixes) { |
| log.stdout(' ${fix.code} $bullet ' |
| '${_format(fix.occurrences)} ${_pluralFix(fix.occurrences)}'); |
| } |
| log.stdout(''); |
| } |
| } |
| |
| /// Report that the [actualCode] produced by applying fixes to the content of |
| /// [filePath] did not match the [expectedCode]. |
| void _reportFailure(String filePath, String actualCode, String expectedCode) { |
| log.stdout('Failed when applying fixes to $filePath'); |
| log.stdout('Expected:'); |
| log.stdout(expectedCode); |
| log.stdout(''); |
| log.stdout('Actual:'); |
| log.stdout(actualCode); |
| } |
| |
| static String _format(int value) => _numberFormat.format(value); |
| } |