| // 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); |
| |
| 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; |
| } |