blob: ea6764859825648befa346c82d23fb233c04149b [file] [log] [blame]
// 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.
import 'dart:collection' show HashSet, Queue;
import 'dart:convert' show BASE64, JSON, UTF8;
import 'dart:io' show File;
import 'package:analyzer/dart/element/element.dart' show LibraryElement;
import 'package:analyzer/analyzer.dart'
show AnalysisError, CompilationUnit, ErrorSeverity;
import 'package:analyzer/file_system/file_system.dart' show ResourceProvider;
import 'package:analyzer/src/generated/engine.dart'
show AnalysisContext, AnalysisEngine;
import 'package:analyzer/src/generated/source.dart' show DartUriResolver;
import 'package:analyzer/src/generated/source_io.dart'
show Source, SourceKind, UriResolver;
import 'package:analyzer/src/summary/package_bundle_reader.dart'
show InSummarySource, InputPackagesResultProvider, SummaryDataStore;
import 'package:analyzer/src/error/codes.dart' show StaticTypeWarningCode;
import 'package:args/args.dart' show ArgParser, ArgResults;
import 'package:args/src/usage_exception.dart' show UsageException;
import 'package:func/func.dart' show Func1;
import 'package:path/path.dart' as path;
import 'package:source_maps/source_maps.dart';
import '../analyzer/context.dart'
show
AnalyzerOptions,
createAnalysisContext,
createSdkPathResolver,
createSourceFactory;
import '../js_ast/js_ast.dart' as JS;
import 'code_generator.dart' show CodeGenerator;
import 'error_helpers.dart' show errorSeverity, formatError, sortErrors;
import 'extension_types.dart' show ExtensionTypeSet;
import 'js_names.dart' as JS;
import 'module_builder.dart' show transformModuleFormat, ModuleFormat;
import 'source_map_printer.dart' show SourceMapPrintingContext;
/// Compiles a set of Dart files into a single JavaScript module.
///
/// For a single [BuildUnit] definition, this will produce a [JSModuleFile].
/// Those objects are record types that record the data consumed and produced
/// for a single compile.
///
/// This class exists to cache global state associated with a single in-memory
/// AnalysisContext, such as information about extension types in the Dart SDK.
/// It can be used once to produce a single module, or reused to save warm-up
/// time. (Currently there is no warm up, but there may be in the future.)
///
/// The SDK source code is assumed to be immutable for the life of this class.
///
/// For all other files, it is up to the [AnalysisContext] to decide whether or
/// not any caching is performed. By default an analysis context will assume
/// sources are immutable for the life of the context, and cache information
/// about them.
class ModuleCompiler {
final AnalysisContext context;
final SummaryDataStore summaryData;
final ExtensionTypeSet _extensionTypes;
ModuleCompiler.withContext(AnalysisContext context, this.summaryData)
: context = context,
_extensionTypes = new ExtensionTypeSet(context) {
if (!context.analysisOptions.strongMode) {
throw new ArgumentError('AnalysisContext must be strong mode');
}
if (!context.sourceFactory.dartSdk.context.analysisOptions.strongMode) {
throw new ArgumentError('AnalysisContext must have strong mode SDK');
}
}
factory ModuleCompiler(AnalyzerOptions options,
{DartUriResolver sdkResolver,
ResourceProvider resourceProvider,
List<UriResolver> fileResolvers}) {
AnalysisEngine.instance.processRequiredPlugins();
sdkResolver ??=
createSdkPathResolver(options.dartSdkSummaryPath, options.dartSdkPath);
// Read the summaries.
var summaryData =
new SummaryDataStore(options.summaryPaths, recordDependencyInfo: true);
var srcFactory = createSourceFactory(options,
sdkResolver: sdkResolver,
fileResolvers: fileResolvers,
summaryData: summaryData,
resourceProvider: resourceProvider);
var context = createAnalysisContext();
context.sourceFactory = srcFactory;
context.typeProvider = sdkResolver.dartSdk.context.typeProvider;
context.resultProvider =
new InputPackagesResultProvider(context, summaryData);
options.declaredVariables.forEach(context.declaredVariables.define);
context.declaredVariables.define('dart.isVM', 'false');
return new ModuleCompiler.withContext(context, summaryData);
}
bool _isFatalError(AnalysisError e, CompilerOptions options) {
if (errorSeverity(context, e) != ErrorSeverity.ERROR) return false;
// These errors are not fatal in the REPL compile mode as we
// allow access to private members across library boundaries
// and those accesses will show up as undefined members unless
// additional analyzer changes are made to support them.
// TODO(jacobr): consider checking that the identifier name
// referenced by the error is private.
return !options.replCompile ||
(e.errorCode != StaticTypeWarningCode.UNDEFINED_GETTER &&
e.errorCode != StaticTypeWarningCode.UNDEFINED_SETTER &&
e.errorCode != StaticTypeWarningCode.UNDEFINED_METHOD);
}
/// Compiles a single Dart build unit into a JavaScript module.
///
/// *Warning* - this may require resolving the entire world.
/// If that is not desired, the analysis context must be pre-configured using
/// summaries before calling this method.
JSModuleFile compile(BuildUnit unit, CompilerOptions options) {
var trees = <CompilationUnit>[];
var errors = <AnalysisError>[];
var librariesToCompile = new Queue<LibraryElement>();
var compilingSdk = false;
for (var sourcePath in unit.sources) {
var sourceUri = Uri.parse(sourcePath);
if (sourceUri.scheme == '') {
sourceUri = path.toUri(path.absolute(sourcePath));
} else if (sourceUri.scheme == 'dart') {
compilingSdk = true;
}
Source source = context.sourceFactory.forUri2(sourceUri);
var fileUsage = 'You need to pass at least one existing .dart file as an'
' argument.';
if (source == null) {
throw new UsageException(
'Could not create a source for "$sourcePath". The file name is in'
' the wrong format or was not found.',
fileUsage);
} else if (!source.exists()) {
throw new UsageException(
'Given file "$sourcePath" does not exist.', fileUsage);
}
// Ignore parts. They need to be handled in the context of their library.
if (context.computeKindOf(source) == SourceKind.PART) {
continue;
}
librariesToCompile.add(context.computeLibraryElement(source));
}
var libraries = new HashSet<LibraryElement>();
while (librariesToCompile.isNotEmpty) {
var library = librariesToCompile.removeFirst();
if (library.source is InSummarySource) continue;
if (!compilingSdk && library.source.isInSystemLibrary) continue;
if (!libraries.add(library)) continue;
librariesToCompile.addAll(library.importedLibraries);
librariesToCompile.addAll(library.exportedLibraries);
var tree = context.resolveCompilationUnit(library.source, library);
trees.add(tree);
errors.addAll(context.computeErrors(library.source));
for (var part in library.parts) {
trees.add(context.resolveCompilationUnit(part.source, library));
errors.addAll(context.computeErrors(part.source));
}
}
sortErrors(context, errors);
var messages = <String>[];
for (var e in errors) {
var m = formatError(context, e);
if (m != null) messages.add(m);
}
if (!options.unsafeForceCompile &&
errors.any((e) => _isFatalError(e, options))) {
return new JSModuleFile.invalid(unit.name, messages, options);
}
var codeGenerator =
new CodeGenerator(context, summaryData, options, _extensionTypes);
return codeGenerator.compile(unit, trees, messages);
}
}
class CompilerOptions {
/// Whether to emit the source mapping file.
///
/// This supports debugging the original source code instead of the generated
/// code.
final bool sourceMap;
/// If [sourceMap] is emitted, this will emit a `sourceMappingUrl` comment
/// into the output JavaScript module.
final bool sourceMapComment;
/// Whether to emit the source mapping file inline as a data url.
final bool inlineSourceMap;
/// Whether to emit a summary file containing API signatures.
///
/// This is required for a modular build process.
final bool summarizeApi;
/// The file extension for summaries.
final String summaryExtension;
/// Whether to preserve metdata only accessible via mirrors
final bool emitMetadata;
/// Whether to force compilation of code with static errors.
final bool unsafeForceCompile;
/// Whether to compile code in a more permissive REPL mode allowing access
/// to private members across library boundaries.
final bool replCompile;
/// Whether to emit Closure Compiler-friendly code.
final bool closure;
/// Hoist the types at instance creation sites
final bool hoistInstanceCreation;
/// Hoist types from class signatures
final bool hoistSignatureTypes;
/// Name types in type tests
final bool nameTypeTests;
/// Hoist types in type tests
final bool hoistTypeTests;
final bool useAngular2Whitelist;
/// Enable ES6 destructuring of named parameters. Off by default.
///
/// Older V8 versions do not accept default values with destructuring in
/// arrow functions yet (e.g. `({a} = {}) => 1`) but happily accepts them
/// with regular functions (e.g. `function({a} = {}) { return 1 }`).
///
/// Supporting the syntax:
/// * Chrome Canary (51)
/// * Firefox
///
/// Not yet supporting:
/// * Atom (1.5.4)
/// * Electron (0.36.3)
// TODO(ochafik): Simplify this code when our target platforms catch up.
final bool destructureNamedParams;
/// Mapping from absolute file paths to bazel short path to substitute in
/// source maps.
final Map<String, String> bazelMapping;
/// If specified, the path to write the summary file.
/// Used when building the SDK.
final String summaryOutPath;
const CompilerOptions(
{this.sourceMap: true,
this.sourceMapComment: true,
this.inlineSourceMap: false,
this.summarizeApi: true,
this.summaryExtension: 'sum',
this.unsafeForceCompile: false,
this.replCompile: false,
this.emitMetadata: false,
this.closure: false,
this.destructureNamedParams: false,
this.hoistInstanceCreation: true,
this.hoistSignatureTypes: false,
this.nameTypeTests: true,
this.hoistTypeTests: true,
this.useAngular2Whitelist: false,
this.bazelMapping: const {},
this.summaryOutPath});
CompilerOptions.fromArguments(ArgResults args)
: sourceMap = args['source-map'],
sourceMapComment = args['source-map-comment'],
inlineSourceMap = args['inline-source-map'],
summarizeApi = args['summarize'],
summaryExtension = args['summary-extension'],
unsafeForceCompile = args['unsafe-force-compile'],
replCompile = args['repl-compile'],
emitMetadata = args['emit-metadata'],
closure = args['closure-experimental'],
destructureNamedParams = args['destructure-named-params'],
hoistInstanceCreation = args['hoist-instance-creation'],
hoistSignatureTypes = args['hoist-signature-types'],
nameTypeTests = args['name-type-tests'],
hoistTypeTests = args['hoist-type-tests'],
useAngular2Whitelist = args['unsafe-angular2-whitelist'],
bazelMapping = _parseBazelMappings(args['bazel-mapping']),
summaryOutPath = args['summary-out'];
static void addArguments(ArgParser parser) {
parser
..addFlag('summarize', help: 'emit an API summary file', defaultsTo: true)
..addOption('summary-extension',
help: 'file extension for Dart summary files',
defaultsTo: 'sum',
hide: true)
..addFlag('source-map', help: 'emit source mapping', defaultsTo: true)
..addFlag('source-map-comment',
help: 'adds a sourceMappingURL comment to the end of the JS,\n'
'disable if using X-SourceMap header',
defaultsTo: true,
hide: true)
..addFlag('inline-source-map',
help: 'emit source mapping inline', defaultsTo: false)
..addFlag('emit-metadata',
help: 'emit metadata annotations queriable via mirrors',
defaultsTo: false)
..addFlag('closure-experimental',
help: 'emit Closure Compiler-friendly code (experimental)',
defaultsTo: false)
..addFlag('destructure-named-params',
help: 'Destructure named parameters', defaultsTo: false, hide: true)
..addFlag('unsafe-force-compile',
help: 'Compile code even if it has errors. ಠ_ಠ\n'
'This has undefined behavior!',
defaultsTo: false,
hide: true)
..addFlag('repl-compile',
help: 'Compile code more permissively when in REPL mode allowing '
'access to private members across library boundaries.',
defaultsTo: false,
hide: true)
..addFlag('hoist-instance-creation',
help: 'Hoist the class type from generic instance creations',
defaultsTo: true,
hide: true)
..addFlag('hoist-signature-types',
help: 'Hoist types from class signatures',
defaultsTo: false,
hide: true)
..addFlag('name-type-tests',
help: 'Name types used in type tests', defaultsTo: true, hide: true)
..addFlag('hoist-type-tests',
help: 'Hoist types used in type tests', defaultsTo: true, hide: true)
..addFlag('unsafe-angular2-whitelist', defaultsTo: false, hide: true)
..addOption('bazel-mapping',
help:
'--bazel-mapping=genfiles/to/library.dart,to/library.dart uses \n'
'to/library.dart as the path for library.dart in source maps.',
allowMultiple: true,
splitCommas: false,
hide: true)
..addOption('summary-out',
help: 'location to write the summary file', hide: true);
}
static Map<String, String> _parseBazelMappings(Iterable argument) {
var mappings = <String, String>{};
for (var mapping in argument) {
var splitMapping = mapping.split(',');
if (splitMapping.length >= 2) {
mappings[path.absolute(splitMapping[0])] = splitMapping[1];
}
}
return mappings;
}
}
/// A unit of Dart code that can be built into a single JavaScript module.
class BuildUnit {
/// The name of this module.
final String name;
/// All library names are relative to this path/prefix.
final String libraryRoot;
/// The list of sources in this module.
///
/// The set of Dart files can be arbitrarily large, but it must contain
/// complete libraries including all of their parts, as well as all libraries
/// that are part of a library cycle.
final List<String> sources;
/// Given an imported library URI, this will determine to what Dart/JS module
/// it belongs to.
// TODO(jmesserly): we should replace this with another way of tracking
// build units.
final Func1<Source, String> libraryToModule;
BuildUnit(this.name, this.libraryRoot, this.sources, this.libraryToModule);
}
/// The output of Dart->JS compilation.
///
/// This contains the file contents of the JS module, as well as a list of
/// Dart libraries that are contained in this module.
class JSModuleFile {
/// The name of this module.
final String name;
/// The list of messages (errors and warnings)
final List<String> errors;
/// The AST that will be used to generate the [code] and [sourceMap] for this
/// module.
final JS.Program moduleTree;
/// The compiler options used to generate this module.
final CompilerOptions options;
/// The binary contents of the API summary file, including APIs from each of
/// the libraries in this module.
final List<int> summaryBytes;
JSModuleFile(
this.name, this.errors, this.options, this.moduleTree, this.summaryBytes);
JSModuleFile.invalid(this.name, this.errors, this.options)
: moduleTree = null,
summaryBytes = null;
/// True if this library was successfully compiled.
bool get isValid => moduleTree != null;
/// Gets the source code and source map for this JS module, given the
/// locations where the JS file and map file will be served from.
///
/// Relative URLs will be used to point from the .js file to the .map file
//
// TODO(jmesserly): this should match our old logic, but I'm not sure we are
// correctly handling the pointer from the .js file to the .map file.
JSModuleCode getCode(ModuleFormat format, String jsUrl, String mapUrl,
{bool singleOutFile: false}) {
var opts = new JS.JavaScriptPrintingOptions(
emitTypes: options.closure,
allowKeywordsInProperties: true,
allowSingleLineIfStatements: true);
JS.SimpleJavaScriptPrintingContext printer;
SourceMapBuilder sourceMap;
if (options.sourceMap) {
var sourceMapContext = new SourceMapPrintingContext();
sourceMap = sourceMapContext.sourceMap;
printer = sourceMapContext;
} else {
printer = new JS.SimpleJavaScriptPrintingContext();
}
var tree =
transformModuleFormat(format, moduleTree, singleOutFile: singleOutFile);
tree.accept(
new JS.Printer(opts, printer, localNamer: new JS.TemporaryNamer(tree)));
Map builtMap;
if (options.sourceMap && sourceMap != null) {
builtMap =
placeSourceMap(sourceMap.build(jsUrl), mapUrl, options.bazelMapping);
if (options.sourceMapComment) {
var relativeMapUrl = path
.toUri(
path.relative(path.fromUri(mapUrl), from: path.dirname(jsUrl)))
.toString();
assert(path.dirname(jsUrl) == path.dirname(mapUrl));
printer.emit('\n//# sourceMappingURL=');
if (options.inlineSourceMap) {
var bytes = UTF8.encode(JSON.encode(builtMap));
var base64 = BASE64.encode(bytes);
printer..emit('data:application/json;base64,')..emit(base64);
} else {
printer.emit(relativeMapUrl);
}
printer.emit('\n');
}
}
return new JSModuleCode(printer.getText(), builtMap);
}
/// Similar to [getCode] but immediately writes the resulting files.
///
/// If [mapPath] is not supplied but [options.sourceMap] is set, mapPath
/// will default to [jsPath].map.
void writeCodeSync(ModuleFormat format, String jsPath,
{bool singleOutFile: false}) {
String mapPath = jsPath + '.map';
var code = getCode(format, jsPath, mapPath, singleOutFile: singleOutFile);
var c = code.code;
if (singleOutFile) {
// In singleOutFile mode we wrap each module in an eval statement to
// leverage sourceURL to improve the debugging experience when source maps
// are not enabled.
//
// Note: We replace all `/` with `.` so that we don't break relative urls
// to sources in the original sourcemap. The name of this file is bogus
// anyways, so it has very little effect on things.
c += '\n//# sourceURL=${name.replaceAll("/", ".")}.js\n';
c = 'eval(${JSON.encode(c)});\n';
}
var file = new File(jsPath);
if (!file.parent.existsSync()) file.parent.createSync(recursive: true);
file.writeAsStringSync(c);
// TODO(jacobr): it is a bit strange we are writing the source map to a file
// even when options.inlineSourceMap is true. To be consistent perhaps we
// should also write a copy of the source file without a sourcemap even when
// inlineSourceMap is true.
if (code.sourceMap != null) {
file = new File(mapPath);
if (!file.parent.existsSync()) file.parent.createSync(recursive: true);
file.writeAsStringSync(JSON.encode(code.sourceMap));
}
}
}
/// The output of compiling a JavaScript module in a particular format.
class JSModuleCode {
/// The JavaScript code for this module.
///
/// If a [sourceMap] is available, this will include the `sourceMappingURL`
/// comment at end of the file.
final String code;
/// The JSON of the source map, if generated, otherwise `null`.
///
/// The source paths will initially be absolute paths. They can be adjusted
/// using [placeSourceMap].
final Map sourceMap;
JSModuleCode(this.code, this.sourceMap);
}
/// Adjusts the source paths in [sourceMap] to be relative to [sourceMapPath],
/// and returns the new map.
// TODO(jmesserly): find a new home for this.
Map placeSourceMap(
Map sourceMap, String sourceMapPath, Map<String, String> bazelMappings) {
var dir = path.dirname(sourceMapPath);
var map = new Map.from(sourceMap);
var list = new List.from(map['sources']);
map['sources'] = list;
String transformUri(String uri) {
var match = bazelMappings[path.absolute(uri)];
if (match != null) return match;
// Fall back to a relative path.
return path.toUri(path.relative(path.fromUri(uri), from: dir)).toString();
}
for (int i = 0; i < list.length; i++) {
list[i] = transformUri(list[i]);
}
map['file'] = transformUri(map['file']);
return map;
}