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