diff --git a/modules/angular2/pubspec.yaml b/modules/angular2/pubspec.yaml index a5243e3025..75b774e020 100644 --- a/modules/angular2/pubspec.yaml +++ b/modules/angular2/pubspec.yaml @@ -18,12 +18,12 @@ dependencies: logging: '>=0.9.0 <0.12.0' observe: '^0.13.1' protobuf: '^0.5.0' - quiver: '^0.21.4' source_span: '^1.0.0' stack_trace: '^1.1.1' dev_dependencies: code_transformers: '>=0.2.9+4 <0.4.0' guinness: '^0.1.18' + quiver: '^0.21.4' test: '^0.12.6' transformers: - angular2 diff --git a/modules_dart/transform/lib/src/transform/deferred_rewriter/rewriter.dart b/modules_dart/transform/lib/src/transform/deferred_rewriter/rewriter.dart index db810054f0..3f781904af 100644 --- a/modules_dart/transform/lib/src/transform/deferred_rewriter/rewriter.dart +++ b/modules_dart/transform/lib/src/transform/deferred_rewriter/rewriter.dart @@ -4,160 +4,176 @@ import 'dart:async'; import 'package:analyzer/analyzer.dart'; import 'package:analyzer/src/generated/ast.dart'; +import 'package:barback/barback.dart'; + import 'package:angular2/src/transform/common/asset_reader.dart'; import 'package:angular2/src/transform/common/logging.dart'; import 'package:angular2/src/transform/common/names.dart'; import 'package:angular2/src/transform/common/url_resolver.dart'; -import 'package:barback/barback.dart'; -import 'package:quiver/iterables.dart' as it; -class Rewriter { - final AssetId _entryPoint; - final AssetReader _reader; - final _FindDeferredLibraries _visitor; +/// Rewrites `loadLibrary` calls to initialize libraries once loaded. +/// +/// 1. Finds all the deferred library imports and loadLibrary invocations in +/// `_entryPoint` +/// 2. Removes any libraries that don't require angular codegen. +/// 3. For the remaining libraries, rewrites the import to the corresponding +/// `.template.dart` file. +/// 4. Chains a future to the `loadLibrary` call which initializes the +/// library. +/// +/// To the extent possible, this method does not change line numbers or +/// offsets in the provided code to facilitate debugging via source maps. +Future rewriteLibrary(AssetId entryPoint, AssetReader reader) async { + var code = await reader.readAsString(entryPoint); - Rewriter(AssetId entryPoint, AssetReader reader) - : _entryPoint = entryPoint, - _reader = reader, - _visitor = new _FindDeferredLibraries(reader, entryPoint); + return logElapsedAsync(() async { + // If we can determine there are no deferred libraries, avoid additional + // parsing the entire file and bail early. + var onlyDirectives = parseDirectives(code, name: entryPoint.path); + if (onlyDirectives == null) { + log.fine('No directives parsed, bailing early.', asset: entryPoint); + return null; + } - /// Rewrites `loadLibrary` calls to initialize libraries once loaded. - /// - /// 1. Finds all the deferred library imports and loadLibrary invocations in - /// `_entryPoint` - /// 2. Removes any libraries that don't require angular codegen. - /// 3. For the remaining libraries, rewrites the import to the corresponding - /// `.template.dart` file. - /// 4. Chains a future to the `loadLibrary` call which initializes the - /// library. - /// - /// To the extent possible, this method does not change line numbers or - /// offsets in the provided code to facilitate debugging via source maps. - Future rewrite() async { - var code = await _reader.readAsString(_entryPoint); + final importVisitor = new _FindDeferredLibraries(reader, entryPoint); + onlyDirectives.directives.accept(importVisitor); - // If we can determine there are no deferred libraries, avoid parsing the - // entire file and bail early. - var onlyDirectives = parseDirectives(code, name: _entryPoint.path); - if (onlyDirectives == null) return null; - onlyDirectives.directives.accept(_visitor); - if (_visitor.deferredImports.isEmpty) return null; + // Get imports that need rewriting. + final deferredImports = await importVisitor.process(); + if (deferredImports.isEmpty) { + log.fine('There are no deferred library imports that need rewriting.', + asset: entryPoint); + return null; + } - var node = parseCompilationUnit(code, name: _entryPoint.path); - if (node == null) return null; + var node = parseCompilationUnit(code, name: entryPoint.path); + if (node == null) { + log.fine('No declarations parsed, bailing early.', asset: entryPoint); + return null; + } - return logElapsedAsync(() async { - node.declarations.accept(_visitor); - // Look to see if we found any deferred libraries - if (!_visitor.hasDeferredLibrariesToRewrite()) return null; - // Remove any libraries that don't need angular codegen. - await _visitor.cull(); - // Check again if there are any deferred libraries. - if (!_visitor.hasDeferredLibrariesToRewrite()) return null; + final declarationsVisitor = new _FindLoadLibraryCalls(deferredImports); + node.declarations.accept(declarationsVisitor); - var compare = (AstNode a, AstNode b) => a.offset - b.offset; - _visitor.deferredImports.sort(compare); - _visitor.loadLibraryInvocations.sort(compare); + // Get libraries that need rewriting. + if (declarationsVisitor.loadLibraryInvocations.isEmpty) { + log.fine( + 'There are no loadLibrary invocations that need to be rewritten.', + asset: entryPoint); + return null; + } - var buf = new StringBuffer(); - var idx = - _visitor.deferredImports.fold(0, (int lastIdx, ImportDirective node) { - // Write from where we left off until the start of the import uri. - buf.write(code.substring(lastIdx, node.uri.offset)); - // Rewrite the uri to be that of the generated file. - buf.write("'${toTemplateExtension('${node.uri.stringValue}')}'"); - // Update the last index we've processed. - return node.uri.end; - }); - - idx = _visitor.loadLibraryInvocations.fold(idx, - (int lastIdx, MethodInvocation node) { - buf.write(code.substring(lastIdx, node.offset)); - var value = node.realTarget as SimpleIdentifier; - var prefix = value.name; - // Chain a future that initializes the reflector. - buf.write('$prefix.loadLibrary().then((_) {$prefix.initReflector();})'); - return node.end; - }); - if (idx < code.length) buf.write(code.substring(idx)); - return '$buf'; - }, operationName: 'rewriteDeferredLibraries', assetId: _entryPoint); - } + return _rewriteLibrary( + code, deferredImports, declarationsVisitor.loadLibraryInvocations); + }, operationName: 'rewriteDeferredLibraries', assetId: entryPoint); } -/// Visitor responsible for finding the deferred libraries that need angular -/// codegen. Those are the libraries that are loaded deferred and have a -/// corresponding generated file. -class _FindDeferredLibraries extends Object with RecursiveAstVisitor { - var deferredImports = new List(); - var loadLibraryInvocations = new List(); - final AssetReader _reader; - final AssetId _entryPoint; +/// Rewrites the original [code] to initialize deferred libraries prior to use. +/// +/// Note: This method may modify the order of [imports] and [loadLibCalls]. +String _rewriteLibrary(String code, List imports, + List loadLibCalls) { + /// Compares two [AstNode]s by position in the source code. + var _compareNodes = (AstNode a, AstNode b) => a.offset - b.offset; + + // Necessary for indexes into [code] to function. + imports.sort(_compareNodes); + loadLibCalls.sort(_compareNodes); + + var buf = new StringBuffer(); + var idx = imports.fold(0, (int lastIdx, ImportDirective node) { + // Write from where we left off until the start of the import uri. + buf.write(code.substring(lastIdx, node.uri.offset)); + // Rewrite the uri to be that of the generated file. + buf.write("'${toTemplateExtension('${node.uri.stringValue}')}'"); + // Update the last index we've processed. + return node.uri.end; + }); + + idx = loadLibCalls.fold(idx, (int lastIdx, MethodInvocation node) { + buf.write(code.substring(lastIdx, node.offset)); + var prefix = (node.realTarget as SimpleIdentifier).name; + // Chain a future that initializes the reflector. + buf.write('$prefix.loadLibrary().then((_) {$prefix.initReflector();})'); + return node.end; + }); + if (idx < code.length) buf.write(code.substring(idx)); + return '$buf'; +} + +/// Finds all `deferred` [ImportDirectives]s in an Ast that require init. +/// +/// Use this to visit all [ImportDirective]s, then call [process] to get only +/// those [ImportDirective]s which are `deferred` and need Angular 2 +/// initialization before use. +class _FindDeferredLibraries extends Object with SimpleAstVisitor { + final _deferredImports = []; final _urlResolver = const TransformerUrlResolver(); - _FindDeferredLibraries(this._reader, this._entryPoint); + final AssetReader _reader; + final AssetId _entryPoint; + final String _entryPointUri; + + _FindDeferredLibraries(this._reader, AssetId entryPoint) + : _entryPoint = entryPoint, + _entryPointUri = toAssetUri(entryPoint); @override Object visitImportDirective(ImportDirective node) { if (node.deferredKeyword != null) { - deferredImports.add(node); + _deferredImports.add(node); } return null; } - @override - Object visitMethodInvocation(MethodInvocation node) { - if (node.methodName.name == 'loadLibrary') { - loadLibraryInvocations.add(node); - } - return super.visitMethodInvocation(node); + /// Gets the [AssetId] for the .ng_meta.json file associated with [import]. + AssetId _getAssociatedMetaAsset(ImportDirective import) { + final importUri = stringLiteralToString(import.uri); + final associatedMetaUri = toMetaExtension(importUri); + return fromUri(_urlResolver.resolve(_entryPointUri, associatedMetaUri)); } - bool hasDeferredLibrariesToRewrite() { - if (deferredImports.isEmpty) { - log.fine('There are no deferred library imports.', asset: _entryPoint); - return false; - } - if (loadLibraryInvocations.isEmpty) { - log.fine( - 'There are no loadLibrary invocations that need to be rewritten.', - asset: _entryPoint); - return false; - } - return true; - } - - // Remove all deferredImports that do not have an associated ng_meta file - // then remove all loadLibrary invocations that are not in the set of - // prefixes that are left. - Future cull() async { - var baseUri = toAssetUri(_entryPoint); - - // Determine whether a deferred import has an associated generated file. - var hasInputs = await Future.wait(deferredImports - .map((import) => stringLiteralToString(import.uri)) - .map((uri) => toMetaExtension(uri)) - .map((metaUri) => fromUri(_urlResolver.resolve(baseUri, metaUri))) - .map((asset) => _reader.hasInput(asset))); + /// Gets a list of `deferred` [ImportDirective]s which need init. + /// + /// Removes all [ImportDirective]s from [_deferredImports] without an + /// associated .ng_meta.json file. + Future> process() async { + // Parallel array with whether the input has an associated .ng_meta.json + // file. + final List hasInputs = await Future.wait( + _deferredImports.map(_getAssociatedMetaAsset).map(_reader.hasInput)); // Filter out any deferred imports that do not have an associated generated // file. - deferredImports = it.zip([deferredImports, hasInputs]) - .where((importHasInput) => importHasInput[1]) - .map((importHasInput) => importHasInput[0]) - .toList(); - - // Find the set of prefixes which have associated generated files. - var prefixes = - new Set.from(deferredImports.map((import) => import.prefix.name)); - - // Filters out any load library invocations where the prefix is not a known - // library with associated generated file. - loadLibraryInvocations = loadLibraryInvocations.where((library) { - var value = library.realTarget as SimpleIdentifier; - return prefixes.contains(value.name); - }).toList(); - - return; + // Iteration order is important! + for (var i = _deferredImports.length - 1; i >= 0; --i) { + if (!hasInputs[i]) { + _deferredImports.removeAt(i); + } + } + return _deferredImports; + } +} + +/// Finds all `loadLibrary` calls in the Ast that require init. +class _FindLoadLibraryCalls extends Object with RecursiveAstVisitor { + /// The prefixes used by all `deferred` [ImportDirective]s that need init. + final Set _deferredPrefixes; + final loadLibraryInvocations = []; + + _FindLoadLibraryCalls(List deferredImports) + : _deferredPrefixes = + new Set.from(deferredImports.map((import) => import.prefix.name)); + + @override + Object visitMethodInvocation(MethodInvocation node) { + if (node.methodName.name == 'loadLibrary') { + var prefix = (node.realTarget as SimpleIdentifier).name; + if (_deferredPrefixes.contains(prefix)) { + loadLibraryInvocations.add(node); + } + } + // Important! Children could include more `loadLibrary` calls. + return super.visitMethodInvocation(node); } } diff --git a/modules_dart/transform/lib/src/transform/deferred_rewriter/transformer.dart b/modules_dart/transform/lib/src/transform/deferred_rewriter/transformer.dart index 6b57fcc35c..a7be78d571 100644 --- a/modules_dart/transform/lib/src/transform/deferred_rewriter/transformer.dart +++ b/modules_dart/transform/lib/src/transform/deferred_rewriter/transformer.dart @@ -81,7 +81,5 @@ class DeferredRewriter extends AggregateTransformer implements LazyTransformer { } // Visible for testing -Future rewriteDeferredLibraries(AssetReader reader, AssetId id) async { - var rewriter = new Rewriter(id, reader); - return await rewriter.rewrite(); -} +Future rewriteDeferredLibraries(AssetReader reader, AssetId id) => + rewriteLibrary(id, reader);