Chromium Code Reviews| Index: tests/compiler/dart2js/sourcemaps/diff_view.dart |
| diff --git a/tests/compiler/dart2js/sourcemaps/diff_view.dart b/tests/compiler/dart2js/sourcemaps/diff_view.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..54c7b8b14ee37c1554b55b8f1bae9702bc8dfc4d |
| --- /dev/null |
| +++ b/tests/compiler/dart2js/sourcemaps/diff_view.dart |
| @@ -0,0 +1,870 @@ |
| +// Copyright (c) 2015, 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; |
|
Siggi Cherem (dart-lang)
2016/01/21 18:18:34
I only did a cursory review of this file, let me k
|
| + |
| +import 'dart:async'; |
| +import 'dart:io'; |
| +import 'package:compiler/src/commandline_options.dart'; |
| +import 'package:compiler/src/diagnostics/invariant.dart'; |
| +import 'package:compiler/src/io/position_information.dart'; |
| +import 'package:compiler/src/js/js.dart' as js; |
| +import 'sourcemap_helper.dart'; |
| +import 'sourcemap_html_helper.dart'; |
| +import 'trace_graph.dart'; |
| +import 'js_tracer.dart'; |
| + |
| +const String WITH_SOURCE_INFO_STYLE = 'background-color:#FF8080;'; |
| +const String WITHOUT_SOURCE_INFO_STYLE = 'border: solid 1px #FF8080;'; |
| +const String ADDITIONAL_SOURCE_INFO_STYLE = 'border: solid 1px #8080FF;'; |
| + |
| +main(List<String> args) async { |
| + DEBUG_MODE = true; |
| + String out = 'out.js.diff_view.html'; |
| + String filename; |
| + List<String> currentOptions = []; |
| + List<List<String>> options = [currentOptions]; |
| + int argGroup = 0; |
| + for (String arg in args) { |
| + if (arg == '--') { |
| + currentOptions = []; |
| + options.add(currentOptions); |
| + argGroup++; |
| + } 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 = options[0]; |
| + List<String> options1; |
| + List<String> options2; |
| + if (options.length == 1) { |
| + // Use default options; comparing SSA and CPS output using the new |
| + // source information strategy. |
|
Siggi Cherem (dart-lang)
2016/01/21 18:18:34
is a goal also to compare the new and old source-i
Johnni Winther
2016/01/22 15:12:38
It is support as well.
|
| + options1 = [USE_NEW_SOURCE_INFO]..addAll(commonArguments); |
| + options2 = [USE_NEW_SOURCE_INFO, Flags.useCpsIr]..addAll(commonArguments); |
| + } else if (options.length == 2) { |
| + // Use alternative options for the second output column. |
| + options1 = commonArguments; |
| + options2 = options[1]..addAll(commonArguments); |
| + } else { |
| + // Use specific options for both output columns. |
| + options1 = options[1]..addAll(commonArguments); |
| + options2 = options[2]..addAll(commonArguments); |
| + } |
| + |
| + print('Compiling ${options1.join(' ')} $filename'); |
| + CodeLinesResult result1 = await computeCodeLines(options1, filename); |
| + print('Compiling ${options2.join(' ')} $filename'); |
| + CodeLinesResult result2 = await computeCodeLines(options2, filename); |
| + |
| + StringBuffer sb = new StringBuffer(); |
| + sb.write(''' |
| +<html> |
| +<head> |
| +<title>Diff for $filename</title> |
| +<style> |
| +.lineNumber { |
| + font-size: smaller; |
| + color: #888; |
| +} |
| +.header { |
| + position: fixed; |
| + width: 50%; |
| + background-color: #400000; |
| + color: #FFFFFF; |
| + height: 20px; |
| + top: 0px; |
| + z-index: 1000; |
| +} |
| +.cell { |
| + max-width:500px; |
| + overflow-x:auto; |
| + vertical-align:top; |
| +} |
| +.corresponding1 { |
| + background-color: #FFFFE0; |
| +} |
| +.corresponding2 { |
| + background-color: #EFEFD0; |
| +} |
| +.identical1 { |
| + background-color: #E0F0E0; |
| +} |
| +.identical2 { |
| + background-color: #C0E0C0; |
| +} |
| +</style> |
| +</head> |
| +<body>'''); |
| + |
| + sb.write(''' |
| +<div class="header" style="left: 0px;">[${options1.join(',')}]</div> |
| +<div class="header" style="right: 0px;">[${options2.join(',')}]</div> |
| +<div style="position:absolute;top:22px;width:100%;height:18px;"> |
| + <span class="identical1"> </span> |
| + <span class="identical2"> </span> |
| + identical blocks |
| + <span class="corresponding1"> </span> |
| + <span class="corresponding2"> </span> |
| + corresponding blocks |
| + <span style="$WITH_SOURCE_INFO_STYLE"> </span> |
| + offset with source information |
| + <span style="$WITHOUT_SOURCE_INFO_STYLE"> </span> |
| + offset without source information |
| + <span style="$ADDITIONAL_SOURCE_INFO_STYLE"> </span> |
| + offset with unneeded source information |
| +</div> |
| +<table style="position:absolute;top:40px;width:100%;"><tr> |
| +'''); |
| + |
| + void addCell(String content) { |
| + sb.write(''' |
| +<td class="cell"><pre> |
| +'''); |
| + sb.write(content); |
| + sb.write(''' |
| +</pre></td> |
| +'''); |
| + } |
| + |
| + List<OutputStructure> structures = [ |
| + OutputStructure.parse(result1.codeLines), |
| + OutputStructure.parse(result2.codeLines)]; |
| + List<List<CodeLine>> inputLines = [result1.codeLines, result2.codeLines]; |
| + List<List<HtmlPart>> outputLines = [<HtmlPart>[], <HtmlPart>[]]; |
| + |
| + /// Marker to alternate output colors. |
| + bool alternating = false; |
| + |
| + /// Enable 'corresponding' background colors for [f]. |
| + void withMatching(f()) { |
| + HtmlPart start = new ConstHtmlPart( |
| + '<div class="corresponding${alternating ? '1' : '2'}">'); |
| + HtmlPart end = new ConstHtmlPart('</div>'); |
| + alternating = !alternating; |
| + outputLines[0].add(start); |
| + outputLines[1].add(start); |
| + f(); |
| + outputLines[0].add(end); |
| + outputLines[1].add(end); |
| + } |
| + |
| + /// Enable 'identical' background colors for [f]. |
| + void withIdentical(f()) { |
| + HtmlPart start = new ConstHtmlPart( |
| + '<div class="identical${alternating ? '1' : '2'}">'); |
| + HtmlPart end = new ConstHtmlPart('</div>'); |
| + alternating = !alternating; |
| + outputLines[0].add(start); |
| + outputLines[1].add(start); |
| + f(); |
| + outputLines[0].add(end); |
| + outputLines[1].add(end); |
| + } |
| + |
| + /// Output code lines in [range] from input number [index], padding the other |
| + /// column with empty lines. |
| + void handleSkew(int index, Interval range) { |
| + int from = range.from; |
| + while (from < range.to) { |
| + outputLines[1 - index].add(const ConstHtmlPart('\n')); |
| + outputLines[index].add( |
| + new CodeLineHtmlPart(inputLines[index][from++])); |
| + } |
| + } |
| + |
| + /// Output code lines of the [indices] from the corresponding inputs. |
| + void addBoth(List<int> indices) { |
| + outputLines[0].add(new CodeLineHtmlPart(inputLines[0][indices[0]])); |
| + outputLines[1].add(new CodeLineHtmlPart(inputLines[1][indices[1]])); |
| + } |
| + |
| + /// Output code lines of the [ranges] from the corresponding inputs. |
| + void addBothLines(List<Interval> ranges) { |
| + Interval range1 = ranges[0]; |
| + Interval range2 = ranges[1]; |
| + int offset = 0; |
| + while (range1.from + offset < range1.to && |
| + range2.from + offset < range2.to) { |
| + addBoth([range1.from + offset, range2.from + offset]); |
| + offset++; |
| + } |
| + if (range1.from + offset < range1.to) { |
| + handleSkew(0, new Interval(range1.from + offset, range1.to)); |
| + } |
| + if (range2.from + offset < range2.to) { |
| + handleSkew(1, new Interval(range2.from + offset, range2.to)); |
| + } |
| + } |
| + |
| + /// Merge the code lines in [range1] and [range2] of the corresponding input. |
| + void addRaw(Interval range1, Interval range2) { |
| + match(a, b) => a.code == b.code; |
| + |
| + List<Interval> currentMatchedIntervals; |
| + |
| + void flushMatching() { |
| + if (currentMatchedIntervals != null) { |
| + withIdentical(() { |
| + addBothLines(currentMatchedIntervals); |
| + }); |
| + } |
| + currentMatchedIntervals = null; |
| + } |
| + |
| + align( |
| + inputLines[0], |
| + inputLines[1], |
| + range1: range1, |
| + range2: range2, |
| + match: match, |
| + handleSkew: (int listIndex, Interval range) { |
| + flushMatching(); |
| + handleSkew(listIndex, range); |
| + }, |
| + handleMatched: (List<int> indices) { |
| + if (currentMatchedIntervals == null) { |
| + currentMatchedIntervals = [ |
| + new Interval(indices[0], indices[0] + 1), |
| + new Interval(indices[1], indices[1] + 1)]; |
| + } else { |
| + currentMatchedIntervals[0] = |
| + new Interval(currentMatchedIntervals[0].from, indices[0] + 1); |
| + currentMatchedIntervals[1] = |
| + new Interval(currentMatchedIntervals[1].from, indices[1] + 1); |
| + } |
| + }, |
| + handleUnmatched: (List<int> indices) { |
| + flushMatching(); |
| + addBoth(indices); |
| + }); |
| + |
| + flushMatching(); |
| + } |
| + |
| + /// Output the lines of the library blocks in [childRange] in |
| + /// `structures[index]`, padding the other column with empty lines. |
| + void addBlock(int index, Interval childRange) { |
| + handleSkew(index, structures[index].getChildInterval(childRange)); |
| + } |
| + |
| + /// Output the members of the [classes] aligned. |
| + void addMatchingClasses(List<LibraryClass> classes) { |
| + withMatching(() { |
| + addBothLines(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) { |
| + handleSkew(listIndex, |
| + classes[listIndex].getChildInterval(childRange)); |
| + }, |
| + handleMatched: (List<int> indices) { |
| + List<Interval> intervals = [ |
| + classes[0].getChild(indices[0]).interval, |
| + classes[1].getChild(indices[1]).interval]; |
| + withMatching(() { |
| + addBothLines(intervals); |
| + }); |
| + }, |
| + handleUnmatched: (List<int> indices) { |
| + List<Interval> intervals = [ |
| + classes[0].getChild(indices[0]).interval, |
| + classes[1].getChild(indices[1]).interval]; |
| + addBothLines(intervals); |
| + }); |
| + withMatching(() { |
| + addBothLines(classes.map((c) => c.footer).toList()); |
| + }); |
| + } |
| + |
| + /// Output the library blocks in [indices] from the corresponding |
| + /// [OutputStructure]s, aligning their content. |
| + void addMatchingBlocks(List<int> indices) { |
| + List<LibraryBlock> blocks = [ |
| + structures[0].getChild(indices[0]), |
| + structures[1].getChild(indices[1])]; |
| + |
| + withMatching(() { |
| + addBothLines(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) { |
| + handleSkew(listIndex, blocks[listIndex].getChildInterval(childRange)); |
| + }, |
| + handleMatched: (List<int> indices) { |
| + List<BasicEntity> entities = [ |
| + blocks[0].getChild(indices[0]), |
| + blocks[1].getChild(indices[1])]; |
| + if (entities.every((e) => e is LibraryClass)) { |
| + addMatchingClasses(entities); |
| + } else { |
| + withMatching(() { |
| + addBothLines(entities.map((e) => e.interval).toList()); |
| + }); |
| + } |
| + }, |
| + handleUnmatched: (List<int> indices) { |
| + List<Interval> intervals = [ |
| + blocks[0].getChild(indices[0]).interval, |
| + blocks[1].getChild(indices[1]).interval]; |
| + addBothLines(intervals); |
| + }); |
| + withMatching(() { |
| + addBothLines(blocks.map((b) => b.footer).toList()); |
| + }); |
| + } |
| + |
| + /// Output the lines of the blocks in [indices] from the corresponding |
| + /// [OutputStructure]s. |
| + void addUnmatchedBlocks(List<int> indices) { |
| + List<LibraryBlock> blocks = [ |
| + structures[0].getChild(indices[0]), |
| + structures[1].getChild(indices[1])]; |
| + addBothLines([blocks[0].interval, blocks[1].interval]); |
| + } |
| + |
| + |
| + 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); |
| + |
| + addCell(htmlPartsToString(outputLines[0], inputLines[0])); |
| + addCell(htmlPartsToString(outputLines[1], inputLines[1])); |
| + |
| + sb.write('''</tr><tr>'''); |
| + addCell(result1.coverage.getCoverageReport()); |
| + addCell(result2.coverage.getCoverageReport()); |
| + |
| + sb.write(''' |
| +</tr></table> |
| +</body> |
| +</html> |
| +'''); |
| + |
| + new File(out).writeAsStringSync(sb.toString()); |
| + print('Diff generated in $out'); |
| +} |
| + |
| +/// 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<int> indices), |
| + void handleUnmatched(List<int> 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)); |
| + } |
| +} |
| + |
| +// Constants used to identify the subsection of the JavaScript output. These |
| +// are specifically for the unminified full_emitter output. |
| +const String HEAD = ' var dart = ['; |
| +const String TAIL = ' }], '; |
| +const String END = ' setupProgram(dart'; |
| + |
| +final RegExp TOP_LEVEL_VALUE = new RegExp(r'^ (".+?"):'); |
| +final RegExp TOP_LEVEL_FUNCTION = |
| + new RegExp(r'^ ([a-zA-Z0-9_$]+): function'); |
| +final RegExp TOP_LEVEL_CLASS = new RegExp(r'^ ([a-zA-Z0-9_$]+): \{'); |
| + |
| +final RegExp MEMBER_VALUE = new RegExp(r'^ (".+?"):'); |
| +final RegExp MEMBER_FUNCTION = new RegExp(r'^ ([a-zA-Z0-9_$]+): function'); |
| +final RegExp MEMBER_OBJECT = new RegExp(r'^ ([a-zA-Z0-9_$]+): \{'); |
| + |
| +/// Subrange of the JavaScript output. |
| +abstract class OutputEntity { |
| + Interval get interval; |
| + Interval get header; |
| + Interval get footer; |
| + |
| + List<OutputEntity> get children; |
| + |
| + Interval getChildInterval(Interval childIndex) { |
| + return new Interval( |
| + children[childIndex.from].interval.from, |
| + children[childIndex.to - 1].interval.to); |
| + |
| + } |
| + |
| + OutputEntity getChild(int index) { |
| + return children[index]; |
| + } |
| +} |
| + |
| +/// The whole JavaScript output. |
| +class OutputStructure extends OutputEntity { |
| + final List<CodeLine> lines; |
| + final int headerEnd; |
| + final int footerStart; |
| + final List<LibraryBlock> children; |
| + |
| + OutputStructure( |
| + this.lines, |
| + this.headerEnd, |
| + this.footerStart, |
| + this.children); |
| + |
| + Interval get interval => new Interval(0, lines.length); |
| + |
| + Interval get header => new Interval(0, headerEnd); |
| + |
| + Interval get footer => new Interval(footerStart, lines.length); |
| + |
| + /// Compute the structure of the JavaScript [lines]. |
| + static OutputStructure parse(List<CodeLine> lines) { |
| + |
| + int findHeaderStart(List<CodeLine> lines) { |
| + int index = 0; |
| + for (CodeLine line in lines) { |
| + if (line.code.startsWith(HEAD)) { |
| + return index; |
| + } |
| + index++; |
| + } |
| + return lines.length; |
| + } |
| + |
| + int findHeaderEnd(int start, List<CodeLine> lines) { |
| + int index = start; |
| + for (CodeLine line in lines.skip(start)) { |
| + if (line.code.startsWith(END)) { |
| + return index; |
| + } |
| + index++; |
| + } |
| + return lines.length; |
| + } |
| + |
| + String readHeader(CodeLine line) { |
| + String code = line.code; |
| + String ssaLineHeader; |
| + if (code.startsWith(HEAD)) { |
| + return code.substring(HEAD.length); |
| + } else if (code.startsWith(TAIL)) { |
| + return code.substring(TAIL.length); |
| + } |
| + return null; |
| + } |
| + |
| + List<LibraryBlock> computeHeaderMap( |
| + List<CodeLine> lines, int start, int end) { |
| + List<LibraryBlock> libraryBlocks = <LibraryBlock>[]; |
| + LibraryBlock current; |
| + for (int index = start; index < end; index++) { |
| + String header = readHeader(lines[index]); |
| + if (header != null) { |
| + if (current != null) { |
| + current.to = index; |
| + } |
| + libraryBlocks.add(current = new LibraryBlock(header, index)); |
| + } |
| + } |
| + if (current != null) { |
| + current.to = end; |
| + } |
| + return libraryBlocks; |
| + } |
| + |
| + int headerEnd = findHeaderStart(lines); |
| + int footerStart = findHeaderEnd(headerEnd, lines); |
| + List<LibraryBlock> libraryBlocks = |
| + computeHeaderMap(lines, headerEnd, footerStart); |
| + for (LibraryBlock block in libraryBlocks) { |
| + block.preprocess(lines); |
| + } |
| + |
| + return new OutputStructure( |
| + lines, headerEnd, footerStart, libraryBlocks); |
| + } |
| +} |
| + |
| +abstract class AbstractEntity extends OutputEntity { |
| + final String name; |
| + final int from; |
| + int to; |
| + |
| + AbstractEntity(this.name, this.from); |
| + |
| + Interval get interval => new Interval(from, to); |
| +} |
| + |
| +/// A block defining the content of a Dart library. |
| +class LibraryBlock extends AbstractEntity { |
| + List<BasicEntity> children = <BasicEntity>[]; |
| + int get headerEnd => from + 2; |
| + int get footerStart => to - 1; |
| + |
| + LibraryBlock(String name, int from) : super(name, from); |
| + |
| + Interval get header => new Interval(from, headerEnd); |
| + |
| + Interval get footer => new Interval(footerStart, to); |
| + |
| + void preprocess(List<CodeLine> lines) { |
| + int index = headerEnd; |
| + BasicEntity current; |
| + while (index < footerStart) { |
| + String line = lines[index].code; |
| + BasicEntity next; |
| + Match matchFunction = TOP_LEVEL_FUNCTION.firstMatch(line); |
| + if (matchFunction != null) { |
| + next = new BasicEntity(matchFunction.group(1), index); |
| + } else { |
| + Match matchClass = TOP_LEVEL_CLASS.firstMatch(line); |
| + if (matchClass != null) { |
| + next = new LibraryClass(matchClass.group(1), index); |
| + } else { |
| + Match matchValue = TOP_LEVEL_VALUE.firstMatch(line); |
| + if (matchValue != null) { |
| + next = new BasicEntity(matchValue.group(1), index); |
| + } |
| + } |
| + } |
| + if (next != null) { |
| + if (current != null) { |
| + current.to = index; |
| + } |
| + children.add(current = next); |
| + } else if (index == headerEnd) { |
| + throw 'Failed to match first library block line:\n$line'; |
| + } |
| + |
| + index++; |
| + } |
| + if (current != null) { |
| + current.to = footerStart; |
| + } |
| + |
| + for (BasicEntity entity in children) { |
| + entity.preprocess(lines); |
| + } |
| + } |
| +} |
| + |
| +/// A simple member of a library or class. |
| +class BasicEntity extends AbstractEntity { |
| + BasicEntity(String name, int from) : super(name, from); |
| + |
| + Interval get header => new Interval(from, to); |
| + |
| + Interval get footer => new Interval(to, to); |
| + |
| + List<OutputEntity> get children => const <OutputEntity>[]; |
| + |
| + void preprocess(List<CodeLine> lines) {} |
| +} |
| + |
| +/// A block defining a Dart class. |
| +class LibraryClass extends BasicEntity { |
| + List<BasicEntity> children = <BasicEntity>[]; |
| + int get headerEnd => from + 1; |
| + int get footerStart => to - 1; |
| + |
| + LibraryClass(String name, int from) : super(name, from); |
| + |
| + Interval get header => new Interval(from, headerEnd); |
| + |
| + Interval get footer => new Interval(footerStart, to); |
| + |
| + void preprocess(List<CodeLine> lines) { |
| + int index = headerEnd; |
| + BasicEntity current; |
| + while (index < footerStart) { |
| + String line = lines[index].code; |
| + BasicEntity next; |
| + Match matchFunction = MEMBER_FUNCTION.firstMatch(line); |
| + if (matchFunction != null) { |
| + next = new BasicEntity(matchFunction.group(1), index); |
| + } else { |
| + Match matchClass = MEMBER_OBJECT.firstMatch(line); |
| + if (matchClass != null) { |
| + next = new BasicEntity(matchClass.group(1), index); |
| + } else { |
| + Match matchValue = MEMBER_VALUE.firstMatch(line); |
| + if (matchValue != null) { |
| + next = new BasicEntity(matchValue.group(1), index); |
| + } |
| + } |
| + } |
| + if (next != null) { |
| + if (current != null) { |
| + current.to = index; |
| + } |
| + children.add(current = next); |
| + } else if (index == headerEnd) { |
| + throw 'Failed to match first library block line:\n$line'; |
| + } |
| + |
| + index++; |
| + } |
| + if (current != null) { |
| + current.to = footerStart; |
| + } |
| + } |
| +} |
| + |
| +class Interval { |
| + final int from; |
| + final int to; |
| + |
| + const Interval(this.from, this.to); |
| + |
| + int get length => to - from; |
| +} |
| + |
| +class HtmlPart { |
| + void printHtmlOn(StringBuffer buffer) {} |
| +} |
| + |
| +class ConstHtmlPart implements HtmlPart { |
| + final String html; |
| + |
| + const ConstHtmlPart(this.html); |
| + |
| + @override |
| + void printHtmlOn(StringBuffer buffer) { |
| + buffer.write(html); |
| + } |
| +} |
| + |
| +class CodeLineHtmlPart implements HtmlPart { |
| + final CodeLine line; |
| + |
| + CodeLineHtmlPart(this.line); |
| + |
| + @override |
| + void printHtmlOn(StringBuffer buffer, [int lineNoWidth]) { |
| + line.printHtmlOn(buffer, lineNoWidth); |
| + } |
| +} |
| + |
| +/// Convert [parts] to an HTML string while checking invariants for [lines]. |
| +String htmlPartsToString(List<HtmlPart> parts, List<CodeLine> lines) { |
| + int lineNoWidth; |
| + if (lines.isNotEmpty) { |
| + lineNoWidth = '${lines.last.lineNo + 1}'.length; |
| + } |
| + StringBuffer buffer = new StringBuffer(); |
| + int expectedLineNo = 0; |
| + for (HtmlPart part in parts) { |
| + if (part is CodeLineHtmlPart) { |
| + if (part.line.lineNo != expectedLineNo) { |
| + print('Expected line no $expectedLineNo, found ${part.line.lineNo}'); |
| + if (part.line.lineNo < expectedLineNo) { |
| + print('Duplicate lines:'); |
| + int index = part.line.lineNo; |
| + while (index <= expectedLineNo) { |
| + print(lines[index++].code); |
| + } |
| + } else { |
| + print('Missing lines:'); |
| + int index = expectedLineNo; |
| + while (index <= part.line.lineNo) { |
| + print(lines[index++].code); |
| + } |
| + } |
| + expectedLineNo = part.line.lineNo; |
| + } |
| + expectedLineNo++; |
| + part.printHtmlOn(buffer, lineNoWidth); |
| + } else { |
| + part.printHtmlOn(buffer); |
| + } |
| + } |
| + return buffer.toString(); |
| +} |
| + |
| +class CodeLinesResult { |
| + final List<CodeLine> codeLines; |
| + final Coverage coverage; |
| + |
| + CodeLinesResult(this.codeLines, this.coverage); |
| +} |
| + |
| +/// Compute [CodeLine]s and [Coverage] for [filename] using the given [options]. |
| +Future<CodeLinesResult> computeCodeLines( |
| + List<String> options, |
| + String filename) async { |
| + SourceMapProcessor processor = new SourceMapProcessor(filename); |
| + List<SourceMapInfo> sourceMapInfoList = |
| + await processor.process(options, perElement: false); |
| + |
| + const int WITH_SOURCE_INFO = 0; |
| + const int WITHOUT_SOURCE_INFO = 1; |
| + const int ADDITIONAL_SOURCE_INFO = 2; |
| + |
| + for (SourceMapInfo info in sourceMapInfoList) { |
| + if (info.element != null) continue; |
| + |
| + List<CodeLine> codeLines; |
| + Coverage coverage = new Coverage(); |
| + List<Annotation> annotations = <Annotation>[]; |
| + String code = info.code; |
| + TraceGraph graph = createTraceGraph(info, coverage); |
| + Set<js.Node> mappedNodes = new Set<js.Node>(); |
| + for (TraceStep step in graph.steps) { |
| + int offset; |
| + if (options.contains(USE_NEW_SOURCE_INFO)) { |
| + offset = step.offset.codeOffset; |
| + } else { |
| + offset = info.jsCodePositions[step.node].startPosition; |
| + } |
| + if (offset != null) { |
| + int id = step.sourceLocation != null |
| + ? WITH_SOURCE_INFO : WITHOUT_SOURCE_INFO; |
| + annotations.add( |
| + new Annotation(id, offset, null)); |
| + } |
| + } |
| + if (!options.contains(USE_NEW_SOURCE_INFO)) { |
| + for (js.Node node in info.nodeMap.nodes) { |
| + if (!mappedNodes.contains(node)) { |
| + int offset = info.jsCodePositions[node].startPosition; |
| + annotations.add( |
| + new Annotation(ADDITIONAL_SOURCE_INFO, offset, null)); |
| + } |
| + } |
| + } |
| + codeLines = convertAnnotatedCodeToCodeLines( |
| + code, |
| + annotations, |
| + colorScheme: new CustomColorScheme( |
| + single: (int id) { |
| + if (id == WITH_SOURCE_INFO) { |
| + return WITH_SOURCE_INFO_STYLE; |
| + } else if (id == ADDITIONAL_SOURCE_INFO) { |
| + return ADDITIONAL_SOURCE_INFO_STYLE; |
| + } |
| + return WITHOUT_SOURCE_INFO_STYLE; |
| + }, |
| + multi: (List ids) { |
| + if (ids.contains(WITH_SOURCE_INFO)) { |
| + return WITH_SOURCE_INFO_STYLE; |
| + } else if (ids.contains(ADDITIONAL_SOURCE_INFO)) { |
| + return ADDITIONAL_SOURCE_INFO_STYLE; |
| + } |
| + return WITHOUT_SOURCE_INFO_STYLE; |
| + } |
| + )); |
| + return new CodeLinesResult(codeLines, coverage); |
| + } |
| +} |