blob: 59424b900e8795981995e670a9802eba78d48d3b [file] [log] [blame]
// 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();
// todo (pq): add go link once redirect is live (https://github.com/dart-lang/sdk/issues/44261)
static const String cmdDescription = '''Fix Dart source code.
This tool looks for and fixes analysis issues that have associated automated
fixes or issues that have associated package API migration information.
To use the tool, run one of:
- 'dart fix --dry-run' for a preview of the proposed changes for a project
- 'dart fix --apply' to apply the changes''';
/// This command is hidden as it's currently experimental.
FixCommand() : super(cmdName, cmdDescription, hidden: true) {
argParser.addFlag('dry-run',
abbr: 'n',
defaultsTo: false,
negatable: false,
help: 'Show which files would be modified 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: true,
);
}
@override
FutureOr<int> run() async {
log.stdout('\n${log.ansi.emphasized('Note:')} The `fix` command is '
'provisional and subject to change or removal in future releases.\n');
var dryRun = argResults['dry-run'];
var testMode = argResults['compare-to-golden'];
var apply = argResults['apply'];
var arguments = argResults.rest;
var argumentCount = arguments.length;
if (argumentCount > 1) {
usageException('Only one file or directory is expected.');
}
if (!apply && !dryRun && !testMode) {
printUsage();
return 0;
}
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);
}