| // 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. |
| |
| // @dart = 2.7 |
| |
| library sourcemap.helper; |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| import 'package:compiler/compiler_new.dart'; |
| import 'package:compiler/src/apiimpl.dart' as api; |
| import 'package:compiler/src/commandline_options.dart'; |
| import 'package:compiler/src/elements/entities.dart'; |
| import 'package:compiler/src/io/code_output.dart'; |
| import 'package:compiler/src/io/source_file.dart'; |
| import 'package:compiler/src/io/source_information.dart'; |
| import 'package:compiler/src/io/position_information.dart'; |
| import 'package:compiler/src/js/js.dart' as js; |
| import 'package:compiler/src/js/js_debug.dart'; |
| import 'package:compiler/src/js/js_source_mapping.dart'; |
| import 'package:compiler/src/js_model/js_strategy.dart'; |
| import 'package:compiler/src/source_file_provider.dart'; |
| import '../../helpers/memory_compiler.dart'; |
| import '../../helpers/output_collector.dart'; |
| |
| class SourceFileSink implements OutputSink { |
| final String filename; |
| StringBuffer sb = new StringBuffer(); |
| SourceFile sourceFile; |
| |
| SourceFileSink(this.filename); |
| |
| @override |
| void add(String event) { |
| sb.write(event); |
| } |
| |
| @override |
| void close() { |
| sourceFile = new StringSourceFile.fromName(filename, sb.toString()); |
| } |
| } |
| |
| class OutputProvider implements CompilerOutput { |
| Map<Uri, SourceFileSink> outputMap = <Uri, SourceFileSink>{}; |
| |
| SourceFile getSourceFile(Uri uri) { |
| SourceFileSink sink = outputMap[uri]; |
| if (sink != null) { |
| return sink.sourceFile; |
| } |
| return null; |
| } |
| |
| SourceFileSink createSourceFileSink( |
| String name, String extension, OutputType type) { |
| String filename = '$name.$extension'; |
| SourceFileSink sink = new SourceFileSink(filename); |
| Uri uri = Uri.parse(filename); |
| outputMap[uri] = sink; |
| return sink; |
| } |
| |
| @override |
| OutputSink createOutputSink(String name, String extension, OutputType type) { |
| return createSourceFileSink(name, extension, type); |
| } |
| |
| @override |
| BinaryOutputSink createBinarySink(Uri uri) => |
| throw new UnsupportedError("OutputProvider.createBinarySink"); |
| } |
| |
| class CloningOutputProvider extends OutputProvider { |
| RandomAccessFileOutputProvider outputProvider; |
| |
| CloningOutputProvider(Uri jsUri, Uri jsMapUri) |
| : outputProvider = new RandomAccessFileOutputProvider(jsUri, jsMapUri); |
| |
| @override |
| OutputSink createOutputSink(String name, String extension, OutputType type) { |
| OutputSink output = outputProvider.createOutputSink(name, extension, type); |
| return new CloningOutputSink( |
| [output, createSourceFileSink(name, extension, type)]); |
| } |
| |
| @override |
| BinaryOutputSink createBinarySink(Uri uri) => |
| throw new UnsupportedError("CloningOutputProvider.createBinarySink"); |
| } |
| |
| abstract class SourceFileManager { |
| SourceFile getSourceFile(var uri); |
| } |
| |
| class ProviderSourceFileManager implements SourceFileManager { |
| final SourceFileProvider sourceFileProvider; |
| final OutputProvider outputProvider; |
| |
| ProviderSourceFileManager(this.sourceFileProvider, this.outputProvider); |
| |
| @override |
| SourceFile getSourceFile(uri) { |
| SourceFile sourceFile = sourceFileProvider.getUtf8SourceFile(uri); |
| sourceFile ??= sourceFileProvider.autoReadFromFile(uri); |
| if (sourceFile == null) { |
| sourceFile = outputProvider.getSourceFile(uri); |
| } |
| return sourceFile; |
| } |
| } |
| |
| class RecordingPrintingContext extends LenientPrintingContext { |
| CodePositionListener listener; |
| Map<js.Node, CodePosition> codePositions = <js.Node, CodePosition>{}; |
| |
| RecordingPrintingContext(this.listener); |
| |
| @override |
| void exitNode( |
| js.Node node, int startPosition, int endPosition, int closingPosition) { |
| codePositions[node] = |
| new CodePosition(startPosition, endPosition, closingPosition); |
| listener.onPositions(node, startPosition, endPosition, closingPosition); |
| } |
| } |
| |
| /// A [SourceMapper] that records the source locations on each node. |
| class RecordingSourceMapperProvider implements SourceMapperProvider { |
| final SourceMapperProvider sourceMapperProvider; |
| final _LocationRecorder nodeToSourceLocationsMap; |
| |
| RecordingSourceMapperProvider( |
| this.sourceMapperProvider, this.nodeToSourceLocationsMap); |
| |
| @override |
| SourceMapper createSourceMapper(String name) { |
| return new RecordingSourceMapper( |
| sourceMapperProvider.createSourceMapper(name), |
| nodeToSourceLocationsMap); |
| } |
| } |
| |
| /// A [SourceMapper] that records the source locations on each node. |
| class RecordingSourceMapper implements SourceMapper { |
| final SourceMapper sourceMapper; |
| final _LocationRecorder nodeToSourceLocationsMap; |
| |
| RecordingSourceMapper(this.sourceMapper, this.nodeToSourceLocationsMap); |
| |
| @override |
| void register(js.Node node, int codeOffset, SourceLocation sourceLocation) { |
| nodeToSourceLocationsMap.register(node, codeOffset, sourceLocation); |
| sourceMapper.register(node, codeOffset, sourceLocation); |
| } |
| |
| @override |
| void registerPush( |
| int codeOffset, SourceLocation sourceLocation, String inlinedMethodName) { |
| sourceMapper.registerPush(codeOffset, sourceLocation, inlinedMethodName); |
| } |
| |
| @override |
| void registerPop(int codeOffset, {bool isEmpty: false}) { |
| sourceMapper.registerPop(codeOffset, isEmpty: isEmpty); |
| } |
| } |
| |
| /// A wrapper of [SourceInformationProcessor] that records source locations and |
| /// code positions. |
| class RecordingSourceInformationProcessor extends SourceInformationProcessor { |
| final RecordingSourceInformationStrategy wrapper; |
| final SourceInformationProcessor processor; |
| final CodePositionRecorder codePositions; |
| final LocationMap nodeToSourceLocationsMap; |
| |
| RecordingSourceInformationProcessor(this.wrapper, this.processor, |
| this.codePositions, this.nodeToSourceLocationsMap); |
| |
| @override |
| void onPositions( |
| js.Node node, int startPosition, int endPosition, int closingPosition) { |
| codePositions.registerPositions( |
| node, startPosition, endPosition, closingPosition); |
| processor.onPositions(node, startPosition, endPosition, closingPosition); |
| } |
| |
| @override |
| void process(js.Node node, BufferedCodeOutput code) { |
| processor.process(node, code); |
| wrapper.registerProcess( |
| node, code, codePositions, nodeToSourceLocationsMap); |
| } |
| } |
| |
| /// Information recording for a use of [SourceInformationProcessor]. |
| class RecordedSourceInformationProcess { |
| final js.Node root; |
| final String code; |
| final CodePositionRecorder codePositions; |
| final LocationMap nodeToSourceLocationsMap; |
| |
| RecordedSourceInformationProcess( |
| this.root, this.code, this.codePositions, this.nodeToSourceLocationsMap); |
| } |
| |
| /// A wrapper of [JavaScriptSourceInformationStrategy] that records |
| /// [RecordedSourceInformationProcess]. |
| class RecordingSourceInformationStrategy |
| extends JavaScriptSourceInformationStrategy { |
| final JavaScriptSourceInformationStrategy strategy; |
| final Map<RecordedSourceInformationProcess, js.Node> processMap = |
| <RecordedSourceInformationProcess, js.Node>{}; |
| final Map<js.Node, RecordedSourceInformationProcess> nodeMap = |
| <js.Node, RecordedSourceInformationProcess>{}; |
| |
| RecordingSourceInformationStrategy(this.strategy); |
| |
| @override |
| SourceInformationBuilder createBuilderForContext(MemberEntity member) { |
| return strategy.createBuilderForContext(member); |
| } |
| |
| @override |
| SourceInformationProcessor createProcessor( |
| SourceMapperProvider provider, SourceInformationReader reader) { |
| LocationMap nodeToSourceLocationsMap = new _LocationRecorder(); |
| CodePositionRecorder codePositions = new CodePositionRecorder(); |
| return new RecordingSourceInformationProcessor( |
| this, |
| strategy.createProcessor( |
| new RecordingSourceMapperProvider( |
| provider, nodeToSourceLocationsMap), |
| reader), |
| codePositions, |
| nodeToSourceLocationsMap); |
| } |
| |
| void registerProcess( |
| js.Node root, |
| BufferedCodeOutput code, |
| CodePositionRecorder codePositions, |
| LocationMap nodeToSourceLocationsMap) { |
| RecordedSourceInformationProcess subProcess = |
| new RecordedSourceInformationProcess( |
| root, code.getText(), codePositions, nodeToSourceLocationsMap); |
| processMap[subProcess] = root; |
| } |
| |
| RecordedSourceInformationProcess subProcessForNode(js.Node node) { |
| return nodeMap.putIfAbsent(node, () { |
| for (RecordedSourceInformationProcess subProcess in processMap.keys) { |
| js.Node root = processMap[subProcess]; |
| FindVisitor visitor = new FindVisitor(node); |
| root.accept(visitor); |
| if (visitor.found) { |
| return new RecordedSourceInformationProcess( |
| node, |
| subProcess.code, |
| subProcess.codePositions, |
| new _FilteredLocationMap( |
| visitor.nodes, subProcess.nodeToSourceLocationsMap)); |
| } |
| return null; |
| } |
| return null; |
| }); |
| } |
| } |
| |
| /// Visitor that collects all nodes that are within a function. Used by the |
| /// [RecordingSourceInformationStrategy] to filter what is recorded in a |
| /// [RecordedSourceInformationProcess]. |
| class FindVisitor extends js.BaseVisitor { |
| final js.Node soughtNode; |
| bool found = false; |
| bool add = false; |
| final Set<js.Node> nodes = new Set<js.Node>(); |
| |
| FindVisitor(this.soughtNode); |
| |
| @override |
| visitNode(js.Node node) { |
| if (node == soughtNode) { |
| found = true; |
| add = true; |
| } |
| if (add) { |
| nodes.add(node); |
| } |
| node.visitChildren(this); |
| if (node == soughtNode) { |
| add = false; |
| } |
| } |
| } |
| |
| /// Processor that computes [SourceMapInfo] for the JavaScript compiled for a |
| /// given Dart file. |
| class SourceMapProcessor { |
| /// If `true` the output from the compilation is written to files. |
| final bool outputToFile; |
| |
| /// The [Uri] of the Dart entrypoint. |
| Uri inputUri; |
| |
| /// The name of the JavaScript output file. |
| String jsPath; |
| |
| /// The [Uri] of the JavaScript output file. |
| Uri targetUri; |
| |
| /// The [Uri] of the JavaScript source map file. |
| Uri sourceMapFileUri; |
| |
| /// The [SourceFileManager] created for the processing. |
| SourceFileManager sourceFileManager; |
| |
| /// Creates a processor for the Dart file [uri]. |
| SourceMapProcessor(Uri uri, {this.outputToFile: false}) { |
| inputUri = Uri.base.resolveUri(uri); |
| jsPath = 'out.js'; |
| targetUri = Uri.base.resolve(jsPath); |
| sourceMapFileUri = Uri.base.resolve('${jsPath}.map'); |
| } |
| |
| /// Computes the [SourceMapInfo] for the compiled elements. |
| Future<SourceMaps> process(List<String> options, |
| {bool verbose: true, bool perElement: true, bool forMain: false}) async { |
| OutputProvider outputProvider = outputToFile |
| ? new CloningOutputProvider(targetUri, sourceMapFileUri) |
| : new OutputProvider(); |
| if (options.contains(Flags.useNewSourceInfo)) { |
| if (verbose) print('Using the new source information system.'); |
| } |
| if (options.contains(Flags.disableInlining)) { |
| if (verbose) print('Inlining disabled'); |
| } |
| CompilationResult result = await runCompiler( |
| entryPoint: inputUri, |
| outputProvider: outputProvider, |
| // TODO(johnniwinther): Use [verbose] to avoid showing diagnostics. |
| options: ['--out=$targetUri', '--source-map=$sourceMapFileUri'] |
| ..addAll(options), |
| beforeRun: (compiler) { |
| JsBackendStrategy backendStrategy = compiler.backendStrategy; |
| dynamic handler = compiler.handler; |
| SourceFileProvider sourceFileProvider = handler.provider; |
| sourceFileManager = |
| new ProviderSourceFileManager(sourceFileProvider, outputProvider); |
| RecordingSourceInformationStrategy strategy = |
| new RecordingSourceInformationStrategy( |
| backendStrategy.sourceInformationStrategy); |
| backendStrategy.sourceInformationStrategy = strategy; |
| }); |
| if (!result.isSuccess) { |
| throw "Compilation failed."; |
| } |
| |
| api.CompilerImpl compiler = result.compiler; |
| JsBackendStrategy backendStrategy = compiler.backendStrategy; |
| RecordingSourceInformationStrategy strategy = |
| backendStrategy.sourceInformationStrategy; |
| SourceMapInfo mainSourceMapInfo; |
| Map<MemberEntity, SourceMapInfo> elementSourceMapInfos = |
| <MemberEntity, SourceMapInfo>{}; |
| if (perElement) { |
| backendStrategy.generatedCode.forEach((_element, js.Expression node) { |
| MemberEntity element = _element; |
| RecordedSourceInformationProcess subProcess = |
| strategy.subProcessForNode(node); |
| if (subProcess == null) { |
| // TODO(johnniwinther): Find out when this is happening and if it |
| // is benign. (Known to happen for `bool#fromString`) |
| print('No subProcess found for $element'); |
| return; |
| } |
| LocationMap nodeMap = subProcess.nodeToSourceLocationsMap; |
| String code = subProcess.code; |
| CodePositionRecorder codePositions = subProcess.codePositions; |
| CodePointComputer visitor = |
| new CodePointComputer(sourceFileManager, code, nodeMap); |
| new JavaScriptTracer( |
| codePositions, const SourceInformationReader(), [visitor]) |
| .apply(node); |
| List<CodePoint> codePoints = visitor.codePoints; |
| elementSourceMapInfos[element] = new SourceMapInfo( |
| element, code, node, codePoints, codePositions, nodeMap); |
| }); |
| } |
| if (forMain) { |
| // TODO(johnniwinther): Supported multiple output units. |
| RecordedSourceInformationProcess process = strategy.processMap.keys.first; |
| js.Node node = strategy.processMap[process]; |
| String code; |
| LocationMap nodeMap; |
| CodePositionRecorder codePositions; |
| nodeMap = process.nodeToSourceLocationsMap; |
| code = process.code; |
| codePositions = process.codePositions; |
| CodePointComputer visitor = |
| new CodePointComputer(sourceFileManager, code, nodeMap); |
| new JavaScriptTracer( |
| codePositions, const SourceInformationReader(), [visitor]) |
| .apply(node); |
| List<CodePoint> codePoints = visitor.codePoints; |
| mainSourceMapInfo = new SourceMapInfo( |
| null, code, node, codePoints, codePositions, nodeMap); |
| } |
| |
| return new SourceMaps( |
| compiler, sourceFileManager, mainSourceMapInfo, elementSourceMapInfos); |
| } |
| } |
| |
| class SourceMaps { |
| final api.CompilerImpl compiler; |
| final SourceFileManager sourceFileManager; |
| // TODO(johnniwinther): Supported multiple output units. |
| final SourceMapInfo mainSourceMapInfo; |
| final Map<MemberEntity, SourceMapInfo> elementSourceMapInfos; |
| |
| SourceMaps(this.compiler, this.sourceFileManager, this.mainSourceMapInfo, |
| this.elementSourceMapInfos); |
| } |
| |
| /// Source mapping information for the JavaScript code of an [Element]. |
| class SourceMapInfo { |
| final String name; |
| final MemberEntity element; |
| final String code; |
| final js.Node node; |
| final List<CodePoint> codePoints; |
| final CodePositionMap jsCodePositions; |
| final LocationMap nodeMap; |
| |
| SourceMapInfo(MemberEntity element, this.code, this.node, this.codePoints, |
| this.jsCodePositions, this.nodeMap) |
| : this.name = |
| element != null ? computeElementNameForSourceMaps(element) : '', |
| this.element = element; |
| |
| @override |
| String toString() { |
| return '$name:$element'; |
| } |
| } |
| |
| /// Collection of JavaScript nodes with their source mapped target offsets |
| /// and source locations. |
| abstract class LocationMap { |
| Iterable<js.Node> get nodes; |
| |
| Map<int, List<SourceLocation>> operator [](js.Node node); |
| |
| factory LocationMap.recorder() = _LocationRecorder; |
| |
| factory LocationMap.filter(Set<js.Node> nodes, LocationMap map) = |
| _FilteredLocationMap; |
| } |
| |
| class _LocationRecorder implements SourceMapper, LocationMap { |
| final Map<js.Node, Map<int, List<SourceLocation>>> _nodeMap = {}; |
| |
| @override |
| void register(js.Node node, int codeOffset, SourceLocation sourceLocation) { |
| _nodeMap |
| .putIfAbsent(node, () => {}) |
| .putIfAbsent(codeOffset, () => []) |
| .add(sourceLocation); |
| } |
| |
| @override |
| void registerPush(int codeOffset, SourceLocation sourceLocation, |
| String inlinedMethodName) {} |
| |
| @override |
| void registerPop(int codeOffset, {bool isEmpty: false}) {} |
| |
| @override |
| Iterable<js.Node> get nodes => _nodeMap.keys; |
| |
| @override |
| Map<int, List<SourceLocation>> operator [](js.Node node) { |
| return _nodeMap[node]; |
| } |
| } |
| |
| class _FilteredLocationMap implements LocationMap { |
| final Set<js.Node> _nodes; |
| final LocationMap map; |
| |
| _FilteredLocationMap(this._nodes, this.map); |
| |
| @override |
| Iterable<js.Node> get nodes => map.nodes.where((n) => _nodes.contains(n)); |
| |
| @override |
| Map<int, List<SourceLocation>> operator [](js.Node node) { |
| return map[node]; |
| } |
| } |
| |
| /// Visitor that computes the [CodePoint]s for source mapping locations. |
| class CodePointComputer extends TraceListener { |
| final SourceFileManager sourceFileManager; |
| final String code; |
| final LocationMap nodeMap; |
| List<CodePoint> codePoints = []; |
| |
| CodePointComputer(this.sourceFileManager, this.code, this.nodeMap); |
| |
| String nodeToString(js.Node node) { |
| js.JavaScriptPrintingOptions options = new js.JavaScriptPrintingOptions( |
| shouldCompressOutput: true, |
| preferSemicolonToNewlineInMinifiedOutput: true); |
| LenientPrintingContext printingContext = new LenientPrintingContext(); |
| new js.Printer(options, printingContext).visit(node); |
| return printingContext.buffer.toString(); |
| } |
| |
| String positionToString(int position) { |
| String line = code.substring(position); |
| int nl = line.indexOf('\n'); |
| if (nl != -1) { |
| line = line.substring(0, nl); |
| } |
| return line; |
| } |
| |
| /// Called when [node] defines a step of the given [kind] at the given |
| /// [offset] when the generated JavaScript code. |
| @override |
| void onStep(js.Node node, Offset offset, StepKind kind) { |
| if (kind == StepKind.ACCESS) return; |
| register(kind, node); |
| } |
| |
| void register(StepKind kind, js.Node node, {bool expectInfo: true}) { |
| String dartCodeFromSourceLocation(SourceLocation sourceLocation) { |
| SourceFile sourceFile = |
| sourceFileManager.getSourceFile(sourceLocation.sourceUri); |
| if (sourceFile == null) { |
| return sourceLocation.shortText; |
| } |
| return sourceFile.kernelSource |
| .getTextLine(sourceLocation.line) |
| .substring(sourceLocation.column - 1) |
| .trim(); |
| } |
| |
| void addLocation( |
| SourceLocation sourceLocation, String jsCode, int targetOffset) { |
| if (sourceLocation == null) { |
| if (expectInfo) { |
| SourceInformation sourceInformation = node.sourceInformation; |
| SourceLocation sourceLocation; |
| String dartCode; |
| if (sourceInformation != null) { |
| sourceLocation = sourceInformation.sourceLocations.first; |
| dartCode = dartCodeFromSourceLocation(sourceLocation); |
| } |
| codePoints.add(new CodePoint( |
| kind, jsCode, targetOffset, sourceLocation, dartCode, |
| isMissing: true)); |
| } |
| } else { |
| codePoints.add(new CodePoint(kind, jsCode, targetOffset, sourceLocation, |
| dartCodeFromSourceLocation(sourceLocation))); |
| } |
| } |
| |
| Map<int, List<SourceLocation>> locationMap = nodeMap[node]; |
| if (locationMap == null) { |
| addLocation(null, nodeToString(node), null); |
| } else { |
| locationMap.forEach((int targetOffset, List<SourceLocation> locations) { |
| String jsCode = nodeToString(node); |
| for (SourceLocation location in locations) { |
| addLocation(location, jsCode, targetOffset); |
| } |
| }); |
| } |
| } |
| } |
| |
| /// A JavaScript code point and its mapped dart source location. |
| class CodePoint { |
| final StepKind kind; |
| final String jsCode; |
| final int targetOffset; |
| final SourceLocation sourceLocation; |
| final String dartCode; |
| final bool isMissing; |
| |
| CodePoint(this.kind, this.jsCode, this.targetOffset, this.sourceLocation, |
| this.dartCode, |
| {this.isMissing: false}); |
| |
| @override |
| String toString() { |
| return 'CodePoint[kind=$kind,js=$jsCode,dart=$dartCode,' |
| 'location=$sourceLocation]'; |
| } |
| } |
| |
| class IOSourceFileManager implements SourceFileManager { |
| final Uri base; |
| |
| Map<Uri, SourceFile> sourceFiles = <Uri, SourceFile>{}; |
| |
| IOSourceFileManager(this.base); |
| |
| @override |
| SourceFile getSourceFile(var uri) { |
| Uri absoluteUri; |
| if (uri is Uri) { |
| absoluteUri = base.resolveUri(uri); |
| } else { |
| absoluteUri = base.resolve(uri); |
| } |
| return sourceFiles.putIfAbsent(absoluteUri, () { |
| String text = new File.fromUri(absoluteUri).readAsStringSync(); |
| return new StringSourceFile.fromUri(absoluteUri, text); |
| }); |
| } |
| } |