// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // @dart = 2.7 library sourcemap.diff_view; import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:_fe_analyzer_shared/src/util/filenames.dart'; import 'package:compiler/src/common/elements.dart'; import 'package:compiler/src/commandline_options.dart'; import 'package:compiler/src/diagnostics/invariant.dart'; import 'package:compiler/src/elements/entities.dart'; import 'package:compiler/src/io/position_information.dart'; import 'package:compiler/src/io/source_information.dart'; import 'package:compiler/src/io/source_file.dart'; import 'package:compiler/src/js/js.dart' as js; import '../helpers/diff.dart'; import '../helpers/html_parts.dart'; import '../helpers/js_tracer.dart'; import '../helpers/output_structure.dart'; import '../helpers/sourcemap_helper.dart'; import '../helpers/sourcemap_html_helper.dart'; import '../helpers/trace_graph.dart'; main(List args) async { DEBUG_MODE = true; String out = 'out.js.diff_view.html'; String filename; List currentOptions = []; List> optionSegments = [currentOptions]; Map loadFrom = {}; Map saveTo = {}; int argGroup = 0; bool showAnnotations = true; for (String arg in args) { if (arg == '--') { currentOptions = []; optionSegments.add(currentOptions); argGroup++; } else if (arg == '-h') { showAnnotations = false; print('Hiding annotations'); } else if (arg == '-l') { loadFrom[argGroup] = 'out.js.diff$argGroup.json'; } else if (arg.startsWith('--load=')) { loadFrom[argGroup] = arg.substring('--load='.length); } else if (arg == '-s') { saveTo[argGroup] = 'out.js.diff$argGroup.json'; } else if (arg.startsWith('--save=')) { saveTo[argGroup] = arg.substring('--save='.length); } else if (arg.startsWith('-o')) { out = arg.substring('-o'.length); } else if (arg.startsWith('--out=')) { out = arg.substring('--out='.length); } else if (arg.startsWith('-')) { currentOptions.add(arg); } else { filename = arg; } } List commonArguments = optionSegments[0]; List> options = >[]; if (optionSegments.length == 1) { // TODO(sigmund, johnniwinther): change default options now that CPS is // deleted. // Use default options; comparing SSA and CPS output using the new // source information strategy. options.add(commonArguments); options.add([Flags.useNewSourceInfo]..addAll(commonArguments)); } else if (optionSegments.length == 2) { // Use alternative options for the second output column. options.add(commonArguments); options.add(optionSegments[1]..addAll(commonArguments)); } else { // Use specific options for both output columns. options.add(optionSegments[1]..addAll(commonArguments)); options.add(optionSegments[2]..addAll(commonArguments)); } SourceFileManager sourceFileManager = new IOSourceFileManager(Uri.base); List outputs = []; for (int i = 0; i < 2; i++) { AnnotatedOutput output; if (loadFrom.containsKey(i)) { output = AnnotatedOutput.loadOutput(loadFrom[i]); } else { print('Compiling ${options[i].join(' ')} $filename'); CodeLinesResult result = await computeCodeLines( options[i], filename, Uri.base.resolve(nativeToUriPath(filename))); OutputStructure structure = OutputStructure.parse(result.codeLines); computeEntityCodeSources(result, structure); output = new AnnotatedOutput( filename, options[i], structure, result.coverage.getCoverageReport()); } if (saveTo.containsKey(i)) { AnnotatedOutput.saveOutput(output, saveTo[i]); } outputs.add(output); } List blocks = createDiffBlocks( outputs.map((o) => o.structure).toList(), sourceFileManager); outputDiffView(out, outputs, blocks, showMarkers: showAnnotations, showSourceMapped: showAnnotations); } /// Attaches [CodeSource]s to the entities in [structure] using the /// element-to-offset in [result]. void computeEntityCodeSources( CodeLinesResult result, OutputStructure structure) { result.elementMap.forEach((int line, MemberEntity element) { OutputEntity entity = structure.getEntityForLine(line); if (entity != null) { entity.codeSource = codeSourceFromElement(element); } }); } class CodeLineAnnotationJsonStrategy implements JsonStrategy { const CodeLineAnnotationJsonStrategy(); @override Map encodeAnnotation(Annotation annotation) { CodeLineAnnotation data = annotation.data; return { 'id': annotation.id, 'codeOffset': annotation.codeOffset, 'title': annotation.title, 'data': data.toJson(this), }; } @override Annotation decodeAnnotation(Map json) { return new Annotation(json['id'], json['codeOffset'], json['title'], data: CodeLineAnnotation.fromJson(json['data'], this)); } @override decodeLineAnnotation(json) { if (json != null) { return CodeSource.fromJson(json); } return null; } @override encodeLineAnnotation(covariant CodeSource lineAnnotation) { if (lineAnnotation != null) { return lineAnnotation.toJson(); } return null; } } /// The structured output of a compilation. class AnnotatedOutput { final String filename; final List options; final OutputStructure structure; final String coverage; AnnotatedOutput(this.filename, this.options, this.structure, this.coverage); List get codeLines => structure.lines; Map toJson() { return { 'filename': filename, 'options': options, 'structure': structure.toJson(const CodeLineAnnotationJsonStrategy()), 'coverage': coverage, }; } static AnnotatedOutput fromJson(Map json) { String filename = json['filename']; List options = json['options']; OutputStructure structure = OutputStructure.fromJson( json['structure'], const CodeLineAnnotationJsonStrategy()); String coverage = json['coverage']; return new AnnotatedOutput(filename, options, structure, coverage); } static AnnotatedOutput loadOutput(filename) { AnnotatedOutput output = AnnotatedOutput.fromJson( json.decode(new File(filename).readAsStringSync())); print('Output loaded from $filename'); return output; } static void saveOutput(AnnotatedOutput output, String filename) { if (filename != null) { new File(filename).writeAsStringSync( const JsonEncoder.withIndent(' ').convert(output.toJson())); print('Output saved in $filename'); } } } void outputDiffView( String out, List outputs, List blocks, {bool showMarkers = true, bool showSourceMapped = true}) { assert(outputs[0].filename == outputs[1].filename); bool usePre = true; StringBuffer sb = new StringBuffer(); sb.write(''' Diff for ${outputs[0].filename} '''); sb.write('''
        identical blocks         corresponding blocks '''); sb.write('''     offset with source information     offset without source information     offset with unneeded source information     offset with unused source information '''); for (int i = 0; i < HUE_COUNT; i++) { sb.write('''   '''); } sb.write(''' mapped source locations '''); /// Marker to alternate output colors. bool alternating = false; List printContexts = []; for (int i = 0; i < 2; i++) { int lineNoWidth; if (outputs[i].codeLines.isNotEmpty) { lineNoWidth = '${outputs[i].codeLines.last.lineNo + 1}'.length; } printContexts.add(new HtmlPrintContext( lineNoWidth: lineNoWidth, getAnnotationData: getAnnotationData, getLineData: getLineData)); } Set allColumns = new Set(); for (DiffBlock block in blocks) { allColumns.addAll(block.columns); } List columns = [column_js0, column_js1, column_dart] .where((c) => allColumns.contains(c)) .toList(); sb.write('''
'''); for (DiffColumn column in columns) { sb.write(''' '''); } sb.write('''
'''); if (column.type == 'js') { sb.write('''[${outputs[column.index].options.join(',')}]'''); } else { sb.write('''Dart code'''); } sb.write('''
'''); for (DiffBlock block in blocks) { String className; switch (block.kind) { case DiffKind.UNMATCHED: className = '${ClassNames.cell}'; break; case DiffKind.MATCHING: className = '${ClassNames.cell} ${ClassNames.corresponding(alternating)}'; alternating = !alternating; break; case DiffKind.IDENTICAL: className = '${ClassNames.cell} ${ClassNames.identical(alternating)}'; alternating = !alternating; break; } sb.write(''); for (DiffColumn column in columns) { sb.write(''''''); } sb.write(''); } sb.write(''''''); for (DiffColumn column in columns) { sb.write(''' '''); } sb.write('''
'''); HtmlPrintContext context = new HtmlPrintContext( lineNoWidth: 4, includeAnnotation: (Annotation annotation) { CodeLineAnnotation data = annotation.data; return data.annotationType == AnnotationType.WITH_SOURCE_INFO || data.annotationType == AnnotationType.ADDITIONAL_SOURCE_INFO; }, getAnnotationData: getAnnotationData, getLineData: getLineData); if (column.type == 'js') { context = printContexts[column.index]; } block.printHtmlOn(column, sb, context); sb.write('''
''');
    if (column.type == 'js') {
      sb.write(outputs[column.index].coverage);
    }
    sb.write('''
Left JavaScript code
Right JavaScript code
Dart code
Inlined Dart code
Source information markers
Source mapped Dart code
'''); File file = new File(out); file.writeAsStringSync(sb.toString()); print('Diff generated in ${file.absolute.uri}'); } class CodeLinesResult { final List codeLines; final Coverage coverage; final Map elementMap; final SourceFileManager sourceFileManager; final CodeSources codeSources; CodeLinesResult(this.codeLines, this.coverage, this.elementMap, this.sourceFileManager, this.codeSources); } class CodeSources { Map codeSourceMap = {}; Map> uriCodeSourceMap = >{}; CodeSources(SourceMapProcessor processor, SourceMaps sourceMaps) { ElementEnvironment elementEnvironment = sourceMaps.compiler.backendClosedWorldForTesting.elementEnvironment; CodeSource computeCodeSource(Entity element) { return codeSourceMap.putIfAbsent(element, () { CodeSource codeSource = codeSourceFromElement(element); if (codeSource.begin != null) { Interval interval = new Interval(codeSource.begin, codeSource.end); Map intervals = uriCodeSourceMap[codeSource.uri]; if (intervals == null) { intervals = {}; uriCodeSourceMap[codeSource.uri] = intervals; } else { for (Interval existingInterval in intervals.keys.toList()) { if (existingInterval.contains(interval.from)) { CodeSource existingCodeSource = intervals[existingInterval]; intervals.remove(existingInterval); if (existingInterval.from < interval.from) { Interval preInterval = new Interval(existingInterval.from, interval.from); intervals[preInterval] = existingCodeSource; } if (interval.to < existingInterval.to) { Interval postInterval = new Interval(interval.to, existingInterval.to); intervals[postInterval] = existingCodeSource; } } } } intervals[interval] = codeSource; } if (element is ClassEntity) { elementEnvironment.forEachConstructor(element, computeCodeSource); elementEnvironment.forEachLocalClassMember( element, computeCodeSource); } return codeSource; }); } for (LibraryEntity library in elementEnvironment.libraries) { elementEnvironment.forEachClass(library, computeCodeSource); elementEnvironment.forEachLibraryMember(library, computeCodeSource); } uriCodeSourceMap.forEach((Uri uri, Map intervals) { List sortedKeys = intervals.keys.toList() ..sort((i1, i2) => i1.from.compareTo(i2.from)); Map sortedintervals = {}; sortedKeys.forEach((Interval interval) { sortedintervals[interval] = intervals[interval]; }); uriCodeSourceMap[uri] = sortedintervals; }); } CodeSource sourceLocationToCodeSource(SourceLocation sourceLocation) { Map intervals = uriCodeSourceMap[sourceLocation.sourceUri]; if (intervals == null) { print('No code source for $sourceLocation(${sourceLocation.offset})'); print(' -- no intervals for ${sourceLocation.sourceUri}'); return null; } for (Interval interval in intervals.keys) { if (interval.contains(sourceLocation.offset)) { return intervals[interval]; } } print('No code source for $sourceLocation(${sourceLocation.offset})'); intervals.forEach((k, v) => print(' $k: ${v.name}')); return null; } } /// Compute [CodeLine]s and [Coverage] for [filename] using the given [options]. Future computeCodeLines( List options, String filename, Uri uri) async { SourceMapProcessor processor = new SourceMapProcessor(uri); SourceMaps sourceMaps = await processor.process(options, perElement: true, forMain: true); CodeSources codeSources = new CodeSources(processor, sourceMaps); SourceMapInfo info = sourceMaps.mainSourceMapInfo; int nextAnnotationId = 0; List codeLines; Coverage coverage = new Coverage(); Map> codeLineAnnotationMap = >{}; /// Create a [CodeLineAnnotation] for [codeOffset]. void addCodeLineAnnotation( {AnnotationType annotationType, int codeOffset, List locations = const [], String stepInfo}) { if (annotationType == AnnotationType.WITHOUT_SOURCE_INFO || annotationType == AnnotationType.UNUSED_SOURCE_INFO) { locations = []; } List codeLocations = locations .where((l) => l.sourceUri != null) .map((l) => new CodeLocation(l.sourceUri, l.sourceName, l.offset)) .toList(); List codeSourceList = locations .where((l) => l.sourceUri != null) .map(codeSources.sourceLocationToCodeSource) .where((c) => c != null) .toList(); CodeLineAnnotation data = new CodeLineAnnotation( annotationId: nextAnnotationId++, annotationType: annotationType, codeLocations: codeLocations, codeSources: codeSourceList, stepInfo: stepInfo); codeLineAnnotationMap .putIfAbsent(codeOffset, () => []) .add(data); } String code = info.code; TraceGraph graph = createTraceGraph(info, coverage); Set mappedNodes = new Set(); /// Add an annotation for [codeOffset] pointing to [locations]. void addSourceLocations( {AnnotationType annotationType, int codeOffset, List locations, String stepInfo}) { locations = locations.where((l) => l != null).toList(); addCodeLineAnnotation( annotationType: annotationType, codeOffset: codeOffset, stepInfo: stepInfo, locations: locations); } /// Add annotations for all mappings created for [node]. bool addSourceLocationsForNode( {AnnotationType annotationType, js.Node node, String stepInfo}) { Map> locations = info.nodeMap[node]; if (locations == null || locations.isEmpty) { return false; } locations.forEach((int offset, List locations) { addSourceLocations( annotationType: annotationType, codeOffset: offset, locations: locations, stepInfo: stepInfo); }); mappedNodes.add(node); return true; } // Add annotations based on trace steps. for (TraceStep step in graph.steps) { String stepInfo = '${step.id}:${step.kind}:${step.offset}'; bool added = addSourceLocationsForNode( annotationType: AnnotationType.WITH_SOURCE_INFO, node: step.node, stepInfo: stepInfo); if (!added) { int offset; if (options.contains(Flags.useNewSourceInfo)) { offset = step.offset.value; } else { offset = info.jsCodePositions[step.node].startPosition; } if (offset != null) { addCodeLineAnnotation( annotationType: AnnotationType.WITHOUT_SOURCE_INFO, codeOffset: offset, stepInfo: stepInfo); } } } // Add additional annotations for mappings created for particular nodes. for (js.Node node in info.nodeMap.nodes) { if (!mappedNodes.contains(node)) { addSourceLocationsForNode( annotationType: AnnotationType.ADDITIONAL_SOURCE_INFO, node: node); } } // Add annotations for unused source information associated with nodes. SourceLocationCollector collector = new SourceLocationCollector(); info.node.accept(collector); collector.sourceLocations .forEach((js.Node node, List locations) { if (!mappedNodes.contains(node)) { int offset = info.jsCodePositions[node].startPosition; addSourceLocations( annotationType: AnnotationType.UNUSED_SOURCE_INFO, codeOffset: offset, locations: locations); } }); // Assign consecutive ids to source mappings. int nextSourceMappedLocationIndex = 0; List annotations = []; for (int codeOffset in codeLineAnnotationMap.keys.toList()..sort()) { bool hasSourceMappedLocation = false; for (CodeLineAnnotation data in codeLineAnnotationMap[codeOffset]) { if (data.annotationType.isSourceMapped) { data.sourceMappingIndex = nextSourceMappedLocationIndex; hasSourceMappedLocation = true; } annotations.add(new Annotation( data.annotationType.index, codeOffset, 'id=${data.annotationId}', data: data)); } if (hasSourceMappedLocation) { nextSourceMappedLocationIndex++; } } // Associate JavaScript offsets with [Element]s. StringSourceFile sourceFile = new StringSourceFile.fromName(filename, code); Map elementMap = {}; sourceMaps.elementSourceMapInfos .forEach((MemberEntity element, SourceMapInfo info) { CodePosition position = info.jsCodePositions[info.node]; elementMap[sourceFile.getLocation(position.startPosition).line - 1] = element; }); codeLines = convertAnnotatedCodeToCodeLines(code, annotations); return new CodeLinesResult(codeLines, coverage, elementMap, sourceMaps.sourceFileManager, codeSources); } /// Visitor that computes a map from [js.Node]s to all attached source /// locations. class SourceLocationCollector extends js.BaseVisitorVoid { Map> sourceLocations = >{}; @override void visitNode(js.Node node) { SourceInformation sourceInformation = node.sourceInformation; if (sourceInformation != null) { sourceLocations[node] = sourceInformation.sourceLocations; } node.visitChildren(this); } } /// Compute a [CodeSource] for source span of [element]. CodeSource codeSourceFromElement(Entity element) { // TODO(johnniwinther): Handle kernel based elements. CodeKind kind; Uri uri; String name; int begin; int end; if (element is LibraryEntity) { kind = CodeKind.LIBRARY; name = element.name; uri = element.canonicalUri; } else if (element is ClassEntity) { kind = CodeKind.CLASS; name = element.name; uri = element.library.canonicalUri; } else if (element is MemberEntity) { kind = CodeKind.MEMBER; uri = element.library.canonicalUri; name = computeElementNameForSourceMaps(element); } return new CodeSource(kind, uri, name, begin, end); } /// Create [LineData] that colors line numbers according to the [CodeSource]s /// origin if available. LineData getLineData(Object lineAnnotation) { if (lineAnnotation != null) { return new LineData( lineClass: ClassNames.line, lineNumberClass: '${ClassNames.lineNumber} ' '${ClassNames.colored(lineAnnotation.hashCode % 4)}'); } return new LineData( lineClass: ClassNames.line, lineNumberClass: ClassNames.lineNumber); } AnnotationData getAnnotationData(Iterable annotations, {bool forSpan}) { for (Annotation annotation in annotations) { CodeLineAnnotation data = annotation.data; if (data.annotationType.isSourceMapped) { if (forSpan) { int index = data.sourceMappingIndex; return new AnnotationData(tag: 'span', properties: { 'class': '${ClassNames.sourceMapping} ' '${ClassNames.sourceMappingIndex(index % HUE_COUNT)}', 'title': 'index=$index', }); } else { return new AnnotationData(tag: 'span', properties: { 'title': annotation.title, 'class': '${ClassNames.marker} ' '${data.annotationType.className}' }); } } } if (forSpan) return null; for (Annotation annotation in annotations) { CodeLineAnnotation data = annotation.data; if (data.annotationType == AnnotationType.UNUSED_SOURCE_INFO) { return new AnnotationData(tag: 'span', properties: { 'title': annotation.title, 'class': '${ClassNames.marker} ' '${data.annotationType.className}' }); } } for (Annotation annotation in annotations) { CodeLineAnnotation data = annotation.data; if (data.annotationType == AnnotationType.WITHOUT_SOURCE_INFO) { return new AnnotationData(tag: 'span', properties: { 'title': annotation.title, 'class': '${ClassNames.marker} ' '${data.annotationType.className}' }); } } return null; }