| // 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 $backslash, $pipe; |
| 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 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'); |
| } |
| |
| 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; |
| 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(super.errors, super.filter, super.out, |
| {super.fileCount, |
| super.elapsedMs, |
| super.fileRoot, |
| super.showStatistics, |
| super.machineOutput, |
| super.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; |
| } |