refactor(dart/transform): Simplify deferred rewriting

Simplify the DeferredRewriting and move package:quiver to a dev
dependency.

Closes #6994
This commit is contained in:
Tim Blasi 2016-02-09 18:05:04 -08:00 committed by Timothy Blasi
parent eb688f2c8e
commit 1fd924f7d5
3 changed files with 145 additions and 131 deletions

View File

@ -18,12 +18,12 @@ dependencies:
logging: '>=0.9.0 <0.12.0' logging: '>=0.9.0 <0.12.0'
observe: '^0.13.1' observe: '^0.13.1'
protobuf: '^0.5.0' protobuf: '^0.5.0'
quiver: '^0.21.4'
source_span: '^1.0.0' source_span: '^1.0.0'
stack_trace: '^1.1.1' stack_trace: '^1.1.1'
dev_dependencies: dev_dependencies:
code_transformers: '>=0.2.9+4 <0.4.0' code_transformers: '>=0.2.9+4 <0.4.0'
guinness: '^0.1.18' guinness: '^0.1.18'
quiver: '^0.21.4'
test: '^0.12.6' test: '^0.12.6'
transformers: transformers:
- angular2 - angular2

View File

@ -4,22 +4,12 @@ import 'dart:async';
import 'package:analyzer/analyzer.dart'; import 'package:analyzer/analyzer.dart';
import 'package:analyzer/src/generated/ast.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/asset_reader.dart';
import 'package:angular2/src/transform/common/logging.dart'; import 'package:angular2/src/transform/common/logging.dart';
import 'package:angular2/src/transform/common/names.dart'; import 'package:angular2/src/transform/common/names.dart';
import 'package:angular2/src/transform/common/url_resolver.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;
Rewriter(AssetId entryPoint, AssetReader reader)
: _entryPoint = entryPoint,
_reader = reader,
_visitor = new _FindDeferredLibraries(reader, entryPoint);
/// Rewrites `loadLibrary` calls to initialize libraries once loaded. /// Rewrites `loadLibrary` calls to initialize libraries once loaded.
/// ///
@ -33,35 +23,65 @@ class Rewriter {
/// ///
/// To the extent possible, this method does not change line numbers or /// To the extent possible, this method does not change line numbers or
/// offsets in the provided code to facilitate debugging via source maps. /// offsets in the provided code to facilitate debugging via source maps.
Future<String> rewrite() async { Future<String> rewriteLibrary(AssetId entryPoint, AssetReader reader) async {
var code = await _reader.readAsString(_entryPoint); var code = await reader.readAsString(entryPoint);
// 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;
var node = parseCompilationUnit(code, name: _entryPoint.path);
if (node == null) return null;
return logElapsedAsync(() async { return logElapsedAsync(() async {
node.declarations.accept(_visitor); // If we can determine there are no deferred libraries, avoid additional
// Look to see if we found any deferred libraries // parsing the entire file and bail early.
if (!_visitor.hasDeferredLibrariesToRewrite()) return null; var onlyDirectives = parseDirectives(code, name: entryPoint.path);
// Remove any libraries that don't need angular codegen. if (onlyDirectives == null) {
await _visitor.cull(); log.fine('No directives parsed, bailing early.', asset: entryPoint);
// Check again if there are any deferred libraries. return null;
if (!_visitor.hasDeferredLibrariesToRewrite()) return null; }
var compare = (AstNode a, AstNode b) => a.offset - b.offset; final importVisitor = new _FindDeferredLibraries(reader, entryPoint);
_visitor.deferredImports.sort(compare); onlyDirectives.directives.accept(importVisitor);
_visitor.loadLibraryInvocations.sort(compare);
// 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) {
log.fine('No declarations parsed, bailing early.', asset: entryPoint);
return null;
}
final declarationsVisitor = new _FindLoadLibraryCalls(deferredImports);
node.declarations.accept(declarationsVisitor);
// 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;
}
return _rewriteLibrary(
code, deferredImports, declarationsVisitor.loadLibraryInvocations);
}, operationName: 'rewriteDeferredLibraries', 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<ImportDirective> imports,
List<MethodInvocation> 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 buf = new StringBuffer();
var idx = var idx = imports.fold(0, (int lastIdx, ImportDirective node) {
_visitor.deferredImports.fold(0, (int lastIdx, ImportDirective node) {
// Write from where we left off until the start of the import uri. // Write from where we left off until the start of the import uri.
buf.write(code.substring(lastIdx, node.uri.offset)); buf.write(code.substring(lastIdx, node.uri.offset));
// Rewrite the uri to be that of the generated file. // Rewrite the uri to be that of the generated file.
@ -70,94 +90,90 @@ class Rewriter {
return node.uri.end; return node.uri.end;
}); });
idx = _visitor.loadLibraryInvocations.fold(idx, idx = loadLibCalls.fold(idx, (int lastIdx, MethodInvocation node) {
(int lastIdx, MethodInvocation node) {
buf.write(code.substring(lastIdx, node.offset)); buf.write(code.substring(lastIdx, node.offset));
var value = node.realTarget as SimpleIdentifier; var prefix = (node.realTarget as SimpleIdentifier).name;
var prefix = value.name;
// Chain a future that initializes the reflector. // Chain a future that initializes the reflector.
buf.write('$prefix.loadLibrary().then((_) {$prefix.initReflector();})'); buf.write('$prefix.loadLibrary().then((_) {$prefix.initReflector();})');
return node.end; return node.end;
}); });
if (idx < code.length) buf.write(code.substring(idx)); if (idx < code.length) buf.write(code.substring(idx));
return '$buf'; return '$buf';
}, operationName: 'rewriteDeferredLibraries', assetId: _entryPoint);
}
} }
/// Visitor responsible for finding the deferred libraries that need angular /// Finds all `deferred` [ImportDirectives]s in an Ast that require init.
/// codegen. Those are the libraries that are loaded deferred and have a ///
/// corresponding generated file. /// Use this to visit all [ImportDirective]s, then call [process] to get only
class _FindDeferredLibraries extends Object with RecursiveAstVisitor<Object> { /// those [ImportDirective]s which are `deferred` and need Angular 2
var deferredImports = new List<ImportDirective>(); /// initialization before use.
var loadLibraryInvocations = new List<MethodInvocation>(); class _FindDeferredLibraries extends Object with SimpleAstVisitor<Object> {
final AssetReader _reader; final _deferredImports = <ImportDirective>[];
final AssetId _entryPoint;
final _urlResolver = const TransformerUrlResolver(); 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 @override
Object visitImportDirective(ImportDirective node) { Object visitImportDirective(ImportDirective node) {
if (node.deferredKeyword != null) { if (node.deferredKeyword != null) {
deferredImports.add(node); _deferredImports.add(node);
} }
return null; return null;
} }
@override /// Gets the [AssetId] for the .ng_meta.json file associated with [import].
Object visitMethodInvocation(MethodInvocation node) { AssetId _getAssociatedMetaAsset(ImportDirective import) {
if (node.methodName.name == 'loadLibrary') { final importUri = stringLiteralToString(import.uri);
loadLibraryInvocations.add(node); final associatedMetaUri = toMetaExtension(importUri);
} return fromUri(_urlResolver.resolve(_entryPointUri, associatedMetaUri));
return super.visitMethodInvocation(node);
} }
bool hasDeferredLibrariesToRewrite() { /// Gets a list of `deferred` [ImportDirective]s which need init.
if (deferredImports.isEmpty) { ///
log.fine('There are no deferred library imports.', asset: _entryPoint); /// Removes all [ImportDirective]s from [_deferredImports] without an
return false; /// associated .ng_meta.json file.
} Future<List<ImportDirective>> process() async {
if (loadLibraryInvocations.isEmpty) { // Parallel array with whether the input has an associated .ng_meta.json
log.fine( // file.
'There are no loadLibrary invocations that need to be rewritten.', final List<bool> hasInputs = await Future.wait(
asset: _entryPoint); _deferredImports.map(_getAssociatedMetaAsset).map(_reader.hasInput));
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)));
// Filter out any deferred imports that do not have an associated generated // Filter out any deferred imports that do not have an associated generated
// file. // file.
deferredImports = it.zip([deferredImports, hasInputs]) // Iteration order is important!
.where((importHasInput) => importHasInput[1]) for (var i = _deferredImports.length - 1; i >= 0; --i) {
.map((importHasInput) => importHasInput[0]) if (!hasInputs[i]) {
.toList(); _deferredImports.removeAt(i);
}
}
return _deferredImports;
}
}
// Find the set of prefixes which have associated generated files. /// Finds all `loadLibrary` calls in the Ast that require init.
var prefixes = class _FindLoadLibraryCalls extends Object with RecursiveAstVisitor<Object> {
/// The prefixes used by all `deferred` [ImportDirective]s that need init.
final Set _deferredPrefixes;
final loadLibraryInvocations = <MethodInvocation>[];
_FindLoadLibraryCalls(List<ImportDirective> deferredImports)
: _deferredPrefixes =
new Set.from(deferredImports.map((import) => import.prefix.name)); new Set.from(deferredImports.map((import) => import.prefix.name));
// Filters out any load library invocations where the prefix is not a known @override
// library with associated generated file. Object visitMethodInvocation(MethodInvocation node) {
loadLibraryInvocations = loadLibraryInvocations.where((library) { if (node.methodName.name == 'loadLibrary') {
var value = library.realTarget as SimpleIdentifier; var prefix = (node.realTarget as SimpleIdentifier).name;
return prefixes.contains(value.name); if (_deferredPrefixes.contains(prefix)) {
}).toList(); loadLibraryInvocations.add(node);
}
return; }
// Important! Children could include more `loadLibrary` calls.
return super.visitMethodInvocation(node);
} }
} }

View File

@ -81,7 +81,5 @@ class DeferredRewriter extends AggregateTransformer implements LazyTransformer {
} }
// Visible for testing // Visible for testing
Future<String> rewriteDeferredLibraries(AssetReader reader, AssetId id) async { Future<String> rewriteDeferredLibraries(AssetReader reader, AssetId id) =>
var rewriter = new Rewriter(id, reader); rewriteLibrary(id, reader);
return await rewriter.rewrite();
}