| // Copyright (c) 2016, 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. |
| |
| // @dart = 2.7 |
| |
| library sourcemap.diff_view; |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:_fe_analyzer_shared/src/util/filenames.dart'; |
| import 'package:compiler/src/common_elements.dart'; |
| import 'package:compiler/src/commandline_options.dart'; |
| import 'package:compiler/src/diagnostics/invariant.dart'; |
| import 'package:compiler/src/elements/entities.dart'; |
| import 'package:compiler/src/io/position_information.dart'; |
| import 'package:compiler/src/io/source_information.dart'; |
| import 'package:compiler/src/io/source_file.dart'; |
| import 'package:compiler/src/js/js.dart' as js; |
| |
| import '../helpers/diff.dart'; |
| import '../helpers/html_parts.dart'; |
| import '../helpers/js_tracer.dart'; |
| import '../helpers/output_structure.dart'; |
| import '../helpers/sourcemap_helper.dart'; |
| import '../helpers/sourcemap_html_helper.dart'; |
| import '../helpers/trace_graph.dart'; |
| |
| main(List<String> args) async { |
| DEBUG_MODE = true; |
| String out = 'out.js.diff_view.html'; |
| String filename; |
| List<String> currentOptions = []; |
| List<List<String>> optionSegments = [currentOptions]; |
| Map<int, String> loadFrom = {}; |
| Map<int, String> saveTo = {}; |
| int argGroup = 0; |
| bool showAnnotations = true; |
| for (String arg in args) { |
| if (arg == '--') { |
| currentOptions = []; |
| optionSegments.add(currentOptions); |
| argGroup++; |
| } else if (arg == '-h') { |
| showAnnotations = false; |
| print('Hiding annotations'); |
| } else if (arg == '-l') { |
| loadFrom[argGroup] = 'out.js.diff$argGroup.json'; |
| } else if (arg.startsWith('--load=')) { |
| loadFrom[argGroup] = arg.substring('--load='.length); |
| } else if (arg == '-s') { |
| saveTo[argGroup] = 'out.js.diff$argGroup.json'; |
| } else if (arg.startsWith('--save=')) { |
| saveTo[argGroup] = arg.substring('--save='.length); |
| } else if (arg.startsWith('-o')) { |
| out = arg.substring('-o'.length); |
| } else if (arg.startsWith('--out=')) { |
| out = arg.substring('--out='.length); |
| } else if (arg.startsWith('-')) { |
| currentOptions.add(arg); |
| } else { |
| filename = arg; |
| } |
| } |
| List<String> commonArguments = optionSegments[0]; |
| List<List<String>> options = <List<String>>[]; |
| if (optionSegments.length == 1) { |
| // TODO(sigmund, johnniwinther): change default options now that CPS is |
| // deleted. |
| // Use default options; comparing SSA and CPS output using the new |
| // source information strategy. |
| options.add(commonArguments); |
| options.add([Flags.useNewSourceInfo]..addAll(commonArguments)); |
| } else if (optionSegments.length == 2) { |
| // Use alternative options for the second output column. |
| options.add(commonArguments); |
| options.add(optionSegments[1]..addAll(commonArguments)); |
| } else { |
| // Use specific options for both output columns. |
| options.add(optionSegments[1]..addAll(commonArguments)); |
| options.add(optionSegments[2]..addAll(commonArguments)); |
| } |
| |
| SourceFileManager sourceFileManager = new IOSourceFileManager(Uri.base); |
| List<AnnotatedOutput> outputs = <AnnotatedOutput>[]; |
| for (int i = 0; i < 2; i++) { |
| AnnotatedOutput output; |
| if (loadFrom.containsKey(i)) { |
| output = AnnotatedOutput.loadOutput(loadFrom[i]); |
| } else { |
| print('Compiling ${options[i].join(' ')} $filename'); |
| CodeLinesResult result = await computeCodeLines( |
| options[i], filename, Uri.base.resolve(nativeToUriPath(filename))); |
| OutputStructure structure = OutputStructure.parse(result.codeLines); |
| computeEntityCodeSources(result, structure); |
| output = new AnnotatedOutput( |
| filename, options[i], structure, result.coverage.getCoverageReport()); |
| } |
| if (saveTo.containsKey(i)) { |
| AnnotatedOutput.saveOutput(output, saveTo[i]); |
| } |
| outputs.add(output); |
| } |
| |
| List<DiffBlock> blocks = createDiffBlocks( |
| outputs.map((o) => o.structure).toList(), sourceFileManager); |
| |
| outputDiffView(out, outputs, blocks, |
| showMarkers: showAnnotations, showSourceMapped: showAnnotations); |
| } |
| |
| /// Attaches [CodeSource]s to the entities in [structure] using the |
| /// element-to-offset in [result]. |
| void computeEntityCodeSources( |
| CodeLinesResult result, OutputStructure structure) { |
| result.elementMap.forEach((int line, MemberEntity element) { |
| OutputEntity entity = structure.getEntityForLine(line); |
| if (entity != null) { |
| entity.codeSource = codeSourceFromElement(element); |
| } |
| }); |
| } |
| |
| class CodeLineAnnotationJsonStrategy implements JsonStrategy { |
| const CodeLineAnnotationJsonStrategy(); |
| |
| @override |
| Map encodeAnnotation(Annotation annotation) { |
| CodeLineAnnotation data = annotation.data; |
| return { |
| 'id': annotation.id, |
| 'codeOffset': annotation.codeOffset, |
| 'title': annotation.title, |
| 'data': data.toJson(this), |
| }; |
| } |
| |
| @override |
| Annotation decodeAnnotation(Map json) { |
| return new Annotation(json['id'], json['codeOffset'], json['title'], |
| data: CodeLineAnnotation.fromJson(json['data'], this)); |
| } |
| |
| @override |
| decodeLineAnnotation(json) { |
| if (json != null) { |
| return CodeSource.fromJson(json); |
| } |
| return null; |
| } |
| |
| @override |
| encodeLineAnnotation(covariant CodeSource lineAnnotation) { |
| if (lineAnnotation != null) { |
| return lineAnnotation.toJson(); |
| } |
| return null; |
| } |
| } |
| |
| /// The structured output of a compilation. |
| class AnnotatedOutput { |
| final String filename; |
| final List<String> options; |
| final OutputStructure structure; |
| final String coverage; |
| |
| AnnotatedOutput(this.filename, this.options, this.structure, this.coverage); |
| |
| List<CodeLine> get codeLines => structure.lines; |
| |
| Map toJson() { |
| return { |
| 'filename': filename, |
| 'options': options, |
| 'structure': structure.toJson(const CodeLineAnnotationJsonStrategy()), |
| 'coverage': coverage, |
| }; |
| } |
| |
| static AnnotatedOutput fromJson(Map json) { |
| String filename = json['filename']; |
| List<String> options = json['options']; |
| OutputStructure structure = OutputStructure.fromJson( |
| json['structure'], const CodeLineAnnotationJsonStrategy()); |
| String coverage = json['coverage']; |
| return new AnnotatedOutput(filename, options, structure, coverage); |
| } |
| |
| static AnnotatedOutput loadOutput(filename) { |
| AnnotatedOutput output = AnnotatedOutput.fromJson( |
| json.decode(new File(filename).readAsStringSync())); |
| print('Output loaded from $filename'); |
| return output; |
| } |
| |
| static void saveOutput(AnnotatedOutput output, String filename) { |
| if (filename != null) { |
| new File(filename).writeAsStringSync( |
| const JsonEncoder.withIndent(' ').convert(output.toJson())); |
| print('Output saved in $filename'); |
| } |
| } |
| } |
| |
| void outputDiffView( |
| String out, List<AnnotatedOutput> outputs, List<DiffBlock> blocks, |
| {bool showMarkers: true, bool showSourceMapped: true}) { |
| assert(outputs[0].filename == outputs[1].filename); |
| bool usePre = true; |
| |
| StringBuffer sb = new StringBuffer(); |
| sb.write(''' |
| <html> |
| <head> |
| <title>Diff for ${outputs[0].filename}</title> |
| <style> |
| .${ClassNames.lineNumber} { |
| font-size: smaller; |
| color: #888; |
| } |
| .${ClassNames.comment} { |
| font-size: smaller; |
| color: #888; |
| font-family: initial; |
| } |
| .${ClassNames.header} { |
| position: fixed; |
| width: 100%; |
| background-color: #FFFFFF; |
| left: 0px; |
| top: 0px; |
| height: 42px; |
| z-index: 1000; |
| } |
| .${ClassNames.headerTable} { |
| width: 100%; |
| background-color: #400000; |
| color: #FFFFFF; |
| border-spacing: 0px; |
| } |
| .${ClassNames.headerColumn} { |
| } |
| .${ClassNames.legend} { |
| padding: 2px; |
| } |
| .${ClassNames.buttons} { |
| position: fixed; |
| right: 0px; |
| top: 0px; |
| width: 220px; |
| background-color: #FFFFFF; |
| border: 1px solid #C0C0C0; |
| z-index: 2000; |
| } |
| .${ClassNames.table} { |
| position: absolute; |
| left: 0px; |
| top: 42px; |
| width: 100%; |
| border-spacing: 0px; |
| } |
| .${ClassNames.cell}, |
| .${ClassNames.innerCell}, |
| .${ClassNames.originalDart}, |
| .${ClassNames.inlinedDart} { |
| overflow-y: hidden; |
| vertical-align: top; |
| '''); |
| if (usePre) { |
| sb.write(''' |
| overflow-x: hidden; |
| white-space: pre-wrap; |
| '''); |
| } else { |
| sb.write(''' |
| overflow-x: hidden; |
| padding-left: 100px; |
| text-indent: -100px; |
| '''); |
| } |
| sb.write(''' |
| font-family: monospace; |
| padding: 0px; |
| } |
| .${ClassNames.cell} { |
| border-top: 1px solid #F0F0F0; |
| border-left: 1px solid #C0C0C0; |
| } |
| .${ClassNames.innerCell} { |
| /*border-top: 1px solid #F8F8F8;*/ |
| width: 50%; |
| max-width: 250px; |
| } |
| .${ClassNames.corresponding(false)} { |
| background-color: #FFFFE0; |
| } |
| .${ClassNames.corresponding(true)} { |
| background-color: #EFEFD0; |
| } |
| .${ClassNames.identical(false)} { |
| background-color: #E0F0E0; |
| } |
| .${ClassNames.identical(true)} { |
| background-color: #C0E0C0; |
| } |
| .${ClassNames.line} { |
| padding-left: 7em; |
| text-indent: -7em; |
| margin: 0px; |
| } |
| .${ClassNames.column(column_js0)} { |
| max-width: 500px; |
| width: 500px; |
| } |
| .${ClassNames.column(column_js1)} { |
| max-width: 500px; |
| width: 500px; |
| } |
| .${ClassNames.column(column_dart)} { |
| max-width: 300px; |
| width: 300px; |
| } |
| .${ClassNames.colored(0)} { |
| color: #FF0000; |
| } |
| .${ClassNames.colored(1)} { |
| color: #C0C000; |
| } |
| .${ClassNames.colored(2)} { |
| color: #008000; |
| } |
| .${ClassNames.colored(3)} { |
| color: #00C0C0; |
| } |
| .${ClassNames.withSourceInfo} { |
| border: solid 1px #FF8080; |
| } |
| .${ClassNames.withoutSourceInfo} { |
| background-color: #8080FF; |
| } |
| .${ClassNames.additionalSourceInfo} { |
| border: solid 1px #80FF80; |
| } |
| .${ClassNames.unusedSourceInfo} { |
| border: solid 1px #8080FF; |
| } |
| .${ClassNames.originalDart} { |
| } |
| .${ClassNames.inlinedDart} { |
| } |
| '''); |
| for (int i = 0; i < HUE_COUNT; i++) { |
| sb.write(''' |
| .${ClassNames.sourceMappingIndex(i)} { |
| background-color: ${toColorCss(i)}; |
| } |
| '''); |
| } |
| sb.write(''' |
| .${ClassNames.sourceMapped} { |
| ${showSourceMapped ? '' : 'display: none;'} |
| } |
| .${ClassNames.sourceMapping} { |
| ${showSourceMapped ? '' : 'border: 0px;'} |
| ${showSourceMapped ? '' : 'background-color: transparent;'} |
| } |
| .${ClassNames.markers} { |
| ${showMarkers ? '' : 'display: none;'} |
| } |
| .${ClassNames.marker} { |
| ${showMarkers ? '' : 'border: 0px;'} |
| ${showMarkers ? '' : 'background-color: transparent;'} |
| } |
| </style> |
| <script> |
| function isChecked(name) { |
| var box = document.getElementById('box-' + name); |
| return box.checked; |
| } |
| function toggleDisplay(name) { |
| var checked = isChecked(name); |
| var styleSheet = document.styleSheets[0]; |
| for (var index = 0; index < styleSheet.cssRules.length; index++) { |
| var cssRule = styleSheet.cssRules[index]; |
| if (cssRule.selectorText == '.' + name) { |
| if (checked) { |
| cssRule.style.removeProperty('display'); |
| } else { |
| cssRule.style.display = 'none'; |
| } |
| } |
| } |
| return checked; |
| } |
| function toggle${ClassNames.sourceMapped}() { |
| var checked = toggleDisplay('${ClassNames.sourceMapped}'); |
| toggleAnnotations(checked, '${ClassNames.sourceMapping}'); |
| } |
| function toggle${ClassNames.markers}() { |
| var checked = toggleDisplay('${ClassNames.markers}'); |
| toggleAnnotations(checked, '${ClassNames.marker}'); |
| } |
| function toggleAnnotations(show, name) { |
| var styleSheet = document.styleSheets[0]; |
| for (var index = 0; index < styleSheet.cssRules.length; index++) { |
| var cssRule = styleSheet.cssRules[index]; |
| if (cssRule.selectorText == '.' + name) { |
| if (show) { |
| cssRule.style.removeProperty('border'); |
| cssRule.style.removeProperty('background-color'); |
| } else { |
| cssRule.style.border = '0px'; |
| cssRule.style.backgroundColor = 'transparent'; |
| } |
| } |
| } |
| } |
| </script> |
| </head> |
| <body>'''); |
| |
| sb.write(''' |
| <div class="${ClassNames.header}"> |
| <div class="${ClassNames.legend}"> |
| <span class="${ClassNames.identical(false)}"> </span> |
| <span class="${ClassNames.identical(true)}"> </span> |
| identical blocks |
| <span class="${ClassNames.corresponding(false)}"> </span> |
| <span class="${ClassNames.corresponding(true)}"> </span> |
| corresponding blocks |
| '''); |
| |
| sb.write(''' |
| <span class="${ClassNames.markers}"> |
| <span class="${ClassNames.withSourceInfo}"> </span> |
| <span title="'offset with source information' means that source information |
| is available for an offset which is expected to have a source location |
| attached. This offset has source information as intended."> |
| offset with source information</span> |
| <span class="${ClassNames.withoutSourceInfo}"> </span> |
| <span title="'offset without source information' means that _no_ source |
| information is available for an offset which was expected to have a source |
| location attached. Source information must be found for this offset."> |
| offset without source information</span> |
| <span class="${ClassNames.additionalSourceInfo}"> </span> |
| <span title="'offset with unneeded source information' means that a source |
| location was attached to an offset which was _not_ expected to have a source |
| location attached. The source location should be removed from this offset."> |
| offset with unneeded source information</span> |
| <span class="${ClassNames.unusedSourceInfo}"> </span> |
| <span title="'offset with unused source information' means that source |
| information is available for an offset which is _not_ expected to have a source |
| location attached. This source information _could_ be used by a parent AST node |
| offset that is an 'offset without source information'."> |
| offset with unused source information</span> |
| </span> |
| <span class="${ClassNames.sourceMapped}"> |
| '''); |
| for (int i = 0; i < HUE_COUNT; i++) { |
| sb.write(''' |
| <span class="${ClassNames.sourceMappingIndex(i)}"> </span>'''); |
| } |
| sb.write(''' |
| <span title="JavaScript offsets and their corresponding Dart Code offset |
| as mapped through source-maps."> |
| mapped source locations</span> |
| </span> |
| '''); |
| |
| /// Marker to alternate output colors. |
| bool alternating = false; |
| |
| List<HtmlPrintContext> printContexts = <HtmlPrintContext>[]; |
| for (int i = 0; i < 2; i++) { |
| int lineNoWidth; |
| if (outputs[i].codeLines.isNotEmpty) { |
| lineNoWidth = '${outputs[i].codeLines.last.lineNo + 1}'.length; |
| } |
| printContexts.add(new HtmlPrintContext( |
| lineNoWidth: lineNoWidth, |
| getAnnotationData: getAnnotationData, |
| getLineData: getLineData)); |
| } |
| |
| Set<DiffColumn> allColumns = new Set<DiffColumn>(); |
| for (DiffBlock block in blocks) { |
| allColumns.addAll(block.columns); |
| } |
| |
| List<DiffColumn> columns = [column_js0, column_js1, column_dart] |
| .where((c) => allColumns.contains(c)) |
| .toList(); |
| |
| sb.write(''' |
| </div> |
| <table class="${ClassNames.headerTable}"><tr>'''); |
| for (DiffColumn column in columns) { |
| sb.write(''' |
| <td class="${ClassNames.headerColumn} ${ClassNames.column(column)}">'''); |
| if (column.type == 'js') { |
| sb.write('''[${outputs[column.index].options.join(',')}]'''); |
| } else { |
| sb.write('''Dart code'''); |
| } |
| sb.write('''</td>'''); |
| } |
| |
| sb.write(''' |
| </tr></table> |
| </div> |
| <table class="${ClassNames.table}"> |
| '''); |
| |
| for (DiffBlock block in blocks) { |
| String className; |
| switch (block.kind) { |
| case DiffKind.UNMATCHED: |
| className = '${ClassNames.cell}'; |
| break; |
| case DiffKind.MATCHING: |
| className = |
| '${ClassNames.cell} ${ClassNames.corresponding(alternating)}'; |
| alternating = !alternating; |
| break; |
| case DiffKind.IDENTICAL: |
| className = '${ClassNames.cell} ${ClassNames.identical(alternating)}'; |
| alternating = !alternating; |
| break; |
| } |
| sb.write('<tr>'); |
| for (DiffColumn column in columns) { |
| sb.write('''<td class="$className ${ClassNames.column(column)}">'''); |
| HtmlPrintContext context = new HtmlPrintContext( |
| lineNoWidth: 4, |
| includeAnnotation: (Annotation annotation) { |
| CodeLineAnnotation data = annotation.data; |
| return data.annotationType == AnnotationType.WITH_SOURCE_INFO || |
| data.annotationType == AnnotationType.ADDITIONAL_SOURCE_INFO; |
| }, |
| getAnnotationData: getAnnotationData, |
| getLineData: getLineData); |
| if (column.type == 'js') { |
| context = printContexts[column.index]; |
| } |
| block.printHtmlOn(column, sb, context); |
| sb.write('''</td>'''); |
| } |
| sb.write('</tr>'); |
| } |
| |
| sb.write('''</tr><tr>'''); |
| |
| for (DiffColumn column in columns) { |
| sb.write(''' |
| <td class="${ClassNames.cell} ${ClassNames.column(column)}"><pre>'''); |
| if (column.type == 'js') { |
| sb.write(outputs[column.index].coverage); |
| } |
| sb.write('''</td>'''); |
| } |
| |
| sb.write(''' |
| </table> |
| <div class="${ClassNames.buttons}"> |
| <input type="checkbox" id="box-${ClassNames.column(column_js0)}" |
| onclick="javascript:toggleDisplay('${ClassNames.column(column_js0)}')" |
| checked> |
| Left JavaScript code<br/> |
| |
| <input type="checkbox" id="box-${ClassNames.column(column_js1)}" |
| onclick="javascript:toggleDisplay('${ClassNames.column(column_js1)}')" |
| checked> |
| Right JavaScript code<br/> |
| |
| <input type="checkbox" id="box-${ClassNames.column(column_dart)}" |
| onclick="javascript:toggleDisplay('${ClassNames.column(column_dart)}')" |
| checked> |
| <span title="Show column with Dart code corresponding to the block."> |
| Dart code</span><br/> |
| |
| <input type="checkbox" id="box-${ClassNames.inlinedDart}" |
| onclick="javascript:toggleDisplay('${ClassNames.inlinedDart}')" checked> |
| <span title="Show Dart code inlined into the block."> |
| Inlined Dart code</span><br/> |
| |
| <input type="checkbox" id="box-${ClassNames.markers}" |
| onclick="javascript:toggle${ClassNames.markers}()" |
| ${showMarkers ? 'checked' : ''}> |
| <span title="Show markers for JavaScript offsets with source information."> |
| Source information markers</span><br/> |
| |
| <input type="checkbox" id="box-${ClassNames.sourceMapped}" |
| onclick="javascript:toggle${ClassNames.sourceMapped}()" |
| ${showSourceMapped ? 'checked' : ''}> |
| <span title="Show line-per-line mappings of JavaScript to Dart code."> |
| Source mapped Dart code</span><br/> |
| </div> |
| </body> |
| </html> |
| '''); |
| |
| File file = new File(out); |
| file.writeAsStringSync(sb.toString()); |
| print('Diff generated in ${file.absolute.uri}'); |
| } |
| |
| class CodeLinesResult { |
| final List<CodeLine> codeLines; |
| final Coverage coverage; |
| final Map<int, MemberEntity> elementMap; |
| final SourceFileManager sourceFileManager; |
| final CodeSources codeSources; |
| |
| CodeLinesResult(this.codeLines, this.coverage, this.elementMap, |
| this.sourceFileManager, this.codeSources); |
| } |
| |
| class CodeSources { |
| Map<Entity, CodeSource> codeSourceMap = <Entity, CodeSource>{}; |
| Map<Uri, Map<Interval, CodeSource>> uriCodeSourceMap = |
| <Uri, Map<Interval, CodeSource>>{}; |
| |
| CodeSources(SourceMapProcessor processor, SourceMaps sourceMaps) { |
| ElementEnvironment elementEnvironment = |
| sourceMaps.compiler.backendClosedWorldForTesting.elementEnvironment; |
| |
| CodeSource computeCodeSource(Entity element) { |
| return codeSourceMap.putIfAbsent(element, () { |
| CodeSource codeSource = codeSourceFromElement(element); |
| if (codeSource.begin != null) { |
| Interval interval = new Interval(codeSource.begin, codeSource.end); |
| Map<Interval, CodeSource> intervals = |
| uriCodeSourceMap[codeSource.uri]; |
| if (intervals == null) { |
| intervals = <Interval, CodeSource>{}; |
| uriCodeSourceMap[codeSource.uri] = intervals; |
| } else { |
| for (Interval existingInterval in intervals.keys.toList()) { |
| if (existingInterval.contains(interval.from)) { |
| CodeSource existingCodeSource = intervals[existingInterval]; |
| intervals.remove(existingInterval); |
| if (existingInterval.from < interval.from) { |
| Interval preInterval = |
| new Interval(existingInterval.from, interval.from); |
| intervals[preInterval] = existingCodeSource; |
| } |
| if (interval.to < existingInterval.to) { |
| Interval postInterval = |
| new Interval(interval.to, existingInterval.to); |
| intervals[postInterval] = existingCodeSource; |
| } |
| } |
| } |
| } |
| intervals[interval] = codeSource; |
| } |
| if (element is ClassEntity) { |
| elementEnvironment.forEachConstructor(element, computeCodeSource); |
| elementEnvironment.forEachLocalClassMember( |
| element, computeCodeSource); |
| } |
| return codeSource; |
| }); |
| } |
| |
| for (LibraryEntity library in elementEnvironment.libraries) { |
| elementEnvironment.forEachClass(library, computeCodeSource); |
| elementEnvironment.forEachLibraryMember(library, computeCodeSource); |
| } |
| |
| uriCodeSourceMap.forEach((Uri uri, Map<Interval, CodeSource> intervals) { |
| List<Interval> sortedKeys = intervals.keys.toList() |
| ..sort((i1, i2) => i1.from.compareTo(i2.from)); |
| Map<Interval, CodeSource> sortedintervals = <Interval, CodeSource>{}; |
| sortedKeys.forEach((Interval interval) { |
| sortedintervals[interval] = intervals[interval]; |
| }); |
| uriCodeSourceMap[uri] = sortedintervals; |
| }); |
| } |
| |
| CodeSource sourceLocationToCodeSource(SourceLocation sourceLocation) { |
| Map<Interval, CodeSource> intervals = |
| uriCodeSourceMap[sourceLocation.sourceUri]; |
| if (intervals == null) { |
| print('No code source for $sourceLocation(${sourceLocation.offset})'); |
| print(' -- no intervals for ${sourceLocation.sourceUri}'); |
| return null; |
| } |
| for (Interval interval in intervals.keys) { |
| if (interval.contains(sourceLocation.offset)) { |
| return intervals[interval]; |
| } |
| } |
| print('No code source for $sourceLocation(${sourceLocation.offset})'); |
| intervals.forEach((k, v) => print(' $k: ${v.name}')); |
| return null; |
| } |
| } |
| |
| /// Compute [CodeLine]s and [Coverage] for [filename] using the given [options]. |
| Future<CodeLinesResult> computeCodeLines( |
| List<String> options, String filename, Uri uri) async { |
| SourceMapProcessor processor = new SourceMapProcessor(uri); |
| SourceMaps sourceMaps = |
| await processor.process(options, perElement: true, forMain: true); |
| |
| CodeSources codeSources = new CodeSources(processor, sourceMaps); |
| |
| SourceMapInfo info = sourceMaps.mainSourceMapInfo; |
| |
| int nextAnnotationId = 0; |
| List<CodeLine> codeLines; |
| Coverage coverage = new Coverage(); |
| Map<int, List<CodeLineAnnotation>> codeLineAnnotationMap = |
| <int, List<CodeLineAnnotation>>{}; |
| |
| /// Create a [CodeLineAnnotation] for [codeOffset]. |
| void addCodeLineAnnotation( |
| {AnnotationType annotationType, |
| int codeOffset, |
| List<SourceLocation> locations: const <SourceLocation>[], |
| String stepInfo}) { |
| if (annotationType == AnnotationType.WITHOUT_SOURCE_INFO || |
| annotationType == AnnotationType.UNUSED_SOURCE_INFO) { |
| locations = []; |
| } |
| List<CodeLocation> codeLocations = locations |
| .where((l) => l.sourceUri != null) |
| .map((l) => new CodeLocation(l.sourceUri, l.sourceName, l.offset)) |
| .toList(); |
| List<CodeSource> codeSourceList = locations |
| .where((l) => l.sourceUri != null) |
| .map(codeSources.sourceLocationToCodeSource) |
| .where((c) => c != null) |
| .toList(); |
| CodeLineAnnotation data = new CodeLineAnnotation( |
| annotationId: nextAnnotationId++, |
| annotationType: annotationType, |
| codeLocations: codeLocations, |
| codeSources: codeSourceList, |
| stepInfo: stepInfo); |
| codeLineAnnotationMap |
| .putIfAbsent(codeOffset, () => <CodeLineAnnotation>[]) |
| .add(data); |
| } |
| |
| String code = info.code; |
| TraceGraph graph = createTraceGraph(info, coverage); |
| |
| Set<js.Node> mappedNodes = new Set<js.Node>(); |
| |
| /// Add an annotation for [codeOffset] pointing to [locations]. |
| void addSourceLocations( |
| {AnnotationType annotationType, |
| int codeOffset, |
| List<SourceLocation> locations, |
| String stepInfo}) { |
| locations = locations.where((l) => l != null).toList(); |
| addCodeLineAnnotation( |
| annotationType: annotationType, |
| codeOffset: codeOffset, |
| stepInfo: stepInfo, |
| locations: locations); |
| } |
| |
| /// Add annotations for all mappings created for [node]. |
| bool addSourceLocationsForNode( |
| {AnnotationType annotationType, js.Node node, String stepInfo}) { |
| Map<int, List<SourceLocation>> locations = info.nodeMap[node]; |
| if (locations == null || locations.isEmpty) { |
| return false; |
| } |
| locations.forEach((int offset, List<SourceLocation> locations) { |
| addSourceLocations( |
| annotationType: annotationType, |
| codeOffset: offset, |
| locations: locations, |
| stepInfo: stepInfo); |
| }); |
| mappedNodes.add(node); |
| return true; |
| } |
| |
| // Add annotations based on trace steps. |
| for (TraceStep step in graph.steps) { |
| String stepInfo = '${step.id}:${step.kind}:${step.offset}'; |
| bool added = addSourceLocationsForNode( |
| annotationType: AnnotationType.WITH_SOURCE_INFO, |
| node: step.node, |
| stepInfo: stepInfo); |
| if (!added) { |
| int offset; |
| if (options.contains(Flags.useNewSourceInfo)) { |
| offset = step.offset.value; |
| } else { |
| offset = info.jsCodePositions[step.node].startPosition; |
| } |
| if (offset != null) { |
| addCodeLineAnnotation( |
| annotationType: AnnotationType.WITHOUT_SOURCE_INFO, |
| codeOffset: offset, |
| stepInfo: stepInfo); |
| } |
| } |
| } |
| |
| // Add additional annotations for mappings created for particular nodes. |
| for (js.Node node in info.nodeMap.nodes) { |
| if (!mappedNodes.contains(node)) { |
| addSourceLocationsForNode( |
| annotationType: AnnotationType.ADDITIONAL_SOURCE_INFO, node: node); |
| } |
| } |
| |
| // Add annotations for unused source information associated with nodes. |
| SourceLocationCollector collector = new SourceLocationCollector(); |
| info.node.accept(collector); |
| collector.sourceLocations |
| .forEach((js.Node node, List<SourceLocation> locations) { |
| if (!mappedNodes.contains(node)) { |
| int offset = info.jsCodePositions[node].startPosition; |
| addSourceLocations( |
| annotationType: AnnotationType.UNUSED_SOURCE_INFO, |
| codeOffset: offset, |
| locations: locations); |
| } |
| }); |
| |
| // Assign consecutive ids to source mappings. |
| int nextSourceMappedLocationIndex = 0; |
| List<Annotation> annotations = <Annotation>[]; |
| for (int codeOffset in codeLineAnnotationMap.keys.toList()..sort()) { |
| bool hasSourceMappedLocation = false; |
| for (CodeLineAnnotation data in codeLineAnnotationMap[codeOffset]) { |
| if (data.annotationType.isSourceMapped) { |
| data.sourceMappingIndex = nextSourceMappedLocationIndex; |
| hasSourceMappedLocation = true; |
| } |
| annotations.add(new Annotation( |
| data.annotationType.index, codeOffset, 'id=${data.annotationId}', |
| data: data)); |
| } |
| if (hasSourceMappedLocation) { |
| nextSourceMappedLocationIndex++; |
| } |
| } |
| |
| // Associate JavaScript offsets with [Element]s. |
| StringSourceFile sourceFile = new StringSourceFile.fromName(filename, code); |
| Map<int, MemberEntity> elementMap = <int, MemberEntity>{}; |
| sourceMaps.elementSourceMapInfos |
| .forEach((MemberEntity element, SourceMapInfo info) { |
| CodePosition position = info.jsCodePositions[info.node]; |
| elementMap[sourceFile.getLocation(position.startPosition).line - 1] = |
| element; |
| }); |
| |
| codeLines = convertAnnotatedCodeToCodeLines(code, annotations); |
| return new CodeLinesResult(codeLines, coverage, elementMap, |
| sourceMaps.sourceFileManager, codeSources); |
| } |
| |
| /// Visitor that computes a map from [js.Node]s to all attached source |
| /// locations. |
| class SourceLocationCollector extends js.BaseVisitor { |
| Map<js.Node, List<SourceLocation>> sourceLocations = |
| <js.Node, List<SourceLocation>>{}; |
| |
| @override |
| visitNode(js.Node node) { |
| SourceInformation sourceInformation = node.sourceInformation; |
| if (sourceInformation != null) { |
| sourceLocations[node] = sourceInformation.sourceLocations; |
| } |
| node.visitChildren(this); |
| } |
| } |
| |
| /// Compute a [CodeSource] for source span of [element]. |
| CodeSource codeSourceFromElement(Entity element) { |
| // TODO(johnniwinther): Handle kernel based elements. |
| CodeKind kind; |
| Uri uri; |
| String name; |
| int begin; |
| int end; |
| if (element is LibraryEntity) { |
| kind = CodeKind.LIBRARY; |
| name = element.name; |
| uri = element.canonicalUri; |
| } else if (element is ClassEntity) { |
| kind = CodeKind.CLASS; |
| name = element.name; |
| uri = element.library.canonicalUri; |
| } else if (element is MemberEntity) { |
| kind = CodeKind.MEMBER; |
| uri = element.library.canonicalUri; |
| name = computeElementNameForSourceMaps(element); |
| } |
| return new CodeSource(kind, uri, name, begin, end); |
| } |
| |
| /// Create [LineData] that colors line numbers according to the [CodeSource]s |
| /// origin if available. |
| LineData getLineData(Object lineAnnotation) { |
| if (lineAnnotation != null) { |
| return new LineData( |
| lineClass: ClassNames.line, |
| lineNumberClass: '${ClassNames.lineNumber} ' |
| '${ClassNames.colored(lineAnnotation.hashCode % 4)}'); |
| } |
| return new LineData( |
| lineClass: ClassNames.line, lineNumberClass: ClassNames.lineNumber); |
| } |
| |
| AnnotationData getAnnotationData(Iterable<Annotation> annotations, |
| {bool forSpan}) { |
| for (Annotation annotation in annotations) { |
| CodeLineAnnotation data = annotation.data; |
| if (data.annotationType.isSourceMapped) { |
| if (forSpan) { |
| int index = data.sourceMappingIndex; |
| return new AnnotationData(tag: 'span', properties: { |
| 'class': '${ClassNames.sourceMapping} ' |
| '${ClassNames.sourceMappingIndex(index % HUE_COUNT)}', |
| 'title': 'index=$index', |
| }); |
| } else { |
| return new AnnotationData(tag: 'span', properties: { |
| 'title': annotation.title, |
| 'class': '${ClassNames.marker} ' |
| '${data.annotationType.className}' |
| }); |
| } |
| } |
| } |
| if (forSpan) return null; |
| for (Annotation annotation in annotations) { |
| CodeLineAnnotation data = annotation.data; |
| if (data.annotationType == AnnotationType.UNUSED_SOURCE_INFO) { |
| return new AnnotationData(tag: 'span', properties: { |
| 'title': annotation.title, |
| 'class': '${ClassNames.marker} ' |
| '${data.annotationType.className}' |
| }); |
| } |
| } |
| for (Annotation annotation in annotations) { |
| CodeLineAnnotation data = annotation.data; |
| if (data.annotationType == AnnotationType.WITHOUT_SOURCE_INFO) { |
| return new AnnotationData(tag: 'span', properties: { |
| 'title': annotation.title, |
| 'class': '${ClassNames.marker} ' |
| '${data.annotationType.className}' |
| }); |
| } |
| } |
| return null; |
| } |