blob: d7aa763be8dc34f44ac9c2243d9688a64552e893 [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 'package:compiler/src/apiimpl.dart' as api;
import 'package:compiler/src/dart2jslib.dart' show NullSink;
import "package:compiler/src/elements/elements.dart";
import 'package:compiler/src/filenames.dart';
import 'package:compiler/src/io/source_file.dart';
import 'package:compiler/src/io/source_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_backend/js_backend.dart';
import 'package:compiler/src/source_file_provider.dart';
import '../memory_compiler.dart';
import '../output_collector.dart';
class OutputProvider {
BufferedEventSink jsMapOutput;
EventSink<String> call(String name, String extension) {
if (extension == 'js.map') {
return jsMapOutput = new BufferedEventSink();
}
return new NullSink('$name.$extension');
}
}
class CloningOutputProvider extends OutputProvider {
RandomAccessFileOutputProvider outputProvider;
CloningOutputProvider(Uri jsUri, Uri jsMapUri)
: outputProvider = new RandomAccessFileOutputProvider(jsUri, jsMapUri);
EventSink<String> call(String name, String extension) {
EventSink<String> output = outputProvider(name, extension);
if (extension == 'js.map') {
output = new CloningEventSink(
[output, jsMapOutput = new BufferedEventSink()]);
}
return output;
}
}
abstract class SourceFileManager {
SourceFile getSourceFile(var uri);
}
class ProviderSourceFileManager implements SourceFileManager {
final SourceFileProvider sourceFileProvider;
ProviderSourceFileManager(this.sourceFileProvider);
@override
SourceFile getSourceFile(uri) {
return sourceFileProvider.getSourceFile(uri);
}
}
class RecordingPrintingContext extends LenientPrintingContext {
CodePositionListener listener;
RecordingPrintingContext(this.listener);
@override
void exitNode(js.Node node,
int startPosition,
int endPosition,
int closingPosition) {
listener.onPositions(
node, startPosition, endPosition, closingPosition);
}
}
/// 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 [filename].
SourceMapProcessor(String filename, {this.outputToFile: false}) {
inputUri = Uri.base.resolve(nativeToUriPath(filename));
jsPath = 'out.js';
targetUri = Uri.base.resolve(jsPath);
sourceMapFileUri = Uri.base.resolve('${jsPath}.map');
}
/// Computes the [SourceMapInfo] for the compiled elements.
Future<List<SourceMapInfo>> process(List<String> options) async {
OutputProvider outputProvider = outputToFile
? new OutputProvider()
: new CloningOutputProvider(targetUri, sourceMapFileUri);
if (options.contains('--use-new-source-info')) {
print('Using the new source information system.');
useNewSourceInfo = true;
}
api.Compiler compiler = await compilerFor({},
outputProvider: outputProvider,
options: ['--out=$targetUri', '--source-map=$sourceMapFileUri']
..addAll(options));
if (options.contains('--disable-inlining')) {
print('Inlining disabled');
compiler.disableInlining = true;
}
JavaScriptBackend backend = compiler.backend;
var handler = compiler.handler;
SourceFileProvider sourceFileProvider = handler.provider;
sourceFileManager = new ProviderSourceFileManager(sourceFileProvider);
await compiler.runCompiler(inputUri);
List<SourceMapInfo> infoList = <SourceMapInfo>[];
backend.generatedCode.forEach((Element element, js.Expression node) {
js.JavaScriptPrintingOptions options =
new js.JavaScriptPrintingOptions();
JavaScriptSourceInformationStrategy sourceInformationStrategy =
compiler.backend.sourceInformationStrategy;
NodeToSourceLocationsMap nodeMap = new NodeToSourceLocationsMap();
SourceInformationProcessor sourceInformationProcessor =
sourceInformationStrategy.createProcessor(nodeMap);
RecordingPrintingContext printingContext =
new RecordingPrintingContext(sourceInformationProcessor);
new js.Printer(options, printingContext).visit(node);
sourceInformationProcessor.process(node);
String code = printingContext.getText();
CodePointComputer visitor =
new CodePointComputer(sourceFileManager, code, nodeMap);
visitor.apply(node);
List<CodePoint> codePoints = visitor.codePoints;
infoList.add(new SourceMapInfo(element, code, node, codePoints, nodeMap));
});
return infoList;
}
}
/// Source mapping information for the JavaScript code of an [Element].
class SourceMapInfo {
final String name;
final Element element;
final String code;
final js.Expression node;
final List<CodePoint> codePoints;
final NodeToSourceLocationsMap nodeMap;
SourceMapInfo(
Element element, this.code, this.node, this.codePoints, this.nodeMap)
: this.name = computeElementNameForSourceMaps(element),
this.element = element;
}
/// Collection of JavaScript nodes with their source mapped target offsets
/// and source locations.
class NodeToSourceLocationsMap implements SourceMapper {
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);
}
Iterable<js.Node> get nodes => _nodeMap.keys;
Map<int, List<SourceLocation>> operator[] (js.Node node) {
return _nodeMap[node];
}
}
/// Visitor that computes the [CodePoint]s for source mapping locations.
class CodePointComputer extends js.BaseVisitor {
final SourceFileManager sourceFileManager;
final String code;
final NodeToSourceLocationsMap 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;
}
void register(String kind, js.Node node, {bool expectInfo: true}) {
String dartCodeFromSourceLocation(SourceLocation sourceLocation) {
SourceFile sourceFile =
sourceFileManager.getSourceFile(sourceLocation.sourceUri);
return sourceFile.getLineText(sourceLocation.line)
.substring(sourceLocation.column).trim();
}
void addLocation(SourceLocation sourceLocation, String jsCode) {
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, sourceLocation, dartCode, isMissing: true));
}
} else {
codePoints.add(new CodePoint(kind, jsCode, sourceLocation,
dartCodeFromSourceLocation(sourceLocation)));
}
}
Map<int, List<SourceLocation>> locationMap = nodeMap[node];
if (locationMap == null) {
addLocation(null, nodeToString(node));
} else {
locationMap.forEach((int targetOffset, List<SourceLocation> locations) {
String jsCode = positionToString(targetOffset);
for (SourceLocation location in locations) {
addLocation(location, jsCode);
}
});
}
}
void apply(js.Node node) {
node.accept(this);
}
void visitNode(js.Node node) {
register('${node.runtimeType}', node, expectInfo: false);
super.visitNode(node);
}
@override
void visitNew(js.New node) {
node.arguments.forEach(apply);
register('New', node);
}
@override
void visitReturn(js.Return node) {
if (node.value != null) {
apply(node.value);
}
register('Return', node);
}
@override
void visitCall(js.Call node) {
apply(node.target);
node.arguments.forEach(apply);
register('Call (${node.target.runtimeType})', node);
}
@override
void visitFun(js.Fun node) {
node.visitChildren(this);
register('Fun', node);
}
@override
visitExpressionStatement(js.ExpressionStatement node) {
node.visitChildren(this);
}
@override
visitBinary(js.Binary node) {
node.visitChildren(this);
}
@override
visitAccess(js.PropertyAccess node) {
node.visitChildren(this);
}
}
/// A JavaScript code point and its mapped dart source location.
class CodePoint {
final String kind;
final String jsCode;
final SourceLocation sourceLocation;
final String dartCode;
final bool isMissing;
CodePoint(
this.kind,
this.jsCode,
this.sourceLocation,
this.dartCode,
{this.isMissing: false});
String toString() {
return 'CodePoint[kind=$kind,js=$jsCode,dart=$dartCode,'
'location=$sourceLocation]';
}
}