| // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library sourcemap.diff_view; |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:compiler/src/commandline_options.dart'; |
| import 'package:compiler/src/diagnostics/invariant.dart'; |
| import 'package:compiler/src/elements/elements.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 'package:compiler/src/js/js_debug.dart'; |
| |
| import 'diff.dart'; |
| import 'html_parts.dart'; |
| import 'js_tracer.dart'; |
| import 'output_structure.dart'; |
| import 'sourcemap_helper.dart'; |
| import 'sourcemap_html_helper.dart'; |
| import 'trace_graph.dart'; |
| |
| const String WITH_SOURCE_INFO_STYLE = 'border: solid 1px #FF8080;'; |
| const String WITHOUT_SOURCE_INFO_STYLE = 'background-color: #8080FF;'; |
| const String ADDITIONAL_SOURCE_INFO_STYLE = 'border: solid 1px #80FF80;'; |
| const String UNUSED_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>> optionSegments = [currentOptions]; |
| Map<int, String> loadFrom = {}; |
| Map<int, String> saveTo = {}; |
| int argGroup = 0; |
| bool addAnnotations = true; |
| for (String arg in args) { |
| if (arg == '--') { |
| currentOptions = []; |
| optionSegments.add(currentOptions); |
| argGroup++; |
| } else if (arg == '-h') { |
| addAnnotations = 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) { |
| // Use default options; comparing SSA and CPS output using the new |
| // source information strategy. |
| options.add([USE_NEW_SOURCE_INFO]..addAll(commonArguments)); |
| options.add([USE_NEW_SOURCE_INFO, Flags.useCpsIr]..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, addAnnotations: addAnnotations); |
| 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, addAnnotations: addAnnotations); |
| } |
| |
| /// 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, Element element) { |
| OutputEntity entity = structure.getEntityForLine(line); |
| if (entity != null) { |
| entity.codeSource = codeSourceFromElement(element); |
| } |
| }); |
| } |
| |
| /// 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(), |
| 'coverage': coverage, |
| }; |
| } |
| |
| static AnnotatedOutput fromJson(Map json) { |
| String filename = json['filename']; |
| List<String> options = json['options']; |
| OutputStructure structure = OutputStructure.fromJson(json['structure']); |
| 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 addAnnotations: 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> |
| .lineNumber { |
| font-size: smaller; |
| color: #888; |
| } |
| .comment { |
| font-size: smaller; |
| color: #888; |
| font-family: initial; |
| } |
| .header { |
| position: fixed; |
| width: 100%; |
| background-color: #FFFFFF; |
| left: 0px; |
| top: 0px; |
| height: 42px; |
| z-index: 1000; |
| } |
| .header-table { |
| width: 100%; |
| background-color: #400000; |
| color: #FFFFFF; |
| border-spacing: 0px; |
| } |
| .header-column { |
| width: 34%; |
| } |
| .legend { |
| padding: 2px; |
| } |
| .table { |
| position: absolute; |
| left: 0px; |
| top: 42px; |
| width: 100%; |
| border-spacing: 0px; |
| } |
| .cell { |
| max-width: 500px; |
| overflow-y: hidden; |
| vertical-align: top; |
| border-top: 1px solid #F0F0F0; |
| border-left: 1px solid #F0F0F0; |
| '''); |
| 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; |
| } |
| .corresponding1 { |
| background-color: #FFFFE0; |
| } |
| .corresponding2 { |
| background-color: #EFEFD0; |
| } |
| .identical1 { |
| background-color: #E0F0E0; |
| } |
| .identical2 { |
| background-color: #C0E0C0; |
| } |
| .line { |
| padding-left: 7em; |
| text-indent: -7em; |
| margin: 0px; |
| } |
| .column0 { |
| } |
| .column1 { |
| } |
| .column2 { |
| } |
| </style> |
| </head> |
| <body>'''); |
| |
| sb.write(''' |
| <div class="header"> |
| <table class="header-table"><tr> |
| <td class="header-column">[${outputs[0].options.join(',')}]</td> |
| <td class="header-column">[${outputs[1].options.join(',')}]</td> |
| <td class="header-column">Dart code</td> |
| </tr></table> |
| <div class="legend"> |
| <span class="identical1"> </span> |
| <span class="identical2"> </span> |
| identical blocks |
| <span class="corresponding1"> </span> |
| <span class="corresponding2"> </span> |
| corresponding blocks |
| '''); |
| |
| if (addAnnotations) { |
| sb.write(''' |
| <span style="$WITH_SOURCE_INFO_STYLE"> </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 style="$WITHOUT_SOURCE_INFO_STYLE"> </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 style="$ADDITIONAL_SOURCE_INFO_STYLE"> </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 style="$UNUSED_SOURCE_INFO_STYLE"> </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> |
| '''); |
| } |
| |
| sb.write(''' |
| </div></div> |
| <table class="table"> |
| '''); |
| |
| void addCell(String content) { |
| sb.write(''' |
| <td class="cell"><pre> |
| '''); |
| sb.write(content); |
| sb.write(''' |
| </pre></td> |
| '''); |
| } |
| |
| /// 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)); |
| } |
| |
| for (DiffBlock block in blocks) { |
| String className; |
| switch (block.kind) { |
| case DiffKind.UNMATCHED: |
| className = 'cell'; |
| break; |
| case DiffKind.MATCHING: |
| className = 'cell corresponding${alternating ? '1' : '2'}'; |
| alternating = !alternating; |
| break; |
| case DiffKind.IDENTICAL: |
| className = 'cell identical${alternating ? '1' : '2'}'; |
| alternating = !alternating; |
| break; |
| } |
| sb.write('<tr>'); |
| for (int index = 0; index < 3; index++) { |
| sb.write('''<td class="$className column$index">'''); |
| List<HtmlPart> lines = block.getColumn(index); |
| if (lines.isNotEmpty) { |
| for (HtmlPart line in lines) { |
| sb.write('<p class="line">'); |
| if (index < printContexts.length) { |
| line.printHtmlOn(sb, printContexts[index]); |
| } else { |
| line.printHtmlOn(sb, new HtmlPrintContext()); |
| } |
| sb.write('</p>'); |
| } |
| } |
| sb.write('''</td>'''); |
| } |
| sb.write('</tr>'); |
| } |
| |
| sb.write('''</tr><tr>'''); |
| |
| addCell(outputs[0].coverage); |
| addCell(outputs[1].coverage); |
| |
| sb.write(''' |
| </table> |
| </body> |
| </html> |
| '''); |
| |
| new File(out).writeAsStringSync(sb.toString()); |
| print('Diff generated in $out'); |
| } |
| |
| class CodeLinesResult { |
| final List<CodeLine> codeLines; |
| final Coverage coverage; |
| final Map<int, Element> elementMap; |
| final SourceFileManager sourceFileManager; |
| |
| CodeLinesResult(this.codeLines, this.coverage, |
| this.elementMap, this.sourceFileManager); |
| } |
| |
| /// Compute [CodeLine]s and [Coverage] for [filename] using the given [options]. |
| Future<CodeLinesResult> computeCodeLines( |
| List<String> options, |
| String filename, |
| {bool addAnnotations: true}) async { |
| SourceMapProcessor processor = new SourceMapProcessor(filename); |
| SourceMaps sourceMaps = |
| await processor.process(options, perElement: true, forMain: true); |
| |
| const int WITH_SOURCE_INFO = 0; |
| const int WITHOUT_SOURCE_INFO = 1; |
| const int ADDITIONAL_SOURCE_INFO = 2; |
| const int UNUSED_SOURCE_INFO = 3; |
| |
| SourceMapInfo info = sourceMaps.mainSourceMapInfo; |
| |
| List<CodeLine> codeLines; |
| Coverage coverage = new Coverage(); |
| List<Annotation> annotations = <Annotation>[]; |
| |
| void addAnnotation(int id, int offset, String title) { |
| annotations.add(new Annotation(id, offset, title)); |
| } |
| |
| String code = info.code; |
| TraceGraph graph = createTraceGraph(info, coverage); |
| if (addAnnotations) { |
| Set<js.Node> mappedNodes = new Set<js.Node>(); |
| |
| void addSourceLocations( |
| int kind, int offset, List<SourceLocation> locations, String prefix) { |
| |
| addAnnotation(kind, offset, |
| '${prefix}${locations |
| .where((l) => l != null) |
| .map((l) => l.shortText) |
| .join('\n')}'); |
| } |
| |
| bool addSourceLocationsForNode(int kind, js.Node node, String prefix) { |
| Map<int, List<SourceLocation>> locations = info.nodeMap[node]; |
| if (locations == null || locations.isEmpty) { |
| return false; |
| } |
| locations.forEach( |
| (int offset, List<SourceLocation> locations) { |
| addSourceLocations(kind, offset, locations, |
| '${prefix}\n${truncate(nodeToString(node), 80)}\n'); |
| }); |
| mappedNodes.add(node); |
| return true; |
| } |
| |
| |
| for (TraceStep step in graph.steps) { |
| String title = '${step.id}:${step.kind}:${step.offset}'; |
| if (!addSourceLocationsForNode(WITH_SOURCE_INFO, step.node, title)) { |
| int offset; |
| if (options.contains(USE_NEW_SOURCE_INFO)) { |
| offset = step.offset.subexpressionOffset; |
| } else { |
| offset = info.jsCodePositions[step.node].startPosition; |
| } |
| if (offset != null) { |
| addAnnotation(WITHOUT_SOURCE_INFO, offset, title); |
| } |
| } |
| } |
| for (js.Node node in info.nodeMap.nodes) { |
| if (!mappedNodes.contains(node)) { |
| addSourceLocationsForNode(ADDITIONAL_SOURCE_INFO, node, ''); |
| } |
| } |
| 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(UNUSED_SOURCE_INFO, offset, locations, ''); |
| } |
| }); |
| } |
| |
| StringSourceFile sourceFile = new StringSourceFile.fromName(filename, code); |
| Map<int, Element> elementMap = <int, Element>{}; |
| sourceMaps.elementSourceMapInfos.forEach( |
| (Element element, SourceMapInfo info) { |
| CodePosition position = info.jsCodePositions[info.node]; |
| elementMap[sourceFile.getLine(position.startPosition)] = element; |
| }); |
| |
| 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; |
| } else if (id == UNUSED_SOURCE_INFO) { |
| return UNUSED_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; |
| } else if (ids.contains(UNUSED_SOURCE_INFO)) { |
| return UNUSED_SOURCE_INFO_STYLE; |
| } |
| return WITHOUT_SOURCE_INFO_STYLE; |
| } |
| )); |
| return new CodeLinesResult(codeLines, coverage, elementMap, |
| sourceMaps.sourceFileManager); |
| } |
| |
| /// Visitor that computes a map from [js.Node]s to all attached source |
| /// locations. |
| class SourceLocationCollector extends js.BaseVisitor { |
| Map<js.Node, List<SourceLocation>> sourceLocations = |
| <js.Node, List<SourceLocation>>{}; |
| |
| @override |
| 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(Element element) { |
| CodeKind kind; |
| Uri uri; |
| String name; |
| int begin; |
| int end; |
| if (element.isLibrary) { |
| LibraryElement library = element; |
| kind = CodeKind.LIBRARY; |
| name = library.libraryOrScriptName; |
| uri = library.entryCompilationUnit.script.resourceUri; |
| } else if (element.isClass) { |
| kind = CodeKind.CLASS; |
| name = element.name; |
| uri = element.compilationUnit.script.resourceUri; |
| } else { |
| AstElement astElement = element.implementation; |
| kind = CodeKind.MEMBER; |
| uri = astElement.compilationUnit.script.resourceUri; |
| name = computeElementNameForSourceMaps(astElement); |
| if (astElement.hasNode) { |
| begin = astElement.node.getBeginToken().charOffset; |
| end = astElement.node.getEndToken().charEnd; |
| } |
| } |
| return new CodeSource(kind, uri, name, begin, end); |
| } |