diff --git a/lib/core/registry_dynamic.dart b/lib/core/registry_dynamic.dart index 7a2edfc06..54ea6573d 100644 --- a/lib/core/registry_dynamic.dart +++ b/lib/core/registry_dynamic.dart @@ -57,24 +57,29 @@ class DynamicMetadataExtractor implements MetadataExtractor { Map fieldMetadataExtractor(Type type) => - _fieldMetadataCache.putIfAbsent(type, () => _fieldMetadataExtractor(type)); + _fieldMetadataCache.putIfAbsent(type, () => _fieldMetadataExtractor(reflectType(type))); - Map _fieldMetadataExtractor(Type type) { - ClassMirror cm = reflectType(type); - final fields = {}; - cm.declarations.forEach((Symbol name, DeclarationMirror decl) { - if (decl is VariableMirror || - decl is MethodMirror && (decl.isGetter || decl.isSetter)) { - var fieldName = MirrorSystem.getName(name); - if (decl is MethodMirror && decl.isSetter) { + Map _fieldMetadataExtractor(ClassMirror cm) { + var fields = {}; + if(cm.superclass != null) { + fields.addAll(_fieldMetadataExtractor(cm.superclass)); + } else { + fields = {}; + } + Map declarations = cm.declarations; + declarations.forEach((symbol, dm) { + if(dm is VariableMirror || + dm is MethodMirror && (dm.isGetter || dm.isSetter)) { + var fieldName = MirrorSystem.getName(symbol); + if (dm is MethodMirror && dm.isSetter) { // Remove "=" from the end of the setter. fieldName = fieldName.substring(0, fieldName.length - 1); } - decl.metadata.forEach((InstanceMirror meta) { + dm.metadata.forEach((InstanceMirror meta) { if (_fieldAnnotations.contains(meta.type)) { if (fields.containsKey(fieldName)) { throw 'Attribute annotation for $fieldName is defined more ' - 'than once in $type'; + 'than once in ${cm.reflectedType}'; } fields[fieldName] = meta.reflectee as DirectiveAnnotation; } diff --git a/lib/tools/source_metadata_extractor.dart b/lib/tools/source_metadata_extractor.dart index ea1bb7df3..3575e8c5e 100644 --- a/lib/tools/source_metadata_extractor.dart +++ b/lib/tools/source_metadata_extractor.dart @@ -1,6 +1,7 @@ library angular.source_metadata_extractor ; import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; import 'package:angular/tools/source_crawler.dart'; import 'package:angular/tools/common.dart'; @@ -181,34 +182,51 @@ class DirectiveMetadataCollectingAstVisitor extends RecursiveAstVisitor { } }); - // Check fields/getters/setter for presense of attr mapping annotations. - clazz.members.forEach((ClassMember member) { - if (member is FieldDeclaration || - (member is MethodDeclaration && - (member.isSetter || member.isGetter))) { - member.metadata.forEach((Annotation ann) { - if (_attrAnnotationsToSpec.containsKey(ann.name.name)) { - String fieldName; - if (member is FieldDeclaration) { - fieldName = member.fields.variables.first.name.name; - } else { // MethodDeclaration - fieldName = (member as MethodDeclaration).name.name; - } - StringLiteral attNameLiteral = ann.arguments.arguments.first; - if (meta.attributeMappings - .containsKey(attNameLiteral.stringValue)) { - throw 'Attribute mapping already defined for ' - '${clazz.name}.$fieldName'; - } - meta.attributeMappings[attNameLiteral.stringValue] = - _attrAnnotationsToSpec[ann.name.name] + fieldName; - } - }); - } - }); + if (meta != null) _walkSuperclassChain(clazz, meta, _extractMappingsFromClass); }); + return super.visitClassDeclaration(clazz); } + + _walkSuperclassChain(ClassDeclaration clazz, DirectiveMetadata meta, + metadataExtractor(ClassDeclaration clazz, DirectiveMetadata meta)) { + while (clazz != null) { + metadataExtractor(clazz, meta); + if (clazz.element != null && clazz.element.supertype != null) { + clazz = clazz.element.supertype.element.node; + } else { + clazz = null; + } + } + } + + _extractMappingsFromClass(ClassDeclaration clazz, DirectiveMetadata meta) { + // Check fields/getters/setter for presence of attr mapping annotations. + clazz.members.forEach((ClassMember member) { + if (member is FieldDeclaration || + (member is MethodDeclaration && + (member.isSetter || member.isGetter))) { + member.metadata.forEach((Annotation ann) { + if (_attrAnnotationsToSpec.containsKey(ann.name.name)) { + String fieldName; + if (member is FieldDeclaration) { + fieldName = member.fields.variables.first.name.name; + } else { // MethodDeclaration + fieldName = (member as MethodDeclaration).name.name; + } + StringLiteral attNameLiteral = ann.arguments.arguments.first; + if (meta.attributeMappings + .containsKey(attNameLiteral.stringValue)) { + throw 'Attribute mapping already defined for ' + '${clazz.name}.$fieldName'; + } + meta.attributeMappings[attNameLiteral.stringValue] = + _attrAnnotationsToSpec[ann.name.name] + fieldName; + } + }); + } + }); + } } class DirectiveMetadataCollectingVisitor { diff --git a/lib/tools/transformer/metadata_extractor.dart b/lib/tools/transformer/metadata_extractor.dart index 953c6169d..c320c15e5 100644 --- a/lib/tools/transformer/metadata_extractor.dart +++ b/lib/tools/transformer/metadata_extractor.dart @@ -242,20 +242,31 @@ class AnnotationExtractor { /// Extracts all of the annotations for the specified class. AnnotatedType extractAnnotations(ClassElement cls) { - if (resolver.getImportUri(cls.library, from: outputId) == null) { - warn('Dropping annotations for ${cls.name} because the ' - 'containing file cannot be imported (must be in a lib folder).', cls); - return null; - } - + var classElement = cls; var visitor = new _AnnotationVisitor(_annotationElements); - cls.node.accept(visitor); + while (classElement != null) { + if (resolver.getImportUri(classElement.library, from: outputId) == null) { + warn('Dropping annotations for ${classElement.name} because the ' + 'containing file cannot be imported (must be in a lib folder).', classElement); + return null; + } + if (classElement.node != null) { + classElement.node.accept(visitor); + } + + if (classElement.supertype != null) { + visitor.visitingSupertype = true; + classElement = classElement.supertype.element; + } else { + classElement = null; + } + } if (!visitor.hasAnnotations) return null; var type = new AnnotatedType(cls); type.annotations = visitor.classAnnotations - .where((annotation) { + .where((Annotation annotation) { var element = annotation.element; if (element != null && !element.isPublic) { warn('Annotation $annotation is not public.', @@ -277,6 +288,7 @@ class AnnotationExtractor { element.enclosingElement.type.isAssignableTo(formatterType.type); }).toList(); + if (type.annotations.isEmpty) return null; var memberAnnotations = {}; visitor.memberAnnotations.forEach((memberName, annotations) { @@ -293,8 +305,6 @@ class AnnotationExtractor { _foldMemberAnnotations(memberAnnotations, type); } - if (type.annotations.isEmpty) return null; - return type; } @@ -309,10 +319,6 @@ class AnnotationExtractor { return element.enclosingElement.type.isAssignableTo( directiveType.type); }); - if (ngAnnotations.isEmpty) { - warn('Found field annotation but no class directives.', type.type); - return; - } var mapType = resolver.getType('dart.core.Map').type; // Find acceptable constructors- ones which take a param named 'map' @@ -410,6 +416,7 @@ class _AnnotationVisitor extends GeneralizingAstVisitor { final List allowedMemberAnnotations; final List classAnnotations = []; final Map> memberAnnotations = {}; + var visitingSupertype = false; _AnnotationVisitor(this.allowedMemberAnnotations); @@ -417,8 +424,9 @@ class _AnnotationVisitor extends GeneralizingAstVisitor { var parent = annotation.parent; if (parent is! Declaration) return; - if (parent.element is ClassElement) { + if (parent.element is ClassElement && !visitingSupertype) { classAnnotations.add(annotation); + } else if (allowedMemberAnnotations.contains(annotation.element)) { if (parent is MethodDeclaration) { memberAnnotations.putIfAbsent(parent.name.name, () => []) diff --git a/perf/pubspec.lock b/perf/pubspec.lock index 967261ff1..a08436011 100644 --- a/perf/pubspec.lock +++ b/perf/pubspec.lock @@ -30,18 +30,15 @@ packages: code_transformers: description: code_transformers source: hosted - version: "0.1.1" + version: "0.1.3" collection: description: collection source: hosted version: "0.9.1" di: - description: - ref: null - resolved-ref: a2de00d84934f4610d1f9e108c39614e66a773f5 - url: "git://github.com/angular/di.dart.git" - source: git - version: "0.0.34" + description: di + source: hosted + version: "0.0.37" html5lib: description: html5lib source: hosted @@ -73,7 +70,7 @@ packages: route_hierarchical: description: route_hierarchical source: hosted - version: "0.4.15" + version: "0.4.18" shadow_dom: description: shadow_dom source: hosted diff --git a/test/core/core_directive_spec.dart b/test/core/core_directive_spec.dart index d61d2e673..48f60b5ed 100644 --- a/test/core/core_directive_spec.dart +++ b/test/core/core_directive_spec.dart @@ -67,6 +67,29 @@ void main() { 'in Bad2Component'); }); }); + + describe("Inheritance", () { + var element; + var nodeAttrs; + + beforeEachModule((Module module) { + module..type(Sub)..type(Base); + }); + + it("should extract attr map from annotated component which inherits other component", (DirectiveMap directives) { + var annotations = directives.annotationsFor(Sub); + expect(annotations.length).toEqual(1); + expect(annotations[0] is Directive).toBeTruthy(); + + Directive annotation = annotations[0]; + expect(annotation.selector).toEqual('[sub]'); + expect(annotation.map).toEqual({ + "foo": "=>foo", + "bar": "=>bar", + "baz": "=>baz" + }); + }); + }); }); } @@ -141,3 +164,18 @@ class Bad2Component { @NgOneWay('foo') set foo(val) {} } + +@Decorator(selector: '[sub]') +class Sub extends Base { + @NgOneWay('bar') + String bar; +} + +class Base { + @NgOneWay('baz') + String baz; + + @NgOneWay('foo') + String foo; +} + diff --git a/test/core/registry_spec.dart b/test/core/registry_spec.dart index deb14ca9e..f06241bf5 100644 --- a/test/core/registry_spec.dart +++ b/test/core/registry_spec.dart @@ -61,4 +61,4 @@ class MyAnnotation { } @MyAnnotation('A') @MyAnnotation('B') class A1 {} -@MyAnnotation('A') class A2 {} +@MyAnnotation('A') class A2 {} \ No newline at end of file diff --git a/test/tools/transformer/expression_generator_spec.dart b/test/tools/transformer/expression_generator_spec.dart index dd012b724..763ef5c77 100644 --- a/test/tools/transformer/expression_generator_spec.dart +++ b/test/tools/transformer/expression_generator_spec.dart @@ -91,6 +91,39 @@ main() { symbols: []); }); + it('should generate expressions for variables found in superclass', () { + return generates(phases, + inputs: { + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgComponent( + templateUrl: 'lib/foo.html', + selector: 'my-component') + class FooComponent extends BarComponent { + @NgAttr('foo') + var foo; + } + + class BarComponent { + @NgAttr('bar') + var bar; + } + + main() {} + ''', + 'a|lib/foo.html': ''' +
{{template.foo}}
+
{{template.bar}}
''', + 'a|web/index.html': ''' + ''', + 'angular|lib/angular.dart': libAngular, + }, + getters: ['foo', 'bar', 'template'], + setters: ['foo', 'bar', 'template'], + symbols: []); + }); + it('should apply additional HTML files', () { htmlFiles.add('web/dummy.html'); htmlFiles.add('/packages/b/bar.html'); @@ -186,4 +219,9 @@ library angular.core.annotation_src; class Component { const Component({String templateUrl, String selector}); } + +class NgAttr { + final _mappingSpec = '@'; + const NgAttr(String attrName); +} '''; diff --git a/test/tools/transformer/metadata_generator_spec.dart b/test/tools/transformer/metadata_generator_spec.dart index e8aae3c8c..f57586825 100644 --- a/test/tools/transformer/metadata_generator_spec.dart +++ b/test/tools/transformer/metadata_generator_spec.dart @@ -58,6 +58,50 @@ main() { }); }); + it('should extract member metadata from superclass', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + @NgOneWay('another-expression') + String anotherExpression; + + @NgCallback('callback') + set callback(Function) {} + + set twoWayStuff(String abc) {} + @NgTwoWay('two-way-stuff') + String get twoWayStuff => null; + } + + @NgDirective(selector: r'[*=/{{.*}}/]') + class InternalCombustionEngine extends Engine { + @NgOneWay('ice-expression') + String iceExpression; + } + main() {} + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.InternalCombustionEngine': [ + 'const import_1.NgDirective(selector: r\'[*=/{{.*}}/]\', ' + 'map: const {' + '\'ice-expression\': \'=>iceExpression\', ' + '\'another-expression\': \'=>anotherExpression\', ' + '\'callback\': \'&callback\', ' + '\'two-way-stuff\': \'<=>twoWayStuff\'' + '})', + ] + }); + }); + it('should warn on multiple annotations', () { return generates(phases, inputs: {