8a883fa54d
Leaves some in parser test: pkg/front_end/parser_testcases/error_recovery/keyword_named_formal_parameter_prime.dart TEST=Refactoring, covered by existing tests. Change-Id: I7a83ef95df3cbd283878b3685b5c747bd89a1b16 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/256125 Reviewed-by: Johnni Winther <johnniwinther@google.com> Commit-Queue: Lasse Nielsen <lrn@google.com>
964 lines
30 KiB
Dart
964 lines
30 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.
|
|
|
|
// @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.BaseVisitorVoid {
|
|
Map<js.Node, List<SourceLocation>> sourceLocations =
|
|
<js.Node, List<SourceLocation>>{};
|
|
|
|
@override
|
|
void 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;
|
|
}
|