blob: e2520230cd77ab066067e64604b9fa87ae805f31 [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.
import 'dart:io';
import 'dart:math';
import 'package:analyzer/error/error.dart';
import 'analyzer.dart';
import 'util/charcodes.dart' show $pipe, $backslash;
import 'util/score_utils.dart';
// Number of times to perform linting to get stable benchmarks.
const benchmarkRuns = 10;
String getLineContents(int? lineNumber, AnalysisError error) {
var path = error.source.fullName;
var file = File(path);
String failureDetails;
if (!file.existsSync()) {
failureDetails = 'file at $path does not exist';
} else {
var lines = file.readAsLinesSync();
var lineIndex = lineNumber! - 1;
if (lines.length > lineIndex) {
return lines[lineIndex];
}
failureDetails =
'line index ($lineIndex), outside of file line range (${lines.length})';
}
throw StateError('Unable to get contents for line: $failureDetails');
}
String pluralize(String word, int? count) =>
"$count ${count == 1 ? word : '${word}s'}";
String shorten(String? fileRoot, String fullName) {
if (fileRoot == null || !fullName.startsWith(fileRoot)) {
return fullName;
}
return fullName.substring(fileRoot.length);
}
Future writeBenchmarks(
IOSink out, List<File> filesToLint, LinterOptions lintOptions) async {
var timings = <String, int>{};
for (var i = 0; i < benchmarkRuns; ++i) {
await lintFiles(DartLinter(lintOptions), filesToLint);
lintRegistry.timers.forEach((n, t) {
var timing = t.elapsedMilliseconds;
var previous = timings[n];
if (previous == null) {
timings[n] = timing;
} else {
timings[n] = min(previous, timing);
}
});
}
var coreRuleset = await coreRules;
var recommendedRuleset = await recommendedRules;
var flutterRuleset = await flutterRules;
var pedanticRuleset = await pedanticRules;
var stats = timings.keys.map((t) {
var sets = <String>[];
if (coreRuleset.contains(t)) {
sets.add('core');
}
if (recommendedRuleset.contains(t)) {
sets.add('recommended');
}
if (flutterRuleset.contains(t)) {
sets.add('flutter');
}
if (pedanticRuleset.contains(t)) {
sets.add('pedantic');
}
var details = sets.isEmpty ? '' : " [${sets.join(', ')}]";
return _Stat('$t$details', timings[t] ?? 0);
}).toList();
_writeTimings(out, stats, 0);
}
String _escapePipe(String input) {
var result = StringBuffer();
for (var c in input.codeUnits) {
if (c == $backslash || c == $pipe) {
result.write('\\');
}
result.writeCharCode(c);
}
return result.toString();
}
void _writeTimings(IOSink out, List<_Stat> timings, int summaryLength) {
var names = timings.map((s) => s.name).toList();
var longestName =
names.fold<int>(0, (prev, element) => max(prev, element.length));
var longestTime = 8;
var tableWidth = max(summaryLength, longestName + longestTime);
var pad = tableWidth - longestName;
var line = ''.padLeft(tableWidth, '-');
out
..writeln()
..writeln(line)
..writeln('${'Timings'.padRight(longestName)}${'ms'.padLeft(pad)}')
..writeln(line);
var totalTime = 0;
timings.sort();
for (var stat in timings) {
totalTime += stat.elapsed;
// TODO: Shame timings slower than 100ms?
// TODO: Present both total times and time per count?
out.writeln(
'${stat.name.padRight(longestName)}${stat.elapsed.toString().padLeft(pad)}');
}
out
..writeln(line)
..writeln(
'${'Total'.padRight(longestName)}${totalTime.toString().padLeft(pad)}')
..writeln(line);
}
class DetailedReporter extends SimpleFormatter {
DetailedReporter(
Iterable<AnalysisErrorInfo> errors, LintFilter? filter, IOSink out,
{int? fileCount,
int? elapsedMs,
String? fileRoot,
bool showStatistics = false,
bool machineOutput = false,
bool quiet = false})
: super(errors, filter, out,
fileCount: fileCount,
fileRoot: fileRoot,
elapsedMs: elapsedMs,
showStatistics: showStatistics,
machineOutput: machineOutput,
quiet: quiet);
@override
void writeLint(AnalysisError error, {int? offset, int? line, int? column}) {
super.writeLint(error, offset: offset, column: column, line: line);
if (!machineOutput) {
var contents = getLineContents(line, error);
out.writeln(contents);
var spaces = column! - 1;
var arrows = max(1, min(error.length, contents.length - spaces));
var result = '${" " * spaces}${"^" * arrows}';
out.writeln(result);
}
}
}
abstract class ReportFormatter {
factory ReportFormatter(
Iterable<AnalysisErrorInfo> errors, LintFilter? filter, IOSink out,
{int? fileCount,
int? elapsedMs,
String? fileRoot,
bool showStatistics = false,
bool machineOutput = false,
bool quiet = false}) =>
DetailedReporter(errors, filter, out,
fileCount: fileCount,
fileRoot: fileRoot,
elapsedMs: elapsedMs,
showStatistics: showStatistics,
machineOutput: machineOutput,
quiet: quiet);
void write();
}
/// Simple formatter suitable for subclassing.
class SimpleFormatter implements ReportFormatter {
final IOSink out;
final Iterable<AnalysisErrorInfo> errors;
final LintFilter? filter;
int errorCount = 0;
int filteredLintCount = 0;
final int? fileCount;
final int? elapsedMs;
final String? fileRoot;
final bool showStatistics;
final bool machineOutput;
final bool quiet;
/// Cached for the purposes of statistics report formatting.
int _summaryLength = 0;
Map<String, int> stats = <String, int>{};
SimpleFormatter(this.errors, this.filter, this.out,
{this.fileCount,
this.fileRoot,
this.elapsedMs,
this.showStatistics = false,
this.quiet = false,
this.machineOutput = false});
/// Override to influence error sorting
int compare(AnalysisError error1, AnalysisError error2) {
// Severity
var compare = error2.errorCode.errorSeverity
.compareTo(error1.errorCode.errorSeverity);
if (compare != 0) {
return compare;
}
// Path
compare = Comparable.compare(error1.source.fullName.toLowerCase(),
error2.source.fullName.toLowerCase());
if (compare != 0) {
return compare;
}
// Offset
return error1.offset - error2.offset;
}
@override
void write() {
writeLints();
writeSummary();
if (showStatistics) {
out.writeln();
writeStatistics();
}
out.writeln();
}
void writeCounts() {
var codes = stats.keys.toList()..sort();
var largestCountGuess = 8;
var longest =
codes.fold(0, (int prev, element) => max(prev, element.length));
var tableWidth = max(_summaryLength, longest + largestCountGuess);
var pad = tableWidth - longest;
var line = ''.padLeft(tableWidth, '-');
out..writeln(line)..writeln('Counts')..writeln(line);
for (var code in codes) {
out
..write(code.padRight(longest))
..writeln(stats[code].toString().padLeft(pad));
}
out.writeln(line);
}
void writeLint(AnalysisError error, {int? offset, int? line, int? column}) {
if (machineOutput) {
//INFO|LINT|constant_identifier_names|test/engine_test.dart|91|22|3|Prefer using lowerCamelCase for constant names.
out
..write(error.errorCode.errorSeverity)
..write('|')
..write(error.errorCode.type)
..write('|')
..write(error.errorCode.name)
..write('|')
..write(_escapePipe(error.source.fullName))
..write('|')
..write(line)
..write('|')
..write(column)
..write('|')
..write(error.length)
..write('|')
..writeln(_escapePipe(error.message));
} else {
// test/engine_test.dart 452:9 [lint] DO name types using UpperCamelCase.
out
..write('${shorten(fileRoot, error.source.fullName)} ')
..write('$line:$column ')
..writeln('[${error.errorCode.type.displayName}] ${error.message}');
}
}
void writeLints() {
var filter = this.filter;
for (var info in errors) {
for (var e in (info.errors.toList()..sort(compare))) {
if (filter != null && filter.filter(e)) {
filteredLintCount++;
} else {
++errorCount;
if (!quiet) {
_writeLint(e, info.lineInfo);
}
_recordStats(e);
}
}
}
if (!quiet) {
out.writeln();
}
}
void writeStatistics() {
writeCounts();
writeTimings();
}
void writeSummary() {
var summary = '${pluralize("file", fileCount)} analyzed, '
'${pluralize("issue", errorCount)} found'
"${filteredLintCount == 0 ? '' : ' ($filteredLintCount filtered)'}, in $elapsedMs ms.";
out.writeln(summary);
// Cache for output table sizing
_summaryLength = summary.length;
}
void writeTimings() {
var timers = lintRegistry.timers;
var timings = timers.keys
.map((t) => _Stat(t, timers[t]?.elapsedMilliseconds ?? 0))
.toList();
_writeTimings(out, timings, _summaryLength);
}
void _recordStats(AnalysisError error) {
var codeName = error.errorCode.name;
stats.putIfAbsent(codeName, () => 0);
stats[codeName] = stats[codeName]! + 1;
}
void _writeLint(AnalysisError error, LineInfo lineInfo) {
var offset = error.offset;
var location = lineInfo.getLocation(offset);
var line = location.lineNumber;
var column = location.columnNumber;
writeLint(error, offset: offset, column: column, line: line);
}
}
class _Stat implements Comparable<_Stat> {
final String name;
final int elapsed;
_Stat(this.name, this.elapsed);
@override
int compareTo(_Stat other) => other.elapsed - elapsed;
}