refactor(dart/transform): Simplify deferred rewriting
Simplify the DeferredRewriting and move package:quiver to a dev dependency. Closes #6994
This commit is contained in:
		
							parent
							
								
									eb688f2c8e
								
							
						
					
					
						commit
						1fd924f7d5
					
				| @ -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 | ||||||
|  | |||||||
| @ -4,160 +4,176 @@ 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 { | /// Rewrites `loadLibrary` calls to initialize libraries once loaded. | ||||||
|   final AssetId _entryPoint; | /// | ||||||
|   final AssetReader _reader; | /// 1. Finds all the deferred library imports and loadLibrary invocations in | ||||||
|   final _FindDeferredLibraries _visitor; | ///    `_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<String> rewriteLibrary(AssetId entryPoint, AssetReader reader) async { | ||||||
|  |   var code = await reader.readAsString(entryPoint); | ||||||
| 
 | 
 | ||||||
|   Rewriter(AssetId entryPoint, AssetReader reader) |   return logElapsedAsync(() async { | ||||||
|       : _entryPoint = entryPoint, |     // If we can determine there are no deferred libraries, avoid additional | ||||||
|         _reader = reader, |     // parsing the entire file and bail early. | ||||||
|         _visitor = new _FindDeferredLibraries(reader, entryPoint); |     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. |     final importVisitor = new _FindDeferredLibraries(reader, entryPoint); | ||||||
|   /// |     onlyDirectives.directives.accept(importVisitor); | ||||||
|   /// 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<String> rewrite() async { |  | ||||||
|     var code = await _reader.readAsString(_entryPoint); |  | ||||||
| 
 | 
 | ||||||
|     // If we can determine there are no deferred libraries, avoid parsing the |     // Get imports that need rewriting. | ||||||
|     // entire file and bail early. |     final deferredImports = await importVisitor.process(); | ||||||
|     var onlyDirectives = parseDirectives(code, name: _entryPoint.path); |     if (deferredImports.isEmpty) { | ||||||
|     if (onlyDirectives == null) return null; |       log.fine('There are no deferred library imports that need rewriting.', | ||||||
|     onlyDirectives.directives.accept(_visitor); |           asset: entryPoint); | ||||||
|     if (_visitor.deferredImports.isEmpty) return null; |       return null; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     var node = parseCompilationUnit(code, name: _entryPoint.path); |     var node = parseCompilationUnit(code, name: entryPoint.path); | ||||||
|     if (node == null) return null; |     if (node == null) { | ||||||
|  |       log.fine('No declarations parsed, bailing early.', asset: entryPoint); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return logElapsedAsync(() async { |     final declarationsVisitor = new _FindLoadLibraryCalls(deferredImports); | ||||||
|       node.declarations.accept(_visitor); |     node.declarations.accept(declarationsVisitor); | ||||||
|       // 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; |  | ||||||
| 
 | 
 | ||||||
|       var compare = (AstNode a, AstNode b) => a.offset - b.offset; |     // Get libraries that need rewriting. | ||||||
|       _visitor.deferredImports.sort(compare); |     if (declarationsVisitor.loadLibraryInvocations.isEmpty) { | ||||||
|       _visitor.loadLibraryInvocations.sort(compare); |       log.fine( | ||||||
|  |           'There are no loadLibrary invocations that need to be rewritten.', | ||||||
|  |           asset: entryPoint); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|       var buf = new StringBuffer(); |     return _rewriteLibrary( | ||||||
|       var idx = |         code, deferredImports, declarationsVisitor.loadLibraryInvocations); | ||||||
|           _visitor.deferredImports.fold(0, (int lastIdx, ImportDirective node) { |   }, operationName: 'rewriteDeferredLibraries', assetId: entryPoint); | ||||||
|         // 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); |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Visitor responsible for finding the deferred libraries that need angular | /// Rewrites the original [code] to initialize deferred libraries prior to use. | ||||||
| /// codegen. Those are the libraries that are loaded deferred and have a | /// | ||||||
| /// corresponding generated file. | /// Note: This method may modify the order of [imports] and [loadLibCalls]. | ||||||
| class _FindDeferredLibraries extends Object with RecursiveAstVisitor<Object> { | String _rewriteLibrary(String code, List<ImportDirective> imports, | ||||||
|   var deferredImports = new List<ImportDirective>(); |     List<MethodInvocation> loadLibCalls) { | ||||||
|   var loadLibraryInvocations = new List<MethodInvocation>(); |   /// Compares two [AstNode]s by position in the source code. | ||||||
|   final AssetReader _reader; |   var _compareNodes = (AstNode a, AstNode b) => a.offset - b.offset; | ||||||
|   final AssetId _entryPoint; | 
 | ||||||
|  |   // 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<Object> { | ||||||
|  |   final _deferredImports = <ImportDirective>[]; | ||||||
|   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); | ||||||
| 
 |       } | ||||||
|     // Find the set of prefixes which have associated generated files. |     } | ||||||
|     var prefixes = |     return _deferredImports; | ||||||
|         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. | /// Finds all `loadLibrary` calls in the Ast that require init. | ||||||
|     loadLibraryInvocations = loadLibraryInvocations.where((library) { | class _FindLoadLibraryCalls extends Object with RecursiveAstVisitor<Object> { | ||||||
|       var value = library.realTarget as SimpleIdentifier; |   /// The prefixes used by all `deferred` [ImportDirective]s that need init. | ||||||
|       return prefixes.contains(value.name); |   final Set _deferredPrefixes; | ||||||
|     }).toList(); |   final loadLibraryInvocations = <MethodInvocation>[]; | ||||||
| 
 | 
 | ||||||
|     return; |   _FindLoadLibraryCalls(List<ImportDirective> 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); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -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(); |  | ||||||
| } |  | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user