blob: c5e257bf9640718f2b02c33e7913f71df32270f5 [file] [log] [blame]
// 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.helper;
import 'dart:async';
import 'dart:io';
import 'package:compiler/compiler_api.dart' as api;
import 'package:compiler/src/commandline_options.dart';
import 'package:compiler/src/compiler.dart' show Compiler;
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/element_map.dart';
import 'package:compiler/src/js_model/js_strategy.dart';
import 'package:compiler/src/source_file_provider.dart';
import 'package:compiler/src/util/memory_compiler.dart';
class SourceFileSink implements api.OutputSink {
final String filename;
StringBuffer sb = StringBuffer();
late final SourceFile sourceFile;
SourceFileSink(this.filename);
@override
void add(String event) {
sb.write(event);
}
@override
void close() {
sourceFile = StringSourceFile.fromName(filename, sb.toString());
}
}
class OutputProvider implements api.CompilerOutput {
Map<Uri, SourceFileSink> outputMap = <Uri, SourceFileSink>{};
SourceFile? getSourceFile(Uri uri) {
SourceFileSink? sink = outputMap[uri];
return sink?.sourceFile;
}
SourceFileSink createSourceFileSink(
String name,
String extension,
api.OutputType type,
) {
String filename = '$name.$extension';
SourceFileSink sink = SourceFileSink(filename);
Uri uri = Uri.parse(filename);
outputMap[uri] = sink;
return sink;
}
@override
api.OutputSink createOutputSink(
String name,
String extension,
api.OutputType type,
) {
return createSourceFileSink(name, extension, type);
}
@override
api.BinaryOutputSink createBinarySink(Uri uri) =>
throw UnsupportedError("OutputProvider.createBinarySink");
}
class CloningOutputProvider extends OutputProvider {
RandomAccessFileOutputProvider outputProvider;
CloningOutputProvider(Uri jsUri, Uri jsMapUri)
: outputProvider = RandomAccessFileOutputProvider(
jsUri,
jsMapUri,
onInfo: _ignore,
onFailure: _fail,
);
static void _ignore(String message) {}
static Never _fail(String message) => throw StateError('unreachable');
@override
api.OutputSink createOutputSink(
String name,
String extension,
api.OutputType type,
) {
api.OutputSink output = outputProvider.createOutputSink(
name,
extension,
type,
);
return CloningOutputSink([
output,
createSourceFileSink(name, extension, type),
]);
}
@override
api.BinaryOutputSink createBinarySink(Uri uri) =>
throw UnsupportedError("CloningOutputProvider.createBinarySink");
}
abstract class SourceFileManager {
SourceFile? getSourceFile(Object? uri);
}
class ProviderSourceFileManager implements SourceFileManager {
final SourceFileProvider sourceFileProvider;
final OutputProvider outputProvider;
ProviderSourceFileManager(this.sourceFileProvider, this.outputProvider);
@override
SourceFile? getSourceFile(covariant Uri? uri) {
if (uri == null) return null;
return (sourceFileProvider.readUtf8FromFileSyncForTesting(uri) ??
outputProvider.getSourceFile(uri))
as 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] = 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 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 onStartPosition(js.Node node, int startPosition) {
processor.onStartPosition(node, startPosition);
}
@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 = {};
RecordingSourceInformationStrategy(this.strategy);
@override
void onElementMapAvailable(JsToElementMap elementMap) {
strategy.onElementMapAvailable(elementMap);
}
@override
SourceInformationBuilder createBuilderForContext(MemberEntity member) {
return strategy.createBuilderForContext(member);
}
@override
SourceInformationProcessor createProcessor(
SourceMapperProvider provider,
SourceInformationReader reader,
) {
final nodeToSourceLocationsMap = _LocationRecorder();
final codePositions = CodePositionRecorder();
return RecordingSourceInformationProcessor(
this,
strategy.createProcessor(
RecordingSourceMapperProvider(provider, nodeToSourceLocationsMap),
reader,
),
codePositions,
nodeToSourceLocationsMap,
);
}
void registerProcess(
js.Node root,
BufferedCodeOutput code,
CodePositionRecorder codePositions,
LocationMap nodeToSourceLocationsMap,
) {
RecordedSourceInformationProcess subProcess =
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 = FindVisitor(node);
root.accept(visitor);
if (visitor.found) {
return RecordedSourceInformationProcess(
node,
subProcess.code,
subProcess.codePositions,
_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.BaseVisitorVoid {
final js.Node soughtNode;
bool found = false;
bool add = false;
final Set<js.Node> nodes = Set<js.Node>();
FindVisitor(this.soughtNode);
@override
void visitNode(js.Node node) {
if (node == soughtNode) {
found = true;
add = true;
}
if (add) {
nodes.add(node);
}
node.visitChildren(this);
if (node == soughtNode) {
add = false;
}
}
}
class HelperOnlinePositionSourceInformationStrategy
implements JavaScriptSourceInformationStrategy {
final List<TraceListener> listeners;
HelperOnlinePositionSourceInformationStrategy(this.listeners);
@override
SourceInformationProcessor createProcessor(
SourceMapperProvider provider,
SourceInformationReader reader,
) {
return OnlineSourceInformationProcessor(provider, reader, listeners);
}
@override
void onComplete() {}
@override
SourceInformation buildSourceMappedMarker() {
return const SourceMappedMarker();
}
@override
SourceInformationBuilder createBuilderForContext(
covariant MemberEntity member,
) {
throw UnimplementedError();
}
@override
void onElementMapAvailable(JsToElementMap elementMap) {}
}
/// 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.
late final 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('out.js'),
sourceMapFileUri = Uri.base.resolve('out.js.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
? CloningOutputProvider(targetUri, sourceMapFileUri)
: OutputProvider();
if (options.contains(Flags.useNewSourceInfo)) {
if (verbose) print('Using the 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 = ProviderSourceFileManager(
sourceFileProvider,
outputProvider,
);
RecordingSourceInformationStrategy strategy =
RecordingSourceInformationStrategy(
backendStrategy.sourceInformationStrategy
as JavaScriptSourceInformationStrategy,
);
backendStrategy.sourceInformationStrategy = strategy;
},
);
if (!result.isSuccess) {
throw "Compilation failed.";
}
var compiler = result.compiler!;
JsBackendStrategy backendStrategy = compiler.backendStrategy;
final strategy =
backendStrategy.sourceInformationStrategy
as RecordingSourceInformationStrategy;
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 = CodePointComputer(
sourceFileManager,
code,
nodeMap,
);
final outBuffer = NoopCodeOutput();
SourceInformationProcessor sourceInformationProcessor =
HelperOnlinePositionSourceInformationStrategy([
visitor,
]).createProcessor(
SourceMapperProviderImpl(outBuffer),
const SourceInformationReader(),
);
js.Dart2JSJavaScriptPrintingContext context =
js.Dart2JSJavaScriptPrintingContext(
null,
outBuffer,
sourceInformationProcessor,
const js.JavaScriptAnnotationMonitor(),
);
js.Printer printer = js.Printer(
const js.JavaScriptPrintingOptions(),
context,
);
printer.visit(node);
List<CodePoint> codePoints = visitor.codePoints;
elementSourceMapInfos[element] = 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 = CodePointComputer(
sourceFileManager,
code,
nodeMap,
);
final outBuffer = NoopCodeOutput();
SourceInformationProcessor sourceInformationProcessor =
HelperOnlinePositionSourceInformationStrategy([
visitor,
]).createProcessor(
SourceMapperProviderImpl(outBuffer),
const SourceInformationReader(),
);
js.Dart2JSJavaScriptPrintingContext context =
js.Dart2JSJavaScriptPrintingContext(
null,
outBuffer,
sourceInformationProcessor,
const js.JavaScriptAnnotationMonitor(),
);
js.Printer printer = js.Printer(
const js.JavaScriptPrintingOptions(),
context,
);
printer.visit(node);
List<CodePoint> codePoints = visitor.codePoints;
mainSourceMapInfo = SourceMapInfo(
null,
code,
node,
codePoints,
codePositions,
nodeMap,
);
}
return SourceMaps(
compiler,
sourceFileManager,
mainSourceMapInfo,
elementSourceMapInfos,
);
}
}
class SourceMaps {
final Compiler 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(
this.element,
this.code,
this.node,
this.codePoints,
this.jsCodePositions,
this.nodeMap,
) : this.name = element != null
? computeElementNameForSourceMaps(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 = js.JavaScriptPrintingOptions(
shouldCompressOutput: true,
preferSemicolonToNewlineInMinifiedOutput: true,
);
LenientPrintingContext printingContext = LenientPrintingContext();
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) {
final sourceInformation =
node.sourceInformation as 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(Object? uri) {
Uri absoluteUri;
if (uri is Uri) {
absoluteUri = base.resolveUri(uri);
} else {
absoluteUri = base.resolve(uri as String);
}
return sourceFiles.putIfAbsent(absoluteUri, () {
String text = File.fromUri(absoluteUri).readAsStringSync();
return StringSourceFile.fromUri(absoluteUri, text);
});
}
}