From d0aceef4e02a220703a570dec0a8543051e7182f Mon Sep 17 00:00:00 2001 From: Tim Blasi Date: Fri, 27 Feb 2015 14:42:21 -0800 Subject: [PATCH] perf(dart/transform) Restructure transform to independent phases Update summary: - Removes the need for resolution, gaining transform speed at the cost of some precision and ability to detect errors - Generates type registrations in the package alongside their declarations - Ensures that line numbers do not change in transformed user code --- .../transform/bind_generator/generator.dart | 92 ++++++++ .../transform/bind_generator/transformer.dart | 42 ++++ .../src/transform/common/formatter.dart | 22 ++ .../src/transform/{ => common}/logging.dart | 9 +- .../angular2/src/transform/common/names.dart | 7 + .../angular2/src/transform/common/ngdata.dart | 35 +++ .../src/transform/common/options.dart | 29 +++ .../src/transform/common/visitor_mixin.dart | 111 ++++++++++ .../transform/directive_linker/linker.dart | 56 +++++ .../directive_linker/transformer.dart | 43 ++++ .../directive_processor/rewriter.dart | 207 ++++++++++++++++++ .../directive_processor/transformer.dart | 54 +++++ .../directive_processor/visitors.dart | 181 +++++++++++++++ .../annotation_processor.dart | 0 .../transform/{ => in_progress}/codegen.dart | 13 +- .../{ => in_progress}/find_bootstrap.dart | 0 .../find_reflection_capabilities.dart | 2 +- .../{ => in_progress}/resolvers.dart | 0 .../{ => in_progress}/traversal.dart | 2 +- modules/angular2/src/transform/options.dart | 41 ---- .../reflection_remover/ast_tester.dart | 36 +++ .../transform/reflection_remover/codegen.dart | 89 ++++++++ .../remove_reflection_capabilities.dart | 21 ++ .../reflection_remover/rewriter.dart | 127 +++++++++++ .../reflection_remover/transformer.dart | 47 ++++ .../angular2/src/transform/transformer.dart | 141 ++---------- .../basic_bind_files/expected/bar.ngDeps.dart | 27 +++ .../expected/index.bootstrap.dart | 26 --- .../transform/chained_deps_files/bar.dart | 10 + .../expected/bar.ngDeps.dart | 23 ++ .../expected/foo.ngDeps.dart | 16 ++ .../expected/index.ngDeps.dart | 13 ++ .../transform/chained_deps_files/foo.dart | 8 + .../transform/chained_deps_files/index.dart | 10 + .../{common.dart => common/application.dart} | 0 .../{ => common}/reflection_capabilities.dart | 0 .../expected/bar.ngDeps.dart | 19 ++ .../expected/index.bootstrap.dart | 17 -- .../reflection_remover_files/README.md | 7 + .../expected/index.dart | 22 ++ .../reflection_remover_files/index.dart | 10 + .../expected/bar.ngDeps.dart | 16 ++ .../expected/index.bootstrap.dart | 14 -- .../expected/index.dart | 4 +- .../expected/index.ngDeps.dart | 15 ++ .../expected/bar.ngDeps.dart | 16 ++ .../expected/index.bootstrap.dart | 14 -- .../test/transform/transform.server.spec.dart | 80 ++++--- .../expected/bar.ngDeps.dart | 20 ++ .../expected/index.bootstrap.dart | 18 -- .../two_deps_files/expected/bar.ngDeps.dart | 20 ++ .../expected/index.bootstrap.dart | 16 -- 52 files changed, 1530 insertions(+), 318 deletions(-) create mode 100644 modules/angular2/src/transform/bind_generator/generator.dart create mode 100644 modules/angular2/src/transform/bind_generator/transformer.dart create mode 100644 modules/angular2/src/transform/common/formatter.dart rename modules/angular2/src/transform/{ => common}/logging.dart (64%) create mode 100644 modules/angular2/src/transform/common/names.dart create mode 100644 modules/angular2/src/transform/common/ngdata.dart create mode 100644 modules/angular2/src/transform/common/options.dart create mode 100644 modules/angular2/src/transform/common/visitor_mixin.dart create mode 100644 modules/angular2/src/transform/directive_linker/linker.dart create mode 100644 modules/angular2/src/transform/directive_linker/transformer.dart create mode 100644 modules/angular2/src/transform/directive_processor/rewriter.dart create mode 100644 modules/angular2/src/transform/directive_processor/transformer.dart create mode 100644 modules/angular2/src/transform/directive_processor/visitors.dart rename modules/angular2/src/transform/{ => in_progress}/annotation_processor.dart (100%) rename modules/angular2/src/transform/{ => in_progress}/codegen.dart (98%) rename modules/angular2/src/transform/{ => in_progress}/find_bootstrap.dart (100%) rename modules/angular2/src/transform/{ => in_progress}/find_reflection_capabilities.dart (99%) rename modules/angular2/src/transform/{ => in_progress}/resolvers.dart (100%) rename modules/angular2/src/transform/{ => in_progress}/traversal.dart (98%) delete mode 100644 modules/angular2/src/transform/options.dart create mode 100644 modules/angular2/src/transform/reflection_remover/ast_tester.dart create mode 100644 modules/angular2/src/transform/reflection_remover/codegen.dart create mode 100644 modules/angular2/src/transform/reflection_remover/remove_reflection_capabilities.dart create mode 100644 modules/angular2/src/transform/reflection_remover/rewriter.dart create mode 100644 modules/angular2/src/transform/reflection_remover/transformer.dart create mode 100644 modules/angular2/test/transform/basic_bind_files/expected/bar.ngDeps.dart delete mode 100644 modules/angular2/test/transform/basic_bind_files/expected/index.bootstrap.dart create mode 100644 modules/angular2/test/transform/chained_deps_files/bar.dart create mode 100644 modules/angular2/test/transform/chained_deps_files/expected/bar.ngDeps.dart create mode 100644 modules/angular2/test/transform/chained_deps_files/expected/foo.ngDeps.dart create mode 100644 modules/angular2/test/transform/chained_deps_files/expected/index.ngDeps.dart create mode 100644 modules/angular2/test/transform/chained_deps_files/foo.dart create mode 100644 modules/angular2/test/transform/chained_deps_files/index.dart rename modules/angular2/test/transform/{common.dart => common/application.dart} (100%) rename modules/angular2/test/transform/{ => common}/reflection_capabilities.dart (100%) create mode 100644 modules/angular2/test/transform/list_of_types_files/expected/bar.ngDeps.dart delete mode 100644 modules/angular2/test/transform/list_of_types_files/expected/index.bootstrap.dart create mode 100644 modules/angular2/test/transform/reflection_remover_files/README.md create mode 100644 modules/angular2/test/transform/reflection_remover_files/expected/index.dart create mode 100644 modules/angular2/test/transform/reflection_remover_files/index.dart create mode 100644 modules/angular2/test/transform/simple_annotation_files/expected/bar.ngDeps.dart delete mode 100644 modules/angular2/test/transform/simple_annotation_files/expected/index.bootstrap.dart create mode 100644 modules/angular2/test/transform/simple_annotation_files/expected/index.ngDeps.dart create mode 100644 modules/angular2/test/transform/synthetic_ctor_files/expected/bar.ngDeps.dart delete mode 100644 modules/angular2/test/transform/synthetic_ctor_files/expected/index.bootstrap.dart create mode 100644 modules/angular2/test/transform/two_annotations_files/expected/bar.ngDeps.dart delete mode 100644 modules/angular2/test/transform/two_annotations_files/expected/index.bootstrap.dart create mode 100644 modules/angular2/test/transform/two_deps_files/expected/bar.ngDeps.dart delete mode 100644 modules/angular2/test/transform/two_deps_files/expected/index.bootstrap.dart diff --git a/modules/angular2/src/transform/bind_generator/generator.dart b/modules/angular2/src/transform/bind_generator/generator.dart new file mode 100644 index 0000000000..e96b32701d --- /dev/null +++ b/modules/angular2/src/transform/bind_generator/generator.dart @@ -0,0 +1,92 @@ +library angular2.src.transform.bind_generator.generator; + +import 'package:analyzer/analyzer.dart'; +import 'package:analyzer/src/generated/java_core.dart'; +import 'package:angular2/src/transform/common/logging.dart'; +import 'package:angular2/src/transform/common/names.dart'; +import 'package:angular2/src/transform/common/visitor_mixin.dart'; + +String createNgSetters(String code, String path) { + if (_noSettersPresent(code)) return code; + + var writer = new PrintStringWriter(); + parseCompilationUnit( + code, name: path).accept(new CreateNgSettersVisitor(writer)); + return writer.toString(); +} + +bool _noSettersPresent(String code) => code.indexOf('bind') < 0; + +class CreateNgSettersVisitor extends ToSourceVisitor with VisitorMixin { + final PrintWriter writer; + final _ExtractSettersVisitor extractVisitor = new _ExtractSettersVisitor(); + + CreateNgSettersVisitor(PrintWriter writer) + : this.writer = writer, + super(writer); + + @override + Object visitMethodInvocation(MethodInvocation node) { + var isRegisterType = node.methodName.toString() == REGISTER_TYPE_METHOD_NAME; + // The first argument to a `registerType` call is the type. + extractVisitor.currentName = node.argumentList.arguments[0] is Identifier + ? node.argumentList.arguments[0] + : null; + extractVisitor.bindPieces.clear(); + + var retVal = super.visitMethodInvocation(node); + if (isRegisterType) { + if (extractVisitor.bindPieces.isNotEmpty) { + writer.print('..${REGISTER_SETTERS_METHOD_NAME}({'); + writer.print(extractVisitor.bindPieces.join(',')); + writer.print('})'); + } + } + return retVal; + } + + @override + Object visitMapLiteralEntry(MapLiteralEntry node) { + if (node.key is StringLiteral && + stringLiteralToString(node.key) == 'annotations') { + node.value.accept(extractVisitor); + } + return super.visitMapLiteralEntry(node); + } +} + +/// Visitor responsible for crawling the "annotations" value in a +/// `registerType` call and generating setters from any "bind" values found. +class _ExtractSettersVisitor extends Object + with RecursiveAstVisitor, VisitorMixin { + final List bindPieces = []; + Identifier currentName = null; + + void _extractFromMapLiteral(MapLiteral map) { + if (currentName == null) { + logger.error('Unexpected code path: `currentName` should never be null'); + } + map.entries.forEach((entry) { + // TODO(kegluneq): Remove this restriction + if (entry.key is SimpleStringLiteral) { + var propName = entry.key.value; + bindPieces.add('"${propName}": (' + '${currentName} o, String value) => o.${propName} = value'); + } else { + logger.error('`bind` currently only supports string literals'); + } + }); + } + + @override + Object visitNamedExpression(NamedExpression node) { + if (node.name.label.toString() == 'bind') { + // TODO(kegluneq): Remove this restriction. + if (node.expression is MapLiteral) { + _extractFromMapLiteral(node.expression); + } + return null; + } + return super.visitNamedExpression(node); + } +} diff --git a/modules/angular2/src/transform/bind_generator/transformer.dart b/modules/angular2/src/transform/bind_generator/transformer.dart new file mode 100644 index 0000000000..b932610b98 --- /dev/null +++ b/modules/angular2/src/transform/bind_generator/transformer.dart @@ -0,0 +1,42 @@ +library angular2.src.transform.bind_generator.transformer; + +import 'dart:async'; +import 'package:angular2/src/transform/common/formatter.dart'; +import 'package:angular2/src/transform/common/logging.dart' as log; +import 'package:angular2/src/transform/common/names.dart'; +import 'package:angular2/src/transform/common/options.dart'; +import 'package:barback/barback.dart'; + +import 'generator.dart'; + +/// Transformer responsible for reading .ngDeps.dart files and generating +/// setters from the "annotations" information in the generated +/// `registerType` calls. +/// +/// These setters are registered in the same `setupReflection` function with +/// the `registerType` calls. +class BindGenerator extends Transformer { + final TransformerOptions options; + + BindGenerator(this.options); + + @override + bool isPrimary(AssetId id) => id.path.endsWith(DEPS_EXTENSION); + + @override + Future apply(Transform transform) async { + log.init(transform); + + try { + var assetCode = await transform.primaryInput.readAsString(); + var assetPath = transform.primaryInput.id.path; + var transformedCode = createNgSetters(assetCode, assetPath); + transform.addOutput(new Asset.fromString(transform.primaryInput.id, + formatter.format(transformedCode, uri: assetPath))); + } catch (ex, stackTrace) { + log.logger.error('Creating ng setters failed.\n' + 'Exception: $ex\n' + 'Stack Trace: $stackTrace'); + } + } +} diff --git a/modules/angular2/src/transform/common/formatter.dart b/modules/angular2/src/transform/common/formatter.dart new file mode 100644 index 0000000000..152474e2c2 --- /dev/null +++ b/modules/angular2/src/transform/common/formatter.dart @@ -0,0 +1,22 @@ +library angular2.src.transform.common.formatter; + +import 'package:dart_style/dart_style.dart'; + +import 'logging.dart'; + +DartFormatter _formatter; + +void init(DartFormatter formatter) { + if (_formatter != null) { + logger.warning('Formatter is being overwritten.'); + } + _formatter = formatter; +} + +DartFormatter get formatter { + if (_formatter == null) { + logger.info('Formatter never initialized, using default formatter.'); + _formatter = new DartFormatter(); + } + return _formatter; +} diff --git a/modules/angular2/src/transform/logging.dart b/modules/angular2/src/transform/common/logging.dart similarity index 64% rename from modules/angular2/src/transform/logging.dart rename to modules/angular2/src/transform/common/logging.dart index 831e208f6b..90892e63fa 100644 --- a/modules/angular2/src/transform/logging.dart +++ b/modules/angular2/src/transform/common/logging.dart @@ -1,4 +1,4 @@ -library angular2.src.transform.logging; +library angular2.src.transform.common.logging; import 'package:barback/barback.dart'; import 'package:code_transformers/messages/build_logger.dart'; @@ -7,12 +7,7 @@ BuildLogger _logger; /// Prepares [logger] for use throughout the transformer. void init(Transform t) { - if (_logger == null) { - _logger = new BuildLogger(t); - } else { - _logger.fine('Attempted to initialize logger multiple times.', - asset: t.primaryInput.id); - } + _logger = new BuildLogger(t); } /// The logger the transformer should use for messaging. diff --git a/modules/angular2/src/transform/common/names.dart b/modules/angular2/src/transform/common/names.dart new file mode 100644 index 0000000000..8c2f44461d --- /dev/null +++ b/modules/angular2/src/transform/common/names.dart @@ -0,0 +1,7 @@ +library angular2.src.transform.common.names; + +const SETUP_METHOD_NAME = 'setupReflection'; +const REFLECTOR_VAR_NAME = 'reflector'; +const DEPS_EXTENSION = '.ngDeps.dart'; +const REGISTER_TYPE_METHOD_NAME = 'registerType'; +const REGISTER_SETTERS_METHOD_NAME = 'registerSetters'; diff --git a/modules/angular2/src/transform/common/ngdata.dart b/modules/angular2/src/transform/common/ngdata.dart new file mode 100644 index 0000000000..aa6ce36c29 --- /dev/null +++ b/modules/angular2/src/transform/common/ngdata.dart @@ -0,0 +1,35 @@ +library angular2.src.transform.common.ng_data; + +import 'dart:convert'; + +const NG_DATA_VERSION = 1; + +class NgData extends Object { + int importOffset = 0; + int registerOffset = 0; + List imports = []; + + NgData(); + + factory NgData.fromJson(String json) { + var data = JSON.decode(json); + return new NgData() + ..importOffset = data['importOffset'] + ..registerOffset = data['registerOffset'] + ..imports = data['imports']; + } + + String toJson() { + return JSON.encode({ + 'version': NG_DATA_VERSION, + 'importOffset': importOffset, + 'registerOffset': registerOffset, + 'imports': imports + }); + } + + @override + String toString() { + return '[NgData: ${toJson()}]'; + } +} diff --git a/modules/angular2/src/transform/common/options.dart b/modules/angular2/src/transform/common/options.dart new file mode 100644 index 0000000000..6d5470d0e2 --- /dev/null +++ b/modules/angular2/src/transform/common/options.dart @@ -0,0 +1,29 @@ +library angular2.src.transform.common.options; + +const ENTRY_POINT_PARAM = 'entry_point'; +const REFLECTION_ENTRY_POINT_PARAM = 'reflection_entry_point'; + +/// Provides information necessary to transform an Angular2 app. +class TransformerOptions { + /// The path to the file where the application's call to [bootstrap] is. + // TODO(kegluneq): Allow multiple entry points. + final String entryPoint; + + /// The reflection entry point, that is, the path to the file where the + /// application's [ReflectionCapabilities] are set. + final String reflectionEntryPoint; + + TransformerOptions._internal(this.entryPoint, this.reflectionEntryPoint); + + factory TransformerOptions(String entryPoint, {String reflectionEntryPoint}) { + if (entryPoint == null) { + throw new ArgumentError.notNull(ENTRY_POINT_PARAM); + } else if (entryPoint.isEmpty) { + throw new ArgumentError.value(entryPoint, 'entryPoint'); + } + if (reflectionEntryPoint == null || entryPoint.isEmpty) { + reflectionEntryPoint = entryPoint; + } + return new TransformerOptions._internal(entryPoint, reflectionEntryPoint); + } +} diff --git a/modules/angular2/src/transform/common/visitor_mixin.dart b/modules/angular2/src/transform/common/visitor_mixin.dart new file mode 100644 index 0000000000..28fa989748 --- /dev/null +++ b/modules/angular2/src/transform/common/visitor_mixin.dart @@ -0,0 +1,111 @@ +library angular2.src.transform.common; + +import 'package:analyzer/analyzer.dart'; +import 'package:analyzer/src/generated/java_core.dart'; +import 'package:analyzer/src/generated/scanner.dart'; + +/// Visitor providing common methods for concrete implementations. +class VisitorMixin { + PrintWriter writer; + + /** + * Visit the given function body, printing the prefix before if given body is not empty. + * + * @param prefix the prefix to be printed if there is a node to visit + * @param body the function body to be visited + */ + void visitFunctionWithPrefix(String prefix, FunctionBody body) { + if (body is! EmptyFunctionBody) { + writer.print(prefix); + } + visitNode(body); + } + + /// Safely visit [node]. + void visitNode(AstNode node) { + if (node != null) { + node.accept(this); + } + } + + /// Print a list of [nodes] without any separation. + void visitNodeList(NodeList nodes) { + visitNodeListWithSeparator(nodes, ""); + } + + /// Print a list of [nodes], separated by the given [separator]. + void visitNodeListWithSeparator(NodeList nodes, String separator) { + if (nodes != null) { + int size = nodes.length; + for (int i = 0; i < size; i++) { + if (i > 0) { + writer.print(separator); + } + nodes[i].accept(this); + } + } + } + + /// Print a list of [nodes], separated by the given [separator] and + /// preceded by the given [prefix]. + void visitNodeListWithSeparatorAndPrefix( + String prefix, NodeList nodes, String separator) { + if (nodes != null) { + int size = nodes.length; + if (size > 0) { + writer.print(prefix); + for (int i = 0; i < size; i++) { + if (i > 0) { + writer.print(separator); + } + nodes[i].accept(this); + } + } + } + } + + /// Print a list of [nodes], separated by the given [separator] and + /// succeeded by the given [suffix]. + void visitNodeListWithSeparatorAndSuffix( + NodeList nodes, String separator, String suffix) { + if (nodes != null) { + int size = nodes.length; + if (size > 0) { + for (int i = 0; i < size; i++) { + if (i > 0) { + writer.print(separator); + } + nodes[i].accept(this); + } + writer.print(suffix); + } + } + } + + /// If [node] is null does nothing. Otherwise, prints [prefix], then + /// visits [node]. + void visitNodeWithPrefix(String prefix, AstNode node) { + if (node != null) { + writer.print(prefix); + node.accept(this); + } + } + + /// If [node] is null does nothing. Otherwise, visits [node], then prints + /// [suffix]. + void visitNodeWithSuffix(AstNode node, String suffix) { + if (node != null) { + node.accept(this); + writer.print(suffix); + } + } + + /// Safely visit [node], printing [suffix] after the node if it is + /// non-`null`. + void visitTokenWithSuffix(Token token, String suffix) { + if (token != null) { + writer.print(token.lexeme); + writer.print(suffix); + } + } +} diff --git a/modules/angular2/src/transform/directive_linker/linker.dart b/modules/angular2/src/transform/directive_linker/linker.dart new file mode 100644 index 0000000000..ae8023e839 --- /dev/null +++ b/modules/angular2/src/transform/directive_linker/linker.dart @@ -0,0 +1,56 @@ +library angular2.src.transform.directive_linker.linker; + +import 'dart:async'; + +import 'package:angular2/src/transform/common/logging.dart'; +import 'package:angular2/src/transform/common/names.dart'; +import 'package:angular2/src/transform/common/ngdata.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/assets.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:path/path.dart' as path; + +Future linkNgDeps(Transform transform, String code, String path) async { + var commentIdx = code.lastIndexOf('//'); + if (commentIdx < 0) return code; + + var ngData = new NgData.fromJson(code.substring(commentIdx + 2)); + + StringBuffer importBuf = + new StringBuffer(code.substring(0, ngData.importOffset)); + StringBuffer declarationBuf = new StringBuffer( + code.substring(ngData.importOffset, ngData.registerOffset)); + String tail = code.substring(ngData.registerOffset, commentIdx); + + var ngDeps = await _processNgImports(transform, ngData.imports); + + for (var i = 0; i < ngDeps.length; ++i) { + importBuf.write('import \'${ngDeps[i]}\' as i${i};'); + declarationBuf.write('i${i}.${SETUP_METHOD_NAME}(${REFLECTOR_VAR_NAME});'); + } + + return '${importBuf}${declarationBuf}${tail}'; +} + +String _toDepsUri(String importUri) => + '${path.withoutExtension(importUri)}${DEPS_EXTENSION}'; + +bool _isNotDartImport(String importUri) { + return !importUri.startsWith('dart:'); +} + +Future> _processNgImports( + Transform transform, List imports) async { + var retVal = []; + + return Future + .wait(imports.where(_isNotDartImport).map(_toDepsUri).map((ngDepsUri) { + var importAsset = uriToAssetId( + transform.primaryInput.id, ngDepsUri, logger, null /* span */); + return transform.hasInput(importAsset).then((hasInput) { + if (hasInput) { + retVal.add(ngDepsUri); + } + }); + })).then((_) => retVal); +} diff --git a/modules/angular2/src/transform/directive_linker/transformer.dart b/modules/angular2/src/transform/directive_linker/transformer.dart new file mode 100644 index 0000000000..ea37b0c7c5 --- /dev/null +++ b/modules/angular2/src/transform/directive_linker/transformer.dart @@ -0,0 +1,43 @@ +library angular2.src.transform.directive_linker.transformer; + +import 'dart:async'; + +import 'package:angular2/src/transform/common/formatter.dart'; +import 'package:angular2/src/transform/common/logging.dart' as log; +import 'package:angular2/src/transform/common/names.dart'; +import 'package:angular2/src/transform/common/options.dart'; +import 'package:barback/barback.dart'; + +import 'linker.dart'; + +/// Transformer responsible for processing .ngDeps.dart files created by +/// [DirectiveProcessor] and ensuring that the generated calls to +/// `setupReflection` call the necessary `setupReflection` method in all +/// dependencies. +class DirectiveLinker extends Transformer { + final TransformerOptions options; + + DirectiveLinker(this.options); + + @override + bool isPrimary(AssetId id) => id.path.endsWith(DEPS_EXTENSION); + + @override + Future apply(Transform transform) async { + log.init(transform); + + try { + var assetCode = await transform.primaryInput.readAsString(); + var assetPath = transform.primaryInput.id.path; + var transformedCode = await linkNgDeps(transform, assetCode, assetPath); + var formattedCode = formatter.format(transformedCode, uri: assetPath); + transform.addOutput( + new Asset.fromString(transform.primaryInput.id, formattedCode)); + } catch (ex, stackTrace) { + log.logger.error('Linking ng directives failed.\n' + 'Exception: $ex\n' + 'Stack Trace: $stackTrace'); + } + return null; + } +} diff --git a/modules/angular2/src/transform/directive_processor/rewriter.dart b/modules/angular2/src/transform/directive_processor/rewriter.dart new file mode 100644 index 0000000000..112bf9c15d --- /dev/null +++ b/modules/angular2/src/transform/directive_processor/rewriter.dart @@ -0,0 +1,207 @@ +library angular2.src.transform.directive_processor; + +import 'package:analyzer/analyzer.dart'; +import 'package:analyzer/src/generated/java_core.dart'; +import 'package:angular2/src/transform/common/logging.dart'; +import 'package:angular2/src/transform/common/names.dart'; +import 'package:angular2/src/transform/common/ngdata.dart'; +import 'package:angular2/src/transform/common/visitor_mixin.dart'; +import 'package:path/path.dart' as path; + +import 'visitors.dart'; + +/// Generates a file registering all Angular 2 `Directive`s found in [code] in +/// ngDeps format [TODO(kegluneq): documentation reference needed]. [path] is +/// the path to the file (or asset) containing [code]. +/// +/// If no Angular 2 `Directive`s are found in [code], returns the empty +/// string unless [forceGenerate] is true, in which case an empty ngDeps +/// file is created. +String createNgDeps(String code, String path, {bool forceGenerate: false}) { + // TODO(kegluneq): Shortcut if we can determine that there are no + // [Directive]s present. + var writer = new PrintStringWriter(); + var visitor = new CreateNgDepsVisitor(writer, path); + parseCompilationUnit(code, name: path).accept(visitor); + if (visitor.foundNgDirectives || forceGenerate) { + return writer.toString(); + } else { + return ''; + } +} + +/// Visitor responsible for processing [CompilationUnit] and creating an +/// associated .ngDeps.dart file. +class CreateNgDepsVisitor extends Object + with SimpleAstVisitor, VisitorMixin { + final PrintWriter writer; + final _Tester _tester = const _Tester(); + bool foundNgDirectives = false; + bool wroteImport = false; + final ToSourceVisitor _copyVisitor; + final FactoryTransformVisitor _factoryVisitor; + final ParameterTransformVisitor _paramsVisitor; + final AnnotationsTransformVisitor _metaVisitor; + final NgData _ngData = new NgData(); + + /// The path to the file which we are parsing. + final String importPath; + + CreateNgDepsVisitor(PrintWriter writer, this.importPath) + : writer = writer, + _copyVisitor = new ToSourceVisitor(writer), + _factoryVisitor = new FactoryTransformVisitor(writer), + _paramsVisitor = new ParameterTransformVisitor(writer), + _metaVisitor = new AnnotationsTransformVisitor(writer); + + @override + void visitCompilationUnit(CompilationUnit node) { + visitNodeListWithSeparator(node.directives, " "); + _openFunctionWrapper(); + visitNodeListWithSeparator(node.declarations, " "); + _closeFunctionWrapper(); + return null; + } + + void _writeImport() { + writer.print('import \'${path.basename(importPath)}\';'); + } + + @override + Object visitImportDirective(ImportDirective node) { + if (!wroteImport) { + _writeImport(); + wroteImport = true; + } + _ngData.imports.add(node.uri.stringValue); + return node.accept(_copyVisitor); + } + + @override + Object visitExportDirective(ExportDirective node) { + _ngData.imports.add(node.uri.stringValue); + return node.accept(_copyVisitor); + } + + void _openFunctionWrapper() { + // TODO(kegluneq): Use a [PrintWriter] with a length getter. + _ngData.importOffset = writer.toString().length; + writer.print('bool _visited = false;' + 'void ${SETUP_METHOD_NAME}(${REFLECTOR_VAR_NAME}) {' + 'if (_visited) return; _visited = true;'); + } + + void _closeFunctionWrapper() { + if (foundNgDirectives) { + writer.print(';'); + } + // TODO(kegluneq): Use a [PrintWriter] with a length getter. + _ngData.registerOffset = writer.toString().length; + writer.print('}'); + writer.print('// ${_ngData.toJson()}'); + } + + ConstructorDeclaration _getCtor(ClassDeclaration node) { + int numCtorsFound = 0; + var ctor = null; + + for (ClassMember classMember in node.members) { + if (classMember is ConstructorDeclaration) { + numCtorsFound++; + ConstructorDeclaration constructor = classMember; + + // Use the unnnamed constructor if it is present. + // Otherwise, use the first encountered. + if (ctor == null) { + ctor = constructor; + } else if (constructor.name == null) { + ctor = constructor; + } + } + } + if (numCtorsFound > 1) { + var ctorName = ctor.name; + ctorName = ctorName == null + ? 'the unnamed constructor' + : 'constructor "${ctorName}"'; + logger.warning('Found ${numCtorsFound} ctors for class ${node.name},' + 'Using ${ctorName}.'); + } + return ctor; + } + + void _generateEmptyFactory(String typeName) { + writer.print('() => new ${typeName}()'); + } + + void _generateEmptyParams() => writer.print('const []'); + + @override + Object visitClassDeclaration(ClassDeclaration node) { + var shouldProcess = node.metadata.any(_tester._isDirective); + + if (shouldProcess) { + var ctor = _getCtor(node); + + if (!foundNgDirectives) { + // The receiver for cascaded calls. + writer.print(REFLECTOR_VAR_NAME); + foundNgDirectives = true; + } + writer.print('..registerType('); + visitNode(node.name); + writer.print(', {"factory": '); + if (ctor == null) { + _generateEmptyFactory(node.name.toString()); + } else { + ctor.accept(_factoryVisitor); + } + writer.print(', "parameters": '); + if (ctor == null) { + _generateEmptyParams(); + } else { + ctor.accept(_paramsVisitor); + } + writer.print(', "annotations": '); + node.accept(_metaVisitor); + writer.print('})'); + + return null; + } + } + + Object _nodeToSource(AstNode node) { + if (node == null) return null; + return node.accept(_copyVisitor); + } + + @override + Object visitLibraryDirective(LibraryDirective node) => _nodeToSource(node); + + @override + Object visitPartOfDirective(PartOfDirective node) { + // TODO(kegluneq): Consider importing [node.libraryName]. + logger.warning('[${importPath}]: ' + 'Found `part of` directive while generating ${DEPS_EXTENSION} file, ' + 'Transform may fail due to missing imports in generated file.'); + return null; + } + + @override + Object visitPrefixedIdentifier(PrefixedIdentifier node) => + _nodeToSource(node); + + @override + Object visitSimpleIdentifier(SimpleIdentifier node) => _nodeToSource(node); +} + +class _Tester { + const _Tester(); + + bool _isDirective(Annotation meta) { + var metaName = meta.name.toString(); + return metaName == 'Component' || + metaName == 'Decorator' || + metaName == 'Template'; + } +} diff --git a/modules/angular2/src/transform/directive_processor/transformer.dart b/modules/angular2/src/transform/directive_processor/transformer.dart new file mode 100644 index 0000000000..08950e04e5 --- /dev/null +++ b/modules/angular2/src/transform/directive_processor/transformer.dart @@ -0,0 +1,54 @@ +library angular2.src.transform.directive_processor.transformer; + +import 'dart:async'; + +import 'package:angular2/src/transform/common/formatter.dart'; +import 'package:angular2/src/transform/common/logging.dart' as log; +import 'package:angular2/src/transform/common/names.dart'; +import 'package:angular2/src/transform/common/options.dart'; +import 'package:barback/barback.dart'; + +import 'rewriter.dart'; + +/// Transformer responsible for processing all .dart assets and creating +/// .ngDeps.dart files which register @Injectable annotated classes with the +/// reflector. +/// +/// This will also create .ngDeps.dart files for classes annotated +/// with @Component, @Template, @Decorator, etc. +/// +/// This transformer is the first phase in a two-phase transform. It should +/// be followed by [DirectiveLinker]. +class DirectiveProcessor extends Transformer { + final TransformerOptions options; + + DirectiveProcessor(this.options); + + @override + bool isPrimary(AssetId id) => id.extension.endsWith('dart'); + + @override + Future apply(Transform transform) async { + log.init(transform); + + try { + var assetCode = await transform.primaryInput.readAsString(); + var ngDepsSrc = createNgDeps(assetCode, transform.primaryInput.id.path, + forceGenerate: transform.primaryInput.id.path == options.entryPoint); + if (ngDepsSrc != null && ngDepsSrc.isNotEmpty) { + var ngDepsAssetId = + transform.primaryInput.id.changeExtension(DEPS_EXTENSION); + var exists = await transform.hasInput(ngDepsAssetId); + if (exists) { + log.logger.error('Clobbering ${ngDepsAssetId}. ' + 'This probably will not end well'); + } + transform.addOutput(new Asset.fromString(ngDepsAssetId, ngDepsSrc)); + } + } catch (ex, stackTrace) { + log.logger.warning('Processing ng directives failed.\n' + 'Exception: $ex\n' + 'Stack Trace: $stackTrace'); + } + } +} diff --git a/modules/angular2/src/transform/directive_processor/visitors.dart b/modules/angular2/src/transform/directive_processor/visitors.dart new file mode 100644 index 0000000000..720e0a3218 --- /dev/null +++ b/modules/angular2/src/transform/directive_processor/visitors.dart @@ -0,0 +1,181 @@ +library angular2.src.transform.directive_processor; + +import 'package:analyzer/analyzer.dart'; +import 'package:analyzer/src/generated/java_core.dart'; +import 'package:angular2/src/transform/common/logging.dart'; +import 'package:angular2/src/transform/common/visitor_mixin.dart'; + +/// SourceVisitor designed to accept [ConstructorDeclaration] nodes. +class _CtorTransformVisitor extends ToSourceVisitor with VisitorMixin { + bool _withParameterTypes = true; + bool _withParameterNames = true; + final PrintWriter writer; + + /// Maps field names to their declared types. This is populated whenever + /// the listener visits a [ConstructorDeclaration] node. + final Map _fieldNameToType = {}; + + _CtorTransformVisitor(PrintWriter writer) + : this.writer = writer, + super(writer); + + /// If [_withParameterTypes] is true, this method outputs [node]'s type. If + /// [_withParameterNames] is true, this method outputs [node]'s identifier. + Object _visitNormalFormalParameter(TypeName type, SimpleIdentifier name) { + if (_withParameterTypes) { + visitNodeWithSuffix(type, ' '); + } + if (_withParameterNames) { + visitNode(name); + } + return null; + } + + void _buildFieldMap(ConstructorDeclaration node) { + ClassDeclaration clazz = + node.getAncestor((node) => node is ClassDeclaration); + _fieldNameToType.clear(); + clazz.members.where((member) => member is FieldDeclaration).forEach( + (FieldDeclaration field) { + var type = field.fields.type; + if (type != null) { + field.fields.variables.forEach((VariableDeclaration decl) { + _fieldNameToType[decl.name.toString()] = type; + }); + } + }); + } + + @override + Object visitSimpleFormalParameter(SimpleFormalParameter node) { + return _visitNormalFormalParameter(node.type, node.identifier); + } + + @override + Object visitFieldFormalParameter(FieldFormalParameter node) { + if (node.parameters != null) { + logger.error('Parameters in ctor not supported ' + '(${node.toSource()})'); + } + var type = node.type; + if (type == null) { + type = _fieldNameToType[node.identifier.toString()]; + } + return _visitNormalFormalParameter(type, node.identifier); + } + + @override + Object visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) { + logger.error('Function typed formal parameters not supported ' + '(${node.toSource()})'); + return _visitNormalFormalParameter(null, node.identifier); + } + + @override + Object visitDefaultFormalParameter(DefaultFormalParameter node) { + visitNode(node.parameter); + // Ignore the declared default value. + return null; + } + + @override + /// Overridden to avoid outputting grouping operators for default parameters. + Object visitFormalParameterList(FormalParameterList node) { + writer.print('('); + NodeList parameters = node.parameters; + int size = parameters.length; + for (int i = 0; i < size; i++) { + if (i > 0) { + writer.print(', '); + } + parameters[i].accept(this); + } + writer.print(')'); + return null; + } +} + +/// ToSourceVisitor designed to print 'parameters' values for Angular2's +/// [registerType] calls. +class ParameterTransformVisitor extends _CtorTransformVisitor { + ParameterTransformVisitor(PrintWriter writer) : super(writer); + + @override + Object visitConstructorDeclaration(ConstructorDeclaration node) { + _buildFieldMap(node); + _withParameterNames = false; + _withParameterTypes = true; + writer.print('const ['); + visitNode(node.parameters); + writer.print(']'); + return null; + } + + @override + Object visitFormalParameterList(FormalParameterList node) { + NodeList parameters = node.parameters; + int size = parameters.length; + for (int i = 0; i < size; i++) { + if (i > 0) { + writer.print(', '); + } + // TODO(kegluneq): Include annotations on parameters. + writer.print('const ['); + parameters[i].accept(this); + writer.print(']'); + } + return null; + } +} + +/// ToSourceVisitor designed to print 'factory' values for Angular2's +/// [registerType] calls. +class FactoryTransformVisitor extends _CtorTransformVisitor { + FactoryTransformVisitor(PrintWriter writer) : super(writer); + + @override + Object visitConstructorDeclaration(ConstructorDeclaration node) { + _buildFieldMap(node); + _withParameterNames = true; + _withParameterTypes = true; + visitNode(node.parameters); + writer.print(' => new '); + visitNode(node.returnType); + visitNodeWithPrefix(".", node.name); + _withParameterTypes = false; + visitNode(node.parameters); + return null; + } +} + +/// ToSourceVisitor designed to print a [ClassDeclaration] node as a +/// 'annotations' value for Angular2's [registerType] calls. +class AnnotationsTransformVisitor extends ToSourceVisitor with VisitorMixin { + final PrintWriter writer; + AnnotationsTransformVisitor(PrintWriter writer) + : this.writer = writer, + super(writer); + + @override + Object visitClassDeclaration(ClassDeclaration node) { + writer.print('const ['); + var size = node.metadata.length; + for (var i = 0; i < size; ++i) { + if (i > 0) { + writer.print(', '); + } + node.metadata[i].accept(this); + } + writer.print(']'); + return null; + } + + @override + Object visitAnnotation(Annotation node) { + writer.print('const '); + visitNode(node.name); + visitNodeWithPrefix(".", node.constructorName); + visitNode(node.arguments); + return null; + } +} diff --git a/modules/angular2/src/transform/annotation_processor.dart b/modules/angular2/src/transform/in_progress/annotation_processor.dart similarity index 100% rename from modules/angular2/src/transform/annotation_processor.dart rename to modules/angular2/src/transform/in_progress/annotation_processor.dart diff --git a/modules/angular2/src/transform/codegen.dart b/modules/angular2/src/transform/in_progress/codegen.dart similarity index 98% rename from modules/angular2/src/transform/codegen.dart rename to modules/angular2/src/transform/in_progress/codegen.dart index 28d9624a10..b623c6ce6b 100644 --- a/modules/angular2/src/transform/codegen.dart +++ b/modules/angular2/src/transform/in_progress/codegen.dart @@ -8,7 +8,7 @@ import 'package:dart_style/dart_style.dart'; import 'package:path/path.dart' as path; import 'annotation_processor.dart'; -import 'logging.dart'; +import 'common/logging.dart'; /// Base class that maintains codegen state. class Context { @@ -72,7 +72,7 @@ String codegenEntryPoint(Context context, {AssetId newEntryPoint}) { if (newEntryPoint == null) { throw new ArgumentError.notNull('newEntryPoint'); } - // TODO(jakemac): copyright and library declaration + // TODO(kegluneq): copyright declaration var outBuffer = new StringBuffer() ..write(_libraryDeclaration) ..write(_reflectorImport); @@ -195,11 +195,14 @@ class _DirectiveRegistryImpl implements DirectiveRegistry { _writer.print('..registerType('); _codegenClassTypeString(element); _writer.print(', {"factory": '); - _codegenFactoryProp(ctorNode, element); + _codegenClassTypeString(element); + _writer.print('.ngFactory'); _writer.print(', "parameters": '); - _codegenParametersProp(ctorNode); + _codegenClassTypeString(element); + _writer.print('.ngParameters'); _writer.print(', "annotations": '); - _codegenAnnotationsProp(entry.node); + _codegenClassTypeString(element); + _writer.print('.ngAnnotations'); _writer.print('})'); } diff --git a/modules/angular2/src/transform/find_bootstrap.dart b/modules/angular2/src/transform/in_progress/find_bootstrap.dart similarity index 100% rename from modules/angular2/src/transform/find_bootstrap.dart rename to modules/angular2/src/transform/in_progress/find_bootstrap.dart diff --git a/modules/angular2/src/transform/find_reflection_capabilities.dart b/modules/angular2/src/transform/in_progress/find_reflection_capabilities.dart similarity index 99% rename from modules/angular2/src/transform/find_reflection_capabilities.dart rename to modules/angular2/src/transform/in_progress/find_reflection_capabilities.dart index d6c0b88229..6c986874cf 100644 --- a/modules/angular2/src/transform/find_reflection_capabilities.dart +++ b/modules/angular2/src/transform/in_progress/find_reflection_capabilities.dart @@ -9,7 +9,7 @@ import 'package:dart_style/dart_style.dart'; import 'package:path/path.dart' as path; import 'codegen.dart'; -import 'logging.dart'; +import 'common/logging.dart'; import 'resolvers.dart'; /// Finds all calls to the Angular2 [ReflectionCapabilities] constructor diff --git a/modules/angular2/src/transform/resolvers.dart b/modules/angular2/src/transform/in_progress/resolvers.dart similarity index 100% rename from modules/angular2/src/transform/resolvers.dart rename to modules/angular2/src/transform/in_progress/resolvers.dart diff --git a/modules/angular2/src/transform/traversal.dart b/modules/angular2/src/transform/in_progress/traversal.dart similarity index 98% rename from modules/angular2/src/transform/traversal.dart rename to modules/angular2/src/transform/in_progress/traversal.dart index 1bbc1a890d..553f8a9759 100644 --- a/modules/angular2/src/transform/traversal.dart +++ b/modules/angular2/src/transform/in_progress/traversal.dart @@ -4,7 +4,7 @@ import 'package:analyzer/src/generated/ast.dart'; import 'package:analyzer/src/generated/element.dart'; import 'annotation_processor.dart'; -import 'logging.dart'; +import 'common/logging.dart'; import 'resolvers.dart'; /// Walks through an Angular2 application, finding all classes matching the diff --git a/modules/angular2/src/transform/options.dart b/modules/angular2/src/transform/options.dart deleted file mode 100644 index 3f64d80286..0000000000 --- a/modules/angular2/src/transform/options.dart +++ /dev/null @@ -1,41 +0,0 @@ -library angular2.src.transform; - -const entryPointParam = 'entry_point'; -const reflectionEntryPointParam = 'reflection_entry_point'; -const newEntryPointParam = 'new_entry_point'; - -/// Provides information necessary to transform an Angular2 app. -class TransformerOptions { - /// The file where the application's call to [bootstrap] is. - // TODO(kegluneq): Allow multiple entry points. - final String entryPoint; - - /// The reflection entry point, that is, where the - /// application's [ReflectionCapabilities] are set. - final String reflectionEntryPoint; - - /// The path where we should generate code. - final String newEntryPoint; - - TransformerOptions._internal( - this.entryPoint, this.reflectionEntryPoint, this.newEntryPoint); - - factory TransformerOptions(String entryPoint, - {String reflectionEntryPoint, String newEntryPoint}) { - if (entryPoint == null || entryPoint.isEmpty) { - throw new ArgumentError.notNull(entryPointParam); - } - if (reflectionEntryPoint == null || entryPoint.isEmpty) { - reflectionEntryPoint = entryPoint; - } - if (newEntryPoint == null || newEntryPoint.isEmpty) { - newEntryPoint = - reflectionEntryPoint.replaceFirst('.dart', '.bootstrap.dart'); - if (newEntryPoint == reflectionEntryPoint) { - newEntryPoint = 'bootstrap.${newEntryPoint}'; - } - } - return new TransformerOptions._internal( - entryPoint, reflectionEntryPoint, newEntryPoint); - } -} diff --git a/modules/angular2/src/transform/reflection_remover/ast_tester.dart b/modules/angular2/src/transform/reflection_remover/ast_tester.dart new file mode 100644 index 0000000000..5c30ba638d --- /dev/null +++ b/modules/angular2/src/transform/reflection_remover/ast_tester.dart @@ -0,0 +1,36 @@ +library angular2.src.transform.reflection_remover.ast_tester; + +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; + +/// An object that checks for [ReflectionCapabilities] syntactically, that is, +/// without resolution information. +class AstTester { + static const REFLECTION_CAPABILITIES_NAME = 'ReflectionCapabilities'; + + const AstTester(); + + bool isNewReflectionCapabilities(InstanceCreationExpression node) => + '${node.constructorName.type.name}' == REFLECTION_CAPABILITIES_NAME; + + bool isReflectionCapabilitiesImport(ImportDirective node) { + return node.uri.stringValue.endsWith("reflection_capabilities.dart"); + } +} + +/// An object that checks for [ReflectionCapabilities] using a fully resolved +/// Ast. +class ResolvedTester implements AstTester { + final ClassElement _forbiddenClass; + + ResolvedTester(this._forbiddenClass); + + bool isNewReflectionCapabilities(InstanceCreationExpression node) { + var typeElement = node.constructorName.type.name.bestElement; + return typeElement != null && typeElement == _forbiddenClass; + } + + bool isReflectionCapabilitiesImport(ImportDirective node) { + return node.uriElement == _forbiddenClass.library; + } +} diff --git a/modules/angular2/src/transform/reflection_remover/codegen.dart b/modules/angular2/src/transform/reflection_remover/codegen.dart new file mode 100644 index 0000000000..48312a622f --- /dev/null +++ b/modules/angular2/src/transform/reflection_remover/codegen.dart @@ -0,0 +1,89 @@ +library angular2.src.transform.reflection_remover.codegen; + +import 'package:analyzer/src/generated/ast.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:path/path.dart' as path; + +import 'package:angular2/src/transform/common/names.dart'; + +class Codegen { + static const _PREFIX_BASE = 'ngStaticInit'; + + /// The prefix used to import our generated file. + final String prefix; + /// The import uri + final String importUri; + + Codegen(String reflectionEntryPointPath, String newEntryPointPath, + {String prefix}) + : this.prefix = prefix == null ? _PREFIX_BASE : prefix, + importUri = path.relative(newEntryPointPath, + from: path.dirname(reflectionEntryPointPath)) { + if (this.prefix.isEmpty) throw new ArgumentError.value('(empty)', 'prefix'); + } + + factory Codegen.fromResolver( + Resolver resolver, AssetId reflectionEntryPoint, AssetId newEntryPoint) { + var lib = resolver.getLibrary(reflectionEntryPoint); + var prefix = _PREFIX_BASE; + var idx = 0; + while (lib.imports.any((import) { + return import.prefix != null && import.prefix == prefix; + })) { + prefix = '${_PREFIX_BASE}${idx++}'; + } + + return new Codegen(reflectionEntryPoint, newEntryPoint, prefix: prefix); + } + + /// Generates code to import the library containing the method which sets up + /// Angular2 reflection statically. + /// + /// The code generated here should follow the example of code generated for + /// an [ImportDirective] node. + String codegenImport() { + return 'import \'${importUri}\' as ${prefix};'; + } + + /// Generates code to call the method which sets up Angular2 reflection + /// statically. + /// + /// If [reflectorAssignment] is provided, it is expected to be the node + /// representing the [ReflectionCapabilities] assignment, and we will + /// attempt to parse the access of [reflector] from it so that [reflector] is + /// properly prefixed if necessary. + String codegenSetupReflectionCall( + {AssignmentExpression reflectorAssignment}) { + var reflectorExpression = null; + if (reflectorAssignment != null) { + reflectorExpression = reflectorAssignment.accept(new _ReflectorVisitor()); + } + if (reflectorExpression == null) { + reflectorExpression = 'reflector'; + } + + return '${prefix}.${SETUP_METHOD_NAME}(${reflectorExpression});'; + } +} + +/// A visitor whose job it is to find the access of [reflector]. +class _ReflectorVisitor extends Object with SimpleAstVisitor { + @override + Expression visitAssignmentExpression(AssignmentExpression node) { + if (node == null || node.leftHandSide == null) return null; + return node.leftHandSide.accept(this); + } + + @override + Expression visitPropertyAccess(PropertyAccess node) { + if (node == null || node.target == null) return; + return node.target; + } + + @override + Expression visitPrefixedIdentifier(PrefixedIdentifier node) { + if (node == null || node.prefix == null) return null; + return node.prefix; + } +} diff --git a/modules/angular2/src/transform/reflection_remover/remove_reflection_capabilities.dart b/modules/angular2/src/transform/reflection_remover/remove_reflection_capabilities.dart new file mode 100644 index 0000000000..23a137ae1b --- /dev/null +++ b/modules/angular2/src/transform/reflection_remover/remove_reflection_capabilities.dart @@ -0,0 +1,21 @@ +library angular2.src.transform.reflection_remover.remove_reflection_capabilities; + +import 'package:analyzer/analyzer.dart'; + +import 'codegen.dart'; +import 'rewriter.dart'; + +/// Finds the call to the Angular2 [ReflectionCapabilities] constructor +/// in [code] and replaces it with a call to `setupReflection` in +/// [newEntryPoint]. +/// +/// [reflectionEntryPointPath] is the path where [code] is defined and is +/// used to display parsing errors. +/// +/// This only searches [code] not `part`s, `import`s, `export`s, etc. +String removeReflectionCapabilities( + String code, String reflectionEntryPointPath, String newEntryPointPath) { + var codegen = new Codegen(reflectionEntryPointPath, newEntryPointPath); + return new Rewriter(code, codegen) + .rewrite(parseCompilationUnit(code, name: reflectionEntryPointPath)); +} diff --git a/modules/angular2/src/transform/reflection_remover/rewriter.dart b/modules/angular2/src/transform/reflection_remover/rewriter.dart new file mode 100644 index 0000000000..fec25b3530 --- /dev/null +++ b/modules/angular2/src/transform/reflection_remover/rewriter.dart @@ -0,0 +1,127 @@ +library angular2.src.transform.reflection_remover.rewriter; + +import 'package:analyzer/src/generated/ast.dart'; +import 'package:angular2/src/transform/common/logging.dart'; + +import 'ast_tester.dart'; +import 'codegen.dart'; + +class Rewriter { + final String _code; + final Codegen _codegen; + final AstTester _tester; + + Rewriter(this._code, this._codegen, {AstTester tester}) + : _tester = tester == null ? const AstTester() : tester; + + /// Rewrites the provided code removing imports of the + /// [ReflectionCapabilities] library and instantiations of + /// [ReflectionCapabilities], as detected by the (potentially) provided + /// [AstTester]. + /// + /// To the extent possible, this method does not change line numbers or + /// offsets in the provided code to facilitate debugging via source maps. + String rewrite(CompilationUnit node) { + if (node == null) throw new ArgumentError.notNull('node'); + + var visitor = new _FindReflectionCapabilitiesVisitor(_tester); + node.accept(visitor); + if (visitor.reflectionCapabilityImports.isEmpty) { + logger.error( + 'Failed to find ${AstTester.REFLECTION_CAPABILITIES_NAME} import.'); + return _code; + } + if (visitor.reflectionCapabilityAssignments.isEmpty) { + logger.error('Failed to find ${AstTester.REFLECTION_CAPABILITIES_NAME} ' + 'instantiation.'); + return _code; + } + + var compare = (AstNode a, AstNode b) => b.offset - a.offset; + visitor.reflectionCapabilityImports.sort(compare); + visitor.reflectionCapabilityAssignments.sort(compare); + + var importAdded = false; + var buf = new StringBuffer(); + var idx = visitor.reflectionCapabilityImports.fold(0, + (int lastIdx, ImportDirective node) { + buf.write(_code.substring(lastIdx, node.offset)); + if ('${node.prefix}' == _codegen.prefix) { + logger.warning( + 'Found import prefix "${_codegen.prefix}" in source file.' + ' Transform may not succeed.'); + } + buf.write(_commentedNode(node)); + if (!importAdded) { + buf.write(_codegen.codegenImport()); + importAdded = true; + } + return node.end; + }); + + var setupAdded = false; + idx = visitor.reflectionCapabilityAssignments.fold(idx, + (int lastIdx, AssignmentExpression assignNode) { + var node = assignNode; + while (node.parent is ExpressionStatement) { + node = node.parent; + } + buf.write(_code.substring(lastIdx, node.offset)); + buf.write(_commentedNode(node)); + if (!setupAdded) { + buf.write(_codegen.codegenSetupReflectionCall( + reflectorAssignment: assignNode)); + setupAdded = true; + } + return node.end; + }); + if (idx < _code.length) buf.write(_code.substring(idx)); + return buf.toString(); + } + + String _commentedNode(AstNode node) { + // TODO(kegluneq): Return commented code once we generate all needed code. + return _code.substring(node.offset, node.end); + } +} + +/// Visitor responsible for rewriting the Angular 2 code which instantiates +/// [ReflectionCapabilities] and removing its associated import. +/// +/// This breaks our dependency on dart:mirrors, which enables smaller code +/// size and better performance. +class _FindReflectionCapabilitiesVisitor extends Object + with RecursiveAstVisitor { + final reflectionCapabilityImports = new List(); + final reflectionCapabilityAssignments = new List(); + final AstTester _tester; + + _FindReflectionCapabilitiesVisitor(this._tester); + + @override + Object visitImportDirective(ImportDirective node) { + if (_tester.isReflectionCapabilitiesImport(node)) { + reflectionCapabilityImports.add(node); + } + return null; + } + + @override + Object visitAssignmentExpression(AssignmentExpression node) { + if (node.rightHandSide is InstanceCreationExpression && + _tester.isNewReflectionCapabilities(node.rightHandSide)) { + reflectionCapabilityAssignments.add(node); + } + return super.visitAssignmentExpression(node); + } + + @override + Object visitInstanceCreationExpression(InstanceCreationExpression node) { + if (_tester.isNewReflectionCapabilities(node) && + !reflectionCapabilityAssignments.contains(node.parent)) { + logger.error('Unexpected format in creation of ' + '${reflectionCapabilitiesTypeName}'); + } + return super.visitInstanceCreationExpression(node); + } +} diff --git a/modules/angular2/src/transform/reflection_remover/transformer.dart b/modules/angular2/src/transform/reflection_remover/transformer.dart new file mode 100644 index 0000000000..5bbcce31f0 --- /dev/null +++ b/modules/angular2/src/transform/reflection_remover/transformer.dart @@ -0,0 +1,47 @@ +library angular2.src.transform.reflection_remover.transformer; + +import 'dart:async'; +import 'package:angular2/src/transform/common/logging.dart' as log; +import 'package:angular2/src/transform/common/names.dart'; +import 'package:angular2/src/transform/common/options.dart'; +import 'package:barback/barback.dart'; + +import 'remove_reflection_capabilities.dart'; + +/// Transformer responsible for removing the import and instantiation of +/// [ReflectionCapabilities]. +/// +/// The goal of this is to break the app's dependency on dart:mirrors. +/// +/// This transformer assumes that [DirectiveProcessor] and [DirectiveLinker] +/// have already been run and that a .ngDeps.dart file has been generated for +/// [options.entryPoint]. The instantiation of [ReflectionCapabilities] is +/// replaced by calling `setupReflection` in that .ngDeps.dart file. +class ReflectionRemover extends Transformer { + final TransformerOptions options; + + ReflectionRemover(this.options); + + @override + bool isPrimary(AssetId id) => options.reflectionEntryPoint == id.path; + + @override + Future apply(Transform transform) async { + log.init(transform); + + try { + var newEntryPoint = new AssetId( + transform.primaryInput.id.package, options.entryPoint) + .changeExtension(DEPS_EXTENSION); + + var assetCode = await transform.primaryInput.readAsString(); + transform.addOutput(new Asset.fromString(transform.primaryInput.id, + removeReflectionCapabilities( + assetCode, transform.primaryInput.id.path, newEntryPoint.path))); + } catch (ex, stackTrace) { + log.logger.error('Removing reflection failed.\n' + 'Exception: $ex\n' + 'Stack Trace: $stackTrace'); + } + } +} diff --git a/modules/angular2/src/transform/transformer.dart b/modules/angular2/src/transform/transformer.dart index bdb0cfca82..30a774b8af 100644 --- a/modules/angular2/src/transform/transformer.dart +++ b/modules/angular2/src/transform/transformer.dart @@ -1,134 +1,35 @@ library angular2.src.transform; -import 'dart:async'; -import 'package:analyzer/src/generated/ast.dart'; -import 'package:analyzer/src/generated/element.dart'; import 'package:barback/barback.dart'; -import 'package:code_transformers/resolver.dart'; +import 'package:dart_style/dart_style.dart'; -import 'annotation_processor.dart'; -import 'codegen.dart' as codegen; -import 'find_bootstrap.dart'; -import 'find_reflection_capabilities.dart'; -import 'logging.dart' as log; -import 'options.dart'; -import 'resolvers.dart'; -import 'traversal.dart'; +import 'directive_linker/transformer.dart'; +import 'directive_processor/transformer.dart'; +import 'bind_generator/transformer.dart'; +import 'reflection_remover/transformer.dart'; +import 'common/formatter.dart' as formatter; +import 'common/options.dart'; -export 'options.dart'; +export 'common/options.dart'; /// Removes the mirror-based initialization logic and replaces it with static /// logic. -class AngularTransformer extends Transformer { - final Resolvers _resolvers; - final TransformerOptions options; - - AngularTransformer(this.options) : _resolvers = createResolvers(); - - factory AngularTransformer.asPlugin(BarbackSettings settings) { - var config = settings.configuration; - return new AngularTransformer(new TransformerOptions( - config[entryPointParam], - reflectionEntryPoint: config[reflectionEntryPointParam], - newEntryPoint: config[newEntryPointParam])); +class AngularTransformerGroup extends TransformerGroup { + AngularTransformerGroup(TransformerOptions options) : super([ + [new DirectiveProcessor(options)], + [new DirectiveLinker(options)], + [new BindGenerator(options), new ReflectionRemover(options)] + ]) { + formatter.init(new DartFormatter()); } - bool isPrimary(AssetId id) => options.reflectionEntryPoint == id.path; - - Future apply(Transform transform) { - log.init(transform); - - var entryPointId = - new AssetId(transform.primaryInput.id.package, options.entryPoint); - var reflectionEntryPointId = new AssetId( - transform.primaryInput.id.package, options.reflectionEntryPoint); - var newEntryPointId = - new AssetId(transform.primaryInput.id.package, options.newEntryPoint); - - var reflectionExists = transform.hasInput(reflectionEntryPointId); - var newEntryPointExists = transform.hasInput(newEntryPointId); - - Resolver myResolver; - return Future - .wait([reflectionExists, newEntryPointExists]) - .then((existsList) { - if (!existsList[0]) { - log.logger.error('Reflection entry point file ' - '${reflectionEntryPointId} does not exist.'); - } else if (existsList[1]) { - log.logger - .error('New entry point file $newEntryPointId already exists.'); - } else { - return _resolvers - .get(transform, [entryPointId, reflectionEntryPointId]) - .then((resolver) { - myResolver = resolver; - try { - String reflectionCapabilitiesCreation = findReflectionCapabilities( - resolver, reflectionEntryPointId, newEntryPointId); - - transform.addOutput(new Asset.fromString( - reflectionEntryPointId, reflectionCapabilitiesCreation)); - // Find the call to `new ReflectionCapabilities()` - // Generate new source. - } catch (err, stackTrace) { - log.logger.error('${err}: ${stackTrace}', - asset: reflectionEntryPointId); - rethrow; - } - - try { - new _BootstrapFileBuilder( - resolver, transform, entryPointId, newEntryPointId).run(); - } catch (err, stackTrace) { - log.logger.error('${err}: ${stackTrace}', - asset: transform.primaryInput.id); - rethrow; - } - }); - } - }).whenComplete(() { - if (myResolver != null) { - myResolver.release(); - } - }); + factory AngularTransformerGroup.asPlugin(BarbackSettings settings) { + return new AngularTransformerGroup(_parseOptions(settings)); } } -class _BootstrapFileBuilder { - final Resolver _resolver; - final Transform _transform; - final AssetId _entryPoint; - final AssetId _newEntryPoint; - - _BootstrapFileBuilder(Resolver resolver, Transform transform, - this._entryPoint, this._newEntryPoint) - : _resolver = resolver, - _transform = transform; - - /// Adds the new entry point file to the transform. Should only be ran once. - void run() { - Set bootstrapCalls = - findBootstrapCalls(_resolver, _resolver.getLibrary(_entryPoint)); - - log.logger.info('found ${bootstrapCalls.length} call(s) to `bootstrap`'); - bootstrapCalls.forEach((BootstrapCallInfo info) { - log.logger.info('Arg1: ${info.bootstrapType}'); - }); - - var types = new Angular2Types(_resolver); - // TODO(kegluneq): Also match [Inject]. - var matcher = new AnnotationMatcher( - new Set.from([types.directiveAnnotation, types.templateAnnotation])); - - var traversal = new AngularVisibleTraversal(types, matcher); - bootstrapCalls.forEach((call) => traversal.traverse(call.bootstrapType)); - - var context = new codegen.Context(); - matcher.matchQueue - .forEach((entry) => context.directiveRegistry.register(entry)); - - _transform.addOutput(new Asset.fromString(_newEntryPoint, - codegen.codegenEntryPoint(context, newEntryPoint: _newEntryPoint))); - } +TransformerOptions _parseOptions(BarbackSettings settings) { + var config = settings.configuration; + return new TransformerOptions(config[ENTRY_POINT_PARAM], + reflectionEntryPoint: config[REFLECTION_ENTRY_POINT_PARAM]); } diff --git a/modules/angular2/test/transform/basic_bind_files/expected/bar.ngDeps.dart b/modules/angular2/test/transform/basic_bind_files/expected/bar.ngDeps.dart new file mode 100644 index 0000000000..aebd3e9ff4 --- /dev/null +++ b/modules/angular2/test/transform/basic_bind_files/expected/bar.ngDeps.dart @@ -0,0 +1,27 @@ +library bar; + +import 'bar.dart'; +import 'package:angular2/src/core/annotations/annotations.dart'; + +bool _visited = false; +void setupReflection(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(MyComponent, { + "factory": () => new MyComponent(), + "parameters": const [], + "annotations": const [ + const Component(selector: 'soup', componentServices: const [ToolTip]) + ] + }) + ..registerType(ToolTip, { + "factory": () => new ToolTip(), + "parameters": const [], + "annotations": const [ + const Decorator( + selector: '[tool-tip]', bind: const {'text': 'tool-tip'}) + ] + }) + ..registerSetters({"text": (ToolTip o, String value) => o.text = value}); +} diff --git a/modules/angular2/test/transform/basic_bind_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/basic_bind_files/expected/index.bootstrap.dart deleted file mode 100644 index 58bb31464d..0000000000 --- a/modules/angular2/test/transform/basic_bind_files/expected/index.bootstrap.dart +++ /dev/null @@ -1,26 +0,0 @@ -library angular2.src.transform.generated; - -import 'package:angular2/src/reflection/reflection.dart' show reflector; -import 'bar.dart' as i0; -import 'package:angular2/src/core/annotations/annotations.dart' as i1; - -setupReflection() { - reflector - ..registerType(i0.MyComponent, { - "factory": () => new i0.MyComponent(), - "parameters": const [const []], - "annotations": const [ - const i1.Component( - selector: 'soup', componentServices: const [i0.ToolTip]) - ] - }) - ..registerType(i0.ToolTip, { - "factory": () => new i0.ToolTip(), - "parameters": const [const []], - "annotations": const [ - const i1.Decorator( - selector: '[tool-tip]', bind: const {'text': 'tool-tip'}) - ] - }) - ..registerSetters({"text": (i0.ToolTip o, String value) => o.text = value}); -} diff --git a/modules/angular2/test/transform/chained_deps_files/bar.dart b/modules/angular2/test/transform/chained_deps_files/bar.dart new file mode 100644 index 0000000000..86f00a1b8d --- /dev/null +++ b/modules/angular2/test/transform/chained_deps_files/bar.dart @@ -0,0 +1,10 @@ +library bar; + +import 'package:angular2/src/core/annotations/annotations.dart'; +import 'foo.dart' as dep; + +@Component( + selector: '[soup]', componentServices: const [dep.DependencyComponent]) +class MyComponent { + MyComponent(); +} diff --git a/modules/angular2/test/transform/chained_deps_files/expected/bar.ngDeps.dart b/modules/angular2/test/transform/chained_deps_files/expected/bar.ngDeps.dart new file mode 100644 index 0000000000..004daf4f17 --- /dev/null +++ b/modules/angular2/test/transform/chained_deps_files/expected/bar.ngDeps.dart @@ -0,0 +1,23 @@ +library bar; + +import 'bar.dart'; +import 'package:angular2/src/core/annotations/annotations.dart'; +import 'foo.dart' as dep; +import 'foo.ngDeps.dart' as i0; + +bool _visited = false; +void setupReflection(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(MyComponent, { + "factory": () => new MyComponent(), + "parameters": const [], + "annotations": const [ + const Component( + selector: '[soup]', + componentServices: const [dep.DependencyComponent]) + ] + }); + i0.setupReflection(reflector); +} diff --git a/modules/angular2/test/transform/chained_deps_files/expected/foo.ngDeps.dart b/modules/angular2/test/transform/chained_deps_files/expected/foo.ngDeps.dart new file mode 100644 index 0000000000..7fbdb9e3b1 --- /dev/null +++ b/modules/angular2/test/transform/chained_deps_files/expected/foo.ngDeps.dart @@ -0,0 +1,16 @@ +library foo; + +import 'foo.dart'; +import 'package:angular2/src/core/annotations/annotations.dart'; + +bool _visited = false; +void setupReflection(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(DependencyComponent, { + "factory": () => new DependencyComponent(), + "parameters": const [], + "annotations": const [const Component(selector: '[salad]')] + }); +} diff --git a/modules/angular2/test/transform/chained_deps_files/expected/index.ngDeps.dart b/modules/angular2/test/transform/chained_deps_files/expected/index.ngDeps.dart new file mode 100644 index 0000000000..e46871eb48 --- /dev/null +++ b/modules/angular2/test/transform/chained_deps_files/expected/index.ngDeps.dart @@ -0,0 +1,13 @@ +library web_foo; + +import 'package:angular2/src/core/application.dart'; +import 'package:angular2/src/reflection/reflection_capabilities.dart'; +import 'bar.dart'; +import 'a:web/bar.ngDeps.dart' as i0; + +bool _visited = false; +void setupReflection(reflector) { + if (_visited) return; + _visited = true; + i0.setupReflection(reflector); +} diff --git a/modules/angular2/test/transform/chained_deps_files/foo.dart b/modules/angular2/test/transform/chained_deps_files/foo.dart new file mode 100644 index 0000000000..446f981fde --- /dev/null +++ b/modules/angular2/test/transform/chained_deps_files/foo.dart @@ -0,0 +1,8 @@ +library foo; + +import 'package:angular2/src/core/annotations/annotations.dart'; + +@Component(selector: '[salad]') +class DependencyComponent { + DependencyComponent(); +} diff --git a/modules/angular2/test/transform/chained_deps_files/index.dart b/modules/angular2/test/transform/chained_deps_files/index.dart new file mode 100644 index 0000000000..505e371e0e --- /dev/null +++ b/modules/angular2/test/transform/chained_deps_files/index.dart @@ -0,0 +1,10 @@ +library web_foo; + +import 'package:angular2/src/core/application.dart'; +import 'package:angular2/src/reflection/reflection_capabilities.dart'; +import 'bar.dart'; + +void main() { + reflector.reflectionCapabilities = new ReflectionCapabilities(); + bootstrap(MyComponent); +} diff --git a/modules/angular2/test/transform/common.dart b/modules/angular2/test/transform/common/application.dart similarity index 100% rename from modules/angular2/test/transform/common.dart rename to modules/angular2/test/transform/common/application.dart diff --git a/modules/angular2/test/transform/reflection_capabilities.dart b/modules/angular2/test/transform/common/reflection_capabilities.dart similarity index 100% rename from modules/angular2/test/transform/reflection_capabilities.dart rename to modules/angular2/test/transform/common/reflection_capabilities.dart diff --git a/modules/angular2/test/transform/list_of_types_files/expected/bar.ngDeps.dart b/modules/angular2/test/transform/list_of_types_files/expected/bar.ngDeps.dart new file mode 100644 index 0000000000..7314f4c639 --- /dev/null +++ b/modules/angular2/test/transform/list_of_types_files/expected/bar.ngDeps.dart @@ -0,0 +1,19 @@ +library bar; + +import 'bar.dart'; +import 'package:angular2/src/core/annotations/annotations.dart'; +import 'foo.dart'; + +bool _visited = false; +void setupReflection(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(MyComponent, { + "factory": (MyContext c) => new MyComponent(c), + "parameters": const [const [MyContext]], + "annotations": const [ + const Component(componentServices: const [MyContext]) + ] + }); +} diff --git a/modules/angular2/test/transform/list_of_types_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/list_of_types_files/expected/index.bootstrap.dart deleted file mode 100644 index 1051dff0e0..0000000000 --- a/modules/angular2/test/transform/list_of_types_files/expected/index.bootstrap.dart +++ /dev/null @@ -1,17 +0,0 @@ -library angular2.src.transform.generated; - -import 'package:angular2/src/reflection/reflection.dart' show reflector; -import 'bar.dart' as i0; -import 'foo.dart' as i1; -import 'package:angular2/src/core/annotations/annotations.dart' as i2; - -setupReflection() { - reflector - ..registerType(i0.MyComponent, { - "factory": (i1.MyContext c) => new i0.MyComponent(c), - "parameters": const [const [i1.MyContext]], - "annotations": const [ - const i2.Component(componentServices: const [i1.MyContext]) - ] - }); -} diff --git a/modules/angular2/test/transform/reflection_remover_files/README.md b/modules/angular2/test/transform/reflection_remover_files/README.md new file mode 100644 index 0000000000..616ff5aefe --- /dev/null +++ b/modules/angular2/test/transform/reflection_remover_files/README.md @@ -0,0 +1,7 @@ +Tests that the reflection removal step: + 1. Comments out the import of reflection_capabilities.dart + 2. Comments out the instantiation of `ReflectionCapabilities` + 3. Adds the appropriate import. + 4. Adds the call to `setupReflection` + 5. Does not change line numbers in the source. + 6. Makes minimal changes to source offsets. \ No newline at end of file diff --git a/modules/angular2/test/transform/reflection_remover_files/expected/index.dart b/modules/angular2/test/transform/reflection_remover_files/expected/index.dart new file mode 100644 index 0000000000..4603c6adb2 --- /dev/null +++ b/modules/angular2/test/transform/reflection_remover_files/expected/index.dart @@ -0,0 +1,22 @@ +library angular2.test.transform.reflection_remover_files; + +// This file is intentionally formatted as a string to avoid having the +// automatic transformer prettify it. +// +// This file represents transformed user code. Because this code will be +// linked to output by a source map, we cannot change line numbers from the +// original code and we therefore add our generated code on the same line as +// those we are removing. + +var code = """ +library web_foo; + +import 'package:angular2/src/core/application.dart'; +import 'package:angular2/src/reflection/reflection.dart'; +import 'package:angular2/src/reflection/reflection_capabilities.dart';import 'index.ngDeps.dart' as ngStaticInit; + +void main() { + reflector.reflectionCapabilities = new ReflectionCapabilities();ngStaticInit.setupReflection(reflector); + bootstrap(MyComponent); +} +"""; diff --git a/modules/angular2/test/transform/reflection_remover_files/index.dart b/modules/angular2/test/transform/reflection_remover_files/index.dart new file mode 100644 index 0000000000..45363b8a19 --- /dev/null +++ b/modules/angular2/test/transform/reflection_remover_files/index.dart @@ -0,0 +1,10 @@ +library web_foo; + +import 'package:angular2/src/core/application.dart'; +import 'package:angular2/src/reflection/reflection.dart'; +import 'package:angular2/src/reflection/reflection_capabilities.dart'; + +void main() { + reflector.reflectionCapabilities = new ReflectionCapabilities(); + bootstrap(MyComponent); +} diff --git a/modules/angular2/test/transform/simple_annotation_files/expected/bar.ngDeps.dart b/modules/angular2/test/transform/simple_annotation_files/expected/bar.ngDeps.dart new file mode 100644 index 0000000000..3092e198e9 --- /dev/null +++ b/modules/angular2/test/transform/simple_annotation_files/expected/bar.ngDeps.dart @@ -0,0 +1,16 @@ +library bar; + +import 'bar.dart'; +import 'package:angular2/src/core/annotations/annotations.dart'; + +bool _visited = false; +void setupReflection(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(MyComponent, { + "factory": () => new MyComponent(), + "parameters": const [], + "annotations": const [const Component(selector: '[soup]')] + }); +} diff --git a/modules/angular2/test/transform/simple_annotation_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/simple_annotation_files/expected/index.bootstrap.dart deleted file mode 100644 index a4ddc3376a..0000000000 --- a/modules/angular2/test/transform/simple_annotation_files/expected/index.bootstrap.dart +++ /dev/null @@ -1,14 +0,0 @@ -library angular2.src.transform.generated; - -import 'package:angular2/src/reflection/reflection.dart' show reflector; -import 'bar.dart' as i0; -import 'package:angular2/src/core/annotations/annotations.dart' as i1; - -setupReflection() { - reflector - ..registerType(i0.MyComponent, { - "factory": () => new i0.MyComponent(), - "parameters": const [const []], - "annotations": const [const i1.Component(selector: '[soup]')] - }); -} diff --git a/modules/angular2/test/transform/simple_annotation_files/expected/index.dart b/modules/angular2/test/transform/simple_annotation_files/expected/index.dart index cea925722d..65a6b9b1f1 100644 --- a/modules/angular2/test/transform/simple_annotation_files/expected/index.dart +++ b/modules/angular2/test/transform/simple_annotation_files/expected/index.dart @@ -2,12 +2,12 @@ library web_foo; import 'package:angular2/src/core/application.dart'; import 'package:angular2/src/reflection/reflection.dart'; -import 'index.bootstrap.dart' as ngStaticInit; +import 'index.ngDeps.dart' as ngStaticInit; import 'package:angular2/src/reflection/reflection_capabilities.dart'; import 'bar.dart'; void main() { - ngStaticInit.setupReflection(); + ngStaticInit.setupReflection(reflector); reflector.reflectionCapabilities = new ReflectionCapabilities(); bootstrap(MyComponent); } diff --git a/modules/angular2/test/transform/simple_annotation_files/expected/index.ngDeps.dart b/modules/angular2/test/transform/simple_annotation_files/expected/index.ngDeps.dart new file mode 100644 index 0000000000..b3d077b62d --- /dev/null +++ b/modules/angular2/test/transform/simple_annotation_files/expected/index.ngDeps.dart @@ -0,0 +1,15 @@ +library web_foo; + +import 'index.dart'; +import 'package:angular2/src/core/application.dart'; +import 'package:angular2/src/reflection/reflection.dart'; +import 'package:angular2/src/reflection/reflection_capabilities.dart'; +import 'bar.dart'; +import 'bar.ngDeps.dart' as i0; + +bool _visited = false; +void setupReflection(reflector) { + if (_visited) return; + _visited = true; + i0.setupReflection(reflector); +} diff --git a/modules/angular2/test/transform/synthetic_ctor_files/expected/bar.ngDeps.dart b/modules/angular2/test/transform/synthetic_ctor_files/expected/bar.ngDeps.dart new file mode 100644 index 0000000000..3092e198e9 --- /dev/null +++ b/modules/angular2/test/transform/synthetic_ctor_files/expected/bar.ngDeps.dart @@ -0,0 +1,16 @@ +library bar; + +import 'bar.dart'; +import 'package:angular2/src/core/annotations/annotations.dart'; + +bool _visited = false; +void setupReflection(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(MyComponent, { + "factory": () => new MyComponent(), + "parameters": const [], + "annotations": const [const Component(selector: '[soup]')] + }); +} diff --git a/modules/angular2/test/transform/synthetic_ctor_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/synthetic_ctor_files/expected/index.bootstrap.dart deleted file mode 100644 index a4ddc3376a..0000000000 --- a/modules/angular2/test/transform/synthetic_ctor_files/expected/index.bootstrap.dart +++ /dev/null @@ -1,14 +0,0 @@ -library angular2.src.transform.generated; - -import 'package:angular2/src/reflection/reflection.dart' show reflector; -import 'bar.dart' as i0; -import 'package:angular2/src/core/annotations/annotations.dart' as i1; - -setupReflection() { - reflector - ..registerType(i0.MyComponent, { - "factory": () => new i0.MyComponent(), - "parameters": const [const []], - "annotations": const [const i1.Component(selector: '[soup]')] - }); -} diff --git a/modules/angular2/test/transform/transform.server.spec.dart b/modules/angular2/test/transform/transform.server.spec.dart index ecb7f81afb..78d6be5e22 100644 --- a/modules/angular2/test/transform/transform.server.spec.dart +++ b/modules/angular2/test/transform/transform.server.spec.dart @@ -1,4 +1,4 @@ -library angular2.test; +library angular2.test.transform; import 'dart:io'; import 'package:barback/barback.dart'; @@ -8,31 +8,30 @@ import 'package:dart_style/dart_style.dart'; import 'package:unittest/unittest.dart'; import 'package:unittest/vm_config.dart'; +import 'reflection_remover_files/expected/index.dart' + as reflection_remover_output; + main() { useVMConfiguration(); - - // TODO(kegluneq): Add a test for generating multiple annotations. - - group('Annotation tests:', _runTests); + group('Integration tests:', _integrationTests); } var formatter = new DartFormatter(); -var transform = new AngularTransformer(new TransformerOptions('web/index.dart', - reflectionEntryPoint: 'web/index.dart', - newEntryPoint: 'web/index.bootstrap.dart')); +var transform = new AngularTransformerGroup(new TransformerOptions( + 'web/index.dart', reflectionEntryPoint: 'web/index.dart')); -class TestConfig { +class IntegrationTestConfig { final String name; final Map assetPathToInputPath; final Map assetPathToExpectedOutputPath; - TestConfig(this.name, + IntegrationTestConfig(this.name, {Map inputs, Map outputs}) : this.assetPathToInputPath = inputs, this.assetPathToExpectedOutputPath = outputs; } -void _runTests() { +void _integrationTests() { /* * Each test has its own directory for inputs & an `expected` directory for * expected outputs. @@ -43,52 +42,53 @@ void _runTests() { var commonInputs = { 'angular2|lib/src/core/annotations/annotations.dart': '../../lib/src/core/annotations/annotations.dart', - 'angular2|lib/src/core/application.dart': 'common.dart', + 'angular2|lib/src/core/application.dart': 'common/application.dart', 'angular2|lib/src/reflection/reflection_capabilities.dart': - 'reflection_capabilities.dart' + 'common/reflection_capabilities.dart' }; var tests = [ - new TestConfig('Simple', + new IntegrationTestConfig('Simple', inputs: { 'a|web/index.dart': 'simple_annotation_files/index.dart', 'a|web/bar.dart': 'simple_annotation_files/bar.dart' }, outputs: { - 'a|web/index.bootstrap.dart': - 'simple_annotation_files/expected/index.bootstrap.dart', - 'a|web/index.dart': 'simple_annotation_files/expected/index.dart', + 'a|web/bar.ngDeps.dart': + 'simple_annotation_files/expected/bar.ngDeps.dart', + 'a|web/index.ngDeps.dart': + 'simple_annotation_files/expected/index.ngDeps.dart' }), - new TestConfig('Two injected dependencies', + new IntegrationTestConfig('Reflection Remover', + inputs: {'a|web/index.dart': 'reflection_remover_files/index.dart'}, + outputs: {'a|web/index.dart': reflection_remover_output.code}), + new IntegrationTestConfig('Two injected dependencies', inputs: { 'a|web/index.dart': 'two_deps_files/index.dart', 'a|web/foo.dart': 'two_deps_files/foo.dart', 'a|web/bar.dart': 'two_deps_files/bar.dart' }, outputs: { - 'a|web/index.bootstrap.dart': - 'two_deps_files/expected/index.bootstrap.dart' + 'a|web/bar.ngDeps.dart': 'two_deps_files/expected/bar.ngDeps.dart' }), - new TestConfig('List of types', + new IntegrationTestConfig('List of types', inputs: { 'a|web/index.dart': 'list_of_types_files/index.dart', 'a|web/foo.dart': 'list_of_types_files/foo.dart', 'a|web/bar.dart': 'list_of_types_files/bar.dart' }, outputs: { - 'a|web/index.bootstrap.dart': - 'list_of_types_files/expected/index.bootstrap.dart' + 'a|web/bar.ngDeps.dart': 'list_of_types_files/expected/bar.ngDeps.dart' }), - new TestConfig('Component with synthetic Constructor', + new IntegrationTestConfig('Component with synthetic Constructor', inputs: { 'a|web/index.dart': 'synthetic_ctor_files/index.dart', 'a|web/bar.dart': 'synthetic_ctor_files/bar.dart' }, outputs: { - 'a|web/index.bootstrap.dart': - 'synthetic_ctor_files/expected/index.bootstrap.dart' + 'a|web/bar.ngDeps.dart': 'synthetic_ctor_files/expected/bar.ngDeps.dart' }), - new TestConfig('Component with two annotations', + new IntegrationTestConfig('Component with two annotations', inputs: { 'a|web/index.dart': 'two_annotations_files/index.dart', 'a|web/bar.dart': 'two_annotations_files/bar.dart', @@ -96,17 +96,25 @@ void _runTests() { '../../lib/src/core/annotations/template.dart' }, outputs: { - 'a|web/index.bootstrap.dart': - 'two_annotations_files/expected/index.bootstrap.dart' + 'a|web/bar.ngDeps.dart': 'two_annotations_files/expected/bar.ngDeps.dart' }), - new TestConfig('Basic `bind`', + new IntegrationTestConfig('Basic `bind`', inputs: { 'a|web/index.dart': 'basic_bind_files/index.dart', 'a|web/bar.dart': 'basic_bind_files/bar.dart' }, outputs: { - 'a|web/index.bootstrap.dart': - 'basic_bind_files/expected/index.bootstrap.dart' + 'a|web/bar.ngDeps.dart': 'basic_bind_files/expected/bar.ngDeps.dart' + }), + new IntegrationTestConfig('Chained dependencies', + inputs: { + 'a|web/index.dart': 'chained_deps_files/index.dart', + 'a|web/foo.dart': 'chained_deps_files/foo.dart', + 'a|web/bar.dart': 'chained_deps_files/bar.dart' + }, + outputs: { + 'a|web/bar.ngDeps.dart': 'chained_deps_files/expected/bar.ngDeps.dart', + 'a|web/foo.ngDeps.dart': 'chained_deps_files/expected/foo.ngDeps.dart' }) ]; @@ -118,8 +126,8 @@ void _runTests() { config.assetPathToInputPath ..addAll(commonInputs) ..forEach((key, value) { - config.assetPathToInputPath[ - key] = cache.putIfAbsent(value, () => _readFile(value)); + config.assetPathToInputPath[key] = + cache.putIfAbsent(value, () => _readFile(value)); }); config.assetPathToExpectedOutputPath.forEach((key, value) { config.assetPathToExpectedOutputPath[key] = cache.putIfAbsent(value, () { @@ -133,7 +141,7 @@ void _runTests() { } } -/// Smoothes over differences in CWD between IDEs and running tests in Travis. +/// Smooths over differences in CWD between IDEs and running tests in Travis. String _readFile(String path) { for (var myPath in [path, 'test/transform/${path}']) { var file = new File(myPath); @@ -141,5 +149,5 @@ String _readFile(String path) { return file.readAsStringSync(); } } - return null; + return path; } diff --git a/modules/angular2/test/transform/two_annotations_files/expected/bar.ngDeps.dart b/modules/angular2/test/transform/two_annotations_files/expected/bar.ngDeps.dart new file mode 100644 index 0000000000..f6da7c81e1 --- /dev/null +++ b/modules/angular2/test/transform/two_annotations_files/expected/bar.ngDeps.dart @@ -0,0 +1,20 @@ +library bar; + +import 'bar.dart'; +import 'package:angular2/src/core/annotations/annotations.dart'; +import 'package:angular2/src/core/annotations/template.dart'; + +bool _visited = false; +void setupReflection(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(MyComponent, { + "factory": () => new MyComponent(), + "parameters": const [], + "annotations": const [ + const Component(selector: '[soup]'), + const Template(inline: 'Salad') + ] + }); +} diff --git a/modules/angular2/test/transform/two_annotations_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/two_annotations_files/expected/index.bootstrap.dart deleted file mode 100644 index d5d665966d..0000000000 --- a/modules/angular2/test/transform/two_annotations_files/expected/index.bootstrap.dart +++ /dev/null @@ -1,18 +0,0 @@ -library angular2.src.transform.generated; - -import 'package:angular2/src/reflection/reflection.dart' show reflector; -import 'bar.dart' as i0; -import 'package:angular2/src/core/annotations/annotations.dart' as i1; -import 'package:angular2/src/core/annotations/template.dart' as i2; - -setupReflection() { - reflector - ..registerType(i0.MyComponent, { - "factory": () => new i0.MyComponent(), - "parameters": const [const []], - "annotations": const [ - const i1.Component(selector: '[soup]'), - const i2.Template(inline: 'Salad') - ] - }); -} diff --git a/modules/angular2/test/transform/two_deps_files/expected/bar.ngDeps.dart b/modules/angular2/test/transform/two_deps_files/expected/bar.ngDeps.dart new file mode 100644 index 0000000000..0e30a77379 --- /dev/null +++ b/modules/angular2/test/transform/two_deps_files/expected/bar.ngDeps.dart @@ -0,0 +1,20 @@ +library bar; + +import 'bar.dart'; +import 'package:angular2/src/core/annotations/annotations.dart'; +import 'foo.dart' as prefix; + +bool _visited = false; +void setupReflection(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(MyComponent, { + "factory": + (prefix.MyContext c, String inValue) => new MyComponent(c, inValue), + "parameters": const [const [prefix.MyContext], const [String]], + "annotations": const [ + const Component(selector: prefix.preDefinedSelector) + ] + }); +} diff --git a/modules/angular2/test/transform/two_deps_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/two_deps_files/expected/index.bootstrap.dart deleted file mode 100644 index 046a923fff..0000000000 --- a/modules/angular2/test/transform/two_deps_files/expected/index.bootstrap.dart +++ /dev/null @@ -1,16 +0,0 @@ -library angular2.src.transform.generated; - -import 'package:angular2/src/reflection/reflection.dart' show reflector; -import 'bar.dart' as i0; -import 'foo.dart' as i1; -import 'package:angular2/src/core/annotations/annotations.dart' as i2; - -setupReflection() { - reflector - ..registerType(i0.MyComponent, { - "factory": - (i1.MyContext c, String inValue) => new i0.MyComponent(c, inValue), - "parameters": const [const [i1.MyContext, String]], - "annotations": const [const i2.Component(selector: i1.preDefinedSelector)] - }); -}