2bb32ec55a
Change-Id: Ib1afd38a7d3694c2e5ed591132f41b147f76d75a Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/427560 Reviewed-by: Stephen Adams <sra@google.com> Commit-Queue: Mayank Patke <fishythefish@google.com>
1047 lines
31 KiB
Dart
1047 lines
31 KiB
Dart
// 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:_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 {
|
|
debugMode = true;
|
|
String out = 'out.js.diff_view.html';
|
|
late 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 = 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 = 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 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
|
|
Map? 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 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) {
|
|
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);
|
|
|
|
StringBuffer sb = 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;
|
|
overflow-x: hidden;
|
|
white-space: pre-wrap;
|
|
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 = 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 = HtmlPrintContext(
|
|
lineNoWidth: 4,
|
|
includeAnnotation: (Annotation annotation) {
|
|
CodeLineAnnotation data = annotation.data;
|
|
return data.annotationType == AnnotationType.withSourceInfo ||
|
|
data.annotationType == AnnotationType.additionalSourceInfo;
|
|
},
|
|
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 = 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 = 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 = Interval(
|
|
existingInterval.from,
|
|
interval.from,
|
|
);
|
|
intervals[preInterval] = existingCodeSource;
|
|
}
|
|
if (interval.to < existingInterval.to) {
|
|
Interval postInterval = 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 = SourceMapProcessor(uri);
|
|
SourceMaps sourceMaps = await processor.process(
|
|
options,
|
|
perElement: true,
|
|
forMain: true,
|
|
);
|
|
|
|
CodeSources codeSources = CodeSources(processor, sourceMaps);
|
|
|
|
SourceMapInfo info = sourceMaps.mainSourceMapInfo!;
|
|
|
|
int nextAnnotationId = 0;
|
|
List<CodeLine> codeLines;
|
|
Coverage coverage = Coverage();
|
|
Map<int, List<CodeLineAnnotation>> codeLineAnnotationMap =
|
|
<int, List<CodeLineAnnotation>>{};
|
|
|
|
/// Create a [CodeLineAnnotation] for [codeOffset].
|
|
void addCodeLineAnnotation({
|
|
required AnnotationType annotationType,
|
|
required int codeOffset,
|
|
List<SourceLocation> locations = const <SourceLocation>[],
|
|
String? stepInfo,
|
|
}) {
|
|
if (annotationType == AnnotationType.withoutSourceInfo ||
|
|
annotationType == AnnotationType.unusedSourceInfo) {
|
|
locations = [];
|
|
}
|
|
List<CodeLocation> codeLocations = locations
|
|
.where((l) => l.sourceUri != null)
|
|
.map((l) => CodeLocation(l.sourceUri!, l.sourceName!, l.offset))
|
|
.toList();
|
|
List<CodeSource> codeSourceList = locations
|
|
.where((l) => l.sourceUri != null)
|
|
.map(codeSources.sourceLocationToCodeSource)
|
|
.whereType<CodeSource>()
|
|
.toList();
|
|
CodeLineAnnotation data = 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 = Set<js.Node>();
|
|
|
|
/// Add an annotation for [codeOffset] pointing to [locations].
|
|
void addSourceLocations({
|
|
required AnnotationType annotationType,
|
|
required int codeOffset,
|
|
required List<SourceLocation?> locations,
|
|
String? stepInfo,
|
|
}) {
|
|
final nonNullLocations = locations.whereType<SourceLocation>().toList();
|
|
addCodeLineAnnotation(
|
|
annotationType: annotationType,
|
|
codeOffset: codeOffset,
|
|
stepInfo: stepInfo,
|
|
locations: nonNullLocations,
|
|
);
|
|
}
|
|
|
|
/// Add annotations for all mappings created for [node].
|
|
bool addSourceLocationsForNode({
|
|
required AnnotationType annotationType,
|
|
required 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.withSourceInfo,
|
|
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.withoutSourceInfo,
|
|
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.additionalSourceInfo,
|
|
node: node,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add annotations for unused source information associated with nodes.
|
|
SourceLocationCollector collector = 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.unusedSourceInfo,
|
|
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 = 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 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.BaseVisitorVoid {
|
|
Map<js.Node, List<SourceLocation>> sourceLocations =
|
|
<js.Node, List<SourceLocation>>{};
|
|
|
|
@override
|
|
void visitNode(js.Node node) {
|
|
final sourceInformation = node.sourceInformation as 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.
|
|
late CodeKind kind;
|
|
late Uri uri;
|
|
late String name;
|
|
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 CodeSource(kind, uri, name, null, null);
|
|
}
|
|
|
|
/// Create [LineData] that colors line numbers according to the [CodeSource]s
|
|
/// origin if available.
|
|
LineData getLineData(Object? lineAnnotation) {
|
|
if (lineAnnotation != null) {
|
|
return LineData(
|
|
lineClass: ClassNames.line,
|
|
lineNumberClass:
|
|
'${ClassNames.lineNumber} '
|
|
'${ClassNames.colored(lineAnnotation.hashCode % 4)}',
|
|
);
|
|
}
|
|
return LineData(
|
|
lineClass: ClassNames.line,
|
|
lineNumberClass: ClassNames.lineNumber,
|
|
);
|
|
}
|
|
|
|
AnnotationData? getAnnotationData(
|
|
Iterable<Annotation> annotations, {
|
|
required bool forSpan,
|
|
}) {
|
|
for (Annotation annotation in annotations) {
|
|
CodeLineAnnotation data = annotation.data;
|
|
if (data.annotationType.isSourceMapped) {
|
|
if (forSpan) {
|
|
int index = data.sourceMappingIndex!;
|
|
return AnnotationData(
|
|
tag: 'span',
|
|
properties: {
|
|
'class':
|
|
'${ClassNames.sourceMapping} '
|
|
'${ClassNames.sourceMappingIndex(index % HUE_COUNT)}',
|
|
'title': 'index=$index',
|
|
},
|
|
);
|
|
} else {
|
|
return 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.unusedSourceInfo) {
|
|
return 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.withoutSourceInfo) {
|
|
return AnnotationData(
|
|
tag: 'span',
|
|
properties: {
|
|
'title': annotation.title,
|
|
'class':
|
|
'${ClassNames.marker} '
|
|
'${data.annotationType.className}',
|
|
},
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|