| 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 |