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(ClassElement type) => |
| 67 new TypeIdentifier(importUrlFor(type.library), type.displayName); |
| 68 |
| 69 /// Adds any declaration and superclass information that is needed to answer a |
| 70 /// query on [type] that matches [options]. |
| 71 void runQuery(ClassElement type, QueryOptions options) { |
| 72 if (type.type.isObject) return; // We don't include Object in query results. |
| 73 var id = _typeFor(type); |
| 74 var parent = type.supertype != null ? type.supertype.element : null; |
| 75 if (options.includeInherited && parent != null && |
| 76 parent != options.includeUpTo) { |
| 77 lookupParent(type); |
| 78 runQuery(parent, options); |
| 79 var parentId = _typeFor(parent); |
| 80 for (var m in type.mixins) { |
| 81 var mixinClass = m.element; |
| 82 var mixinId = _mixins[parentId][mixinClass]; |
| 83 _runQueryInternal(mixinClass, mixinId, options); |
| 84 parentId = mixinId; |
| 85 } |
| 86 } |
| 87 _runQueryInternal(type, id, options); |
| 88 } |
| 89 |
| 90 /// Helper for [runQuery]. This runs the query only on a specific [type], |
| 91 /// which could be a class or a mixin labeled by [id]. |
| 92 // TODO(sigmund): currently we materialize mixins in smoke/static.dart, |
| 93 // we should consider to include the mixin declaration information directly, |
| 94 // and remove the duplication we have for mixins today. |
| 95 void _runQueryInternal(ClassElement type, TypeIdentifier id, |
| 96 QueryOptions options) { |
| 97 |
| 98 skipBecauseOfAnnotations(Element e) { |
| 99 if (options.withAnnotations == null) return false; |
| 100 return !_matchesAnnotation(e.metadata, options.withAnnotations); |
| 101 } |
| 102 |
| 103 if (options.includeFields) { |
| 104 for (var f in type.fields) { |
| 105 if (f.isStatic) continue; |
| 106 if (f.isSynthetic) continue; // exclude getters |
| 107 if (options.excludeFinal && f.isFinal) continue; |
| 108 if (skipBecauseOfAnnotations(f)) continue; |
| 109 generator.addDeclaration(id, f.displayName, _typeFor(f.type.element), |
| 110 isField: true, isFinal: f.isFinal, |
| 111 annotations: _copyAnnotations(f)); |
| 112 } |
| 113 } |
| 114 |
| 115 if (options.includeProperties) { |
| 116 for (var a in type.accessors) { |
| 117 if (a is! PropertyAccessorElement) continue; |
| 118 if (a.isStatic || !a.isGetter) continue; |
| 119 var v = a.variable; |
| 120 if (v is FieldElement && !v.isSynthetic) continue; // exclude fields |
| 121 if (options.excludeFinal && v.isFinal) continue; |
| 122 if (skipBecauseOfAnnotations(v)) continue; |
| 123 generator.addDeclaration(id, v.displayName, _typeFor(v.type.element), |
| 124 isProperty: true, isFinal: v.isFinal, |
| 125 annotations: _copyAnnotations(a)); |
| 126 } |
| 127 } |
| 128 |
| 129 if (options.includeMethods) { |
| 130 for (var m in type.methods) { |
| 131 if (m.isStatic) continue; |
| 132 if (skipBecauseOfAnnotations(m)) continue; |
| 133 generator.addDeclaration(id, m.displayName, |
| 134 new TypeIdentifier('dart:core', 'Function'), isMethod: true, |
| 135 annotations: _copyAnnotations(m)); |
| 136 } |
| 137 } |
| 138 } |
| 139 |
| 140 /// Adds the declaration of [name] if it was found in [type]. If [recursive] |
| 141 /// is true, then we continue looking up [name] in the parent classes until we |
| 142 /// find it or we reach Object. |
| 143 void lookupMember(ClassElement type, String name, {bool recursive: false}) { |
| 144 _lookupMemberInternal(type, _typeFor(type), name, recursive); |
| 145 } |
| 146 |
| 147 /// Helper for [lookupMember] that walks up the type hierarchy including mixin |
| 148 /// classes. |
| 149 bool _lookupMemberInternal(ClassElement type, TypeIdentifier id, String name, |
| 150 bool recursive) { |
| 151 // Exclude members from [Object]. |
| 152 if (type.type.isObject) return false; |
| 153 generator.addEmptyDeclaration(id); |
| 154 for (var f in type.fields) { |
| 155 if (f.displayName != name) continue; |
| 156 if (f.isSynthetic) continue; // exclude getters |
| 157 generator.addDeclaration(id, f.displayName, |
| 158 _typeFor(f.type.element), isField: true, isFinal: f.isFinal, |
| 159 isStatic: f.isStatic, annotations: _copyAnnotations(f)); |
| 160 return true; |
| 161 } |
| 162 |
| 163 for (var a in type.accessors) { |
| 164 if (a is! PropertyAccessorElement) continue; |
| 165 // TODO(sigmund): support setters without getters. |
| 166 if (!a.isGetter) continue; |
| 167 if (a.displayName != name) continue; |
| 168 var v = a.variable; |
| 169 if (v is FieldElement && !v.isSynthetic) continue; // exclude fields |
| 170 generator.addDeclaration(id, v.displayName, |
| 171 _typeFor(v.type.element), isProperty: true, isFinal: v.isFinal, |
| 172 isStatic: v.isStatic, annotations: _copyAnnotations(a)); |
| 173 return true; |
| 174 } |
| 175 |
| 176 for (var m in type.methods) { |
| 177 if (m.displayName != name) continue; |
| 178 generator.addDeclaration(id, m.displayName, |
| 179 new TypeIdentifier('dart:core', 'Function'), isMethod: true, |
| 180 isStatic: m.isStatic, annotations: _copyAnnotations(m)); |
| 181 return true; |
| 182 } |
| 183 |
| 184 if (recursive) { |
| 185 lookupParent(type); |
| 186 var parent = type.supertype != null ? type.supertype.element : null; |
| 187 if (parent == null) return false; |
| 188 var parentId = _typeFor(parent); |
| 189 for (var m in type.mixins) { |
| 190 var mixinClass = m.element; |
| 191 var mixinId = _mixins[parentId][mixinClass]; |
| 192 if (_lookupMemberInternal(mixinClass, mixinId, name, false)) { |
| 193 return true; |
| 194 } |
| 195 parentId = mixinId; |
| 196 } |
| 197 return _lookupMemberInternal(parent, parentId, name, true); |
| 198 } |
| 199 return false; |
| 200 } |
| 201 |
| 202 |
| 203 /// Copy metadata associated with the declaration of [target]. |
| 204 List<ConstExpression> _copyAnnotations(Element target) { |
| 205 var node = target.node; |
| 206 // [node] is the initialization expression, we walk up to get to the actual |
| 207 // member declaration where the metadata is attached to. |
| 208 while (node is! ClassMember) node = node.parent; |
| 209 return node.metadata.map(_convertAnnotation).toList(); |
| 210 } |
| 211 |
| 212 /// Converts annotations into [ConstExpression]s supported by the codegen |
| 213 /// library. |
| 214 ConstExpression _convertAnnotation(Annotation annotation) { |
| 215 var element = annotation.element; |
| 216 if (element is ConstructorElement) { |
| 217 if (!element.name.isEmpty) { |
| 218 throw new UnimplementedError( |
| 219 'named constructors are not implemented in smoke.codegen.recorder'); |
| 220 } |
| 221 |
| 222 var positionalArgs = []; |
| 223 for (var arg in annotation.arguments.arguments) { |
| 224 if (arg is NamedExpression) { |
| 225 throw new UnimplementedError( |
| 226 'named arguments in constructors are not implemented in ' |
| 227 'smoke.codegen.recorder'); |
| 228 } |
| 229 positionalArgs.add(_convertExpression(arg)); |
| 230 } |
| 231 |
| 232 return new ConstructorExpression(importUrlFor(element.library), |
| 233 element.enclosingElement.name, positionalArgs, const {}); |
| 234 } |
| 235 |
| 236 if (element is PropertyAccessorElement) { |
| 237 return new TopLevelIdentifier( |
| 238 importUrlFor(element.library), element.name); |
| 239 } |
| 240 |
| 241 throw new UnsupportedError('unsupported annotation $annotation'); |
| 242 } |
| 243 |
| 244 /// Converts [expression] into a [ConstExpression]. |
| 245 ConstExpression _convertExpression(Expression expression) { |
| 246 if (expression is StringLiteral) { |
| 247 return new ConstExpression.string(expression.stringValue); |
| 248 } |
| 249 |
| 250 if (expression is BooleanLiteral || expression is DoubleLiteral || |
| 251 expression is IntegerLiteral || expression is NullLiteral) { |
| 252 return new CodeAsConstExpression("${(expression as dynamic).value}"); |
| 253 } |
| 254 |
| 255 if (expression is Identifier) { |
| 256 var element = expression.bestElement; |
| 257 if (element == null || !element.isPublic) { |
| 258 throw new UnsupportedError('private constants are not supported'); |
| 259 } |
| 260 |
| 261 var url = importUrlFor(element.library); |
| 262 if (element is ClassElement) { |
| 263 return new TopLevelIdentifier(url, element.name); |
| 264 } |
| 265 |
| 266 if (element is PropertyAccessorElement) { |
| 267 var variable = element.variable; |
| 268 if (variable is FieldElement) { |
| 269 var cls = variable.enclosingElement; |
| 270 return new TopLevelIdentifier(url, '${cls.name}.${variable.name}'); |
| 271 } else if (variable is TopLevelVariableElement) { |
| 272 return new TopLevelIdentifier(url, variable.name); |
| 273 } |
| 274 } |
| 275 } |
| 276 |
| 277 throw new UnimplementedError('expression convertion not implemented in ' |
| 278 'smoke.codegen.recorder (${expression.runtimeType} $expression)'); |
| 279 } |
| 280 } |
| 281 |
| 282 /// Returns whether [metadata] contains any annotation that is either equal to |
| 283 /// an annotation in [queryAnnotations] or whose type is a subclass of a type |
| 284 /// listed in [queryAnnotations]. This is equivalent to the check done in |
| 285 /// `src/common.dart#matchesAnnotation`, except that this is applied to |
| 286 /// static metadata as it was provided by the analyzer. |
| 287 bool _matchesAnnotation(Iterable<ElementAnnotation> metadata, |
| 288 Iterable<Element> queryAnnotations) { |
| 289 for (var meta in metadata) { |
| 290 var element = meta.element; |
| 291 var exp; |
| 292 var type; |
| 293 if (element is PropertyAccessorElement) { |
| 294 exp = element.variable; |
| 295 type = exp.evaluationResult.value.type; |
| 296 } else if (element is ConstructorElement) { |
| 297 exp = element; |
| 298 type = element.enclosingElement.type; |
| 299 } else { |
| 300 throw new UnimplementedError('Unsupported annotation: ${meta}'); |
| 301 } |
| 302 for (var queryMeta in queryAnnotations) { |
| 303 if (exp == queryMeta) return true; |
| 304 if (queryMeta is ClassElement && type.isSubtypeOf(queryMeta.type)) { |
| 305 return true; |
| 306 } |
| 307 } |
| 308 } |
| 309 return false; |
| 310 } |
| 311 |
| 312 /// Options equivalent to `smoke.dart#QueryOptions`, except that type |
| 313 /// information and annotations are denoted by resolver's elements. |
| 314 class QueryOptions { |
| 315 /// Whether to include fields (default is true). |
| 316 final bool includeFields; |
| 317 |
| 318 /// Whether to include getters and setters (default is true). Note that to |
| 319 /// include fields you also need to enable [includeFields]. |
| 320 final bool includeProperties; |
| 321 |
| 322 /// Whether to include symbols from the given type and its superclasses |
| 323 /// (except [Object]). |
| 324 final bool includeInherited; |
| 325 |
| 326 /// If [includeInherited], walk up the type hierarchy up to this type |
| 327 /// (defaults to [Object]). |
| 328 final ClassElement includeUpTo; |
| 329 |
| 330 /// Whether to include final fields and getter-only properties. |
| 331 final bool excludeFinal; |
| 332 |
| 333 /// Whether to include methods (default is false). |
| 334 final bool includeMethods; |
| 335 |
| 336 /// If [withAnnotation] is not null, then it should be a list of types, so |
| 337 /// only symbols that are annotated with instances of those types are |
| 338 /// included. |
| 339 final List<Element> withAnnotations; |
| 340 |
| 341 const QueryOptions({ |
| 342 this.includeFields: true, |
| 343 this.includeProperties: true, |
| 344 this.includeInherited: true, |
| 345 this.includeUpTo: null, |
| 346 this.excludeFinal: false, |
| 347 this.includeMethods: false, |
| 348 this.withAnnotations: null}); |
| 349 } |
OLD | NEW |