generate an html report of the compilation results cleanup css
diff --git a/lib/src/compiler.dart b/lib/src/compiler.dart index a38f43b..a2d7301 100644 --- a/lib/src/compiler.dart +++ b/lib/src/compiler.dart
@@ -32,6 +32,7 @@ show AnalyzerMessage, CheckerResults, LibraryInfo, LibraryUnit; import 'options.dart'; import 'report.dart'; +import 'report/html_reporter.dart'; /// Sets up the type checker logger to print a span that highlights error /// messages. @@ -67,7 +68,9 @@ var reporter = createErrorReporter(context, options); bool status = new BatchCompiler(context, options, reporter: reporter).run(); - if (options.dumpInfo && reporter is SummaryReporter) { + if (reporter is HtmlReporter) { + (reporter as HtmlReporter).finish(options); + } else if (options.dumpInfo && reporter is SummaryReporter) { var result = (reporter as SummaryReporter).result; print(summaryToString(result)); if (options.dumpInfoFile != null) { @@ -476,7 +479,7 @@ AnalysisErrorListener createErrorReporter( AnalysisContext context, CompilerOptions options) { - return options.dumpInfo + return options.htmlReport ? new HtmlReporter(context) : options.dumpInfo ? new SummaryReporter(context, options.logLevel) : new LogReporter(context, useColors: options.useColors); }
diff --git a/lib/src/options.dart b/lib/src/options.dart index 03d47fc..01bef1d 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart
@@ -102,6 +102,8 @@ /// Whether to dump summary information on the console. final bool dumpInfo; + final bool htmlReport; + /// If not null, path to a file that will store a json representation of the /// summary information (only used if [dumpInfo] is true). final String dumpInfoFile; @@ -151,6 +153,7 @@ this.runnerOptions: const RunnerOptions(), this.checkSdk: false, this.dumpInfo: false, + this.htmlReport: false, this.dumpInfoFile, this.useColors: true, this.help: false, @@ -200,6 +203,8 @@ var dumpInfo = args['dump-info']; if (dumpInfo == null) dumpInfo = serverMode; + var htmlReport = args['html-report']; + var v8Binary = args['v8-binary']; if (v8Binary == null) v8Binary = 'iojs'; @@ -237,6 +242,7 @@ runnerOptions: new RunnerOptions(v8Binary: v8Binary), checkSdk: args['sdk-check'], dumpInfo: dumpInfo, + htmlReport: htmlReport, dumpInfoFile: args['dump-info-file'], useColors: useColors, help: showUsage, @@ -311,6 +317,8 @@ ..addOption('log', abbr: 'l', help: 'Logging level (defaults to warning)') ..addFlag('dump-info', abbr: 'i', help: 'Dump summary information', defaultsTo: null) + ..addFlag('html-report', + help: 'Output compilation results to html', defaultsTo: null) ..addOption('v8-binary', help: 'V8-based binary to run JavaScript output with (iojs, node, d8)', defaultsTo: 'iojs')
diff --git a/lib/src/report/html_gen.dart b/lib/src/report/html_gen.dart new file mode 100644 index 0000000..3e1514e --- /dev/null +++ b/lib/src/report/html_gen.dart
@@ -0,0 +1,150 @@ +// 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 dev_compiler.src.html_gen; + +/// A class to generate an html page. +class HtmlGen { + final StringBuffer _buffer = new StringBuffer(); + final List<String> _tags = []; + final List<bool> _indents = []; + + bool _startOfLine = true; + String _indent = ''; + + HtmlGen() { + _init(); + } + + void _init() { + writeln('<!DOCTYPE html>'); + writeln(); + writeln('<!-- generated by dev_compiler -->'); + writeln(); + } + + void start( + {String title, + String cssRef, + String theme, + String jsScript, + String inlineStyle}) { + startTag('html', newLine: false); + writeln(); + startTag('head'); + writeln('<meta charset="utf-8">'); + writeln( + '<meta name="viewport" content="width=device-width, initial-scale=1.0">'); + if (title != null) { + writeln('<title>${title}</title>'); + } + if (cssRef != null) { + writeln('<link href="${cssRef}" rel="stylesheet" media="screen">'); + } + if (theme != null) { + writeln('<link href="${theme}" rel="stylesheet">'); + } + if (jsScript != null) { + writeln('<script src="${jsScript}"></script>'); + } + if (inlineStyle != null) { + startTag('style'); + writeln(inlineStyle); + endTag(); + } + endTag(); + writeln(); + startTag('body', newLine: false); + writeln(); + } + + void startTag(String tag, {String attributes, String c, bool newLine: true}) { + if (c != null && c.isNotEmpty) { + if (attributes == null) { + attributes = 'class="${c}"'; + } else { + attributes += ' class="${c}"'; + } + } + + if (attributes != null) { + if (newLine) { + writeln('<${tag} ${attributes}>'); + } else { + write('<${tag} ${attributes}>'); + } + } else { + if (newLine) { + writeln('<${tag}>'); + } else { + write('<${tag}>'); + } + } + _indents.add(newLine); + if (newLine) { + _indent = '$_indent\t'; + } + _tags.add(tag); + } + + void span({String text, String c}) => tag('span', text: text, c: c); + + void tag(String tag, + {String text, String c, String href, String attributes}) { + if (attributes == null) attributes = ''; + if (text == null) text = ''; + + if (c != null && c.isNotEmpty) attributes += ' class="${c}"'; + if (href != null) attributes += ' href="${href}"'; + + if (attributes.isNotEmpty) attributes = ' ${attributes.trim()}'; + + writeln('<$tag$attributes>$text</$tag>'); + } + + void endTag() { + String tag = _tags.removeLast(); + bool wasIndent = _indents.removeLast(); + if (wasIndent) { + _indent = _indent.substring(0, _indent.length - 1); + } + writeln('</${tag}>'); + } + + void end() { + // body + endTag(); + // html + endTag(); + } + + String toString() => _buffer.toString(); + + void reset() { + _buffer.clear(); + _startOfLine = true; + _tags.clear(); + _indents.clear(); + _indent = ''; + + _init(); + } + + void write(String str) { + if (_startOfLine) { + _buffer.write(_indent); + _startOfLine = false; + } + _buffer.write(str); + } + + void writeln([String str]) { + if (str == null) { + write('\n'); + } else { + write('${str}\n'); + } + _startOfLine = true; + } +}
diff --git a/lib/src/report/html_reporter.dart b/lib/src/report/html_reporter.dart new file mode 100644 index 0000000..f05a9ad --- /dev/null +++ b/lib/src/report/html_reporter.dart
@@ -0,0 +1,526 @@ +// 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 dev_compiler.src.html_reporter; + +import 'dart:collection' show LinkedHashSet; +import 'dart:convert' show HTML_ESCAPE; +import 'dart:io'; + +import 'package:analyzer/src/generated/engine.dart'; +import 'package:analyzer/src/generated/error.dart'; +import 'package:analyzer/src/generated/source.dart'; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart' as yaml; + +import '../../devc.dart'; +import '../options.dart'; +import '../report.dart'; +import '../summary.dart'; +import 'html_gen.dart'; + +/// Generate a compilation summary using the [Primer](http://primercss.io) css. +class HtmlReporter implements AnalysisErrorListener { + final AnalysisContext context; + SummaryReporter reporter; + List<AnalysisError> errors = []; + + HtmlReporter(this.context) { + reporter = new SummaryReporter(context); + } + + void onError(AnalysisError error) { + try { + reporter.onError(error); + } catch (e, st) { + // TOOD: This can fail when extracting context spans. + print(e); + print(st); + } + + errors.add(error); + } + + void finish(CompilerOptions options) { + GlobalSummary result = reporter.result; + + // Find all referenced packages - both those with and without issues. + List<String> allPackages = context.sources + .where((s) => s.uriKind == UriKind.PACKAGE_URI) + .map((s) => s.uri.pathSegments.first) + .toSet() + .toList(); + + String input = options.inputs.first; + List<SummaryInfo> summaries = []; + + // Hoist the self-ref package to an `Application` category. + String packageName = _getPackageName(); + if (result.packages.containsKey(packageName)) { + PackageSummary summary = result.packages[packageName]; + List<MessageSummary> issues = summary.libraries.values + .expand((LibrarySummary l) => l.messages) + .toList(); + summaries.add(new SummaryInfo( + 'Application code', packageName, 'package:${packageName}', issues)); + } + + // package: code + List<String> keys = result.packages.keys.toList(); + allPackages.forEach((name) { + if (!keys.contains(name)) keys.add(name); + }); + keys.sort(); + + for (String name in keys) { + if (name == packageName) continue; + + PackageSummary summary = result.packages[name]; + + if (summary == null) { + summaries.add(new SummaryInfo('Package: code', name)); + } else { + List<MessageSummary> issues = summary.libraries.values + .expand((LibrarySummary summary) => summary.messages) + .toList(); + summaries.add( + new SummaryInfo('Package: code', name, 'package:${name}', issues)); + } + } + + // dart: code + keys = result.system.keys.toList()..sort(); + for (String name in keys) { + LibrarySummary summary = result.system[name]; + summaries.add(new SummaryInfo( + 'Dart: code', name, 'dart:${name}', summary.messages)); + } + + // Loose files + if (result.loose.isNotEmpty) { + List<MessageSummary> issues = result.loose.values + .expand((IndividualSummary summary) => summary.messages) + .toList(); + summaries.add(new SummaryInfo('Files', 'files', 'files', issues)); + } + + // Write the html report. + Page page = new Page(input, input, summaries); + String path = '${input.replaceAll('.', '_')}_results.html'; + new File(path).writeAsStringSync(page.create()); + print('Compilation report available at ${path}; ${errors.length} issues.'); + } + + String _getPackageName() { + File file = new File('pubspec.yaml'); + if (file.existsSync()) { + var doc = yaml.loadYaml(file.readAsStringSync()); + return doc['name']; + } else { + return null; + } + } +} + +class SummaryInfo { + static int _compareIssues(MessageSummary a, MessageSummary b) { + int result = _compareSeverity(a.level, b.level); + if (result != 0) return result; + result = a.span.sourceUrl.toString().compareTo(b.span.sourceUrl.toString()); + if (result != 0) return result; + return a.span.start.compareTo(b.span.start); + } + + static const _sevTable = const {'error': 0, 'warning': 1, 'info': 2}; + + static int _compareSeverity(String a, String b) => + _sevTable[a] - _sevTable[b]; + + final String category; + final String shortTitle; + final String longTitle; + final List<MessageSummary> issues; + + SummaryInfo(this.category, this.shortTitle, [this.longTitle, this.issues]) { + issues?.sort(_compareIssues); + } + + String get ref => longTitle == null ? null : longTitle.replaceAll(':', '_'); + + int get errorCount => + issues == null ? 0 : issues.where((i) => i.level == 'error').length; + int get warningCount => + issues == null ? 0 : issues.where((i) => i.level == 'warning').length; + int get infoCount => + issues == null ? 0 : issues.where((i) => i.level == 'info').length; + + bool get hasIssues => issues == null ? false : issues.isNotEmpty; +} + +class Page extends HtmlGen { + final String pageTitle; + final String inputFile; + final List<SummaryInfo> summaries; + + Page(this.pageTitle, this.inputFile, this.summaries); + + String get subTitle => 'DDC compilation report for ${inputFile}'; + + String create() { + start( + title: 'DDC ${pageTitle}', + theme: 'http://primercss.io/docs.css', + inlineStyle: _css); + + header(); + startTag('div', c: "container"); + startTag('div', c: "columns docs-layout"); + + startTag('div', c: "column one-fourth"); + nav(); + endTag(); + + startTag('div', c: "column three-fourths"); + subtitle(); + contents(); + endTag(); + + endTag(); + footer(); + endTag(); + end(); + + return toString(); + } + + void header() { + startTag('header', c: "masthead"); + startTag('div', c: "container"); + title(); + startTag('nav', c: "masthead-nav"); + tag("a", + href: + "https://github.com/dart-lang/dev_compiler/blob/master/STRONG_MODE.md", + text: "Strong Mode"); + tag("a", + href: "https://github.com/dart-lang/dev_compiler", text: "DDC Repo"); + endTag(); + endTag(); + endTag(); + } + + void title() { + tag("a", c: "masthead-logo", text: pageTitle); + } + + void subtitle() { + tag("h1", text: subTitle, c: "page-title"); + } + + void contents() { + int errorCount = summaries.fold( + 0, (int count, SummaryInfo info) => count + info.errorCount); + int warningCount = summaries.fold( + 0, (int count, SummaryInfo info) => count + info.warningCount); + int infoCount = summaries.fold( + 0, (int count, SummaryInfo info) => count + info.infoCount); + + List<String> messages = []; + + if (errorCount > 0) { + messages.add("${_comma(errorCount)} ${_pluralize(errorCount, 'error')}"); + } + if (warningCount > 0) { + messages.add( + "${_comma(warningCount)} ${_pluralize(warningCount, 'warning')}"); + } + if (infoCount > 0) { + messages.add("${_comma(infoCount)} ${_pluralize(infoCount, 'info')}"); + } + + String message; + + if (messages.isEmpty) { + message = 'no issues'; + } else if (messages.length == 2) { + message = messages.join(' and '); + } else { + message = messages.join(', '); + } + + tag("p", text: 'Found ${message}.'); + + for (SummaryInfo info in summaries) { + if (!info.hasIssues) continue; + + tag("h2", text: info.longTitle, attributes: "id=${info.ref}"); + contentItem(info); + } + } + + void nav() { + startTag("nav", c: "menu docs-menu"); + Iterable<String> categories = + new LinkedHashSet.from(summaries.map((s) => s.category)); + for (String category in categories) { + navItems(category, summaries.where((s) => s.category == category)); + } + endTag(); + } + + void navItems(String category, List<SummaryInfo> infos) { + if (infos.isEmpty) return; + + span(c: "menu-heading", text: category); + + for (SummaryInfo info in infos) { + if (info.hasIssues) { + startTag("a", c: "menu-item", attributes: 'href="#${info.ref}"'); + + span(text: info.shortTitle); + + int errorCount = info.errorCount; + int warningCount = info.warningCount; + int infoCount = info.infoCount; + + if (infoCount > 0) { + span(c: "counter info", text: '${_comma(infoCount)}'); + } + if (warningCount > 0) { + span(c: "counter warning", text: '${_comma(warningCount)}'); + } + if (errorCount > 0) { + span(c: "counter error", text: '${_comma(errorCount)}'); + } + + endTag(); + } else { + tag("div", c: "menu-item", text: info.shortTitle); + } + } + } + + void footer() { + startTag('footer', c: "footer"); + writeln("${inputFile} • DDC version ${devCompilerVersion}"); + endTag(); + } + + void contentItem(SummaryInfo info) { + int errors = info.errorCount; + int warnings = info.warningCount; + int infos = info.infoCount; + + if (errors > 0) { + span( + c: 'counter error', + text: '${_comma(errors)} ${_pluralize(errors, 'error')}'); + } + if (warnings > 0) { + span( + c: 'counter warning', + text: '${_comma(warnings)} ${_pluralize(warnings, 'warning')}'); + } + if (infos > 0) { + span( + c: 'counter info', + text: '${_comma(infos)} ${_pluralize(infos, 'info')}'); + } + + info.issues.forEach(emitMessage); + } + + void emitMessage(MessageSummary issue) { + startTag('div', c: 'file'); + startTag('div', c: 'file-header'); + span(c: 'counter ${issue.level}', text: issue.kind); + span(c: 'file-info', text: issue.span.sourceUrl.toString()); + endTag(); + + startTag('div', c: 'blob-wrapper'); + startTag('table'); + startTag('tbody'); + + // TODO: Widen the line extracts - +2 on either side. + // TODO: Highlight error ranges. + if (issue.span is SourceSpanWithContext) { + SourceSpanWithContext context = issue.span; + String text = context.context.trimRight(); + int lineNum = context.start.line; + + for (String line in text.split('\n')) { + lineNum++; + startTag('tr'); + tag('td', c: 'blob-num', text: lineNum.toString()); + tag('td', + c: 'blob-code blob-code-inner', text: HTML_ESCAPE.convert(line)); + endTag(); + } + } + + startTag('tr', c: 'row-expandable'); + tag('td', c: 'blob-num blob-num-expandable'); + tag('td', + c: 'blob-code blob-code-expandable', + text: HTML_ESCAPE.convert(issue.message)); + endTag(); + + endTag(); + endTag(); + endTag(); + + endTag(); + } +} + +String _pluralize(int count, String item) => count == 1 ? item : '${item}s'; + +String _comma(int count) { + String str = '${count}'; + if (str.length <= 3) return str; + int pos = str.length - 3; + return str.substring(0, pos) + ',' + str.substring(pos); +} + +/// Deltas from the baseline Primer css (http://primercss.io/docs.css). +const String _css = ''' +h2 { + margin-top: 2em; + padding-bottom: 0.3em; + font-size: 1.75em; + line-height: 1.225; + border-bottom: 1px solid #eee; +} + +.error { + background-color: #bf1515; +} + +.menu-item .counter { + margin-bottom: 0; +} + +.counter.error { + color: #eee; + text-shadow: none; +} + +.warning { + background-color: #ffe5a7; +} + +.counter.warning { + color: #777; +} + +.counter.error, +.counter.warning, +.counter.info { + margin-bottom: 0; +} + +nav.menu .menu-item { + overflow-x: auto; +} + +.info { + background-color: #eee; +} + +/* code snippets styles */ + +.file { + position: relative; + margin-top: 20px; + margin-bottom: 15px; + border: 1px solid #ddd; + border-radius: 3px; +} + +.file-header { + padding: 5px 10px; + background-color: #f7f7f7; + border-bottom: 1px solid #d8d8d8; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +.file-info { + font-size: 12px; + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; +} + +table { + border-collapse: collapse; + border-spacing: 0; + margin-bottom: 0; +} + +.blob-wrapper { + overflow-x: auto; + overflow-y: hidden; +} + +.blob-num { + width: 1%; + min-width: 50px; + white-space: nowrap; + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; + line-height: 18px; + color: rgba(0,0,0,0.3); + vertical-align: top; + text-align: right; + border: solid #eee; + border-width: 0 1px 0 0; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + padding-left: 10px; + padding-right: 10px; +} + +.blob-code { + padding-left: 10px; + padding-right: 10px; + vertical-align: top; +} + +.blob-code-inner { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; + color: #333; + white-space: pre; + overflow: visible; + word-wrap: normal; +} + +.row-expandable { + border-top: 1px solid #d8d8d8; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} + +.blob-num-expandable, +.blob-code-expandable { + vertical-align: middle; + font-size: 14px; + border-color: #d2dff0; +} + +.blob-num-expandable { + background-color: #edf2f9; + border-bottom-left-radius: 3px; +} + +.blob-code-expandable { + padding-top: 4px; + padding-bottom: 4px; + background-color: #f4f7fb; + border-width: 1px 0; + border-bottom-right-radius: 3px; +} +''';