| 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 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 '''; |
| OLD | NEW |