blob: ea6456b27431795e89ba16817f57b79d8faf3df4 [file] [log] [blame]
// 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.
library sourcemap.diff_view;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
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/filenames.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();
Map encodeAnnotation(Annotation annotation) {
CodeLineAnnotation data = annotation.data;
return {
'id': annotation.id,
'codeOffset': annotation.codeOffset,
'title': annotation.title,
'data': data.toJson(this),
};
}
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)}">&nbsp;&nbsp;&nbsp;</span>
<span class="${ClassNames.identical(true)}">&nbsp;&nbsp;&nbsp;</span>
identical blocks
<span class="${ClassNames.corresponding(false)}">&nbsp;&nbsp;&nbsp;</span>
<span class="${ClassNames.corresponding(true)}">&nbsp;&nbsp;&nbsp;</span>
corresponding blocks
''');
sb.write('''
<span class="${ClassNames.markers}">
<span class="${ClassNames.withSourceInfo}">&nbsp;&nbsp;&nbsp;</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}">&nbsp;&nbsp;&nbsp;</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}">&nbsp;&nbsp;&nbsp;</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}">&nbsp;&nbsp;&nbsp;</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)}">&nbsp;&nbsp;</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;
}