From 8ad4ad57d13b1135e2f89ad10fc66d82e8d62421 Mon Sep 17 00:00:00 2001 From: Tim Blasi Date: Tue, 21 Jul 2015 12:44:10 -0700 Subject: [PATCH] feat(dart/transform): Populate `lifecycle` from lifecycle interfaces When a `Directive` implements a lifecycle interface (e.g. `OnChange` or `OnInit`), populate its `lifecycle` property if not already populated). Closes #3181 --- .../transform/common/annotation_matcher.dart | 201 ++++++------------ .../transform/common/class_matcher_base.dart | 121 +++++++++++ .../transform/common/interface_matcher.dart | 77 +++++++ .../src/transform/common/options.dart | 2 +- .../src/transform/common/options_reader.dart | 7 +- .../directive_processor/rewriter.dart | 40 ++-- .../directive_processor/transformer.dart | 3 +- .../directive_processor/visitors.dart | 99 ++++++++- .../directive_processor/all_tests.dart | 28 ++- .../expected/soup.ng_deps.dart | 59 +++++ .../interface_lifecycle_files/soup.dart | 18 ++ .../expected/soup.ng_deps.dart | 27 +++ .../soup.dart | 6 + .../expected/soup.ng_deps.dart | 23 ++ .../soup.dart | 6 + .../expected/soup.ng_deps.dart | 21 ++ .../superclass_lifecycle_files/soup.dart | 6 + 17 files changed, 570 insertions(+), 174 deletions(-) create mode 100644 modules/angular2/src/transform/common/class_matcher_base.dart create mode 100644 modules/angular2/src/transform/common/interface_matcher.dart create mode 100644 modules/angular2/test/transform/directive_processor/interface_lifecycle_files/expected/soup.ng_deps.dart create mode 100644 modules/angular2/test/transform/directive_processor/interface_lifecycle_files/soup.dart create mode 100644 modules/angular2/test/transform/directive_processor/multiple_interface_lifecycle_files/expected/soup.ng_deps.dart create mode 100644 modules/angular2/test/transform/directive_processor/multiple_interface_lifecycle_files/soup.dart create mode 100644 modules/angular2/test/transform/directive_processor/prefixed_interface_lifecycle_files/expected/soup.ng_deps.dart create mode 100644 modules/angular2/test/transform/directive_processor/prefixed_interface_lifecycle_files/soup.dart create mode 100644 modules/angular2/test/transform/directive_processor/superclass_lifecycle_files/expected/soup.ng_deps.dart create mode 100644 modules/angular2/test/transform/directive_processor/superclass_lifecycle_files/soup.dart diff --git a/modules/angular2/src/transform/common/annotation_matcher.dart b/modules/angular2/src/transform/common/annotation_matcher.dart index fff919506c..5c5f16c11e 100644 --- a/modules/angular2/src/transform/common/annotation_matcher.dart +++ b/modules/angular2/src/transform/common/annotation_matcher.dart @@ -2,176 +2,97 @@ library angular2.transform.common.annotation_matcher; import 'package:analyzer/src/generated/ast.dart'; import 'package:barback/barback.dart' show AssetId; -import 'package:code_transformers/assets.dart'; -import 'package:path/path.dart' as path; -import 'logging.dart' show logger; +import 'class_matcher_base.dart'; -/// [AnnotationDescriptor]s for the default angular annotations that can appear +export 'class_matcher_base.dart' show ClassDescriptor; + +/// [ClassDescriptor]s for the default angular annotations that can appear /// on a class. These classes are re-exported in many places so this covers all /// the possible libraries which could provide them. const INJECTABLES = const [ - const AnnotationDescriptor( - 'Injectable', 'package:angular2/src/di/decorators.dart', null), - const AnnotationDescriptor('Injectable', 'package:angular2/di.dart', null), - const AnnotationDescriptor( - 'Injectable', 'package:angular2/angular2.dart', null), + const ClassDescriptor( + 'Injectable', 'package:angular2/src/di/decorators.dart'), + const ClassDescriptor('Injectable', 'package:angular2/di.dart'), + const ClassDescriptor('Injectable', 'package:angular2/angular2.dart'), ]; const DIRECTIVES = const [ - const AnnotationDescriptor('Directive', - 'package:angular2/src/core/annotations/annotations.dart', 'Injectable'), - const AnnotationDescriptor('Directive', - 'package:angular2/src/core/annotations/decorators.dart', 'Injectable'), - const AnnotationDescriptor('Directive', + const ClassDescriptor( + 'Directive', 'package:angular2/src/core/annotations/annotations.dart', + superClass: 'Injectable'), + const ClassDescriptor( + 'Directive', 'package:angular2/src/core/annotations/decorators.dart', + superClass: 'Injectable'), + const ClassDescriptor('Directive', 'package:angular2/src/core/annotations_impl/annotations.dart', - 'Injectable'), - const AnnotationDescriptor( - 'Directive', 'package:angular2/annotations.dart', 'Injectable'), - const AnnotationDescriptor( - 'Directive', 'package:angular2/angular2.dart', 'Injectable'), - const AnnotationDescriptor( - 'Directive', 'package:angular2/core.dart', 'Injectable'), + superClass: 'Injectable'), + const ClassDescriptor('Directive', 'package:angular2/annotations.dart', + superClass: 'Injectable'), + const ClassDescriptor('Directive', 'package:angular2/angular2.dart', + superClass: 'Injectable'), + const ClassDescriptor('Directive', 'package:angular2/core.dart', + superClass: 'Injectable'), ]; const COMPONENTS = const [ - const AnnotationDescriptor('Component', - 'package:angular2/src/core/annotations/annotations.dart', 'Directive'), - const AnnotationDescriptor('Component', - 'package:angular2/src/core/annotations/decorators.dart', 'Directive'), - const AnnotationDescriptor('Component', + const ClassDescriptor( + 'Component', 'package:angular2/src/core/annotations/annotations.dart', + superClass: 'Directive'), + const ClassDescriptor( + 'Component', 'package:angular2/src/core/annotations/decorators.dart', + superClass: 'Directive'), + const ClassDescriptor('Component', 'package:angular2/src/core/annotations_impl/annotations.dart', - 'Directive'), - const AnnotationDescriptor( - 'Component', 'package:angular2/annotations.dart', 'Directive'), - const AnnotationDescriptor( - 'Component', 'package:angular2/angular2.dart', 'Directive'), - const AnnotationDescriptor( - 'Component', 'package:angular2/core.dart', 'Directive'), + superClass: 'Directive'), + const ClassDescriptor('Component', 'package:angular2/annotations.dart', + superClass: 'Directive'), + const ClassDescriptor('Component', 'package:angular2/angular2.dart', + superClass: 'Directive'), + const ClassDescriptor('Component', 'package:angular2/core.dart', + superClass: 'Directive'), ]; const VIEWS = const [ - const AnnotationDescriptor('View', 'package:angular2/view.dart', null), - const AnnotationDescriptor('View', 'package:angular2/angular2.dart', null), - const AnnotationDescriptor('View', 'package:angular2/core.dart', null), - const AnnotationDescriptor( - 'View', 'package:angular2/src/core/annotations/view.dart', null), - const AnnotationDescriptor( - 'View', 'package:angular2/src/core/annotations_impl/view.dart', null), + const ClassDescriptor('View', 'package:angular2/view.dart'), + const ClassDescriptor('View', 'package:angular2/angular2.dart'), + const ClassDescriptor('View', 'package:angular2/core.dart'), + const ClassDescriptor( + 'View', 'package:angular2/src/core/annotations/view.dart'), + const ClassDescriptor( + 'View', 'package:angular2/src/core/annotations_impl/view.dart'), ]; /// Checks if a given [Annotation] matches any of the given -/// [AnnotationDescriptors]. -class AnnotationMatcher { - /// Always start out with the default angular [AnnotationDescriptor]s. - final List _annotations = [] - ..addAll(VIEWS) - ..addAll(COMPONENTS) - ..addAll(INJECTABLES) - ..addAll(DIRECTIVES); +/// [ClassDescriptors]. +class AnnotationMatcher extends ClassMatcherBase { + AnnotationMatcher._(classDescriptors) : super(classDescriptors); - AnnotationMatcher(); + factory AnnotationMatcher() { + return new AnnotationMatcher._([] + ..addAll(COMPONENTS) + ..addAll(DIRECTIVES) + ..addAll(INJECTABLES) + ..addAll(VIEWS)); + } - /// Adds a new [AnnotationDescriptor]. - void add(AnnotationDescriptor annotation) => _annotations.add(annotation); - - /// Adds a number of [AnnotationDescriptor]s. - void addAll(Iterable annotations) => - _annotations.addAll(annotations); - - /// Returns the first [AnnotationDescriptor] that matches the given - /// [Annotation] node which appears in `assetId`. - AnnotationDescriptor firstMatch(Annotation annotation, AssetId assetId) => - _annotations.firstWhere((a) => _matchAnnotation(annotation, a, assetId), - orElse: () => null); - - /// Checks whether an [Annotation] node matches any [AnnotationDescriptor]. - bool hasMatch(Annotation annotation, AssetId assetId) => - _annotations.any((a) => _matchAnnotation(annotation, a, assetId)); + bool _implementsWithWarning( + ClassDescriptor descriptor, List interfaces) => + implements(descriptor, interfaces, + missingSuperClassWarning: 'Missing `custom_annotation` entry for `${descriptor.superClass}`.'); /// Checks if an [Annotation] node implements [Injectable]. bool isInjectable(Annotation annotation, AssetId assetId) => - _implements(firstMatch(annotation, assetId), INJECTABLES); + _implementsWithWarning(firstMatch(annotation.name, assetId), INJECTABLES); /// Checks if an [Annotation] node implements [Directive]. bool isDirective(Annotation annotation, AssetId assetId) => - _implements(firstMatch(annotation, assetId), DIRECTIVES); + _implementsWithWarning(firstMatch(annotation.name, assetId), DIRECTIVES); /// Checks if an [Annotation] node implements [Component]. bool isComponent(Annotation annotation, AssetId assetId) => - _implements(firstMatch(annotation, assetId), COMPONENTS); + _implementsWithWarning(firstMatch(annotation.name, assetId), COMPONENTS); /// Checks if an [Annotation] node implements [View]. bool isView(Annotation annotation, AssetId assetId) => - _implements(firstMatch(annotation, assetId), VIEWS); - - /// Checks if `descriptor` extends or is any of the supplied `interfaces`. - bool _implements( - AnnotationDescriptor descriptor, List interfaces) { - if (descriptor == null) return false; - if (interfaces.contains(descriptor)) return true; - if (descriptor.superClass == null) return false; - var superClass = _annotations.firstWhere( - (a) => a.name == descriptor.superClass, orElse: () => null); - if (superClass == null) { - logger.warning( - 'Missing `custom_annotation` entry for `${descriptor.superClass}`.'); - return false; - } - return _implements(superClass, interfaces); - } - - // Checks if an [Annotation] matches an [AnnotationDescriptor]. - static bool _matchAnnotation( - Annotation annotation, AnnotationDescriptor descriptor, AssetId assetId) { - String name; - Identifier prefix; - if (annotation.name is PrefixedIdentifier) { - // TODO(jakemac): Shouldn't really need a cast here, remove once - // https://github.com/dart-lang/sdk/issues/23798 is fixed. - var prefixedName = annotation.name as PrefixedIdentifier; - name = prefixedName.identifier.name; - prefix = prefixedName.prefix; - } else { - name = annotation.name.name; - } - if (name != descriptor.name) return false; - return (annotation.root as CompilationUnit).directives - .where((d) => d is ImportDirective) - .any((ImportDirective i) { - var importMatch = false; - var uriString = i.uri.stringValue; - if (uriString == descriptor.import) { - importMatch = true; - } else if (uriString.startsWith('package:') || - uriString.startsWith('dart:')) { - return false; - } else { - importMatch = descriptor.assetId == - uriToAssetId(assetId, uriString, logger, null); - } - - if (!importMatch) return false; - if (prefix == null) return i.prefix == null; - if (i.prefix == null) return false; - return prefix.name == i.prefix.name; - }); - } -} - -/// String based description of an annotation class and its location. -class AnnotationDescriptor { - /// The name of the class. - final String name; - /// A `package:` style import path to the file where the class is defined. - final String import; - /// The class that this class extends or implements. This is the only optional - /// field. - final String superClass; - - AssetId get assetId => new AssetId(package, packagePath); - String get package => path.split(import.replaceFirst('package:', '')).first; - String get packagePath => path.joinAll(['lib'] - ..addAll(path.split(import.replaceFirst('package:', ''))..removeAt(0))); - - const AnnotationDescriptor(this.name, this.import, this.superClass); + _implementsWithWarning(firstMatch(annotation.name, assetId), VIEWS); } diff --git a/modules/angular2/src/transform/common/class_matcher_base.dart b/modules/angular2/src/transform/common/class_matcher_base.dart new file mode 100644 index 0000000000..f62e0f4bbf --- /dev/null +++ b/modules/angular2/src/transform/common/class_matcher_base.dart @@ -0,0 +1,121 @@ +library angular2.transform.common.class_matcher_base; + +import 'package:analyzer/src/generated/ast.dart'; +import 'package:barback/barback.dart' show AssetId; +import 'package:code_transformers/assets.dart'; +import 'package:path/path.dart' as path; +import 'logging.dart' show logger; + +/// Checks if a given [Identifier] matches any of the given [ClassDescriptor]s. +abstract class ClassMatcherBase { + /// Always start out with the default angular [ClassDescriptor]s. + final List _classDescriptors; + + ClassMatcherBase(this._classDescriptors); + + /// Adds a new [ClassDescriptor]. + void add(ClassDescriptor classDescriptor) => + _classDescriptors.add(classDescriptor); + + /// Adds a number of [ClassDescriptor]s. + void addAll(Iterable classDescriptors) => + _classDescriptors.addAll(classDescriptors); + + /// Returns the first [ClassDescriptor] that matches the given + /// [Identifier] node which appears in `assetId`. + ClassDescriptor firstMatch(Identifier className, AssetId assetId) => + _classDescriptors.firstWhere((a) => isMatch(className, a, assetId), + orElse: () => null); + + /// Checks whether an [Identifier] matches any [ClassDescriptor]. + bool hasMatch(Identifier className, AssetId assetId) => + _classDescriptors.any((a) => isMatch(className, a, assetId)); + + /// Checks whether an [Identifier] matches any [ClassDescriptor]. + ImportDirective getMatchingImport(Identifier className, AssetId assetId) { + for (var d in _classDescriptors) { + var matchingImport = _getMatchingImport(className, d, assetId); + if (matchingImport != null) { + return matchingImport; + } + } + return null; + } + + /// Checks if `descriptor` extends or is any of the supplied `interfaces`. + bool implements(ClassDescriptor descriptor, List interfaces, + {String missingSuperClassWarning}) { + if (descriptor == null) return false; + if (interfaces.contains(descriptor)) return true; + if (descriptor.superClass == null) return false; + var superClass = _classDescriptors.firstWhere( + (a) => a.name == descriptor.superClass, orElse: () => null); + if (superClass == null) { + if (missingSuperClassWarning != null && + missingSuperClassWarning.isNotEmpty) { + logger.warning(missingSuperClassWarning); + } + return false; + } + return implements(superClass, interfaces); + } +} + +// Returns an [ImportDirective] matching `descriptor` for `className` which appears in `assetId`, or `null` if none exists. +ImportDirective _getMatchingImport( + Identifier className, ClassDescriptor descriptor, AssetId assetId) { + if (className == null) return null; + String name; + Identifier prefix; + if (className is PrefixedIdentifier) { + name = className.identifier.name; + prefix = className.prefix; + } else { + name = className.name; + } + if (name != descriptor.name) return null; + return (className.root as CompilationUnit).directives + .where((d) => d is ImportDirective) + .firstWhere((ImportDirective i) { + var importMatch = false; + var uriString = i.uri.stringValue; + if (uriString == descriptor.import) { + importMatch = true; + } else if (uriString.startsWith('package:') || + uriString.startsWith('dart:')) { + return false; + } else { + importMatch = + descriptor.assetId == uriToAssetId(assetId, uriString, logger, null); + } + + if (!importMatch) return false; + if (prefix == null) return i.prefix == null; + if (i.prefix == null) return false; + return prefix.name == i.prefix.name; + }, orElse: () => null); +} + +// Checks if `className` which appears in `assetId` matches a [ClassDescriptor]. +bool isMatch( + Identifier className, ClassDescriptor descriptor, AssetId assetId) { + return _getMatchingImport(className, descriptor, assetId) != null; +} + +/// String based description of a class and its location. +class ClassDescriptor { + /// The name of the class. + final String name; + /// A `package:` style import path to the file where the class is defined. + final String import; + /// The class that this class extends or implements. This is the only optional + /// field. + final String superClass; + + AssetId get assetId => new AssetId(package, packagePath); + String get package => path.split(import.replaceFirst('package:', '')).first; + String get packagePath => path.joinAll(['lib'] + ..addAll(path.split(import.replaceFirst('package:', ''))..removeAt(0))); + + const ClassDescriptor(this.name, this.import, {this.superClass}); +} diff --git a/modules/angular2/src/transform/common/interface_matcher.dart b/modules/angular2/src/transform/common/interface_matcher.dart new file mode 100644 index 0000000000..c366fe4add --- /dev/null +++ b/modules/angular2/src/transform/common/interface_matcher.dart @@ -0,0 +1,77 @@ +library angular2.transform.common.annotati_ON_matcher; + +import 'package:analyzer/src/generated/ast.dart'; +import 'package:barback/barback.dart' show AssetId; +import 'class_matcher_base.dart'; + +export 'class_matcher_base.dart' show ClassDescriptor; + +/// [ClassDescriptor]s for the default angular interfaces that may be +/// implemented by a class. These classes are re-exported in many places so this +/// covers all libraries which provide them. +const _ON_CHANGE_INTERFACES = const [ + const ClassDescriptor('OnChange', 'package:angular2/angular2.dart'), + const ClassDescriptor('OnChange', 'package:angular2/annotations.dart'), + const ClassDescriptor( + 'OnChange', 'package:angular2/src/core/compiler/interfaces.dart'), +]; +const _ON_DESTROY_INTERFACES = const [ + const ClassDescriptor('OnDestroy', 'package:angular2/angular2.dart'), + const ClassDescriptor('OnDestroy', 'package:angular2/annotations.dart'), + const ClassDescriptor( + 'OnDestroy', 'package:angular2/src/core/compiler/interfaces.dart'), +]; +const _ON_CHECK_INTERFACES = const [ + const ClassDescriptor('OnCheck', 'package:angular2/angular2.dart'), + const ClassDescriptor('OnCheck', 'package:angular2/annotations.dart'), + const ClassDescriptor( + 'OnCheck', 'package:angular2/src/core/compiler/interfaces.dart'), +]; +const _ON_INIT_INTERFACES = const [ + const ClassDescriptor('OnInit', 'package:angular2/angular2.dart'), + const ClassDescriptor('OnInit', 'package:angular2/annotations.dart'), + const ClassDescriptor( + 'OnInit', 'package:angular2/src/core/compiler/interfaces.dart'), +]; +const _ON_ALL_CHANGES_DONE_INTERFACES = const [ + const ClassDescriptor('OnAllChangesDone', 'package:angular2/angular2.dart'), + const ClassDescriptor( + 'OnAllChangesDone', 'package:angular2/annotations.dart'), + const ClassDescriptor( + 'OnAllChangesDone', 'package:angular2/src/core/compiler/interfaces.dart') +]; + +/// Checks if a given [Annotation] matches any of the given +/// [ClassDescriptors]. +class InterfaceMatcher extends ClassMatcherBase { + InterfaceMatcher._(classDescriptors) : super(classDescriptors); + + factory InterfaceMatcher() { + return new InterfaceMatcher._([] + ..addAll(_ON_CHANGE_INTERFACES) + ..addAll(_ON_DESTROY_INTERFACES) + ..addAll(_ON_CHECK_INTERFACES) + ..addAll(_ON_INIT_INTERFACES) + ..addAll(_ON_ALL_CHANGES_DONE_INTERFACES)); + } + + /// Checks if an [Identifier] implements [OnChange]. + bool isOnChange(Identifier typeName, AssetId assetId) => + implements(firstMatch(typeName, assetId), _ON_CHANGE_INTERFACES); + + /// Checks if an [Identifier] implements [OnDestroy]. + bool isOnDestroy(Identifier typeName, AssetId assetId) => + implements(firstMatch(typeName, assetId), _ON_DESTROY_INTERFACES); + + /// Checks if an [Identifier] implements [OnCheck]. + bool isOnCheck(Identifier typeName, AssetId assetId) => + implements(firstMatch(typeName, assetId), _ON_CHECK_INTERFACES); + + /// Checks if an [Identifier] implements [OnInit]. + bool isOnInit(Identifier typeName, AssetId assetId) => + implements(firstMatch(typeName, assetId), _ON_INIT_INTERFACES); + + /// Checks if an [Identifier] implements [OnAllChangesDone]. + bool isOnAllChangesDone(Identifier typeName, AssetId assetId) => implements( + firstMatch(typeName, assetId), _ON_ALL_CHANGES_DONE_INTERFACES); +} diff --git a/modules/angular2/src/transform/common/options.dart b/modules/angular2/src/transform/common/options.dart index 55447f8cda..877b69f136 100644 --- a/modules/angular2/src/transform/common/options.dart +++ b/modules/angular2/src/transform/common/options.dart @@ -59,7 +59,7 @@ class TransformerOptions { factory TransformerOptions(List entryPoints, {List reflectionEntryPoints, String modeName: 'release', MirrorMode mirrorMode: MirrorMode.none, bool initReflector: true, - List customAnnotationDescriptors: const [], + List customAnnotationDescriptors: const [], int optimizationPhases: DEFAULT_OPTIMIZATION_PHASES, bool inlineViews: true, bool generateChangeDetectors: true}) { if (reflectionEntryPoints == null || reflectionEntryPoints.isEmpty) { diff --git a/modules/angular2/src/transform/common/options_reader.dart b/modules/angular2/src/transform/common/options_reader.dart index fea17511a1..4dfc0c9b1d 100644 --- a/modules/angular2/src/transform/common/options_reader.dart +++ b/modules/angular2/src/transform/common/options_reader.dart @@ -83,8 +83,8 @@ int _readInt(Map config, String paramName, {int defaultValue: null}) { } /// Parse the [CUSTOM_ANNOTATIONS_PARAM] options out of the transformer into -/// [AnnotationDescriptor]s. -List _readCustomAnnotations(Map config) { +/// [ClassDescriptor]s. +List _readCustomAnnotations(Map config) { var descriptors = []; var customAnnotations = config[CUSTOM_ANNOTATIONS_PARAM]; if (customAnnotations == null) return descriptors; @@ -104,7 +104,8 @@ List _readCustomAnnotations(Map config) { error = true; continue; } - descriptors.add(new AnnotationDescriptor(name, import, superClass)); + descriptors + .add(new ClassDescriptor(name, import, superClass: superClass)); } } if (error) { diff --git a/modules/angular2/src/transform/directive_processor/rewriter.dart b/modules/angular2/src/transform/directive_processor/rewriter.dart index 72823d3831..af47bdc54c 100644 --- a/modules/angular2/src/transform/directive_processor/rewriter.dart +++ b/modules/angular2/src/transform/directive_processor/rewriter.dart @@ -7,6 +7,7 @@ import 'package:angular2/src/render/xhr.dart' show XHR; import 'package:angular2/src/transform/common/annotation_matcher.dart'; import 'package:angular2/src/transform/common/asset_reader.dart'; import 'package:angular2/src/transform/common/async_string_writer.dart'; +import 'package:angular2/src/transform/common/interface_matcher.dart'; import 'package:angular2/src/transform/common/logging.dart'; import 'package:angular2/src/transform/common/names.dart'; import 'package:angular2/src/transform/common/xhr_impl.dart'; @@ -22,13 +23,15 @@ import 'visitors.dart'; /// 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. -Future createNgDeps(AssetReader reader, AssetId assetId, - AnnotationMatcher annotationMatcher, bool inlineViews) async { +Future createNgDeps( + AssetReader reader, AssetId assetId, AnnotationMatcher annotationMatcher, + {bool inlineViews}) async { // TODO(kegluneq): Shortcut if we can determine that there are no // [Directive]s present, taking into account `export`s. var writer = new AsyncStringWriter(); var visitor = new CreateNgDepsVisitor(writer, assetId, - new XhrImpl(reader, assetId), annotationMatcher, inlineViews); + new XhrImpl(reader, assetId), annotationMatcher, _interfaceMatcher, + inlineViews: inlineViews); var code = await reader.readAsString(assetId); parseCompilationUnit(code, name: assetId.path).accept(visitor); @@ -40,6 +43,8 @@ Future createNgDeps(AssetReader reader, AssetId assetId, return await writer.asyncToString(); } +InterfaceMatcher _interfaceMatcher = new InterfaceMatcher(); + /// Visitor responsible for processing [CompilationUnit] and creating an /// associated .ng_deps.dart file. class CreateNgDepsVisitor extends Object with SimpleAstVisitor { @@ -56,18 +61,24 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor { final ParameterTransformVisitor _paramsVisitor; final AnnotationsTransformVisitor _metaVisitor; final AnnotationMatcher _annotationMatcher; + final InterfaceMatcher _interfaceMatcher; /// The assetId for the file which we are parsing. final AssetId assetId; - CreateNgDepsVisitor(AsyncStringWriter writer, this.assetId, XHR xhr, - this._annotationMatcher, inlineViews) + CreateNgDepsVisitor(AsyncStringWriter writer, AssetId assetId, XHR xhr, + AnnotationMatcher annotationMatcher, InterfaceMatcher interfaceMatcher, + {bool inlineViews}) : writer = writer, _copyVisitor = new ToSourceVisitor(writer), _factoryVisitor = new FactoryTransformVisitor(writer), _paramsVisitor = new ParameterTransformVisitor(writer), _metaVisitor = new AnnotationsTransformVisitor( - writer, xhr, inlineViews); + writer, xhr, annotationMatcher, interfaceMatcher, assetId, + inlineViews: inlineViews), + _annotationMatcher = annotationMatcher, + _interfaceMatcher = interfaceMatcher, + this.assetId = assetId; void _visitNodeListWithSeparator(NodeList list, String separator) { if (list == null) return; @@ -174,7 +185,8 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor { @override Object visitClassDeclaration(ClassDeclaration node) { - if (!node.metadata.any((a) => _annotationMatcher.hasMatch(a, assetId))) { + if (!node.metadata + .any((a) => _annotationMatcher.hasMatch(a.name, assetId))) { return null; } @@ -200,11 +212,12 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor { if (node.implementsClause != null && node.implementsClause.interfaces != null && node.implementsClause.interfaces.isNotEmpty) { - writer.print(''', 'interfaces': const ['''); - writer.print(node.implementsClause.interfaces - .map((interface) => interface.name) - .join(', ')); - writer.print(']'); + writer + ..print(''', 'interfaces': const [''') + ..print(node.implementsClause.interfaces + .map((interface) => interface.name) + .join(', ')) + ..print(']'); } writer.print('})'); return null; @@ -243,7 +256,8 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor { @override bool visitFunctionDeclaration(FunctionDeclaration node) { - if (!node.metadata.any((a) => _annotationMatcher.hasMatch(a, assetId))) { + if (!node.metadata + .any((a) => _annotationMatcher.hasMatch(a.name, assetId))) { return null; } diff --git a/modules/angular2/src/transform/directive_processor/transformer.dart b/modules/angular2/src/transform/directive_processor/transformer.dart index e26f05e366..cb06f61e1c 100644 --- a/modules/angular2/src/transform/directive_processor/transformer.dart +++ b/modules/angular2/src/transform/directive_processor/transformer.dart @@ -35,7 +35,8 @@ class DirectiveProcessor extends Transformer { var asset = transform.primaryInput; var reader = new AssetReader.fromTransform(transform); var ngDepsSrc = await createNgDeps( - reader, asset.id, options.annotationMatcher, options.inlineViews); + reader, asset.id, options.annotationMatcher, + inlineViews: options.inlineViews); if (ngDepsSrc != null && ngDepsSrc.isNotEmpty) { var ngDepsAssetId = transform.primaryInput.id.changeExtension(DEPS_EXTENSION); diff --git a/modules/angular2/src/transform/directive_processor/visitors.dart b/modules/angular2/src/transform/directive_processor/visitors.dart index fe4b245581..c8cc41afbc 100644 --- a/modules/angular2/src/transform/directive_processor/visitors.dart +++ b/modules/angular2/src/transform/directive_processor/visitors.dart @@ -4,8 +4,11 @@ import 'dart:async'; import 'package:analyzer/analyzer.dart'; import 'package:analyzer/src/generated/java_core.dart'; import 'package:angular2/src/render/xhr.dart' show XHR; +import 'package:angular2/src/transform/common/annotation_matcher.dart'; import 'package:angular2/src/transform/common/async_string_writer.dart'; +import 'package:angular2/src/transform/common/interface_matcher.dart'; import 'package:angular2/src/transform/common/logging.dart'; +import 'package:barback/barback.dart'; /// `ToSourceVisitor` designed to accept {@link ConstructorDeclaration} nodes. class _CtorTransformVisitor extends ToSourceVisitor { @@ -203,25 +206,84 @@ class FactoryTransformVisitor extends _CtorTransformVisitor { } } -// TODO(kegluenq): Use pull #1772 to detect when available. -bool _isViewAnnotation(Annotation node) => '${node.name}' == 'View'; - /// ToSourceVisitor designed to print a `ClassDeclaration` node as a /// 'annotations' value for Angular2's `registerType` calls. class AnnotationsTransformVisitor extends ToSourceVisitor { final AsyncStringWriter writer; final XHR _xhr; + final AnnotationMatcher _annotationMatcher; + final InterfaceMatcher _interfaceMatcher; + final AssetId _assetId; final bool _inlineViews; final ConstantEvaluator _evaluator = new ConstantEvaluator(); - bool _processingView = false; + bool _isProcessingView = false; + bool _isProcessingDirective = false; + String _lifecycleValue = null; - AnnotationsTransformVisitor( - AsyncStringWriter writer, this._xhr, this._inlineViews) + AnnotationsTransformVisitor(AsyncStringWriter writer, this._xhr, + this._annotationMatcher, this._interfaceMatcher, this._assetId, + {bool inlineViews}) : this.writer = writer, + _inlineViews = inlineViews, super(writer); + /// Determines if the `node` has interface-based lifecycle methods and + /// populates `_lifecycleValue` with the appropriate values if so. If none are + /// present, `_lifecycleValue` is not modified. + void _populateLifecycleValue(ClassDeclaration node) { + var lifecycleEntries = []; + var prefix = ''; + var populateImport = (Identifier name) { + if (prefix.isNotEmpty) return; + var import = _interfaceMatcher.getMatchingImport(name, _assetId); + prefix = + import != null && import.prefix != null ? '${import.prefix}.' : ''; + }; + + var namesToTest = []; + + if (node.implementsClause != null && + node.implementsClause.interfaces != null && + node.implementsClause.interfaces.isNotEmpty) { + namesToTest.addAll(node.implementsClause.interfaces.map((i) => i.name)); + } + + if (node.extendsClause != null) { + namesToTest.add(node.extendsClause.superclass.name); + } + + namesToTest.forEach((name) { + if (_interfaceMatcher.isOnChange(name, _assetId)) { + lifecycleEntries.add('onChange'); + populateImport(name); + } + if (_interfaceMatcher.isOnDestroy(name, _assetId)) { + lifecycleEntries.add('onDestroy'); + populateImport(name); + } + if (_interfaceMatcher.isOnCheck(name, _assetId)) { + lifecycleEntries.add('onCheck'); + populateImport(name); + } + if (_interfaceMatcher.isOnInit(name, _assetId)) { + lifecycleEntries.add('onInit'); + populateImport(name); + } + if (_interfaceMatcher.isOnAllChangesDone(name, _assetId)) { + lifecycleEntries.add('onAllChangesDone'); + populateImport(name); + } + }); + if (lifecycleEntries.isNotEmpty) { + _lifecycleValue = 'const [${prefix}LifecycleEvent.' + '${lifecycleEntries.join(", ${prefix}LifecycleEvent.")}]'; + } + } + @override Object visitClassDeclaration(ClassDeclaration node) { + _populateLifecycleValue(node); + writer.print('const ['); var size = node.metadata.length; for (var i = 0; i < size; ++i) { @@ -231,6 +293,8 @@ class AnnotationsTransformVisitor extends ToSourceVisitor { node.metadata[i].accept(this); } writer.print(']'); + + _lifecycleValue = null; return null; } @@ -238,15 +302,30 @@ class AnnotationsTransformVisitor extends ToSourceVisitor { Object visitAnnotation(Annotation node) { writer.print('const '); if (node.name != null) { - _processingView = _isViewAnnotation(node); + _isProcessingDirective = _annotationMatcher.isDirective(node, _assetId); + _isProcessingView = _annotationMatcher.isView(node, _assetId); node.name.accept(this); + } else { + _isProcessingDirective = false; + _isProcessingView = false; } if (node.constructorName != null) { writer.print('.'); node.constructorName.accept(this); } - if (node.arguments != null) { - node.arguments.accept(this); + if (node.arguments != null && node.arguments.arguments != null) { + var args = node.arguments.arguments; + writer.print('('); + for (var i = 0, iLen = args.length; i < iLen; ++i) { + if (i != 0) { + writer.print(', '); + } + args[i].accept(this); + } + if (_lifecycleValue != null && _isProcessingDirective) { + writer.print(''', lifecycle: $_lifecycleValue '''); + } + writer.print(')'); } return null; } @@ -255,7 +334,7 @@ class AnnotationsTransformVisitor extends ToSourceVisitor { @override Object visitNamedExpression(NamedExpression node) { // TODO(kegluneq): Remove this limitation. - if (!_processingView || + if (!_isProcessingView || node.name is! Label || node.name.label is! SimpleIdentifier) { return super.visitNamedExpression(node); diff --git a/modules/angular2/test/transform/directive_processor/all_tests.dart b/modules/angular2/test/transform/directive_processor/all_tests.dart index b279b03d10..8e33d24b7b 100644 --- a/modules/angular2/test/transform/directive_processor/all_tests.dart +++ b/modules/angular2/test/transform/directive_processor/all_tests.dart @@ -24,20 +24,22 @@ void allTests() { _testNgDeps('should recognize custom annotations with package: imports', 'custom_metadata/package_soup.dart', customDescriptors: [ - const AnnotationDescriptor('Soup', 'package:soup/soup.dart', 'Component'), + const ClassDescriptor('Soup', 'package:soup/soup.dart', + superClass: 'Component'), ]); _testNgDeps('should recognize custom annotations with relative imports', 'custom_metadata/relative_soup.dart', assetId: new AssetId('soup', 'lib/relative_soup.dart'), customDescriptors: [ - const AnnotationDescriptor( - 'Soup', 'package:soup/annotations/soup.dart', 'Component'), + const ClassDescriptor('Soup', 'package:soup/annotations/soup.dart', + superClass: 'Component'), ]); _testNgDeps('Requires the specified import.', 'custom_metadata/bad_soup.dart', customDescriptors: [ - const AnnotationDescriptor('Soup', 'package:soup/soup.dart', 'Component'), + const ClassDescriptor('Soup', 'package:soup/soup.dart', + superClass: 'Component'), ]); _testNgDeps( @@ -81,6 +83,20 @@ void allTests() { _testNgDeps('should not include superclasses in `interfaces`.', 'superclass_files/soup.dart'); + _testNgDeps( + 'should populate `lifecycle` when lifecycle interfaces are present.', + 'interface_lifecycle_files/soup.dart'); + + _testNgDeps('should populate multiple `lifecycle` values when necessary.', + 'multiple_interface_lifecycle_files/soup.dart'); + + _testNgDeps( + 'should populate `lifecycle` when lifecycle superclass is present.', + 'superclass_lifecycle_files/soup.dart'); + + _testNgDeps('should populate `lifecycle` with prefix when necessary.', + 'prefixed_interface_lifecycle_files/soup.dart'); + _testNgDeps( 'should not throw/hang on invalid urls', 'invalid_url_files/hello.dart', expectedLogs: [ @@ -119,8 +135,8 @@ void _testNgDeps(String name, String inputPath, var expectedId = _assetIdForPath(expectedPath); var annotationMatcher = new AnnotationMatcher()..addAll(customDescriptors); - var output = - await createNgDeps(reader, inputId, annotationMatcher, inlineViews); + var output = await createNgDeps(reader, inputId, annotationMatcher, + inlineViews: inlineViews); if (output == null) { expect(await reader.hasInput(expectedId)).toBeFalse(); } else { diff --git a/modules/angular2/test/transform/directive_processor/interface_lifecycle_files/expected/soup.ng_deps.dart b/modules/angular2/test/transform/directive_processor/interface_lifecycle_files/expected/soup.ng_deps.dart new file mode 100644 index 0000000000..1597243aed --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/interface_lifecycle_files/expected/soup.ng_deps.dart @@ -0,0 +1,59 @@ +library dinner.soup.ng_deps.dart; + +import 'soup.dart'; +export 'soup.dart'; +import 'package:angular2/src/reflection/reflection.dart' as _ngRef; +import 'package:angular2/annotations.dart'; + +var _visited = false; +void initReflector() { + if (_visited) return; + _visited = true; + _ngRef.reflector + ..registerType(OnChangeSoupComponent, { + 'factory': () => new OnChangeSoupComponent(), + 'parameters': const [], + 'annotations': const [ + const Component( + selector: '[soup]', lifecycle: const [LifecycleEvent.onChange]) + ], + 'interfaces': const [OnChange] + }) + ..registerType(OnDestroySoupComponent, { + 'factory': () => new OnDestroySoupComponent(), + 'parameters': const [], + 'annotations': const [ + const Component( + selector: '[soup]', lifecycle: const [LifecycleEvent.onDestroy]) + ], + 'interfaces': const [OnDestroy] + }) + ..registerType(OnCheckSoupComponent, { + 'factory': () => new OnCheckSoupComponent(), + 'parameters': const [], + 'annotations': const [ + const Component( + selector: '[soup]', lifecycle: const [LifecycleEvent.onCheck]) + ], + 'interfaces': const [OnCheck] + }) + ..registerType(OnInitSoupComponent, { + 'factory': () => new OnInitSoupComponent(), + 'parameters': const [], + 'annotations': const [ + const Component( + selector: '[soup]', lifecycle: const [LifecycleEvent.onInit]) + ], + 'interfaces': const [OnInit] + }) + ..registerType(OnAllChangesDoneSoupComponent, { + 'factory': () => new OnAllChangesDoneSoupComponent(), + 'parameters': const [], + 'annotations': const [ + const Component( + selector: '[soup]', + lifecycle: const [LifecycleEvent.onAllChangesDone]) + ], + 'interfaces': const [OnAllChangesDone] + }); +} diff --git a/modules/angular2/test/transform/directive_processor/interface_lifecycle_files/soup.dart b/modules/angular2/test/transform/directive_processor/interface_lifecycle_files/soup.dart new file mode 100644 index 0000000000..b0173c000b --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/interface_lifecycle_files/soup.dart @@ -0,0 +1,18 @@ +library dinner.soup; + +import 'package:angular2/annotations.dart'; + +@Component(selector: '[soup]') +class OnChangeSoupComponent implements OnChange {} + +@Component(selector: '[soup]') +class OnDestroySoupComponent implements OnDestroy {} + +@Component(selector: '[soup]') +class OnCheckSoupComponent implements OnCheck {} + +@Component(selector: '[soup]') +class OnInitSoupComponent implements OnInit {} + +@Component(selector: '[soup]') +class OnAllChangesDoneSoupComponent implements OnAllChangesDone {} diff --git a/modules/angular2/test/transform/directive_processor/multiple_interface_lifecycle_files/expected/soup.ng_deps.dart b/modules/angular2/test/transform/directive_processor/multiple_interface_lifecycle_files/expected/soup.ng_deps.dart new file mode 100644 index 0000000000..b4560a0dc6 --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/multiple_interface_lifecycle_files/expected/soup.ng_deps.dart @@ -0,0 +1,27 @@ +library dinner.soup.ng_deps.dart; + +import 'soup.dart'; +export 'soup.dart'; +import 'package:angular2/src/reflection/reflection.dart' as _ngRef; +import 'package:angular2/annotations.dart'; + +var _visited = false; +void initReflector() { + if (_visited) return; + _visited = true; + _ngRef.reflector + ..registerType(MultiSoupComponent, { + 'factory': () => new MultiSoupComponent(), + 'parameters': const [], + 'annotations': const [ + const Component( + selector: '[soup]', + lifecycle: const [ + LifecycleEvent.onChange, + LifecycleEvent.onDestroy, + LifecycleEvent.onInit + ]) + ], + 'interfaces': const [OnChange, OnDestroy, OnInit] + }); +} diff --git a/modules/angular2/test/transform/directive_processor/multiple_interface_lifecycle_files/soup.dart b/modules/angular2/test/transform/directive_processor/multiple_interface_lifecycle_files/soup.dart new file mode 100644 index 0000000000..8ef9fb91fb --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/multiple_interface_lifecycle_files/soup.dart @@ -0,0 +1,6 @@ +library dinner.soup; + +import 'package:angular2/annotations.dart'; + +@Component(selector: '[soup]') +class MultiSoupComponent implements OnChange, OnDestroy, OnInit {} diff --git a/modules/angular2/test/transform/directive_processor/prefixed_interface_lifecycle_files/expected/soup.ng_deps.dart b/modules/angular2/test/transform/directive_processor/prefixed_interface_lifecycle_files/expected/soup.ng_deps.dart new file mode 100644 index 0000000000..edf39de363 --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/prefixed_interface_lifecycle_files/expected/soup.ng_deps.dart @@ -0,0 +1,23 @@ +library dinner.soup.ng_deps.dart; + +import 'soup.dart'; +export 'soup.dart'; +import 'package:angular2/src/reflection/reflection.dart' as _ngRef; +import 'package:angular2/annotations.dart' as prefix; + +var _visited = false; +void initReflector() { + if (_visited) return; + _visited = true; + _ngRef.reflector + ..registerType(OnChangeSoupComponent, { + 'factory': () => new OnChangeSoupComponent(), + 'parameters': const [], + 'annotations': const [ + const prefix.Component( + selector: '[soup]', + lifecycle: const [prefix.LifecycleEvent.onChange]) + ], + 'interfaces': const [prefix.OnChange] + }); +} diff --git a/modules/angular2/test/transform/directive_processor/prefixed_interface_lifecycle_files/soup.dart b/modules/angular2/test/transform/directive_processor/prefixed_interface_lifecycle_files/soup.dart new file mode 100644 index 0000000000..73f411a6b1 --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/prefixed_interface_lifecycle_files/soup.dart @@ -0,0 +1,6 @@ +library dinner.soup; + +import 'package:angular2/annotations.dart' as prefix; + +@prefix.Component(selector: '[soup]') +class OnChangeSoupComponent implements prefix.OnChange {} diff --git a/modules/angular2/test/transform/directive_processor/superclass_lifecycle_files/expected/soup.ng_deps.dart b/modules/angular2/test/transform/directive_processor/superclass_lifecycle_files/expected/soup.ng_deps.dart new file mode 100644 index 0000000000..945cd1755d --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/superclass_lifecycle_files/expected/soup.ng_deps.dart @@ -0,0 +1,21 @@ +library dinner.soup.ng_deps.dart; + +import 'soup.dart'; +export 'soup.dart'; +import 'package:angular2/src/reflection/reflection.dart' as _ngRef; +import 'package:angular2/annotations.dart'; + +var _visited = false; +void initReflector() { + if (_visited) return; + _visited = true; + _ngRef.reflector + ..registerType(OnChangeSoupComponent, { + 'factory': () => new OnChangeSoupComponent(), + 'parameters': const [], + 'annotations': const [ + const Component( + selector: '[soup]', lifecycle: const [LifecycleEvent.onChange]) + ] + }); +} diff --git a/modules/angular2/test/transform/directive_processor/superclass_lifecycle_files/soup.dart b/modules/angular2/test/transform/directive_processor/superclass_lifecycle_files/soup.dart new file mode 100644 index 0000000000..88449969ef --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/superclass_lifecycle_files/soup.dart @@ -0,0 +1,6 @@ +library dinner.soup; + +import 'package:angular2/annotations.dart'; + +@Component(selector: '[soup]') +class OnChangeSoupComponent extends OnChange {}