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