OLD | NEW |
(Empty) | |
| 1 library angular.metadata_extractor; |
| 2 |
| 3 import 'package:analyzer/src/generated/ast.dart'; |
| 4 import 'package:analyzer/src/generated/element.dart'; |
| 5 import 'package:analyzer/src/generated/parser.dart' show ResolutionCopier; |
| 6 import 'package:analyzer/src/generated/scanner.dart'; |
| 7 import 'package:analyzer/src/generated/utilities_dart.dart' show ParameterKind; |
| 8 import 'package:barback/barback.dart'; |
| 9 import 'package:code_transformers/resolver.dart'; |
| 10 |
| 11 class AnnotatedType { |
| 12 final ClassElement type; |
| 13 Iterable<Annotation> annotations; |
| 14 |
| 15 AnnotatedType(this.type); |
| 16 |
| 17 /** |
| 18 * Finds all the libraries referenced by the annotations |
| 19 */ |
| 20 Iterable<LibraryElement> get referencedLibraries { |
| 21 var libs = new Set(); |
| 22 libs.add(type.library); |
| 23 |
| 24 var libCollector = new _LibraryCollector(); |
| 25 for (var annotation in annotations) { |
| 26 annotation.accept(libCollector); |
| 27 } |
| 28 libs.addAll(libCollector.libraries); |
| 29 |
| 30 return libs; |
| 31 } |
| 32 |
| 33 void writeClassAnnotations(StringBuffer sink, TransformLogger logger, |
| 34 Resolver resolver, Map<LibraryElement, String> prefixes) { |
| 35 sink.write(' ${prefixes[type.library]}${type.name}: const [\n'); |
| 36 var writer = new _AnnotationWriter(sink, prefixes); |
| 37 for (var annotation in annotations) { |
| 38 sink.write(' '); |
| 39 if (writer.writeAnnotation(annotation)) { |
| 40 sink.write(',\n'); |
| 41 } else { |
| 42 sink.write('null,\n'); |
| 43 logger.warning('Unable to serialize annotation $annotation.', |
| 44 asset: resolver.getSourceAssetId(annotation.parent.element), |
| 45 span: resolver.getSourceSpan(annotation.parent.element)); |
| 46 } |
| 47 } |
| 48 sink.write(' ],\n'); |
| 49 } |
| 50 } |
| 51 |
| 52 /** |
| 53 * Helper which finds all libraries referenced within the provided AST. |
| 54 */ |
| 55 class _LibraryCollector extends GeneralizingAstVisitor { |
| 56 final Set<LibraryElement> libraries = new Set<LibraryElement>(); |
| 57 void visitSimpleIdentifier(SimpleIdentifier s) { |
| 58 var element = s.bestElement; |
| 59 if (element != null) { |
| 60 libraries.add(element.library); |
| 61 } |
| 62 } |
| 63 } |
| 64 |
| 65 /** |
| 66 * Helper class which writes annotations out to the buffer. |
| 67 * This does not support every syntax possible, but will return false when |
| 68 * the annotation cannot be serialized. |
| 69 */ |
| 70 class _AnnotationWriter { |
| 71 final StringBuffer sink; |
| 72 final Map<LibraryElement, String> prefixes; |
| 73 |
| 74 _AnnotationWriter(this.sink, this.prefixes); |
| 75 |
| 76 /** |
| 77 * Returns true if the annotation was successfully serialized. |
| 78 * If the annotation could not be written then the buffer is returned to its |
| 79 * original state. |
| 80 */ |
| 81 bool writeAnnotation(Annotation annotation) { |
| 82 // Record the current location in the buffer and if writing fails then |
| 83 // back up the buffer to where we started. |
| 84 var len = sink.length; |
| 85 if (!_writeAnnotation(annotation)) { |
| 86 var str = sink.toString(); |
| 87 sink.clear(); |
| 88 sink.write(str.substring(0, len)); |
| 89 return false; |
| 90 } |
| 91 return true; |
| 92 } |
| 93 |
| 94 bool _writeAnnotation(Annotation annotation) { |
| 95 var element = annotation.element; |
| 96 if (element is ConstructorElement) { |
| 97 sink.write('const ${prefixes[element.library]}' |
| 98 '${element.enclosingElement.name}'); |
| 99 // Named constructors |
| 100 if (!element.name.isEmpty) { |
| 101 sink.write('.${element.name}'); |
| 102 } |
| 103 sink.write('('); |
| 104 if (!_writeArguments(annotation)) return false; |
| 105 sink.write(')'); |
| 106 return true; |
| 107 } else if (element is PropertyAccessorElement) { |
| 108 sink.write('${prefixes[element.library]}${element.name}'); |
| 109 return true; |
| 110 } |
| 111 |
| 112 return false; |
| 113 } |
| 114 |
| 115 /** Writes the arguments for a type constructor. */ |
| 116 bool _writeArguments(Annotation annotation) { |
| 117 var args = annotation.arguments; |
| 118 var index = 0; |
| 119 for (var arg in args.arguments) { |
| 120 if (arg is NamedExpression) { |
| 121 sink.write('${arg.name.label.name}: '); |
| 122 if (!_writeExpression(arg.expression)) return false; |
| 123 } else { |
| 124 if (!_writeExpression(arg)) return false; |
| 125 } |
| 126 if (++index < args.arguments.length) { |
| 127 sink.write(', '); |
| 128 } |
| 129 } |
| 130 return true; |
| 131 } |
| 132 |
| 133 /** Writes an expression. */ |
| 134 bool _writeExpression(Expression expression) { |
| 135 if (expression is StringLiteral) { |
| 136 sink.write(expression.toSource()); |
| 137 return true; |
| 138 } |
| 139 if (expression is ListLiteral) { |
| 140 sink.write('const ['); |
| 141 for (var element in expression.elements) { |
| 142 if (!_writeExpression(element)) return false; |
| 143 sink.write(','); |
| 144 } |
| 145 sink.write(']'); |
| 146 return true; |
| 147 } |
| 148 if (expression is MapLiteral) { |
| 149 sink.write('const {'); |
| 150 var index = 0; |
| 151 for (var entry in expression.entries) { |
| 152 if (!_writeExpression(entry.key)) return false; |
| 153 sink.write(': '); |
| 154 if (!_writeExpression(entry.value)) return false; |
| 155 if (++index < expression.entries.length) { |
| 156 sink.write(', '); |
| 157 } |
| 158 } |
| 159 sink.write('}'); |
| 160 return true; |
| 161 } |
| 162 if (expression is Identifier) { |
| 163 var element = expression.bestElement; |
| 164 if (element == null || !element.isPublic) return false; |
| 165 |
| 166 if (element is ClassElement) { |
| 167 sink.write('${prefixes[element.library]}${element.name}'); |
| 168 return true; |
| 169 } |
| 170 if (element is PropertyAccessorElement) { |
| 171 var variable = element.variable; |
| 172 if (variable is FieldElement) { |
| 173 var cls = variable.enclosingElement; |
| 174 sink.write('${prefixes[cls.library]}${cls.name}.${variable.name}'); |
| 175 return true; |
| 176 } else if (variable is TopLevelVariableElement) { |
| 177 sink.write('${prefixes[variable.library]}${variable.name}'); |
| 178 return true; |
| 179 } |
| 180 } |
| 181 |
| 182 if (element is MethodElement) { |
| 183 var cls = element.enclosingElement; |
| 184 sink.write('${prefixes[cls.library]}${cls.name}.${element.name}'); |
| 185 return true; |
| 186 } |
| 187 } |
| 188 if (expression is BooleanLiteral || expression is DoubleLiteral || |
| 189 expression is IntegerLiteral || expression is NullLiteral) { |
| 190 sink.write(expression.toSource()); |
| 191 return true; |
| 192 } |
| 193 return false; |
| 194 } |
| 195 } |
| 196 |
| 197 class AnnotationExtractor { |
| 198 final TransformLogger logger; |
| 199 final Resolver resolver; |
| 200 final AssetId outputId; |
| 201 |
| 202 static const List<String> _angularAnnotationNames = const [ |
| 203 'angular.core.annotation_src.NgAttr', |
| 204 'angular.core.annotation_src.NgOneWay', |
| 205 'angular.core.annotation_src.NgOneWayOneTime', |
| 206 'angular.core.annotation_src.NgTwoWay', |
| 207 'angular.core.annotation_src.NgCallback' |
| 208 ]; |
| 209 |
| 210 static const Map<String, String> _annotationToMapping = const { |
| 211 'NgAttr': '@', |
| 212 'NgOneWay': '=>', |
| 213 'NgOneWayOneTime': '=>!', |
| 214 'NgTwoWay': '<=>', |
| 215 'NgCallback': '&', |
| 216 }; |
| 217 |
| 218 ClassElement directiveType; |
| 219 ClassElement formatterType; |
| 220 |
| 221 /// Resolved annotations that this will pick up for members. |
| 222 final List<Element> _annotationElements = <Element>[]; |
| 223 |
| 224 AnnotationExtractor(this.logger, this.resolver, this.outputId) { |
| 225 for (var annotation in _angularAnnotationNames) { |
| 226 var type = resolver.getType(annotation); |
| 227 if (type == null) { |
| 228 logger.warning('Unable to resolve $annotation, skipping metadata.'); |
| 229 continue; |
| 230 } |
| 231 _annotationElements.add(type.unnamedConstructor); |
| 232 } |
| 233 directiveType = resolver.getType('angular.core.annotation_src.Directive'); |
| 234 formatterType = resolver.getType('angular.core.annotation_src.Formatter'); |
| 235 if (directiveType == null) { |
| 236 logger.warning('Unable to resolve Directive, skipping member annotations.'
); |
| 237 } |
| 238 if (formatterType == null) { |
| 239 logger.warning('Unable to resolve Formatter.'); |
| 240 } |
| 241 } |
| 242 |
| 243 /// Extracts all of the annotations for the specified class. |
| 244 AnnotatedType extractAnnotations(ClassElement cls) { |
| 245 var classElement = cls; |
| 246 var visitor = new _AnnotationVisitor(_annotationElements); |
| 247 while (classElement != null) { |
| 248 if (resolver.getImportUri(classElement.library, from: outputId) == null) { |
| 249 warn('Dropping annotations for ${classElement.name} because the ' |
| 250 'containing file cannot be imported (must be in a lib folder).', cla
ssElement); |
| 251 return null; |
| 252 } |
| 253 if (classElement.node != null) { |
| 254 classElement.node.accept(visitor); |
| 255 } |
| 256 |
| 257 if (classElement.supertype != null) { |
| 258 visitor.visitingSupertype = true; |
| 259 classElement = classElement.supertype.element; |
| 260 } else { |
| 261 classElement = null; |
| 262 } |
| 263 } |
| 264 |
| 265 if (!visitor.hasAnnotations) return null; |
| 266 |
| 267 var type = new AnnotatedType(cls); |
| 268 type.annotations = visitor.classAnnotations |
| 269 .where((Annotation annotation) { |
| 270 var element = annotation.element; |
| 271 if (element != null && !element.isPublic) { |
| 272 warn('Annotation $annotation is not public.', |
| 273 annotation.parent.element); |
| 274 return false; |
| 275 } |
| 276 if (element is! ConstructorElement) { |
| 277 // Only keeping constructor elements. |
| 278 return false; |
| 279 } |
| 280 ConstructorElement ctor = element; |
| 281 var cls = ctor.enclosingElement; |
| 282 if (!cls.isPublic) { |
| 283 warn('Annotation $annotation is not public.', |
| 284 annotation.parent.element); |
| 285 return false; |
| 286 } |
| 287 return element.enclosingElement.type.isAssignableTo(directiveType.type
) || |
| 288 element.enclosingElement.type.isAssignableTo(formatterType.type
); |
| 289 }).toList(); |
| 290 |
| 291 if (type.annotations.isEmpty) return null; |
| 292 |
| 293 var memberAnnotations = {}; |
| 294 visitor.memberAnnotations.forEach((memberName, annotations) { |
| 295 if (annotations.length > 1) { |
| 296 warn('$memberName can only have one annotation.', |
| 297 annotations[0].parent.element); |
| 298 return; |
| 299 } |
| 300 |
| 301 memberAnnotations[memberName] = annotations[0]; |
| 302 }); |
| 303 |
| 304 if (memberAnnotations.isNotEmpty) { |
| 305 _foldMemberAnnotations(memberAnnotations, type); |
| 306 } |
| 307 |
| 308 return type; |
| 309 } |
| 310 |
| 311 /// Folds all AttrFieldAnnotations into the Directive annotation on the |
| 312 /// class. |
| 313 void _foldMemberAnnotations(Map<String, Annotation> memberAnnotations, |
| 314 AnnotatedType type) { |
| 315 // Filter down to Directive constructors. |
| 316 var ngAnnotations = type.annotations.where((a) { |
| 317 var element = a.element; |
| 318 if (element is! ConstructorElement) return false; |
| 319 return element.enclosingElement.type.isAssignableTo( |
| 320 directiveType.type); |
| 321 }); |
| 322 |
| 323 var mapType = resolver.getType('dart.core.Map').type; |
| 324 // Find acceptable constructors- ones which take a param named 'map' |
| 325 var acceptableAnnotations = ngAnnotations.where((a) { |
| 326 var ctor = a.element; |
| 327 |
| 328 for (var param in ctor.parameters) { |
| 329 if (param.parameterKind != ParameterKind.NAMED) { |
| 330 continue; |
| 331 } |
| 332 if (param.name == 'map' && param.type.isAssignableTo(mapType)) { |
| 333 return true; |
| 334 } |
| 335 } |
| 336 return false; |
| 337 }); |
| 338 |
| 339 if (acceptableAnnotations.isEmpty) { |
| 340 warn('Could not find a constructor for member annotations in ' |
| 341 '$ngAnnotations', type.type); |
| 342 return; |
| 343 } |
| 344 |
| 345 // Merge attribute annotations in all of the class annotations |
| 346 acceptableAnnotations.forEach((srcAnnotation) { |
| 347 // Clone the annotation so we don't modify the one in the persistent AST. |
| 348 var index = type.annotations.indexOf(srcAnnotation); |
| 349 var annotation = new AstCloner().visitAnnotation(srcAnnotation); |
| 350 ResolutionCopier.copyResolutionData(srcAnnotation, annotation); |
| 351 type.annotations[index] = annotation; |
| 352 |
| 353 var mapArg = annotation.arguments.arguments.firstWhere( |
| 354 (arg) => (arg is NamedExpression) && (arg.name.label.name == 'map'), |
| 355 orElse: () => null); |
| 356 |
| 357 // If we don't have a 'map' parameter yet, add one. |
| 358 if (mapArg == null) { |
| 359 var map = new MapLiteral(null, null, null, [], null); |
| 360 var label = new Label(new SimpleIdentifier( |
| 361 new _GeneratedToken(TokenType.STRING, 'map')), |
| 362 new _GeneratedToken(TokenType.COLON, ':')); |
| 363 mapArg = new NamedExpression(label, map); |
| 364 annotation.arguments.arguments.add(mapArg); |
| 365 } |
| 366 |
| 367 var map = mapArg.expression; |
| 368 if (map is! MapLiteral) { |
| 369 warn('Expected \'map\' argument of $annotation to be a map literal', |
| 370 type.type); |
| 371 return; |
| 372 } |
| 373 memberAnnotations.forEach((memberName, annotation) { |
| 374 var key = annotation.arguments.arguments.first; |
| 375 // If the key already exists then it means we have two annotations for |
| 376 // same member. |
| 377 if (map.entries.any((entry) => entry.key.toString() == key.toString()))
{ |
| 378 warn('Directive $annotation already contains an entry for $key', |
| 379 type.type); |
| 380 return; |
| 381 } |
| 382 |
| 383 var typeName = annotation.element.enclosingElement.name; |
| 384 var value = '${_annotationToMapping[typeName]}$memberName'; |
| 385 var entry = new MapLiteralEntry( |
| 386 key, |
| 387 new _GeneratedToken(TokenType.COLON, ':'), |
| 388 new SimpleStringLiteral(stringToken(value), value)); |
| 389 map.entries.add(entry); |
| 390 }); |
| 391 }); |
| 392 } |
| 393 |
| 394 Token stringToken(String str) => |
| 395 new _GeneratedToken(TokenType.STRING, '\'$str\''); |
| 396 |
| 397 void warn(String msg, Element element) { |
| 398 logger.warning(msg, asset: resolver.getSourceAssetId(element), |
| 399 span: resolver.getSourceSpan(element)); |
| 400 } |
| 401 } |
| 402 |
| 403 /// Subclass for tokens which we're generating here. |
| 404 class _GeneratedToken extends Token { |
| 405 final String lexeme; |
| 406 _GeneratedToken(TokenType type, this.lexeme) : super(type, 0); |
| 407 } |
| 408 |
| 409 |
| 410 /** |
| 411 * AST visitor which walks the current AST and finds all annotated |
| 412 * classes and members. |
| 413 */ |
| 414 class _AnnotationVisitor extends GeneralizingAstVisitor { |
| 415 final List<Element> allowedMemberAnnotations; |
| 416 final List<Annotation> classAnnotations = []; |
| 417 final Map<String, List<Annotation>> memberAnnotations = {}; |
| 418 var visitingSupertype = false; |
| 419 |
| 420 _AnnotationVisitor(this.allowedMemberAnnotations); |
| 421 |
| 422 void visitAnnotation(Annotation annotation) { |
| 423 var parent = annotation.parent; |
| 424 if (parent is! Declaration) return; |
| 425 |
| 426 if (parent.element is ClassElement && !visitingSupertype) { |
| 427 classAnnotations.add(annotation); |
| 428 |
| 429 } else if (allowedMemberAnnotations.contains(annotation.element)) { |
| 430 if (parent is MethodDeclaration) { |
| 431 memberAnnotations.putIfAbsent(parent.name.name, () => []) |
| 432 .add(annotation); |
| 433 } else if (parent is FieldDeclaration) { |
| 434 var name = parent.fields.variables.first.name.name; |
| 435 memberAnnotations.putIfAbsent(name, () => []).add(annotation); |
| 436 } |
| 437 } |
| 438 } |
| 439 |
| 440 bool get hasAnnotations => |
| 441 classAnnotations.isNotEmpty || memberAnnotations.isNotEmpty; |
| 442 } |
OLD | NEW |