Index: third_party/pkg/angular/lib/tools/transformer/metadata_extractor.dart |
diff --git a/third_party/pkg/angular/lib/tools/transformer/metadata_extractor.dart b/third_party/pkg/angular/lib/tools/transformer/metadata_extractor.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..f1858be868c02aea8e6bb45bedaf1088e7579a45 |
--- /dev/null |
+++ b/third_party/pkg/angular/lib/tools/transformer/metadata_extractor.dart |
@@ -0,0 +1,442 @@ |
+library angular.metadata_extractor; |
+ |
+import 'package:analyzer/src/generated/ast.dart'; |
+import 'package:analyzer/src/generated/element.dart'; |
+import 'package:analyzer/src/generated/parser.dart' show ResolutionCopier; |
+import 'package:analyzer/src/generated/scanner.dart'; |
+import 'package:analyzer/src/generated/utilities_dart.dart' show ParameterKind; |
+import 'package:barback/barback.dart'; |
+import 'package:code_transformers/resolver.dart'; |
+ |
+class AnnotatedType { |
+ final ClassElement type; |
+ Iterable<Annotation> annotations; |
+ |
+ AnnotatedType(this.type); |
+ |
+ /** |
+ * Finds all the libraries referenced by the annotations |
+ */ |
+ Iterable<LibraryElement> get referencedLibraries { |
+ var libs = new Set(); |
+ libs.add(type.library); |
+ |
+ var libCollector = new _LibraryCollector(); |
+ for (var annotation in annotations) { |
+ annotation.accept(libCollector); |
+ } |
+ libs.addAll(libCollector.libraries); |
+ |
+ return libs; |
+ } |
+ |
+ void writeClassAnnotations(StringBuffer sink, TransformLogger logger, |
+ Resolver resolver, Map<LibraryElement, String> prefixes) { |
+ sink.write(' ${prefixes[type.library]}${type.name}: const [\n'); |
+ var writer = new _AnnotationWriter(sink, prefixes); |
+ for (var annotation in annotations) { |
+ sink.write(' '); |
+ if (writer.writeAnnotation(annotation)) { |
+ sink.write(',\n'); |
+ } else { |
+ sink.write('null,\n'); |
+ logger.warning('Unable to serialize annotation $annotation.', |
+ asset: resolver.getSourceAssetId(annotation.parent.element), |
+ span: resolver.getSourceSpan(annotation.parent.element)); |
+ } |
+ } |
+ sink.write(' ],\n'); |
+ } |
+} |
+ |
+/** |
+ * Helper which finds all libraries referenced within the provided AST. |
+ */ |
+class _LibraryCollector extends GeneralizingAstVisitor { |
+ final Set<LibraryElement> libraries = new Set<LibraryElement>(); |
+ void visitSimpleIdentifier(SimpleIdentifier s) { |
+ var element = s.bestElement; |
+ if (element != null) { |
+ libraries.add(element.library); |
+ } |
+ } |
+} |
+ |
+/** |
+ * Helper class which writes annotations out to the buffer. |
+ * This does not support every syntax possible, but will return false when |
+ * the annotation cannot be serialized. |
+ */ |
+class _AnnotationWriter { |
+ final StringBuffer sink; |
+ final Map<LibraryElement, String> prefixes; |
+ |
+ _AnnotationWriter(this.sink, this.prefixes); |
+ |
+ /** |
+ * Returns true if the annotation was successfully serialized. |
+ * If the annotation could not be written then the buffer is returned to its |
+ * original state. |
+ */ |
+ bool writeAnnotation(Annotation annotation) { |
+ // Record the current location in the buffer and if writing fails then |
+ // back up the buffer to where we started. |
+ var len = sink.length; |
+ if (!_writeAnnotation(annotation)) { |
+ var str = sink.toString(); |
+ sink.clear(); |
+ sink.write(str.substring(0, len)); |
+ return false; |
+ } |
+ return true; |
+ } |
+ |
+ bool _writeAnnotation(Annotation annotation) { |
+ var element = annotation.element; |
+ if (element is ConstructorElement) { |
+ sink.write('const ${prefixes[element.library]}' |
+ '${element.enclosingElement.name}'); |
+ // Named constructors |
+ if (!element.name.isEmpty) { |
+ sink.write('.${element.name}'); |
+ } |
+ sink.write('('); |
+ if (!_writeArguments(annotation)) return false; |
+ sink.write(')'); |
+ return true; |
+ } else if (element is PropertyAccessorElement) { |
+ sink.write('${prefixes[element.library]}${element.name}'); |
+ return true; |
+ } |
+ |
+ return false; |
+ } |
+ |
+ /** Writes the arguments for a type constructor. */ |
+ bool _writeArguments(Annotation annotation) { |
+ var args = annotation.arguments; |
+ var index = 0; |
+ for (var arg in args.arguments) { |
+ if (arg is NamedExpression) { |
+ sink.write('${arg.name.label.name}: '); |
+ if (!_writeExpression(arg.expression)) return false; |
+ } else { |
+ if (!_writeExpression(arg)) return false; |
+ } |
+ if (++index < args.arguments.length) { |
+ sink.write(', '); |
+ } |
+ } |
+ return true; |
+ } |
+ |
+ /** Writes an expression. */ |
+ bool _writeExpression(Expression expression) { |
+ if (expression is StringLiteral) { |
+ sink.write(expression.toSource()); |
+ return true; |
+ } |
+ if (expression is ListLiteral) { |
+ sink.write('const ['); |
+ for (var element in expression.elements) { |
+ if (!_writeExpression(element)) return false; |
+ sink.write(','); |
+ } |
+ sink.write(']'); |
+ return true; |
+ } |
+ if (expression is MapLiteral) { |
+ sink.write('const {'); |
+ var index = 0; |
+ for (var entry in expression.entries) { |
+ if (!_writeExpression(entry.key)) return false; |
+ sink.write(': '); |
+ if (!_writeExpression(entry.value)) return false; |
+ if (++index < expression.entries.length) { |
+ sink.write(', '); |
+ } |
+ } |
+ sink.write('}'); |
+ return true; |
+ } |
+ if (expression is Identifier) { |
+ var element = expression.bestElement; |
+ if (element == null || !element.isPublic) return false; |
+ |
+ if (element is ClassElement) { |
+ sink.write('${prefixes[element.library]}${element.name}'); |
+ return true; |
+ } |
+ if (element is PropertyAccessorElement) { |
+ var variable = element.variable; |
+ if (variable is FieldElement) { |
+ var cls = variable.enclosingElement; |
+ sink.write('${prefixes[cls.library]}${cls.name}.${variable.name}'); |
+ return true; |
+ } else if (variable is TopLevelVariableElement) { |
+ sink.write('${prefixes[variable.library]}${variable.name}'); |
+ return true; |
+ } |
+ } |
+ |
+ if (element is MethodElement) { |
+ var cls = element.enclosingElement; |
+ sink.write('${prefixes[cls.library]}${cls.name}.${element.name}'); |
+ return true; |
+ } |
+ } |
+ if (expression is BooleanLiteral || expression is DoubleLiteral || |
+ expression is IntegerLiteral || expression is NullLiteral) { |
+ sink.write(expression.toSource()); |
+ return true; |
+ } |
+ return false; |
+ } |
+} |
+ |
+class AnnotationExtractor { |
+ final TransformLogger logger; |
+ final Resolver resolver; |
+ final AssetId outputId; |
+ |
+ static const List<String> _angularAnnotationNames = const [ |
+ 'angular.core.annotation_src.NgAttr', |
+ 'angular.core.annotation_src.NgOneWay', |
+ 'angular.core.annotation_src.NgOneWayOneTime', |
+ 'angular.core.annotation_src.NgTwoWay', |
+ 'angular.core.annotation_src.NgCallback' |
+ ]; |
+ |
+ static const Map<String, String> _annotationToMapping = const { |
+ 'NgAttr': '@', |
+ 'NgOneWay': '=>', |
+ 'NgOneWayOneTime': '=>!', |
+ 'NgTwoWay': '<=>', |
+ 'NgCallback': '&', |
+ }; |
+ |
+ ClassElement directiveType; |
+ ClassElement formatterType; |
+ |
+ /// Resolved annotations that this will pick up for members. |
+ final List<Element> _annotationElements = <Element>[]; |
+ |
+ AnnotationExtractor(this.logger, this.resolver, this.outputId) { |
+ for (var annotation in _angularAnnotationNames) { |
+ var type = resolver.getType(annotation); |
+ if (type == null) { |
+ logger.warning('Unable to resolve $annotation, skipping metadata.'); |
+ continue; |
+ } |
+ _annotationElements.add(type.unnamedConstructor); |
+ } |
+ directiveType = resolver.getType('angular.core.annotation_src.Directive'); |
+ formatterType = resolver.getType('angular.core.annotation_src.Formatter'); |
+ if (directiveType == null) { |
+ logger.warning('Unable to resolve Directive, skipping member annotations.'); |
+ } |
+ if (formatterType == null) { |
+ logger.warning('Unable to resolve Formatter.'); |
+ } |
+ } |
+ |
+ /// Extracts all of the annotations for the specified class. |
+ AnnotatedType extractAnnotations(ClassElement cls) { |
+ var classElement = cls; |
+ var visitor = new _AnnotationVisitor(_annotationElements); |
+ 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 annotation) { |
+ var element = annotation.element; |
+ if (element != null && !element.isPublic) { |
+ warn('Annotation $annotation is not public.', |
+ annotation.parent.element); |
+ return false; |
+ } |
+ if (element is! ConstructorElement) { |
+ // Only keeping constructor elements. |
+ return false; |
+ } |
+ ConstructorElement ctor = element; |
+ var cls = ctor.enclosingElement; |
+ if (!cls.isPublic) { |
+ warn('Annotation $annotation is not public.', |
+ annotation.parent.element); |
+ return false; |
+ } |
+ return element.enclosingElement.type.isAssignableTo(directiveType.type) || |
+ element.enclosingElement.type.isAssignableTo(formatterType.type); |
+ }).toList(); |
+ |
+ if (type.annotations.isEmpty) return null; |
+ |
+ var memberAnnotations = {}; |
+ visitor.memberAnnotations.forEach((memberName, annotations) { |
+ if (annotations.length > 1) { |
+ warn('$memberName can only have one annotation.', |
+ annotations[0].parent.element); |
+ return; |
+ } |
+ |
+ memberAnnotations[memberName] = annotations[0]; |
+ }); |
+ |
+ if (memberAnnotations.isNotEmpty) { |
+ _foldMemberAnnotations(memberAnnotations, type); |
+ } |
+ |
+ return type; |
+ } |
+ |
+ /// Folds all AttrFieldAnnotations into the Directive annotation on the |
+ /// class. |
+ void _foldMemberAnnotations(Map<String, Annotation> memberAnnotations, |
+ AnnotatedType type) { |
+ // Filter down to Directive constructors. |
+ var ngAnnotations = type.annotations.where((a) { |
+ var element = a.element; |
+ if (element is! ConstructorElement) return false; |
+ return element.enclosingElement.type.isAssignableTo( |
+ directiveType.type); |
+ }); |
+ |
+ var mapType = resolver.getType('dart.core.Map').type; |
+ // Find acceptable constructors- ones which take a param named 'map' |
+ var acceptableAnnotations = ngAnnotations.where((a) { |
+ var ctor = a.element; |
+ |
+ for (var param in ctor.parameters) { |
+ if (param.parameterKind != ParameterKind.NAMED) { |
+ continue; |
+ } |
+ if (param.name == 'map' && param.type.isAssignableTo(mapType)) { |
+ return true; |
+ } |
+ } |
+ return false; |
+ }); |
+ |
+ if (acceptableAnnotations.isEmpty) { |
+ warn('Could not find a constructor for member annotations in ' |
+ '$ngAnnotations', type.type); |
+ return; |
+ } |
+ |
+ // Merge attribute annotations in all of the class annotations |
+ acceptableAnnotations.forEach((srcAnnotation) { |
+ // Clone the annotation so we don't modify the one in the persistent AST. |
+ var index = type.annotations.indexOf(srcAnnotation); |
+ var annotation = new AstCloner().visitAnnotation(srcAnnotation); |
+ ResolutionCopier.copyResolutionData(srcAnnotation, annotation); |
+ type.annotations[index] = annotation; |
+ |
+ var mapArg = annotation.arguments.arguments.firstWhere( |
+ (arg) => (arg is NamedExpression) && (arg.name.label.name == 'map'), |
+ orElse: () => null); |
+ |
+ // If we don't have a 'map' parameter yet, add one. |
+ if (mapArg == null) { |
+ var map = new MapLiteral(null, null, null, [], null); |
+ var label = new Label(new SimpleIdentifier( |
+ new _GeneratedToken(TokenType.STRING, 'map')), |
+ new _GeneratedToken(TokenType.COLON, ':')); |
+ mapArg = new NamedExpression(label, map); |
+ annotation.arguments.arguments.add(mapArg); |
+ } |
+ |
+ var map = mapArg.expression; |
+ if (map is! MapLiteral) { |
+ warn('Expected \'map\' argument of $annotation to be a map literal', |
+ type.type); |
+ return; |
+ } |
+ memberAnnotations.forEach((memberName, annotation) { |
+ var key = annotation.arguments.arguments.first; |
+ // If the key already exists then it means we have two annotations for |
+ // same member. |
+ if (map.entries.any((entry) => entry.key.toString() == key.toString())) { |
+ warn('Directive $annotation already contains an entry for $key', |
+ type.type); |
+ return; |
+ } |
+ |
+ var typeName = annotation.element.enclosingElement.name; |
+ var value = '${_annotationToMapping[typeName]}$memberName'; |
+ var entry = new MapLiteralEntry( |
+ key, |
+ new _GeneratedToken(TokenType.COLON, ':'), |
+ new SimpleStringLiteral(stringToken(value), value)); |
+ map.entries.add(entry); |
+ }); |
+ }); |
+ } |
+ |
+ Token stringToken(String str) => |
+ new _GeneratedToken(TokenType.STRING, '\'$str\''); |
+ |
+ void warn(String msg, Element element) { |
+ logger.warning(msg, asset: resolver.getSourceAssetId(element), |
+ span: resolver.getSourceSpan(element)); |
+ } |
+} |
+ |
+/// Subclass for tokens which we're generating here. |
+class _GeneratedToken extends Token { |
+ final String lexeme; |
+ _GeneratedToken(TokenType type, this.lexeme) : super(type, 0); |
+} |
+ |
+ |
+/** |
+ * AST visitor which walks the current AST and finds all annotated |
+ * classes and members. |
+ */ |
+class _AnnotationVisitor extends GeneralizingAstVisitor { |
+ final List<Element> allowedMemberAnnotations; |
+ final List<Annotation> classAnnotations = []; |
+ final Map<String, List<Annotation>> memberAnnotations = {}; |
+ var visitingSupertype = false; |
+ |
+ _AnnotationVisitor(this.allowedMemberAnnotations); |
+ |
+ void visitAnnotation(Annotation annotation) { |
+ var parent = annotation.parent; |
+ if (parent is! Declaration) return; |
+ |
+ if (parent.element is ClassElement && !visitingSupertype) { |
+ classAnnotations.add(annotation); |
+ |
+ } else if (allowedMemberAnnotations.contains(annotation.element)) { |
+ if (parent is MethodDeclaration) { |
+ memberAnnotations.putIfAbsent(parent.name.name, () => []) |
+ .add(annotation); |
+ } else if (parent is FieldDeclaration) { |
+ var name = parent.fields.variables.first.name.name; |
+ memberAnnotations.putIfAbsent(name, () => []).add(annotation); |
+ } |
+ } |
+ } |
+ |
+ bool get hasAnnotations => |
+ classAnnotations.isNotEmpty || memberAnnotations.isNotEmpty; |
+} |