| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 /// Records accesses to Dart program declarations and generates code that will | |
| 6 /// allow to do the same accesses at runtime using `package:smoke/static.dart`. | |
| 7 /// Internally, this library relies on the `analyzer` to extract data from the | |
| 8 /// program, and then uses [SmokeCodeGenerator] to produce the code needed by | |
| 9 /// the smoke system. | |
| 10 /// | |
| 11 /// This library only uses the analyzer to consume data previously produced by | |
| 12 /// running the resolver. This library does not provide any hooks to integrate | |
| 13 /// running the analyzer itself. See `package:code_transformers` to integrate | |
| 14 /// the analyzer into pub transformers. | |
| 15 library smoke.codegen.recorder; | |
| 16 | |
| 17 import 'package:analyzer/src/generated/element.dart'; | |
| 18 import 'package:analyzer/src/generated/ast.dart'; | |
| 19 import 'generator.dart'; | |
| 20 | |
| 21 typedef String ImportUrlResolver(LibraryElement lib); | |
| 22 | |
| 23 /// A recorder that tracks how elements are accessed in order to generate code | |
| 24 /// that replicates those accesses with the smoke runtime. | |
| 25 class Recorder { | |
| 26 /// Underlying code generator. | |
| 27 SmokeCodeGenerator generator; | |
| 28 | |
| 29 /// Function that provides the import url for a library element. This may | |
| 30 /// internally use the resolver to resolve the import url. | |
| 31 ImportUrlResolver importUrlFor; | |
| 32 | |
| 33 Recorder(this.generator, this.importUrlFor); | |
| 34 | |
| 35 /// Stores mixins that have been recorded and associates a type identifier | |
| 36 /// with them. Mixins don't have an associated identifier in the code, so we | |
| 37 /// generate a unique identifier for them and use it throughout the code. | |
| 38 Map<TypeIdentifier, Map<ClassElement, TypeIdentifier>> _mixins = {}; | |
| 39 | |
| 40 /// Adds the superclass information of [type] (including intermediate mixins). | |
| 41 /// This will not generate data for direct subtypes of Object, as that is | |
| 42 /// considered redundant information. | |
| 43 void lookupParent(ClassElement type) { | |
| 44 var parent = type.supertype; | |
| 45 var mixins = type.mixins; | |
| 46 if (parent == null && mixins.isEmpty) return; // type is Object | |
| 47 var baseType = parent.element; | |
| 48 var baseId = _typeFor(baseType); | |
| 49 if (mixins.isNotEmpty) { | |
| 50 _mixins.putIfAbsent(baseId, () => {}); | |
| 51 for (var m in mixins) { | |
| 52 var mixinType = m.element; | |
| 53 var mixinId = _mixins[baseId].putIfAbsent(mixinType, () { | |
| 54 var comment = '${baseId.name} & ${mixinType.name}'; | |
| 55 return generator.createMixinType(comment); | |
| 56 }); | |
| 57 if (!baseType.type.isObject) generator.addParent(mixinId, baseId); | |
| 58 baseType = mixinType; | |
| 59 baseId = mixinId; | |
| 60 _mixins.putIfAbsent(mixinId, () => {}); | |
| 61 } | |
| 62 } | |
| 63 if (!baseType.type.isObject) generator.addParent(_typeFor(type), baseId); | |
| 64 } | |
| 65 | |
| 66 TypeIdentifier _typeFor(Element type) => new TypeIdentifier( | |
| 67 type.library == null ? 'dart:core' : importUrlFor(type.library), | |
| 68 type.displayName); | |
| 69 | |
| 70 /// Adds any declaration and superclass information that is needed to answer a | |
| 71 /// query on [type] that matches [options]. Also adds symbols, getters, and | |
| 72 /// setters if [includeAccessors] is true. If [results] is not null, it will | |
| 73 /// be filled up with the members that match the query. | |
| 74 void runQuery(ClassElement type, QueryOptions options, | |
| 75 {bool includeAccessors: true, List results}) { | |
| 76 if (type.type.isObject) return; // We don't include Object in query results. | |
| 77 var id = _typeFor(type); | |
| 78 var parent = type.supertype != null ? type.supertype.element : null; | |
| 79 if (options.includeInherited && | |
| 80 parent != null && | |
| 81 parent != options.includeUpTo) { | |
| 82 lookupParent(type); | |
| 83 runQuery(parent, options, includeAccessors: includeAccessors); | |
| 84 var parentId = _typeFor(parent); | |
| 85 for (var m in type.mixins) { | |
| 86 var mixinClass = m.element; | |
| 87 var mixinId = _mixins[parentId][mixinClass]; | |
| 88 _runQueryInternal( | |
| 89 mixinClass, mixinId, options, includeAccessors, results); | |
| 90 parentId = mixinId; | |
| 91 } | |
| 92 } | |
| 93 _runQueryInternal(type, id, options, includeAccessors, results); | |
| 94 } | |
| 95 | |
| 96 /// Helper for [runQuery]. This runs the query only on a specific [type], | |
| 97 /// which could be a class or a mixin labeled by [id]. | |
| 98 // TODO(sigmund): currently we materialize mixins in smoke/static.dart, | |
| 99 // we should consider to include the mixin declaration information directly, | |
| 100 // and remove the duplication we have for mixins today. | |
| 101 void _runQueryInternal(ClassElement type, TypeIdentifier id, | |
| 102 QueryOptions options, bool includeAccessors, List results) { | |
| 103 skipBecauseOfAnnotations(Element e) { | |
| 104 if (options.withAnnotations == null) return false; | |
| 105 return !_matchesAnnotation(e.metadata, options.withAnnotations); | |
| 106 } | |
| 107 | |
| 108 if (options.includeFields) { | |
| 109 for (var f in type.fields) { | |
| 110 if (f.isStatic) continue; | |
| 111 if (f.isSynthetic) continue; // exclude getters | |
| 112 if (options.excludeFinal && f.isFinal) continue; | |
| 113 var name = f.displayName; | |
| 114 if (options.matches != null && !options.matches(name)) continue; | |
| 115 if (skipBecauseOfAnnotations(f)) continue; | |
| 116 if (results != null) results.add(f); | |
| 117 generator.addDeclaration(id, name, _typeFor(f.type.element), | |
| 118 isField: true, | |
| 119 isFinal: f.isFinal, | |
| 120 annotations: _copyAnnotations(f)); | |
| 121 if (includeAccessors) _addAccessors(name, !f.isFinal); | |
| 122 } | |
| 123 } | |
| 124 | |
| 125 if (options.includeProperties) { | |
| 126 for (var a in type.accessors) { | |
| 127 if (a is! PropertyAccessorElement) continue; | |
| 128 if (a.isStatic || !a.isGetter) continue; | |
| 129 var v = a.variable; | |
| 130 if (v is FieldElement && !v.isSynthetic) continue; // exclude fields | |
| 131 if (options.excludeFinal && v.isFinal) continue; | |
| 132 var name = v.displayName; | |
| 133 if (options.matches != null && !options.matches(name)) continue; | |
| 134 if (skipBecauseOfAnnotations(a)) continue; | |
| 135 if (results != null) results.add(a); | |
| 136 generator.addDeclaration(id, name, _typeFor(a.type.returnType.element), | |
| 137 isProperty: true, | |
| 138 isFinal: v.isFinal, | |
| 139 annotations: _copyAnnotations(a)); | |
| 140 if (includeAccessors) _addAccessors(name, !v.isFinal); | |
| 141 } | |
| 142 } | |
| 143 | |
| 144 if (options.includeMethods) { | |
| 145 for (var m in type.methods) { | |
| 146 if (m.isStatic) continue; | |
| 147 var name = m.displayName; | |
| 148 if (options.matches != null && !options.matches(name)) continue; | |
| 149 if (skipBecauseOfAnnotations(m)) continue; | |
| 150 if (results != null) results.add(m); | |
| 151 generator.addDeclaration( | |
| 152 id, name, new TypeIdentifier('dart:core', 'Function'), | |
| 153 isMethod: true, annotations: _copyAnnotations(m)); | |
| 154 if (includeAccessors) _addAccessors(name, false); | |
| 155 } | |
| 156 } | |
| 157 } | |
| 158 | |
| 159 /// Adds the declaration of [name] if it was found in [type]. If [recursive] | |
| 160 /// is true, then we continue looking up [name] in the parent classes until we | |
| 161 /// find it or we reach [includeUpTo] or Object. Returns whether the | |
| 162 /// declaration was found. When a declaration is found, add also a symbol, | |
| 163 /// getter, and setter if [includeAccessors] is true. | |
| 164 bool lookupMember(ClassElement type, String name, {bool recursive: false, | |
| 165 bool includeAccessors: true, ClassElement includeUpTo}) => | |
| 166 _lookupMemberInternal( | |
| 167 type, _typeFor(type), name, recursive, includeAccessors, includeUpTo); | |
| 168 | |
| 169 /// Helper for [lookupMember] that walks up the type hierarchy including mixin | |
| 170 /// classes. | |
| 171 bool _lookupMemberInternal(ClassElement type, TypeIdentifier id, String name, | |
| 172 bool recursive, bool includeAccessors, ClassElement includeUpTo) { | |
| 173 // Exclude members from [Object]. | |
| 174 if (type.type.isObject) return false; | |
| 175 generator.addEmptyDeclaration(id); | |
| 176 for (var f in type.fields) { | |
| 177 if (f.displayName != name) continue; | |
| 178 if (f.isSynthetic) continue; // exclude getters | |
| 179 generator.addDeclaration(id, name, _typeFor(f.type.element), | |
| 180 isField: true, | |
| 181 isFinal: f.isFinal, | |
| 182 isStatic: f.isStatic, | |
| 183 annotations: _copyAnnotations(f)); | |
| 184 if (includeAccessors && !f.isStatic) _addAccessors(name, !f.isFinal); | |
| 185 return true; | |
| 186 } | |
| 187 | |
| 188 for (var a in type.accessors) { | |
| 189 if (a is! PropertyAccessorElement) continue; | |
| 190 // TODO(sigmund): support setters without getters. | |
| 191 if (!a.isGetter) continue; | |
| 192 if (a.displayName != name) continue; | |
| 193 var v = a.variable; | |
| 194 if (v is FieldElement && !v.isSynthetic) continue; // exclude fields | |
| 195 generator.addDeclaration(id, name, _typeFor(a.type.returnType.element), | |
| 196 isProperty: true, | |
| 197 isFinal: v.isFinal, | |
| 198 isStatic: a.isStatic, | |
| 199 annotations: _copyAnnotations(a)); | |
| 200 if (includeAccessors && !v.isStatic) _addAccessors(name, !v.isFinal); | |
| 201 return true; | |
| 202 } | |
| 203 | |
| 204 for (var m in type.methods) { | |
| 205 if (m.displayName != name) continue; | |
| 206 generator.addDeclaration( | |
| 207 id, name, new TypeIdentifier('dart:core', 'Function'), | |
| 208 isMethod: true, | |
| 209 isStatic: m.isStatic, | |
| 210 annotations: _copyAnnotations(m)); | |
| 211 if (includeAccessors) { | |
| 212 if (m.isStatic) { | |
| 213 generator.addStaticMethod(id, name); | |
| 214 generator.addSymbol(name); | |
| 215 } else { | |
| 216 _addAccessors(name, false); | |
| 217 } | |
| 218 } | |
| 219 return true; | |
| 220 } | |
| 221 | |
| 222 if (recursive) { | |
| 223 lookupParent(type); | |
| 224 var parent = type.supertype != null ? type.supertype.element : null; | |
| 225 if (parent == null || parent == includeUpTo) return false; | |
| 226 var parentId = _typeFor(parent); | |
| 227 for (var m in type.mixins) { | |
| 228 var mixinClass = m.element; | |
| 229 var mixinId = _mixins[parentId][mixinClass]; | |
| 230 if (_lookupMemberInternal( | |
| 231 mixinClass, mixinId, name, false, includeAccessors, includeUpTo)) { | |
| 232 return true; | |
| 233 } | |
| 234 parentId = mixinId; | |
| 235 } | |
| 236 return _lookupMemberInternal( | |
| 237 parent, parentId, name, true, includeAccessors, includeUpTo); | |
| 238 } | |
| 239 return false; | |
| 240 } | |
| 241 | |
| 242 /// Add information so smoke can invoke the static method [type].[name]. | |
| 243 void addStaticMethod(ClassElement type, String name) { | |
| 244 generator.addStaticMethod(_typeFor(type), name); | |
| 245 } | |
| 246 | |
| 247 /// Adds [name] as a symbol, a getter, and optionally a setter in [generator]. | |
| 248 _addAccessors(String name, bool includeSetter) { | |
| 249 generator.addSymbol(name); | |
| 250 generator.addGetter(name); | |
| 251 if (includeSetter) generator.addSetter(name); | |
| 252 } | |
| 253 | |
| 254 /// Copy metadata associated with the declaration of [target]. | |
| 255 List<ConstExpression> _copyAnnotations(Element target) { | |
| 256 var node = target.node; | |
| 257 // [node] is the initialization expression, we walk up to get to the actual | |
| 258 // member declaration where the metadata is attached to. | |
| 259 while (node is! ClassMember) node = node.parent; | |
| 260 return node.metadata.map(_convertAnnotation).toList(); | |
| 261 } | |
| 262 | |
| 263 /// Converts annotations into [ConstExpression]s supported by the codegen | |
| 264 /// library. | |
| 265 ConstExpression _convertAnnotation(Annotation annotation) { | |
| 266 var element = annotation.element; | |
| 267 if (element is ConstructorElement) { | |
| 268 if (!element.name.isEmpty) { | |
| 269 throw new UnimplementedError( | |
| 270 'named constructors are not implemented in smoke.codegen.recorder'); | |
| 271 } | |
| 272 | |
| 273 var positionalArgs = []; | |
| 274 var namedArgs = {}; | |
| 275 for (var arg in annotation.arguments.arguments) { | |
| 276 if (arg is NamedExpression) { | |
| 277 namedArgs[arg.name.label.name] = _convertExpression(arg.expression); | |
| 278 } else { | |
| 279 positionalArgs.add(_convertExpression(arg)); | |
| 280 } | |
| 281 } | |
| 282 | |
| 283 return new ConstructorExpression(importUrlFor(element.library), | |
| 284 element.enclosingElement.name, positionalArgs, namedArgs); | |
| 285 } | |
| 286 | |
| 287 if (element is PropertyAccessorElement) { | |
| 288 return new TopLevelIdentifier( | |
| 289 importUrlFor(element.library), element.name); | |
| 290 } | |
| 291 | |
| 292 throw new UnsupportedError('unsupported annotation $annotation'); | |
| 293 } | |
| 294 | |
| 295 /// Converts [expression] into a [ConstExpression]. | |
| 296 ConstExpression _convertExpression(Expression expression) { | |
| 297 if (expression is StringLiteral) { | |
| 298 return new ConstExpression.string(expression.stringValue); | |
| 299 } | |
| 300 | |
| 301 if (expression is BooleanLiteral || | |
| 302 expression is DoubleLiteral || | |
| 303 expression is IntegerLiteral || | |
| 304 expression is NullLiteral) { | |
| 305 return new CodeAsConstExpression("${(expression as dynamic).value}"); | |
| 306 } | |
| 307 | |
| 308 if (expression is Identifier) { | |
| 309 var element = expression.bestElement; | |
| 310 if (element == null || !element.isPublic) { | |
| 311 throw new UnsupportedError('private constants are not supported'); | |
| 312 } | |
| 313 | |
| 314 var url = importUrlFor(element.library); | |
| 315 if (element is ClassElement) { | |
| 316 return new TopLevelIdentifier(url, element.name); | |
| 317 } | |
| 318 | |
| 319 if (element is PropertyAccessorElement) { | |
| 320 var variable = element.variable; | |
| 321 if (variable is FieldElement) { | |
| 322 var cls = variable.enclosingElement; | |
| 323 return new TopLevelIdentifier(url, '${cls.name}.${variable.name}'); | |
| 324 } else if (variable is TopLevelVariableElement) { | |
| 325 return new TopLevelIdentifier(url, variable.name); | |
| 326 } | |
| 327 } | |
| 328 } | |
| 329 | |
| 330 throw new UnimplementedError('expression convertion not implemented in ' | |
| 331 'smoke.codegen.recorder (${expression.runtimeType} $expression)'); | |
| 332 } | |
| 333 } | |
| 334 | |
| 335 /// Returns whether [metadata] contains any annotation that is either equal to | |
| 336 /// an annotation in [queryAnnotations] or whose type is a subclass of a type | |
| 337 /// listed in [queryAnnotations]. This is equivalent to the check done in | |
| 338 /// `src/common.dart#matchesAnnotation`, except that this is applied to | |
| 339 /// static metadata as it was provided by the analyzer. | |
| 340 bool _matchesAnnotation( | |
| 341 Iterable<ElementAnnotation> metadata, Iterable<Element> queryAnnotations) { | |
| 342 for (var meta in metadata) { | |
| 343 var element = meta.element; | |
| 344 var exp; | |
| 345 var type; | |
| 346 if (element is PropertyAccessorElement) { | |
| 347 exp = element.variable; | |
| 348 type = exp.evaluationResult.value.type; | |
| 349 } else if (element is ConstructorElement) { | |
| 350 exp = element; | |
| 351 type = element.enclosingElement.type; | |
| 352 } else { | |
| 353 throw new UnimplementedError('Unsupported annotation: ${meta}'); | |
| 354 } | |
| 355 for (var queryMeta in queryAnnotations) { | |
| 356 if (exp == queryMeta) return true; | |
| 357 if (queryMeta is ClassElement && type.isSubtypeOf(queryMeta.type)) { | |
| 358 return true; | |
| 359 } | |
| 360 } | |
| 361 } | |
| 362 return false; | |
| 363 } | |
| 364 | |
| 365 /// Options equivalent to `smoke.dart#QueryOptions`, except that type | |
| 366 /// information and annotations are denoted by resolver's elements. | |
| 367 class QueryOptions { | |
| 368 /// Whether to include fields (default is true). | |
| 369 final bool includeFields; | |
| 370 | |
| 371 /// Whether to include getters and setters (default is true). Note that to | |
| 372 /// include fields you also need to enable [includeFields]. | |
| 373 final bool includeProperties; | |
| 374 | |
| 375 /// Whether to include symbols from the given type and its superclasses | |
| 376 /// (except [Object]). | |
| 377 final bool includeInherited; | |
| 378 | |
| 379 /// If [includeInherited], walk up the type hierarchy up to this type | |
| 380 /// (defaults to [Object]). | |
| 381 final ClassElement includeUpTo; | |
| 382 | |
| 383 /// Whether to include final fields and getter-only properties. | |
| 384 final bool excludeFinal; | |
| 385 | |
| 386 /// Whether to include methods (default is false). | |
| 387 final bool includeMethods; | |
| 388 | |
| 389 /// If [withAnnotation] is not null, then it should be a list of types, so | |
| 390 /// only symbols that are annotated with instances of those types are | |
| 391 /// included. | |
| 392 final List<Element> withAnnotations; | |
| 393 | |
| 394 /// If [matches] is not null, then only those fields, properties, or methods | |
| 395 /// that match will be included. | |
| 396 final NameMatcher matches; | |
| 397 | |
| 398 const QueryOptions({this.includeFields: true, this.includeProperties: true, | |
| 399 this.includeInherited: true, this.includeUpTo: null, | |
| 400 this.excludeFinal: false, this.includeMethods: false, | |
| 401 this.withAnnotations: null, this.matches: null}); | |
| 402 } | |
| 403 | |
| 404 /// Predicate that tells whether [name] should be included in query results. | |
| 405 typedef bool NameMatcher(String name); | |
| OLD | NEW |