blob: 5e533f456c309ee180cb350921d9bcc8db8d59b4 [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:cli_util/cli_logging.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import '../analysis_server.dart';
import '../core.dart';
import '../sdk.dart';
import '../utils.dart';
class AnalyzeCommand extends DartdevCommand {
static const String cmdName = 'analyze';
/// The maximum length of any of the existing severity labels.
static const int _severityWidth = 7;
/// The number of spaces needed to indent follow-on lines (the body) under the
/// message. The width left for the severity label plus the separator width.
static const int _bodyIndentWidth = _severityWidth + 3;
static final String _bodyIndent = ' ' * _bodyIndentWidth;
static final int _pipeCodeUnit = '|'.codeUnitAt(0);
static final int _slashCodeUnit = '\\'.codeUnitAt(0);
static final int _newline = '\n'.codeUnitAt(0);
static final int _return = '\r'.codeUnitAt(0);
AnalyzeCommand({bool verbose = false})
: super(cmdName, "Analyze the project's Dart code.") {
argParser
..addFlag('fatal-infos',
help: 'Treat info level issues as fatal.', negatable: false)
..addFlag('fatal-warnings',
help: 'Treat warning level issues as fatal.', defaultsTo: true)
// Options hidden by default.
..addOption(
'format',
valueHelp: 'value',
help: 'Specifies the format to display errors.',
allowed: ['default', 'machine'],
allowedHelp: {
'default':
'The default output format. This format is intended to be user '
'consumable.\nThe format is not specified and can change '
'between releases.',
'machine': 'A machine readable output. The format is:\n\n'
'SEVERITY|TYPE|ERROR_CODE|FILE_PATH|LINE|COLUMN|LENGTH|ERROR_MESSAGE\n\n'
'Note that the pipe character is escaped with backslashes for '
'the file path and error message fields.',
},
hide: !verbose,
);
}
@override
String get invocation => '${super.invocation} [<directory>]';
@override
FutureOr<int> run() async {
if (argResults.rest.length > 1) {
usageException('Only one directory is expected.');
}
// find directory from argResults.rest
var dir = argResults.rest.isEmpty
? io.Directory.current
: io.Directory(argResults.rest.single);
if (!dir.existsSync()) {
usageException("Directory doesn't exist: ${dir.path}");
}
final List<AnalysisError> errors = <AnalysisError>[];
final machineFormat = argResults['format'] == 'machine';
var progress = machineFormat
? null
: log.progress('Analyzing ${path.basename(dir.path)}');
final AnalysisServer server = AnalysisServer(
io.Directory(sdk.sdkPath),
dir,
);
server.onErrors.listen((FileAnalysisErrors fileErrors) {
// Record the issues found (but filter out to do comments).
errors.addAll(fileErrors.errors
.where((AnalysisError error) => error.type != 'TODO'));
});
await server.start();
bool analysisFinished = false;
// ignore: unawaited_futures
server.onExit.then((int exitCode) {
if (!analysisFinished) {
io.exitCode = exitCode;
}
});
await server.analysisFinished;
analysisFinished = true;
await server.shutdown(timeout: Duration(milliseconds: 100));
progress?.finish(showTiming: true);
errors.sort();
if (errors.isEmpty) {
if (!machineFormat) {
log.stdout('No issues found!');
}
return 0;
}
if (machineFormat) {
emitMachineFormat(log, errors);
} else {
emitDefaultFormat(log, errors, relativeToDir: dir, verbose: verbose);
}
bool hasErrors = false;
bool hasWarnings = false;
bool hasInfos = false;
for (final AnalysisError error in errors) {
hasErrors |= error.isError;
hasWarnings |= error.isWarning;
hasInfos |= error.isInfo;
}
// Return an error code in the range [0-3] dependent on the severity of
// the issue(s) found.
if (hasErrors) {
return 3;
}
bool fatalWarnings = argResults['fatal-warnings'];
bool fatalInfos = argResults['fatal-infos'];
if (fatalWarnings && hasWarnings) {
return 2;
} else if (fatalInfos && hasInfos) {
return 1;
} else {
return 0;
}
}
@visibleForTesting
static void emitDefaultFormat(
Logger log,
List<AnalysisError> errors, {
io.Directory relativeToDir,
bool verbose = false,
}) {
final bullet = log.ansi.bullet;
log.stdout('');
final wrapWidth = dartdevUsageLineLength == null
? null
: (dartdevUsageLineLength - _bodyIndentWidth);
for (final AnalysisError error in errors) {
// error • Message ... at path.dart:line:col • (code)
var filePath = path.relative(error.file, from: relativeToDir?.path);
var severity = error.severity.toLowerCase().padLeft(_severityWidth);
if (error.isError) {
severity = log.ansi.error(severity);
}
log.stdout(
'$severity $bullet '
'${log.ansi.emphasized(error.messageSentenceFragment)} '
'at $filePath:${error.startLine}:${error.startColumn} $bullet '
'(${error.code})',
);
if (verbose) {
for (var message in error.contextMessages) {
// Wrap longer context messages.
var contextMessage = wrapText(
'${message.message} at '
'${message.filePath}:${message.line}:${message.column}',
width: wrapWidth);
log.stdout('$_bodyIndent'
'${contextMessage.replaceAll('\n', '\n$_bodyIndent')}');
}
}
if (error.correction != null) {
// Wrap longer correction messages.
var correction = wrapText(error.correction, width: wrapWidth);
log.stdout(
'$_bodyIndent${correction.replaceAll('\n', '\n$_bodyIndent')}');
}
if (verbose) {
if (error.url != null) {
log.stdout('$_bodyIndent${error.url}');
}
}
}
log.stdout('');
final errorCount = errors.length;
log.stdout('$errorCount ${pluralize('issue', errorCount)} found.');
}
@visibleForTesting
static void emitMachineFormat(Logger log, List<AnalysisError> errors) {
for (final AnalysisError error in errors) {
log.stdout([
error.severity,
error.type,
error.code.toUpperCase(),
_escapeForMachineMode(error.file),
error.startLine.toString(),
error.startColumn.toString(),
error.length.toString(),
_escapeForMachineMode(error.message),
].join('|'));
}
}
static String _escapeForMachineMode(String input) {
var result = StringBuffer();
for (var c in input.codeUnits) {
if (c == _newline) {
result.write(r'\n');
} else if (c == _return) {
result.write(r'\r');
} else {
if (c == _slashCodeUnit || c == _pipeCodeUnit) {
result.write('\\');
}
result.writeCharCode(c);
}
}
return result.toString();
}
}