| 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 |