// 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:_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 { debugMode = true; String out = 'out.js.diff_view.html'; late 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 = 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 = 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 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 Map? 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 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) { 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); StringBuffer sb = 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 = 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 = HtmlPrintContext( lineNoWidth: 4, includeAnnotation: (Annotation annotation) { CodeLineAnnotation data = annotation.data; return data.annotationType == AnnotationType.withSourceInfo || data.annotationType == AnnotationType.additionalSourceInfo; }, 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 = 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 = 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 = Interval( existingInterval.from, interval.from, ); intervals[preInterval] = existingCodeSource; } if (interval.to < existingInterval.to) { Interval postInterval = 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 = SourceMapProcessor(uri); SourceMaps sourceMaps = await processor.process( options, perElement: true, forMain: true, ); CodeSources codeSources = CodeSources(processor, sourceMaps); SourceMapInfo info = sourceMaps.mainSourceMapInfo!; int nextAnnotationId = 0; List codeLines; Coverage coverage = Coverage(); Map> codeLineAnnotationMap = >{}; /// Create a [CodeLineAnnotation] for [codeOffset]. void addCodeLineAnnotation({ required AnnotationType annotationType, required int codeOffset, List locations = const [], String? stepInfo, }) { if (annotationType == AnnotationType.withoutSourceInfo || annotationType == AnnotationType.unusedSourceInfo) { locations = []; } List codeLocations = locations .where((l) => l.sourceUri != null) .map((l) => CodeLocation(l.sourceUri!, l.sourceName!, l.offset)) .toList(); List codeSourceList = locations .where((l) => l.sourceUri != null) .map(codeSources.sourceLocationToCodeSource) .whereType() .toList(); CodeLineAnnotation data = 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 = Set(); /// Add an annotation for [codeOffset] pointing to [locations]. void addSourceLocations({ required AnnotationType annotationType, required int codeOffset, required List locations, String? stepInfo, }) { final nonNullLocations = locations.whereType().toList(); addCodeLineAnnotation( annotationType: annotationType, codeOffset: codeOffset, stepInfo: stepInfo, locations: nonNullLocations, ); } /// Add annotations for all mappings created for [node]. bool addSourceLocationsForNode({ required AnnotationType annotationType, required 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.withSourceInfo, 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.withoutSourceInfo, 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.additionalSourceInfo, node: node, ); } } // Add annotations for unused source information associated with nodes. SourceLocationCollector collector = 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.unusedSourceInfo, 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 = 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 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) { final sourceInformation = node.sourceInformation as 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. late CodeKind kind; late Uri uri; late String name; 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 CodeSource(kind, uri, name, null, null); } /// Create [LineData] that colors line numbers according to the [CodeSource]s /// origin if available. LineData getLineData(Object? lineAnnotation) { if (lineAnnotation != null) { return LineData( lineClass: ClassNames.line, lineNumberClass: '${ClassNames.lineNumber} ' '${ClassNames.colored(lineAnnotation.hashCode % 4)}', ); } return LineData( lineClass: ClassNames.line, lineNumberClass: ClassNames.lineNumber, ); } AnnotationData? getAnnotationData( Iterable annotations, { required bool forSpan, }) { for (Annotation annotation in annotations) { CodeLineAnnotation data = annotation.data; if (data.annotationType.isSourceMapped) { if (forSpan) { int index = data.sourceMappingIndex!; return AnnotationData( tag: 'span', properties: { 'class': '${ClassNames.sourceMapping} ' '${ClassNames.sourceMappingIndex(index % HUE_COUNT)}', 'title': 'index=$index', }, ); } else { return 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.unusedSourceInfo) { return 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.withoutSourceInfo) { return AnnotationData( tag: 'span', properties: { 'title': annotation.title, 'class': '${ClassNames.marker} ' '${data.annotationType.className}', }, ); } } return null; }