blob: 6c74fe1ee2c435217d4c3273bac07f1a8ecb2238 [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.
/// Summarizes the information produced by the checker.
library dev_compiler.src.report;
import 'dart:math' show max;
import 'package:analyzer/src/generated/engine.dart' show AnalysisContext;
import 'package:analyzer/src/generated/error.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;
import 'package:source_span/source_span.dart';
import 'utils.dart';
import 'summary.dart';
final _checkerLogger = new Logger('dev_compiler.checker');
/// Collects errors, and then sorts them and sends them
class ErrorCollector implements AnalysisErrorListener {
final AnalysisErrorListener listener;
final List<AnalysisError> _errors = [];
ErrorCollector(this.listener);
/// Flushes errors to the log. Until this is called, errors are buffered.
void flush() {
// TODO(jmesserly): this code was taken from analyzer_cli.
// sort errors
_errors.sort((AnalysisError error1, AnalysisError error2) {
// severity
var severity1 = _strongModeErrorSeverity(error1);
var severity2 = _strongModeErrorSeverity(error2);
int compare = severity2.compareTo(severity1);
if (compare != 0) return compare;
// path
compare = Comparable.compare(error1.source.fullName.toLowerCase(),
error2.source.fullName.toLowerCase());
if (compare != 0) return compare;
// offset
compare = error1.offset - error2.offset;
if (compare != 0) return compare;
// compare message, in worst case.
return error1.message.compareTo(error2.message);
});
_errors.forEach(listener.onError);
_errors.clear();
}
void onError(AnalysisError error) {
_errors.add(error);
}
}
ErrorSeverity _strongModeErrorSeverity(AnalysisError error) {
// Upgrade analyzer warnings to errors.
// TODO(jmesserly: reconcile this with analyzer_cli
var severity = error.errorCode.errorSeverity;
if (!error.errorCode.name.startsWith('dev_compiler.') &&
severity == ErrorSeverity.WARNING) {
return ErrorSeverity.ERROR;
}
return severity;
}
/// Simple reporter that logs checker messages as they are seen.
class LogReporter implements AnalysisErrorListener {
final AnalysisContext _context;
final bool useColors;
final List<AnalysisError> _errors = [];
LogReporter(this._context, {this.useColors: false});
void onError(AnalysisError error) {
var level = _severityToLevel[_strongModeErrorSeverity(error)];
// TODO(jmesserly): figure out what to do with the error's name.
var lineInfo = _context.computeLineInfo(error.source);
var location = lineInfo.getLocation(error.offset);
// [warning] 'foo' is not a... (/Users/.../tmp/foo.dart, line 1, col 2)
var text = new StringBuffer()
..write('[${errorCodeName(error.errorCode)}] ')
..write(error.message)
..write(' (${path.prettyUri(error.source.uri)}')
..write(', line ${location.lineNumber}, col ${location.columnNumber})');
// TODO(jmesserly): just print these instead of sending through logger?
_checkerLogger.log(level, text);
}
}
// TODO(jmesserly): remove log levels, instead just use severity.
const _severityToLevel = const {
ErrorSeverity.ERROR: Level.SEVERE,
ErrorSeverity.WARNING: Level.WARNING,
ErrorSeverity.INFO: Level.INFO
};
/// A reporter that gathers all the information in a [GlobalSummary].
class SummaryReporter implements AnalysisErrorListener {
GlobalSummary result = new GlobalSummary();
final Level _level;
final AnalysisContext _context;
SummaryReporter(this._context, [this._level = Level.ALL]);
IndividualSummary _getIndividualSummary(Uri uri) {
if (uri.path.endsWith('.html')) {
return result.loose.putIfAbsent('$uri', () => new HtmlSummary('$uri'));
}
var container;
if (uri.scheme == 'package') {
var pname = path.split(uri.path)[0];
result.packages.putIfAbsent(pname, () => new PackageSummary(pname));
container = result.packages[pname].libraries;
} else if (uri.scheme == 'dart') {
container = result.system;
} else {
container = result.loose;
}
return container.putIfAbsent('$uri', () => new LibrarySummary('$uri'));
}
void onError(AnalysisError error) {
// Only summarize messages per configured logging level
var code = error.errorCode;
if (_severityToLevel[code.errorSeverity] < _level) return;
var span = _toSpan(_context, error);
var summary = _getIndividualSummary(error.source.uri);
if (summary is LibrarySummary) {
summary.countSourceLines(_context, error.source);
}
summary.messages.add(new MessageSummary(errorCodeName(code),
code.errorSeverity.displayName, span, error.message));
}
// TODO(jmesserly): fix to not depend on SourceSpan. This will be really slow
// because it will reload source text from disk, for every single message...
SourceSpanWithContext _toSpan(AnalysisContext context, AnalysisError error) {
var source = error.source;
var lineInfo = context.computeLineInfo(source);
var content = context.getContents(source).data;
var start = error.offset;
var end = start + error.length;
return createSpanHelper(lineInfo, start, end, source, content);
}
void clearLibrary(Uri uri) {
(_getIndividualSummary(uri) as LibrarySummary).clear();
}
void clearHtml(Uri uri) {
HtmlSummary htmlSummary = result.loose['$uri'];
if (htmlSummary != null) htmlSummary.messages.clear();
}
}
/// Produces a string representation of the summary.
String summaryToString(GlobalSummary summary) {
var counter = new _Counter();
summary.accept(counter);
var table = new _Table();
// Declare columns and add header
table.declareColumn('package');
table.declareColumn('AnalyzerError', abbreviate: true);
var activeInfoTypes = counter.totals.keys;
activeInfoTypes.forEach((t) => table.declareColumn(t, abbreviate: true));
table.declareColumn('LinesOfCode', abbreviate: true);
table.addHeader();
// Add entries for each package
appendCount(count) => table.addEntry(count == null ? 0 : count);
for (var package in counter.errorCount.keys) {
appendCount(package);
appendCount(counter.errorCount[package]['AnalyzerError']);
activeInfoTypes.forEach((t) => appendCount(counter.errorCount[package][t]));
appendCount(counter.linesOfCode[package]);
}
// Add totals, percents and a new header for quick reference
table.addEmptyRow();
table.addHeader();
table.addEntry('total');
appendCount(counter.totals['AnalyzerError']);
activeInfoTypes.forEach((t) => appendCount(counter.totals[t]));
appendCount(counter.totalLinesOfCode);
appendPercent(count, total) {
if (count == null) count = 0;
var value = (count * 100 / total).toStringAsFixed(2);
table.addEntry(value);
}
var totalLOC = counter.totalLinesOfCode;
table.addEntry('%');
appendPercent(counter.totals['AnalyzerError'], totalLOC);
activeInfoTypes.forEach((t) => appendPercent(counter.totals[t], totalLOC));
appendCount(100);
return table.toString();
}
/// Helper class to combine all the information in table form.
class _Table {
int _totalColumns = 0;
int get totalColumns => _totalColumns;
/// Abbreviations, used to make headers shorter.
Map<String, String> abbreviations = {};
/// Width of each column.
List<int> widths = <int>[];
/// The header for each column (`header.length == totalColumns`).
List header = [];
/// Each row on the table. Note that all rows have the same size
/// (`rows[*].length == totalColumns`).
List<List> rows = [];
/// Whether we started adding entries. Indicates that no more columns can be
/// added.
bool _sealed = false;
/// Current row being built by [addEntry].
List _currentRow;
/// Add a column with the given [name].
void declareColumn(String name, {bool abbreviate: false}) {
assert(!_sealed);
var headerName = name;
if (abbreviate) {
// abbreviate the header by using only the capital initials.
headerName = name.replaceAll(new RegExp('[a-z]'), '');
while (abbreviations[headerName] != null) headerName = "$headerName'";
abbreviations[headerName] = name;
}
widths.add(max(5, headerName.length + 1) as int);
header.add(headerName);
_totalColumns++;
}
/// Add an entry in the table, creating a new row each time [totalColumns]
/// entries are added.
void addEntry(entry) {
if (_currentRow == null) {
_sealed = true;
_currentRow = [];
}
int pos = _currentRow.length;
assert(pos < _totalColumns);
widths[pos] = max(widths[pos], '$entry'.length + 1);
_currentRow.add('$entry');
if (pos + 1 == _totalColumns) {
rows.add(_currentRow);
_currentRow = [];
}
}
/// Add an empty row to divide sections of the table.
void addEmptyRow() {
var emptyRow = [];
for (int i = 0; i < _totalColumns; i++) {
emptyRow.add('-' * widths[i]);
}
rows.add(emptyRow);
}
/// Enter the header titles. OK to do so more than once in long tables.
void addHeader() {
rows.add(header);
}
/// Generates a string representation of the table to print on a terminal.
// TODO(sigmund): add also a .csv format
String toString() {
var sb = new StringBuffer();
sb.write('\n');
for (var row in rows) {
for (int i = 0; i < _totalColumns; i++) {
var entry = row[i];
// Align first column to the left, everything else to the right.
sb.write(
i == 0 ? entry.padRight(widths[i]) : entry.padLeft(widths[i] + 1));
}
sb.write('\n');
}
sb.write('\nWhere:\n');
for (var id in abbreviations.keys) {
sb.write(' $id:'.padRight(7));
sb.write(' ${abbreviations[id]}\n');
}
return sb.toString();
}
}
/// An example visitor that counts the number of errors per package and total.
class _Counter extends RecursiveSummaryVisitor {
String _currentPackage;
String get currentPackage =>
_currentPackage != null ? _currentPackage : "*other*";
var sb = new StringBuffer();
Map<String, Map<String, int>> errorCount = <String, Map<String, int>>{};
Map<String, int> linesOfCode = <String, int>{};
Map<String, int> totals = <String, int>{};
int totalLinesOfCode = 0;
void visitGlobal(GlobalSummary global) {
if (!global.system.isEmpty) {
for (var lib in global.system.values) {
lib.accept(this);
}
}
if (!global.packages.isEmpty) {
for (var lib in global.packages.values) {
lib.accept(this);
}
}
if (!global.loose.isEmpty) {
for (var lib in global.loose.values) {
lib.accept(this);
}
}
}
void visitPackage(PackageSummary package) {
_currentPackage = package.name;
super.visitPackage(package);
_currentPackage = null;
}
void visitLibrary(LibrarySummary lib) {
super.visitLibrary(lib);
linesOfCode.putIfAbsent(currentPackage, () => 0);
linesOfCode[currentPackage] += lib.lines;
totalLinesOfCode += lib.lines;
}
visitMessage(MessageSummary message) {
var kind = message.kind;
errorCount.putIfAbsent(currentPackage, () => <String, int>{});
errorCount[currentPackage].putIfAbsent(kind, () => 0);
errorCount[currentPackage][kind]++;
totals.putIfAbsent(kind, () => 0);
totals[kind]++;
}
}