blob: 086edfc52198b2e9f5af0d98d673b0745b35fcbf [file] [log] [blame]
// Copyright (c) 2015, 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.
library analyzer_cli.src.error_formatter;
import 'package:analyzer/error/error.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer_cli/src/ansi.dart';
import 'package:analyzer_cli/src/options.dart';
import 'package:path/path.dart' as path;
/// Returns the given error's severity.
ProcessedSeverity _severityIdentity(AnalysisError error) =>
new ProcessedSeverity(error.errorCode.errorSeverity);
String _pluralize(String word, int count) => count == 1 ? word : word + "s";
/// Returns desired severity for the given [error] (or `null` if it's to be
/// suppressed).
typedef ProcessedSeverity SeverityProcessor(AnalysisError error);
/// Analysis statistics counter.
class AnalysisStats {
/// The total number of diagnostics sent to [formatErrors].
int unfilteredCount = 0;
int errorCount = 0;
int hintCount = 0;
int lintCount = 0;
int warnCount = 0;
AnalysisStats();
/// The total number of diagnostics reported to the user.
int get filteredCount => errorCount + warnCount + hintCount + lintCount;
/// Print statistics to [out].
void print(StringSink out) {
bool hasErrors = errorCount != 0;
bool hasWarns = warnCount != 0;
bool hasHints = hintCount != 0;
bool hasLints = lintCount != 0;
bool hasContent = false;
if (hasErrors) {
out.write(errorCount);
out.write(' ');
out.write(_pluralize("error", errorCount));
hasContent = true;
}
if (hasWarns) {
if (hasContent) {
if (!hasHints && !hasLints) {
out.write(' and ');
} else {
out.write(", ");
}
}
out.write(warnCount);
out.write(' ');
out.write(_pluralize("warning", warnCount));
hasContent = true;
}
if (hasLints) {
if (hasContent) {
out.write(hasHints ? ', ' : ' and ');
}
out.write(lintCount);
out.write(' ');
out.write(_pluralize("lint", lintCount));
hasContent = true;
}
if (hasHints) {
if (hasContent) {
out.write(" and ");
}
out.write(hintCount);
out.write(' ');
out.write(_pluralize("hint", hintCount));
hasContent = true;
}
if (hasContent) {
out.writeln(" found.");
} else {
out.writeln("No issues found!");
}
}
}
/// Helper for formatting [AnalysisError]s.
///
/// The two format options are a user consumable format and a machine consumable
/// format.
abstract class ErrorFormatter {
final StringSink out;
final CommandLineOptions options;
final AnalysisStats stats;
SeverityProcessor _severityProcessor;
ErrorFormatter(this.out, this.options, this.stats,
{SeverityProcessor severityProcessor}) {
_severityProcessor =
severityProcessor == null ? _severityIdentity : severityProcessor;
}
/// Compute the severity for this [error] or `null` if this error should be
/// filtered.
ErrorSeverity _computeSeverity(AnalysisError error) =>
_severityProcessor(error)?.severity;
void formatErrors(List<AnalysisErrorInfo> errorInfos) {
stats.unfilteredCount += errorInfos.length;
List<AnalysisError> errors = new List<AnalysisError>();
Map<AnalysisError, LineInfo> errorToLine =
new Map<AnalysisError, LineInfo>();
for (AnalysisErrorInfo errorInfo in errorInfos) {
for (AnalysisError error in errorInfo.errors) {
if (_computeSeverity(error) != null) {
errors.add(error);
errorToLine[error] = errorInfo.lineInfo;
}
}
}
for (AnalysisError error in errors) {
formatError(errorToLine, error);
}
}
void formatError(
Map<AnalysisError, LineInfo> errorToLine, AnalysisError error);
/// Call to write any batched up errors from [formatErrors].
void flush();
}
class MachineErrorFormatter extends ErrorFormatter {
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);
MachineErrorFormatter(
StringSink out, CommandLineOptions options, AnalysisStats stats,
{SeverityProcessor severityProcessor})
: super(out, options, stats, severityProcessor: severityProcessor);
void formatError(
Map<AnalysisError, LineInfo> errorToLine, AnalysisError error) {
Source source = error.source;
LineInfo_Location location = errorToLine[error].getLocation(error.offset);
int length = error.length;
ProcessedSeverity processedSeverity = _severityProcessor(error);
ErrorSeverity severity = processedSeverity.severity;
if (!processedSeverity.overridden) {
if (severity == ErrorSeverity.WARNING && options.warningsAreFatal) {
severity = ErrorSeverity.ERROR;
}
}
if (severity == ErrorSeverity.ERROR) {
stats.errorCount++;
} else if (severity == ErrorSeverity.WARNING) {
// Only treat a warning as an error if it's not been set by a processor.
if (!processedSeverity.overridden && options.warningsAreFatal) {
stats.errorCount++;
} else {
stats.warnCount++;
}
} else if (error.errorCode.type == ErrorType.HINT) {
stats.hintCount++;
} else if (error.errorCode.type == ErrorType.LINT) {
stats.lintCount++;
}
out.write(severity);
out.write('|');
out.write(error.errorCode.type);
out.write('|');
out.write(error.errorCode.name);
out.write('|');
out.write(_escapeForMachineMode(source.fullName));
out.write('|');
out.write(location.lineNumber);
out.write('|');
out.write(location.columnNumber);
out.write('|');
out.write(length);
out.write('|');
out.write(_escapeForMachineMode(error.message));
out.writeln();
}
static String _escapeForMachineMode(String input) {
StringBuffer result = new StringBuffer();
for (int 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();
}
void flush() {}
}
class HumanErrorFormatter extends ErrorFormatter {
AnsiLogger ansi;
// This is a Set in order to de-dup CLI errors.
Set<CLIError> batchedErrors = new Set();
HumanErrorFormatter(
StringSink out, CommandLineOptions options, AnalysisStats stats,
{SeverityProcessor severityProcessor})
: super(out, options, stats, severityProcessor: severityProcessor) {
ansi = new AnsiLogger(this.options.color);
}
void formatError(
Map<AnalysisError, LineInfo> errorToLine, AnalysisError error) {
Source source = error.source;
LineInfo_Location location = errorToLine[error].getLocation(error.offset);
ProcessedSeverity processedSeverity = _severityProcessor(error);
ErrorSeverity severity = processedSeverity.severity;
// Get display name; translate INFOs into LINTS and HINTS.
String errorType = severity.displayName;
if (severity == ErrorSeverity.INFO) {
if (error.errorCode.type == ErrorType.HINT ||
error.errorCode.type == ErrorType.LINT) {
errorType = error.errorCode.type.displayName;
}
}
// warning • 'foo' is not a bar at lib/foo.dart:1:2 • foo_warning
String message = error.message;
// Remove any terminating '.' from the end of the message.
if (message.endsWith('.')) {
message = message.substring(0, message.length - 1);
}
String sourcePath;
if (source.uriKind == UriKind.DART_URI) {
sourcePath = source.uri.toString();
} else if (source.uriKind == UriKind.PACKAGE_URI) {
sourcePath = _relative(source.fullName);
if (sourcePath == source.fullName) {
// If we weren't able to shorten the path name, use the package: version.
sourcePath = source.uri.toString();
}
} else {
sourcePath = _relative(source.fullName);
}
batchedErrors.add(new CLIError(
severity: errorType,
sourcePath: sourcePath,
offset: error.offset,
line: location.lineNumber,
column: location.columnNumber,
message: message,
errorCode: error.errorCode.name.toLowerCase(),
correction: error.correction,
));
}
void flush() {
// sort
List<CLIError> sortedErrors = batchedErrors.toList()..sort();
// print
for (CLIError error in sortedErrors) {
if (error.isError) {
stats.errorCount++;
} else if (error.isWarning) {
stats.warnCount++;
} else if (error.isLint) {
stats.lintCount++;
} else if (error.isHint) {
stats.hintCount++;
}
// warning • 'foo' is not a bar at lib/foo.dart:1:2 • foo_warning
String issueColor = (error.isError == ErrorSeverity.ERROR ||
error.isWarning == ErrorSeverity.WARNING)
? ansi.red
: '';
out.write(' $issueColor${error.severity}${ansi.none} '
'${ansi.bullet} ${ansi.bold}${error.message}${ansi.none} ');
out.write('at ${error.sourcePath}');
out.write(':${error.line}:${error.column} ');
out.write('${ansi.bullet} ${error.errorCode}');
out.writeln();
// If verbose, also print any associated correction.
if (options.verbose && error.correction != null) {
out.writeln(
'${' '.padLeft(error.severity.length + 2)}${error.correction}');
}
}
// clear out batched errors
batchedErrors.clear();
}
}
final Map<String, int> _severityCompare = {
'error': 5,
'warning': 4,
'info': 3,
'lint': 2,
'hint': 1,
};
/// An [AnalysisError] with line and column information.
class CLIError implements Comparable<CLIError> {
final String severity;
final String sourcePath;
final int offset;
final int line;
final int column;
final String message;
final String errorCode;
final String correction;
CLIError({
this.severity,
this.sourcePath,
this.offset,
this.line,
this.column,
this.message,
this.errorCode,
this.correction,
});
bool get isError => severity == 'error';
bool get isWarning => severity == 'warning';
bool get isLint => severity == 'lint';
bool get isHint => severity == 'hint';
@override
int get hashCode =>
severity.hashCode ^ sourcePath.hashCode ^ errorCode.hashCode ^ offset;
@override
bool operator ==(other) {
if (other is! CLIError) return false;
return severity == other.severity &&
sourcePath == other.sourcePath &&
errorCode == other.errorCode &&
offset == other.offset;
}
@override
int compareTo(CLIError other) {
// severity
int compare =
_severityCompare[other.severity] - _severityCompare[this.severity];
if (compare != 0) return compare;
// path
compare = Comparable.compare(
this.sourcePath.toLowerCase(), other.sourcePath.toLowerCase());
if (compare != 0) return compare;
// offset
return this.offset - other.offset;
}
}
/// A severity with awareness of whether it was overridden by a processor.
class ProcessedSeverity {
final ErrorSeverity severity;
final bool overridden;
ProcessedSeverity(this.severity, [this.overridden = false]);
}
/// Given an absolute path, return a relative path if the file is contained in
/// the current directory; return the original path otherwise.
String _relative(String file) {
return file.startsWith(path.current) ? path.relative(file) : file;
}