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>
862 lines
27 KiB
Dart
862 lines
27 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;
|
|
|
|
import 'package:compiler/src/io/source_file.dart';
|
|
|
|
import 'html_parts.dart';
|
|
import 'output_structure.dart';
|
|
import 'sourcemap_helper.dart';
|
|
import 'sourcemap_html_helper.dart';
|
|
|
|
enum DiffKind { UNMATCHED, MATCHING, IDENTICAL }
|
|
|
|
/// Id for an output column.
|
|
class DiffColumn {
|
|
final String type;
|
|
final int? index;
|
|
|
|
const DiffColumn(this.type, [this.index]);
|
|
|
|
@override
|
|
int get hashCode => type.hashCode * 19 + index.hashCode * 23;
|
|
|
|
@override
|
|
bool operator ==(other) {
|
|
if (identical(this, other)) return true;
|
|
if (other is! DiffColumn) return false;
|
|
return type == other.type && index == other.index;
|
|
}
|
|
|
|
@override
|
|
String toString() => '$type${index ?? ''}';
|
|
}
|
|
|
|
/// A block of code in an output column.
|
|
abstract class DiffColumnBlock {
|
|
void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context);
|
|
}
|
|
|
|
/// A block consisting of pure HTML parts.
|
|
class PartsColumnBlock extends DiffColumnBlock {
|
|
final List<HtmlPart> parts;
|
|
|
|
PartsColumnBlock(this.parts);
|
|
|
|
@override
|
|
void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context) {
|
|
if (parts.isNotEmpty) {
|
|
for (HtmlPart part in parts) {
|
|
part.printHtmlOn(htmlBuffer, context);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A block consisting of line-per-line JavaScript and source mapped Dart code.
|
|
class CodeLinesColumnBlock extends DiffColumnBlock {
|
|
final List<CodeLine> jsCodeLines;
|
|
final Map<CodeLine, List<CodeLine>> jsToDartMap;
|
|
|
|
CodeLinesColumnBlock(this.jsCodeLines, this.jsToDartMap);
|
|
|
|
@override
|
|
void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context) {
|
|
if (jsCodeLines.isNotEmpty) {
|
|
htmlBuffer.write('<table style="width:100%">');
|
|
for (CodeLine codeLine in jsCodeLines) {
|
|
htmlBuffer.write('<tr><td class="${ClassNames.innerCell}">');
|
|
codeLine.printHtmlOn(htmlBuffer, context);
|
|
htmlBuffer.write(
|
|
'</td><td '
|
|
'class="${ClassNames.innerCell} ${ClassNames.sourceMapped}">',
|
|
);
|
|
List<CodeLine>? lines = jsToDartMap[codeLine];
|
|
if (lines != null) {
|
|
for (CodeLine line in lines) {
|
|
line.printHtmlOn(
|
|
htmlBuffer,
|
|
context.from(
|
|
includeAnnotation: (a) {
|
|
CodeLineAnnotation annotation = a.data;
|
|
return annotation.annotationType.isSourceMapped;
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
htmlBuffer.write('</td></tr>');
|
|
}
|
|
htmlBuffer.write('</table>');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A list of columns that should align in output.
|
|
class DiffBlock {
|
|
final DiffKind kind;
|
|
final Map<DiffColumn, DiffColumnBlock> _columns =
|
|
<DiffColumn, DiffColumnBlock>{};
|
|
|
|
DiffBlock(this.kind);
|
|
|
|
void addColumnBlock(DiffColumn column, DiffColumnBlock block) {
|
|
_columns[column] = block;
|
|
}
|
|
|
|
Iterable<DiffColumn> get columns => _columns.keys;
|
|
|
|
void printHtmlOn(
|
|
DiffColumn column,
|
|
StringBuffer htmlBuffer,
|
|
HtmlPrintContext context,
|
|
) {
|
|
DiffColumnBlock? block = _columns[column];
|
|
if (block != null) {
|
|
block.printHtmlOn(htmlBuffer, context);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool _eq(a, b) => a == b;
|
|
|
|
/// Align the content of [list1] and [list2].
|
|
///
|
|
/// If provided, [range1] and [range2] aligned the subranges of [list1] and
|
|
/// [list2], otherwise the whole lists are aligned.
|
|
///
|
|
/// If provided, [match] determines the equality between members of [list1] and
|
|
/// [list2], otherwise `==` is used.
|
|
///
|
|
/// [handleSkew] is called when a subrange of one list is not found in the
|
|
/// other.
|
|
///
|
|
/// [handleMatched] is called when two indices match up.
|
|
///
|
|
/// [handleUnmatched] is called when two indices don't match up (none are found
|
|
/// in the other list).
|
|
void align(
|
|
List list1,
|
|
List list2, {
|
|
Interval? range1,
|
|
Interval? range2,
|
|
bool match(a, b) = _eq,
|
|
required void handleSkew(int listIndex, Interval range),
|
|
required void handleMatched(List<int> indices),
|
|
required void handleUnmatched(List<int> indices),
|
|
}) {
|
|
range1 ??= Interval(0, list1.length);
|
|
range2 ??= Interval(0, list2.length);
|
|
|
|
Interval? findInOther(
|
|
List thisLines,
|
|
Interval thisRange,
|
|
List otherLines,
|
|
Interval otherRange,
|
|
) {
|
|
for (int index = otherRange.from; index < otherRange.to; index++) {
|
|
if (match(thisLines[thisRange.from], otherLines[index])) {
|
|
int offset = 1;
|
|
while (thisRange.from + offset < thisRange.to &&
|
|
otherRange.from + offset < otherRange.to &&
|
|
match(
|
|
thisLines[thisRange.from + offset],
|
|
otherLines[otherRange.from + offset],
|
|
)) {
|
|
offset++;
|
|
}
|
|
return Interval(index, index + offset);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
int start1 = range1.from;
|
|
int end1 = range1.to;
|
|
int start2 = range2.from;
|
|
int end2 = range2.to;
|
|
|
|
const int ALIGN1 = -1;
|
|
const int UNMATCHED = 0;
|
|
const int ALIGN2 = 1;
|
|
|
|
while (start1 < end1 && start2 < end2) {
|
|
if (match(list1[start1], list2[start2])) {
|
|
handleMatched([start1++, start2++]);
|
|
} else {
|
|
Interval subrange1 = Interval(start1, end1);
|
|
Interval subrange2 = Interval(start2, end2);
|
|
Interval? element2inList1 = findInOther(
|
|
list1,
|
|
subrange1,
|
|
list2,
|
|
subrange2,
|
|
);
|
|
Interval? element1inList2 = findInOther(
|
|
list2,
|
|
subrange2,
|
|
list1,
|
|
subrange1,
|
|
);
|
|
int choice = 0;
|
|
if (element2inList1 != null) {
|
|
if (element1inList2 != null) {
|
|
if (element1inList2.length > 1 && element2inList1.length > 1) {
|
|
choice = element2inList1.from < element1inList2.from
|
|
? ALIGN2
|
|
: ALIGN1;
|
|
} else if (element2inList1.length > 1) {
|
|
choice = ALIGN2;
|
|
} else if (element1inList2.length > 1) {
|
|
choice = ALIGN1;
|
|
} else {
|
|
choice = element2inList1.from < element1inList2.from
|
|
? ALIGN2
|
|
: ALIGN1;
|
|
}
|
|
} else {
|
|
choice = ALIGN2;
|
|
}
|
|
} else if (element1inList2 != null) {
|
|
choice = ALIGN1;
|
|
}
|
|
switch (choice) {
|
|
case ALIGN1:
|
|
handleSkew(0, Interval(start1, element1inList2!.from));
|
|
start1 = element1inList2.from;
|
|
break;
|
|
case ALIGN2:
|
|
handleSkew(1, Interval(start2, element2inList1!.from));
|
|
start2 = element2inList1.from;
|
|
break;
|
|
case UNMATCHED:
|
|
handleUnmatched([start1++, start2++]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (start1 < end1) {
|
|
handleSkew(0, Interval(start1, end1));
|
|
}
|
|
if (start2 < end2) {
|
|
handleSkew(1, Interval(start2, end2));
|
|
}
|
|
}
|
|
|
|
/// Create a list of blocks containing the diff of the two output [structures]
|
|
/// and the corresponding Dart code.
|
|
List<DiffBlock> createDiffBlocks(
|
|
List<OutputStructure> structures,
|
|
SourceFileManager sourceFileManager,
|
|
) {
|
|
return DiffCreator(structures, sourceFileManager).computeBlocks();
|
|
}
|
|
|
|
class DiffCreator {
|
|
final List<OutputStructure> structures;
|
|
final SourceFileManager sourceFileManager;
|
|
|
|
List<List<CodeLine>> inputLines;
|
|
|
|
List<int> nextInputLine = [0, 0];
|
|
|
|
List<DiffBlock> blocks = <DiffBlock>[];
|
|
|
|
DiffCreator(List<OutputStructure> structures, this.sourceFileManager)
|
|
: this.structures = structures,
|
|
this.inputLines = structures.map((s) => s.lines).toList();
|
|
|
|
/// Compute [CodeSource]s defined by [entities].
|
|
Iterable<CodeSource> codeSourceFromEntities(Iterable<OutputEntity> entities) {
|
|
Set<CodeSource> sources = Set<CodeSource>();
|
|
for (OutputEntity entity in entities) {
|
|
if (entity.codeSource != null) {
|
|
sources.add(entity.codeSource!);
|
|
}
|
|
}
|
|
return sources;
|
|
}
|
|
|
|
/// Create a block with the code from [codeSources]. The [CodeSource]s in
|
|
/// [mainSources] are tagged as original code sources, the rest as inlined
|
|
/// code sources.
|
|
DiffColumnBlock codeLinesFromCodeSources(
|
|
Iterable<CodeSource> mainSources,
|
|
Iterable<CodeSource> codeSources,
|
|
) {
|
|
List<HtmlPart> parts = <HtmlPart>[];
|
|
for (CodeSource codeSource in codeSources) {
|
|
//parts.addAll(codeLinesFromCodeSource(codeSource));
|
|
String className = mainSources.contains(codeSource)
|
|
? ClassNames.originalDart
|
|
: ClassNames.inlinedDart;
|
|
parts.add(
|
|
new TagPart(
|
|
'div',
|
|
properties: {'class': className},
|
|
content: codeLinesFromCodeSource(codeSource),
|
|
),
|
|
);
|
|
}
|
|
return PartsColumnBlock(parts);
|
|
}
|
|
|
|
/// Adds all [CodeSource]s used in [dartCodeLines] to [codeSourceSet].
|
|
void collectCodeSources(
|
|
Set<CodeSource> codeSourceSet,
|
|
Map<CodeLine, List<CodeLine>> dartCodeLines,
|
|
) {
|
|
for (List<CodeLine> codeLines in dartCodeLines.values) {
|
|
for (CodeLine dartCodeLine in codeLines) {
|
|
if (dartCodeLine.lineAnnotation != null) {
|
|
codeSourceSet.add(dartCodeLine.lineAnnotation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Checks that lines are added in sequence without gaps or duplicates.
|
|
void checkLineInvariant(int index, Interval range) {
|
|
int expectedLineNo = nextInputLine[index];
|
|
if (range.from != expectedLineNo) {
|
|
print('Expected line no $expectedLineNo, found ${range.from}');
|
|
if (range.from < expectedLineNo) {
|
|
print('Duplicate lines:');
|
|
int i = range.from;
|
|
while (i <= expectedLineNo) {
|
|
print(inputLines[index][i++].code);
|
|
}
|
|
} else {
|
|
print('Missing lines:');
|
|
int i = expectedLineNo;
|
|
while (i <= range.from) {
|
|
print(inputLines[index][i++].code);
|
|
}
|
|
}
|
|
}
|
|
nextInputLine[index] = range.to;
|
|
}
|
|
|
|
/// Creates a block containing the code lines in [range] from input number
|
|
/// [index]. If [codeSource] is provided, the block will contain a
|
|
/// corresponding Dart code column.
|
|
void handleSkew(
|
|
int index,
|
|
Interval range, [
|
|
Iterable<CodeSource> mainCodeSources = const <CodeSource>[],
|
|
]) {
|
|
if (range.isEmpty) return;
|
|
|
|
Set<CodeSource> codeSources = Set<CodeSource>();
|
|
codeSources.addAll(mainCodeSources);
|
|
|
|
DiffBlock block = DiffBlock(DiffKind.UNMATCHED);
|
|
checkLineInvariant(index, range);
|
|
List<CodeLine> jsCodeLines = inputLines[index].sublist(
|
|
range.from,
|
|
range.to,
|
|
);
|
|
Map<CodeLine, List<CodeLine>> dartCodeLines = dartCodeLinesFromJsCodeLines(
|
|
jsCodeLines,
|
|
);
|
|
block.addColumnBlock(
|
|
new DiffColumn('js', index),
|
|
CodeLinesColumnBlock(jsCodeLines, dartCodeLines),
|
|
);
|
|
collectCodeSources(codeSources, dartCodeLines);
|
|
|
|
if (codeSources.isNotEmpty) {
|
|
block.addColumnBlock(
|
|
const DiffColumn('dart'),
|
|
codeLinesFromCodeSources(mainCodeSources, codeSources),
|
|
);
|
|
}
|
|
blocks.add(block);
|
|
}
|
|
|
|
/// Create a block containing the code lines in [ranges] from the
|
|
/// corresponding JavaScript inputs. If [codeSource] is provided, the block
|
|
/// will contain a corresponding Dart code column.
|
|
void addLines(
|
|
DiffKind kind,
|
|
List<Interval> ranges, [
|
|
Iterable<CodeSource> mainCodeSources = const <CodeSource>[],
|
|
]) {
|
|
if (ranges.every((range) => range.isEmpty)) return;
|
|
|
|
Set<CodeSource> codeSources = Set<CodeSource>();
|
|
codeSources.addAll(mainCodeSources);
|
|
|
|
DiffBlock block = DiffBlock(kind);
|
|
for (int i = 0; i < ranges.length; i++) {
|
|
checkLineInvariant(i, ranges[i]);
|
|
List<CodeLine> jsCodeLines = inputLines[i].sublist(
|
|
ranges[i].from,
|
|
ranges[i].to,
|
|
);
|
|
Map<CodeLine, List<CodeLine>> dartCodeLines =
|
|
dartCodeLinesFromJsCodeLines(jsCodeLines);
|
|
block.addColumnBlock(
|
|
new DiffColumn('js', i),
|
|
CodeLinesColumnBlock(jsCodeLines, dartCodeLines),
|
|
);
|
|
collectCodeSources(codeSources, dartCodeLines);
|
|
}
|
|
if (codeSources.isNotEmpty) {
|
|
block.addColumnBlock(
|
|
const DiffColumn('dart'),
|
|
codeLinesFromCodeSources(mainCodeSources, codeSources),
|
|
);
|
|
}
|
|
blocks.add(block);
|
|
}
|
|
|
|
/// Merge the code lines in [range1] and [range2] of the corresponding input.
|
|
void addRaw(Interval range1, Interval range2) {
|
|
if (range1.isEmpty && range2.isEmpty) return;
|
|
|
|
match(a, b) => a.code == b.code;
|
|
|
|
List<Interval>? currentMatchedIntervals;
|
|
List<Interval>? currentUnmatchedIntervals;
|
|
|
|
void flushMatching() {
|
|
if (currentMatchedIntervals != null) {
|
|
addLines(DiffKind.IDENTICAL, currentMatchedIntervals!);
|
|
}
|
|
currentMatchedIntervals = null;
|
|
}
|
|
|
|
void flushUnmatched() {
|
|
if (currentUnmatchedIntervals != null) {
|
|
addLines(DiffKind.UNMATCHED, currentUnmatchedIntervals!);
|
|
}
|
|
currentUnmatchedIntervals = null;
|
|
}
|
|
|
|
List<Interval> updateIntervals(List<Interval>? current, List<int> indices) {
|
|
if (current == null) {
|
|
return [
|
|
Interval(indices[0], indices[0] + 1),
|
|
Interval(indices[1], indices[1] + 1),
|
|
];
|
|
} else {
|
|
current[0] = Interval(current[0].from, indices[0] + 1);
|
|
current[1] = Interval(current[1].from, indices[1] + 1);
|
|
return current;
|
|
}
|
|
}
|
|
|
|
align(
|
|
inputLines[0],
|
|
inputLines[1],
|
|
range1: range1,
|
|
range2: range2,
|
|
match: match,
|
|
handleSkew: (int listIndex, Interval range) {
|
|
flushMatching();
|
|
flushUnmatched();
|
|
handleSkew(listIndex, range);
|
|
},
|
|
handleMatched: (List<int> indices) {
|
|
flushUnmatched();
|
|
currentMatchedIntervals = updateIntervals(
|
|
currentMatchedIntervals,
|
|
indices,
|
|
);
|
|
},
|
|
handleUnmatched: (List<int> indices) {
|
|
flushMatching();
|
|
currentUnmatchedIntervals = updateIntervals(
|
|
currentUnmatchedIntervals,
|
|
indices,
|
|
);
|
|
},
|
|
);
|
|
|
|
flushMatching();
|
|
flushUnmatched();
|
|
}
|
|
|
|
/// Adds the top level blocks in [childRange] for structure [index].
|
|
void addBlock(int index, Interval childRange) {
|
|
addSkewedChildren(index, structures[index], childRange);
|
|
}
|
|
|
|
/// Adds the [entity] from structure [index]. If the [entity] supports child
|
|
/// entities, these are process individually. Otherwise the lines from
|
|
/// [entity] are added directly.
|
|
void addSkewedEntity(int index, OutputEntity entity) {
|
|
if (entity.canHaveChildren) {
|
|
handleSkew(index, entity.header);
|
|
addSkewedChildren(index, entity, Interval(0, entity.children.length));
|
|
handleSkew(index, entity.footer);
|
|
} else {
|
|
handleSkew(index, entity.interval, codeSourceFromEntities([entity]));
|
|
}
|
|
}
|
|
|
|
/// Adds the children of [parent] in [childRange] from structure [index].
|
|
void addSkewedChildren(int index, OutputEntity parent, Interval childRange) {
|
|
for (int i = childRange.from; i < childRange.to; i++) {
|
|
addSkewedEntity(index, parent.getChild(i));
|
|
}
|
|
}
|
|
|
|
/// Adds the members of the [classes] aligned.
|
|
void addMatchingContainers(List<OutputEntity> classes) {
|
|
addLines(DiffKind.MATCHING, classes.map((c) => c.header).toList());
|
|
align(
|
|
classes[0].children,
|
|
classes[1].children,
|
|
match: (a, b) => a.name == b.name,
|
|
handleSkew: (int listIndex, Interval childRange) {
|
|
addSkewedChildren(listIndex, classes[listIndex], childRange);
|
|
},
|
|
handleMatched: (List<int> indices) {
|
|
List<OutputEntity> entities = [
|
|
classes[0].getChild(indices[0]),
|
|
classes[1].getChild(indices[1]),
|
|
];
|
|
if (entities.every((e) => e is Statics)) {
|
|
addMatchingContainers(entities);
|
|
} else {
|
|
addLines(
|
|
DiffKind.MATCHING,
|
|
entities.map((e) => e.interval).toList(),
|
|
codeSourceFromEntities(entities),
|
|
);
|
|
}
|
|
},
|
|
handleUnmatched: (List<int> indices) {
|
|
List<Interval> intervals = [
|
|
classes[0].getChild(indices[0]).interval,
|
|
classes[1].getChild(indices[1]).interval,
|
|
];
|
|
addLines(DiffKind.UNMATCHED, intervals);
|
|
},
|
|
);
|
|
addLines(DiffKind.MATCHING, classes.map((c) => c.footer).toList());
|
|
}
|
|
|
|
/// Adds the library blocks in [indices] from the corresponding
|
|
/// [OutputStructure]s, aligning their content.
|
|
void addMatchingBlocks(List<int> indices) {
|
|
List<OutputEntity> blocks = [
|
|
structures[0].getChild(indices[0]),
|
|
structures[1].getChild(indices[1]),
|
|
];
|
|
|
|
addLines(DiffKind.MATCHING, blocks.map((b) => b.header).toList());
|
|
align(
|
|
blocks[0].children,
|
|
blocks[1].children,
|
|
match: (a, b) => a.name == b.name,
|
|
handleSkew: (int listIndex, Interval childRange) {
|
|
addSkewedChildren(listIndex, blocks[listIndex], childRange);
|
|
},
|
|
handleMatched: (List<int> indices) {
|
|
List<OutputEntity> entities = [
|
|
blocks[0].getChild(indices[0]),
|
|
blocks[1].getChild(indices[1]),
|
|
];
|
|
if (entities.every((e) => e is LibraryClass)) {
|
|
addMatchingContainers(entities);
|
|
} else {
|
|
addLines(
|
|
DiffKind.MATCHING,
|
|
entities.map((e) => e.interval).toList(),
|
|
codeSourceFromEntities(entities),
|
|
);
|
|
}
|
|
},
|
|
handleUnmatched: (List<int> indices) {
|
|
List<Interval> intervals = [
|
|
blocks[0].getChild(indices[0]).interval,
|
|
blocks[1].getChild(indices[1]).interval,
|
|
];
|
|
addLines(DiffKind.UNMATCHED, intervals);
|
|
},
|
|
);
|
|
addLines(DiffKind.MATCHING, blocks.map((b) => b.footer).toList());
|
|
}
|
|
|
|
/// Adds the lines of the blocks in [indices] from the corresponding
|
|
/// [OutputStructure]s.
|
|
void addUnmatchedBlocks(List<int> indices) {
|
|
List<OutputEntity> blocks = [
|
|
structures[0].getChild(indices[0]),
|
|
structures[1].getChild(indices[1]),
|
|
];
|
|
addLines(DiffKind.UNMATCHED, [blocks[0].interval, blocks[1].interval]);
|
|
}
|
|
|
|
/// Computes the diff blocks for [OutputStructure]s.
|
|
List<DiffBlock> computeBlocks() {
|
|
addRaw(structures[0].header, structures[1].header);
|
|
|
|
align(
|
|
structures[0].children,
|
|
structures[1].children,
|
|
match: (a, b) => a.name == b.name,
|
|
handleSkew: addBlock,
|
|
handleMatched: addMatchingBlocks,
|
|
handleUnmatched: addUnmatchedBlocks,
|
|
);
|
|
|
|
addRaw(structures[0].footer, structures[1].footer);
|
|
|
|
return blocks;
|
|
}
|
|
|
|
/// Creates html lines for code lines in [codeSource]. The [sourceFileManager]
|
|
/// is used to read that text from the source URIs.
|
|
List<HtmlPart> codeLinesFromCodeSource(CodeSource codeSource) {
|
|
List<HtmlPart> lines = <HtmlPart>[];
|
|
SourceFile sourceFile = sourceFileManager.getSourceFile(codeSource.uri)!;
|
|
String elementName = codeSource.name;
|
|
HtmlLine line = HtmlLine();
|
|
line.htmlParts.add(new ConstHtmlPart('<span class="comment">'));
|
|
line.htmlParts.add(new HtmlText('${elementName}: ${sourceFile.filename}'));
|
|
line.htmlParts.add(new ConstHtmlPart('</span>'));
|
|
lines.add(line);
|
|
if (codeSource.begin != null) {
|
|
int startLine = sourceFile.getLocation(codeSource.begin!).line - 1;
|
|
int endLine = sourceFile.getLocation(codeSource.end!).line;
|
|
for (CodeLine codeLine in convertAnnotatedCodeToCodeLines(
|
|
sourceFile.slowText(),
|
|
const <Annotation>[],
|
|
startLine: startLine,
|
|
endLine: endLine,
|
|
)) {
|
|
codeLine.lineAnnotation = codeSource;
|
|
lines.add(codeLine);
|
|
}
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
/// Creates a map from JavaScript [CodeLine]s in [jsCodeLines] to the Dart
|
|
/// [CodeLine]s references in the source information.
|
|
Map<CodeLine, List<CodeLine>> dartCodeLinesFromJsCodeLines(
|
|
List<CodeLine> jsCodeLines,
|
|
) {
|
|
Map<CodeLine, Interval> codeLineInterval = <CodeLine, Interval>{};
|
|
Map<CodeLine, List<CodeLine>> jsToDartMap = <CodeLine, List<CodeLine>>{};
|
|
List<Annotation> annotations = <Annotation>[];
|
|
Uri? currentUri;
|
|
late Interval interval;
|
|
|
|
Map<Uri, Set<CodeSource>> codeSourceMap = <Uri, Set<CodeSource>>{};
|
|
|
|
for (CodeLine jsCodeLine in jsCodeLines) {
|
|
for (Annotation annotation in jsCodeLine.annotations) {
|
|
CodeLineAnnotation codeLineAnnotation = annotation.data;
|
|
for (CodeSource codeSource in codeLineAnnotation.codeSources) {
|
|
codeSourceMap
|
|
.putIfAbsent(codeSource.uri, () => Set<CodeSource>())
|
|
.add(codeSource);
|
|
}
|
|
}
|
|
}
|
|
|
|
void flush() {
|
|
if (currentUri == null) return;
|
|
|
|
Set<CodeSource>? codeSources = codeSourceMap[currentUri];
|
|
SourceFile sourceFile = sourceFileManager.getSourceFile(currentUri)!;
|
|
List<CodeLine> annotatedDartCodeLines = convertAnnotatedCodeToCodeLines(
|
|
sourceFile.slowText(),
|
|
annotations,
|
|
startLine: interval.from,
|
|
endLine: interval.to,
|
|
uri: currentUri,
|
|
);
|
|
if (codeSources != null) {
|
|
CodeSource? currentCodeSource;
|
|
late Interval currentLineInterval;
|
|
for (CodeLine dartCodeLine in annotatedDartCodeLines) {
|
|
if (currentCodeSource == null ||
|
|
!currentLineInterval.contains(dartCodeLine.lineNo)) {
|
|
currentCodeSource = null;
|
|
for (CodeSource codeSource in codeSources) {
|
|
Interval interval = Interval(
|
|
sourceFile.getLocation(codeSource.begin!).line - 1,
|
|
sourceFile.getLocation(codeSource.end!).line,
|
|
);
|
|
if (interval.contains(dartCodeLine.lineNo)) {
|
|
currentCodeSource = codeSource;
|
|
currentLineInterval = interval;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (currentCodeSource != null) {
|
|
dartCodeLine.lineAnnotation = currentCodeSource;
|
|
}
|
|
}
|
|
}
|
|
|
|
int index = 0;
|
|
for (CodeLine jsCodeLine in codeLineInterval.keys) {
|
|
List<CodeLine> dartCodeLines = jsToDartMap.putIfAbsent(
|
|
jsCodeLine,
|
|
() => <CodeLine>[],
|
|
);
|
|
if (dartCodeLines.isEmpty && index < annotatedDartCodeLines.length) {
|
|
dartCodeLines.add(annotatedDartCodeLines[index++]);
|
|
}
|
|
}
|
|
while (index < annotatedDartCodeLines.length) {
|
|
jsToDartMap[codeLineInterval.keys.last]!.add(
|
|
annotatedDartCodeLines[index++],
|
|
);
|
|
}
|
|
|
|
currentUri = null;
|
|
}
|
|
|
|
void restart(CodeLine codeLine, CodeLocation codeLocation, int line) {
|
|
flush();
|
|
|
|
currentUri = codeLocation.uri;
|
|
interval = Interval(line, line + 1);
|
|
annotations = <Annotation>[];
|
|
codeLineInterval.clear();
|
|
codeLineInterval[codeLine] = interval;
|
|
}
|
|
|
|
for (CodeLine jsCodeLine in jsCodeLines) {
|
|
for (Annotation annotation in jsCodeLine.annotations) {
|
|
CodeLineAnnotation codeLineAnnotation = annotation.data;
|
|
|
|
for (CodeLocation location in codeLineAnnotation.codeLocations) {
|
|
SourceFile sourceFile = sourceFileManager.getSourceFile(
|
|
location.uri,
|
|
)!;
|
|
int line = sourceFile.getLocation(location.offset).line - 1;
|
|
if (currentUri != location.uri) {
|
|
restart(jsCodeLine, location, line);
|
|
} else if (interval.inWindow(line, windowSize: 2)) {
|
|
interval = interval.include(line);
|
|
codeLineInterval[jsCodeLine] = interval;
|
|
} else {
|
|
restart(jsCodeLine, location, line);
|
|
}
|
|
|
|
annotations.add(
|
|
new Annotation(
|
|
codeLineAnnotation.annotationType,
|
|
location.offset,
|
|
'id=${codeLineAnnotation.annotationId}',
|
|
data: codeLineAnnotation,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
flush();
|
|
return jsToDartMap;
|
|
}
|
|
}
|
|
|
|
const DiffColumn column_js0 = const DiffColumn('js', 0);
|
|
const DiffColumn column_js1 = const DiffColumn('js', 1);
|
|
const DiffColumn column_dart = const DiffColumn('dart');
|
|
|
|
class ClassNames {
|
|
static String column(DiffColumn column) => 'column_${column}';
|
|
static String identical(bool alternate) =>
|
|
'identical${alternate ? '1' : '2'}';
|
|
static String corresponding(bool alternate) =>
|
|
'corresponding${alternate ? '1' : '2'}';
|
|
|
|
static const String buttons = 'buttons';
|
|
static const String comment = 'comment';
|
|
static const String header = 'header';
|
|
static const String headerTable = 'header_table';
|
|
static const String headerColumn = 'header_column';
|
|
static const String legend = 'legend';
|
|
static const String table = 'table';
|
|
|
|
static const String cell = 'cell';
|
|
static const String innerCell = 'inner_cell';
|
|
|
|
static const String originalDart = 'main_dart';
|
|
static const String inlinedDart = 'inlined_dart';
|
|
|
|
static const String line = 'line';
|
|
static const String lineNumber = 'line_number';
|
|
static String colored(int index) => 'colored${index}';
|
|
|
|
static const String withSourceInfo = 'with_source_info';
|
|
static const String withoutSourceInfo = 'without_source_info';
|
|
static const String additionalSourceInfo = 'additional_source_info';
|
|
static const String unusedSourceInfo = 'unused_source_info';
|
|
|
|
static const String sourceMapped = 'source_mapped';
|
|
static const String sourceMapping = 'source_mapping';
|
|
static String sourceMappingIndex(int index) => 'source_mapping${index}';
|
|
|
|
static const String markers = 'markers';
|
|
static const String marker = 'marker';
|
|
}
|
|
|
|
enum AnnotationType {
|
|
withSourceInfo(ClassNames.withSourceInfo, true),
|
|
withoutSourceInfo(ClassNames.withoutSourceInfo, false),
|
|
additionalSourceInfo(ClassNames.additionalSourceInfo, true),
|
|
unusedSourceInfo(ClassNames.unusedSourceInfo, false);
|
|
|
|
final String className;
|
|
final bool isSourceMapped;
|
|
|
|
const AnnotationType(this.className, this.isSourceMapped);
|
|
}
|
|
|
|
class CodeLineAnnotation {
|
|
final int annotationId;
|
|
final AnnotationType annotationType;
|
|
final List<CodeLocation> codeLocations;
|
|
final List<CodeSource> codeSources;
|
|
final String? stepInfo;
|
|
int? sourceMappingIndex;
|
|
|
|
CodeLineAnnotation({
|
|
required this.annotationId,
|
|
required this.annotationType,
|
|
required this.codeLocations,
|
|
required this.codeSources,
|
|
this.stepInfo,
|
|
this.sourceMappingIndex,
|
|
});
|
|
|
|
Map toJson(JsonStrategy strategy) {
|
|
return {
|
|
'annotationId': annotationId,
|
|
'annotationType': annotationType.index,
|
|
'codeLocations': codeLocations.map((l) => l.toJson(strategy)).toList(),
|
|
'codeSources': codeSources.map((c) => c.toJson()).toList(),
|
|
'stepInfo': stepInfo,
|
|
'sourceMappingIndex': sourceMappingIndex,
|
|
};
|
|
}
|
|
|
|
static fromJson(Map json, JsonStrategy strategy) {
|
|
return CodeLineAnnotation(
|
|
annotationId: json['id'],
|
|
annotationType: AnnotationType.values[json['annotationType']],
|
|
codeLocations: json['codeLocations']
|
|
.map((j) => CodeLocation.fromJson(j, strategy))
|
|
.toList(),
|
|
codeSources: json['codeSources']
|
|
.map((j) => CodeSource.fromJson(j))
|
|
.toList(),
|
|
stepInfo: json['stepInfo'],
|
|
sourceMappingIndex: json['sourceMappingIndex'],
|
|
);
|
|
}
|
|
}
|