Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(364)

Side by Side Diff: lib/src/report/html_reporter.dart

Issue 1419973003: generate an html report of the compilation results (Closed) Base URL: https://github.com/dart-lang/dev_compiler.git@master
Patch Set: address review comments Created 5 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « lib/src/report/html_gen.dart ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 library dev_compiler.src.html_reporter;
6
7 import 'dart:collection' show LinkedHashSet;
8 import 'dart:convert' show HTML_ESCAPE;
9 import 'dart:io';
10
11 import 'package:analyzer/src/generated/engine.dart';
12 import 'package:analyzer/src/generated/error.dart';
13 import 'package:analyzer/src/generated/source.dart';
14 import 'package:source_span/source_span.dart';
15 import 'package:yaml/yaml.dart' as yaml;
16
17 import '../../devc.dart';
18 import '../options.dart';
19 import '../report.dart';
20 import '../summary.dart';
21 import 'html_gen.dart';
22
23 /// Generate a compilation summary using the [Primer](http://primercss.io) css.
24 class HtmlReporter implements AnalysisErrorListener {
25 final AnalysisContext context;
26 SummaryReporter reporter;
27 List<AnalysisError> errors = [];
28
29 HtmlReporter(this.context) {
30 reporter = new SummaryReporter(context);
31 }
32
33 void onError(AnalysisError error) {
34 try {
35 reporter.onError(error);
36 } catch (e, st) {
37 // TODO: This can fail when extracting context spans.
38 print('${e}:${st}');
39 }
40
41 errors.add(error);
42 }
43
44 void finish(CompilerOptions options) {
45 GlobalSummary result = reporter.result;
46
47 // Find all referenced packages - both those with and without issues.
48 List<String> allPackages = context.sources
49 .where((s) => s.uriKind == UriKind.PACKAGE_URI)
50 .map((s) => s.uri.pathSegments.first)
51 .toSet()
52 .toList();
53
54 String input = options.inputs.first;
55 List<SummaryInfo> summaries = [];
56
57 // Hoist the self-ref package to an `Application` category.
58 String packageName = _getPackageName();
59 if (result.packages.containsKey(packageName)) {
60 PackageSummary summary = result.packages[packageName];
61 List<MessageSummary> issues = summary.libraries.values
62 .expand((LibrarySummary l) => l.messages)
63 .toList();
64 summaries.add(new SummaryInfo(
65 'Application code', packageName, 'package:${packageName}', issues));
66 }
67
68 // package: code
69 List<String> keys = result.packages.keys.toList();
70 allPackages.forEach((name) {
71 if (!keys.contains(name)) keys.add(name);
72 });
73 keys.sort();
74
75 for (String name in keys) {
76 if (name == packageName) continue;
77
78 PackageSummary summary = result.packages[name];
79
80 if (summary == null) {
81 summaries.add(new SummaryInfo('Package: code', name));
82 } else {
83 List<MessageSummary> issues = summary.libraries.values
84 .expand((LibrarySummary summary) => summary.messages)
85 .toList();
86 summaries.add(
87 new SummaryInfo('Package: code', name, 'package:${name}', issues));
88 }
89 }
90
91 // dart: code
92 keys = result.system.keys.toList()..sort();
93 for (String name in keys) {
94 LibrarySummary summary = result.system[name];
95 summaries.add(new SummaryInfo(
96 'Dart: code', name, 'dart:${name}', summary.messages));
97 }
98
99 // Loose files
100 if (result.loose.isNotEmpty) {
101 List<MessageSummary> issues = result.loose.values
102 .expand((IndividualSummary summary) => summary.messages)
103 .toList();
104 summaries.add(new SummaryInfo('Files', 'files', 'files', issues));
105 }
106
107 // Write the html report.
108 Page page = new Page(input, input, summaries);
109 String path = '${input.replaceAll('.', '_')}_results.html';
110 new File(path).writeAsStringSync(page.create());
111 print('Compilation report available at ${path}; ${errors.length} issues.');
112 }
113
114 String _getPackageName() {
115 File file = new File('pubspec.yaml');
116 if (file.existsSync()) {
117 var doc = yaml.loadYaml(file.readAsStringSync());
118 return doc['name'];
119 } else {
120 return null;
121 }
122 }
123 }
124
125 class SummaryInfo {
126 static int _compareIssues(MessageSummary a, MessageSummary b) {
127 int result = _compareSeverity(a.level, b.level);
128 if (result != 0) return result;
129 result = a.span.sourceUrl.toString().compareTo(b.span.sourceUrl.toString());
130 if (result != 0) return result;
131 return a.span.start.compareTo(b.span.start);
132 }
133
134 static const _sevTable = const {'error': 0, 'warning': 1, 'info': 2};
135
136 static int _compareSeverity(String a, String b) =>
137 _sevTable[a] - _sevTable[b];
138
139 final String category;
140 final String shortTitle;
141 final String longTitle;
142 final List<MessageSummary> issues;
143
144 SummaryInfo(this.category, this.shortTitle, [this.longTitle, this.issues]) {
145 issues?.sort(_compareIssues);
146 }
147
148 String get ref => longTitle == null ? null : longTitle.replaceAll(':', '_');
149
150 int get errorCount =>
151 issues == null ? 0 : issues.where((i) => i.level == 'error').length;
152 int get warningCount =>
153 issues == null ? 0 : issues.where((i) => i.level == 'warning').length;
154 int get infoCount =>
155 issues == null ? 0 : issues.where((i) => i.level == 'info').length;
156
157 bool get hasIssues => issues == null ? false : issues.isNotEmpty;
158 }
159
160 class Page extends HtmlGen {
161 final String pageTitle;
162 final String inputFile;
163 final List<SummaryInfo> summaries;
164
165 Page(this.pageTitle, this.inputFile, this.summaries);
166
167 String get subTitle => 'DDC compilation report for ${inputFile}';
168
169 String create() {
170 start(
171 title: 'DDC ${pageTitle}',
172 theme: 'http://primercss.io/docs.css',
173 inlineStyle: _css);
174
175 header();
176 startTag('div', c: "container");
177 startTag('div', c: "columns docs-layout");
178
179 startTag('div', c: "column one-fourth");
180 nav();
181 endTag();
182
183 startTag('div', c: "column three-fourths");
184 subtitle();
185 contents();
186 endTag();
187
188 endTag();
189 footer();
190 endTag();
191 end();
192
193 return toString();
194 }
195
196 void header() {
197 startTag('header', c: "masthead");
198 startTag('div', c: "container");
199 title();
200 startTag('nav', c: "masthead-nav");
201 tag("a",
202 href:
203 "https://github.com/dart-lang/dev_compiler/blob/master/STRONG_MODE.m d",
204 text: "Strong Mode");
205 tag("a",
206 href: "https://github.com/dart-lang/dev_compiler", text: "DDC Repo");
207 endTag();
208 endTag();
209 endTag();
210 }
211
212 void title() {
213 tag("a", c: "masthead-logo", text: pageTitle);
214 }
215
216 void subtitle() {
217 tag("h1", text: subTitle, c: "page-title");
218 }
219
220 void contents() {
221 int errorCount = summaries.fold(
222 0, (int count, SummaryInfo info) => count + info.errorCount);
223 int warningCount = summaries.fold(
224 0, (int count, SummaryInfo info) => count + info.warningCount);
225 int infoCount = summaries.fold(
226 0, (int count, SummaryInfo info) => count + info.infoCount);
227
228 List<String> messages = [];
229
230 if (errorCount > 0) {
231 messages.add("${_comma(errorCount)} ${_pluralize(errorCount, 'error')}");
232 }
233 if (warningCount > 0) {
234 messages.add(
235 "${_comma(warningCount)} ${_pluralize(warningCount, 'warning')}");
236 }
237 if (infoCount > 0) {
238 messages.add("${_comma(infoCount)} ${_pluralize(infoCount, 'info')}");
239 }
240
241 String message;
242
243 if (messages.isEmpty) {
244 message = 'no issues';
245 } else if (messages.length == 2) {
246 message = messages.join(' and ');
247 } else {
248 message = messages.join(', ');
249 }
250
251 tag("p", text: 'Found ${message}.');
252
253 for (SummaryInfo info in summaries) {
254 if (!info.hasIssues) continue;
255
256 tag("h2", text: info.longTitle, attributes: "id=${info.ref}");
257 contentItem(info);
258 }
259 }
260
261 void nav() {
262 startTag("nav", c: "menu docs-menu");
263 Iterable<String> categories =
264 new LinkedHashSet.from(summaries.map((s) => s.category));
265 for (String category in categories) {
266 navItems(category, summaries.where((s) => s.category == category));
267 }
268 endTag();
269 }
270
271 void navItems(String category, List<SummaryInfo> infos) {
272 if (infos.isEmpty) return;
273
274 span(c: "menu-heading", text: category);
275
276 for (SummaryInfo info in infos) {
277 if (info.hasIssues) {
278 startTag("a", c: "menu-item", attributes: 'href="#${info.ref}"');
279
280 span(text: info.shortTitle);
281
282 int errorCount = info.errorCount;
283 int warningCount = info.warningCount;
284 int infoCount = info.infoCount;
285
286 if (infoCount > 0) {
287 span(c: "counter info", text: '${_comma(infoCount)}');
288 }
289 if (warningCount > 0) {
290 span(c: "counter warning", text: '${_comma(warningCount)}');
291 }
292 if (errorCount > 0) {
293 span(c: "counter error", text: '${_comma(errorCount)}');
294 }
295
296 endTag();
297 } else {
298 tag("div", c: "menu-item", text: info.shortTitle);
299 }
300 }
301 }
302
303 void footer() {
304 startTag('footer', c: "footer");
305 writeln("${inputFile} • DDC version ${devCompilerVersion}");
306 endTag();
307 }
308
309 void contentItem(SummaryInfo info) {
310 int errors = info.errorCount;
311 int warnings = info.warningCount;
312 int infos = info.infoCount;
313
314 if (errors > 0) {
315 span(
316 c: 'counter error',
317 text: '${_comma(errors)} ${_pluralize(errors, 'error')}');
318 }
319 if (warnings > 0) {
320 span(
321 c: 'counter warning',
322 text: '${_comma(warnings)} ${_pluralize(warnings, 'warning')}');
323 }
324 if (infos > 0) {
325 span(
326 c: 'counter info',
327 text: '${_comma(infos)} ${_pluralize(infos, 'info')}');
328 }
329
330 info.issues.forEach(emitMessage);
331 }
332
333 void emitMessage(MessageSummary issue) {
334 startTag('div', c: 'file');
335 startTag('div', c: 'file-header');
336 span(c: 'counter ${issue.level}', text: issue.kind);
337 span(c: 'file-info', text: issue.span.sourceUrl.toString());
338 endTag();
339
340 startTag('div', c: 'blob-wrapper');
341 startTag('table');
342 startTag('tbody');
343
344 // TODO: Widen the line extracts - +2 on either side.
345 // TODO: Highlight error ranges.
346 if (issue.span is SourceSpanWithContext) {
347 SourceSpanWithContext context = issue.span;
348 String text = context.context.trimRight();
349 int lineNum = context.start.line;
350
351 for (String line in text.split('\n')) {
352 lineNum++;
353 startTag('tr');
354 tag('td', c: 'blob-num', text: lineNum.toString());
355 tag('td',
356 c: 'blob-code blob-code-inner', text: HTML_ESCAPE.convert(line));
357 endTag();
358 }
359 }
360
361 startTag('tr', c: 'row-expandable');
362 tag('td', c: 'blob-num blob-num-expandable');
363 tag('td',
364 c: 'blob-code blob-code-expandable',
365 text: HTML_ESCAPE.convert(issue.message));
366 endTag();
367
368 endTag();
369 endTag();
370 endTag();
371
372 endTag();
373 }
374 }
375
376 String _pluralize(int count, String item) => count == 1 ? item : '${item}s';
377
378 String _comma(int count) {
379 String str = '${count}';
380 if (str.length <= 3) return str;
381 int pos = str.length - 3;
382 return str.substring(0, pos) + ',' + str.substring(pos);
383 }
384
385 /// Deltas from the baseline Primer css (http://primercss.io/docs.css).
386 const String _css = '''
387 h2 {
388 margin-top: 2em;
389 padding-bottom: 0.3em;
390 font-size: 1.75em;
391 line-height: 1.225;
392 border-bottom: 1px solid #eee;
393 }
394
395 .error {
396 background-color: #bf1515;
397 }
398
399 .menu-item .counter {
400 margin-bottom: 0;
401 }
402
403 .counter.error {
404 color: #eee;
405 text-shadow: none;
406 }
407
408 .warning {
409 background-color: #ffe5a7;
410 }
411
412 .counter.warning {
413 color: #777;
414 }
415
416 .counter.error,
417 .counter.warning,
418 .counter.info {
419 margin-bottom: 0;
420 }
421
422 nav.menu .menu-item {
423 overflow-x: auto;
424 }
425
426 .info {
427 background-color: #eee;
428 }
429
430 /* code snippets styles */
431
432 .file {
433 position: relative;
434 margin-top: 20px;
435 margin-bottom: 15px;
436 border: 1px solid #ddd;
437 border-radius: 3px;
438 }
439
440 .file-header {
441 padding: 5px 10px;
442 background-color: #f7f7f7;
443 border-bottom: 1px solid #d8d8d8;
444 border-top-left-radius: 2px;
445 border-top-right-radius: 2px;
446 }
447
448 .file-info {
449 font-size: 12px;
450 font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
451 }
452
453 table {
454 border-collapse: collapse;
455 border-spacing: 0;
456 margin-bottom: 0;
457 }
458
459 .blob-wrapper {
460 overflow-x: auto;
461 overflow-y: hidden;
462 }
463
464 .blob-num {
465 width: 1%;
466 min-width: 50px;
467 white-space: nowrap;
468 font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
469 font-size: 12px;
470 line-height: 18px;
471 color: rgba(0,0,0,0.3);
472 vertical-align: top;
473 text-align: right;
474 border: solid #eee;
475 border-width: 0 1px 0 0;
476 cursor: pointer;
477 -webkit-user-select: none;
478 -moz-user-select: none;
479 -ms-user-select: none;
480 user-select: none;
481 padding-left: 10px;
482 padding-right: 10px;
483 }
484
485 .blob-code {
486 padding-left: 10px;
487 padding-right: 10px;
488 vertical-align: top;
489 }
490
491 .blob-code-inner {
492 font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
493 font-size: 12px;
494 color: #333;
495 white-space: pre;
496 overflow: visible;
497 word-wrap: normal;
498 }
499
500 .row-expandable {
501 border-top: 1px solid #d8d8d8;
502 border-bottom-left-radius: 3px;
503 border-bottom-right-radius: 3px;
504 }
505
506 .blob-num-expandable,
507 .blob-code-expandable {
508 vertical-align: middle;
509 font-size: 14px;
510 border-color: #d2dff0;
511 }
512
513 .blob-num-expandable {
514 background-color: #edf2f9;
515 border-bottom-left-radius: 3px;
516 }
517
518 .blob-code-expandable {
519 padding-top: 4px;
520 padding-bottom: 4px;
521 background-color: #f4f7fb;
522 border-width: 1px 0;
523 border-bottom-right-radius: 3px;
524 }
525 ''';
OLDNEW
« no previous file with comments | « lib/src/report/html_gen.dart ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698