// 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; 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 != null ? 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 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 jsCodeLines; final Map> jsToDartMap; CodeLinesColumnBlock(this.jsCodeLines, this.jsToDartMap); @override void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context) { if (jsCodeLines.isNotEmpty) { htmlBuffer.write(''); for (CodeLine codeLine in jsCodeLines) { htmlBuffer.write(''); } htmlBuffer.write('
'); codeLine.printHtmlOn(htmlBuffer, context); htmlBuffer.write(''); List 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('
'); } } } /// A list of columns that should align in output. class DiffBlock { final DiffKind kind; Map _columns = {}; DiffBlock(this.kind); void addColumnBlock(DiffColumn column, DiffColumnBlock block) { _columns[column] = block; } Iterable get columns => _columns.keys; void printHtmlOn( DiffColumn column, StringBuffer htmlBuffer, HtmlPrintContext context) { DiffColumnBlock block = _columns[column]; if (block != null) { block.printHtmlOn(htmlBuffer, context); } } } /// 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), void handleSkew(int listIndex, Interval range), void handleMatched(List indices), void handleUnmatched(List indices)}) { if (match == null) { match = (a, b) => a == b; } if (range1 == null) { range1 = new Interval(0, list1.length); } if (range2 == null) { range2 = new 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 new 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 = new Interval(start1, end1); Interval subrange2 = new 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, new Interval(start1, element1inList2.from)); start1 = element1inList2.from; break; case ALIGN2: handleSkew(1, new Interval(start2, element2inList1.from)); start2 = element2inList1.from; break; case UNMATCHED: handleUnmatched([start1++, start2++]); break; } } } if (start1 < end1) { handleSkew(0, new Interval(start1, end1)); } if (start2 < end2) { handleSkew(1, new Interval(start2, end2)); } } /// Create a list of blocks containing the diff of the two output [structures] /// and the corresponding Dart code. List createDiffBlocks( List structures, SourceFileManager sourceFileManager) { return new DiffCreator(structures, sourceFileManager).computeBlocks(); } class DiffCreator { final List structures; final SourceFileManager sourceFileManager; List> inputLines; List nextInputLine = [0, 0]; List blocks = []; DiffCreator(List structures, this.sourceFileManager) : this.structures = structures, this.inputLines = structures.map((s) => s.lines).toList(); /// Compute [CodeSource]s defined by [entities]. Iterable codeSourceFromEntities(Iterable entities) { Set sources = new Set(); 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 mainSources, Iterable codeSources) { List parts = []; 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 new PartsColumnBlock(parts); } /// Adds all [CodeSource]s used in [dartCodeLines] to [codeSourceSet]. void collectCodeSources(Set codeSourceSet, Map> dartCodeLines) { for (List 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 mainCodeSources = const []]) { if (range.isEmpty) return; Set codeSources = new Set(); codeSources.addAll(mainCodeSources); DiffBlock block = new DiffBlock(DiffKind.UNMATCHED); checkLineInvariant(index, range); List jsCodeLines = inputLines[index].sublist(range.from, range.to); Map> dartCodeLines = dartCodeLinesFromJsCodeLines(jsCodeLines); block.addColumnBlock(new DiffColumn('js', index), new 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 ranges, [Iterable mainCodeSources = const []]) { if (ranges.every((range) => range.isEmpty)) return; Set codeSources = new Set(); codeSources.addAll(mainCodeSources); DiffBlock block = new DiffBlock(kind); for (int i = 0; i < ranges.length; i++) { checkLineInvariant(i, ranges[i]); List jsCodeLines = inputLines[i].sublist(ranges[i].from, ranges[i].to); Map> dartCodeLines = dartCodeLinesFromJsCodeLines(jsCodeLines); block.addColumnBlock(new DiffColumn('js', i), new 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 currentMatchedIntervals; List currentUnmatchedIntervals; void flushMatching() { if (currentMatchedIntervals != null) { addLines(DiffKind.IDENTICAL, currentMatchedIntervals); } currentMatchedIntervals = null; } void flushUnmatched() { if (currentUnmatchedIntervals != null) { addLines(DiffKind.UNMATCHED, currentUnmatchedIntervals); } currentUnmatchedIntervals = null; } List updateIntervals(List current, List indices) { if (current == null) { return [ new Interval(indices[0], indices[0] + 1), new Interval(indices[1], indices[1] + 1) ]; } else { current[0] = new Interval(current[0].from, indices[0] + 1); current[1] = new 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 indices) { flushUnmatched(); currentMatchedIntervals = updateIntervals(currentMatchedIntervals, indices); }, handleUnmatched: (List 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, new 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 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 indices) { List 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 indices) { List 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 indices) { List 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 indices) { List 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 indices) { List 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 indices) { List 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 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 codeLinesFromCodeSource(CodeSource codeSource) { List lines = []; SourceFile sourceFile = sourceFileManager.getSourceFile(codeSource.uri); String elementName = codeSource.name; HtmlLine line = new HtmlLine(); line.htmlParts.add(new ConstHtmlPart('')); line.htmlParts.add(new HtmlText('${elementName}: ${sourceFile.filename}')); line.htmlParts.add(new ConstHtmlPart('')); 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 [], 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> dartCodeLinesFromJsCodeLines( List jsCodeLines) { Map codeLineInterval = {}; Map> jsToDartMap = >{}; List annotations = []; Uri currentUri; Interval interval; Map> codeSourceMap = >{}; 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, () => new Set()) .add(codeSource); } } } void flush() { if (currentUri == null) return; Set codeSources = codeSourceMap[currentUri]; SourceFile sourceFile = sourceFileManager.getSourceFile(currentUri); List annotatedDartCodeLines = convertAnnotatedCodeToCodeLines( sourceFile.slowText(), annotations, startLine: interval.from, endLine: interval.to, uri: currentUri); if (codeSources != null) { CodeSource currentCodeSource; Interval currentLineInterval; for (CodeLine dartCodeLine in annotatedDartCodeLines) { if (currentCodeSource == null || !currentLineInterval.contains(dartCodeLine.lineNo)) { currentCodeSource = null; for (CodeSource codeSource in codeSources) { Interval interval = new 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 dartCodeLines = jsToDartMap.putIfAbsent(jsCodeLine, () => []); 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 = new Interval(line, line + 1); annotations = []; 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'; } class AnnotationType { static const WITH_SOURCE_INFO = const AnnotationType(0, ClassNames.withSourceInfo, true); static const WITHOUT_SOURCE_INFO = const AnnotationType(1, ClassNames.withoutSourceInfo, false); static const ADDITIONAL_SOURCE_INFO = const AnnotationType(2, ClassNames.additionalSourceInfo, true); static const UNUSED_SOURCE_INFO = const AnnotationType(3, ClassNames.unusedSourceInfo, false); final int index; final String className; final bool isSourceMapped; const AnnotationType(this.index, this.className, this.isSourceMapped); static const List values = const [ WITH_SOURCE_INFO, WITHOUT_SOURCE_INFO, ADDITIONAL_SOURCE_INFO, UNUSED_SOURCE_INFO ]; } class CodeLineAnnotation { final int annotationId; final AnnotationType annotationType; final List codeLocations; final List codeSources; final String stepInfo; int sourceMappingIndex; CodeLineAnnotation( {this.annotationId, this.annotationType, this.codeLocations, 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 new 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']); } }