blob: be4c96b9080e2ca7c6aec70935818faeda761c19 [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Records accesses to Dart program declarations and generates code that will
/// allow to do the same accesses at runtime using `package:smoke/static.dart`.
/// Internally, this library relies on the `analyzer` to extract data from the
/// program, and then uses [SmokeCodeGenerator] to produce the code needed by
/// the smoke system.
///
/// This library only uses the analyzer to consume data previously produced by
/// running the resolver. This library does not provide any hooks to integrate
/// running the analyzer itself. See `package:code_transformers` to integrate
/// the analyzer into pub transformers.
library smoke.codegen.recorder;
import 'package:analyzer/src/generated/element.dart';
import 'package:analyzer/src/generated/ast.dart';
import 'generator.dart';
typedef String ImportUrlResolver(LibraryElement lib);
/// A recorder that tracks how elements are accessed in order to generate code
/// that replicates those accesses with the smoke runtime.
class Recorder {
/// Underlying code generator.
SmokeCodeGenerator generator;
/// Function that provides the import url for a library element. This may
/// internally use the resolver to resolve the import url.
ImportUrlResolver importUrlFor;
Recorder(this.generator, this.importUrlFor);
/// Stores mixins that have been recorded and associates a type identifier
/// with them. Mixins don't have an associated identifier in the code, so we
/// generate a unique identifier for them and use it throughout the code.
Map<TypeIdentifier, Map<ClassElement, TypeIdentifier>> _mixins = {};
/// Adds the superclass information of [type] (including intermediate mixins).
/// This will not generate data for direct subtypes of Object, as that is
/// considered redundant information.
void lookupParent(ClassElement type) {
var parent = type.supertype;
var mixins = type.mixins;
if (parent == null && mixins.isEmpty) return; // type is Object
var baseType = parent.element;
var baseId = _typeFor(baseType);
if (mixins.isNotEmpty) {
_mixins.putIfAbsent(baseId, () => {});
for (var m in mixins) {
var mixinType = m.element;
var mixinId = _mixins[baseId].putIfAbsent(mixinType, () {
var comment = '${baseId.name} & ${mixinType.name}';
return generator.createMixinType(comment);
});
if (!baseType.type.isObject) generator.addParent(mixinId, baseId);
baseType = mixinType;
baseId = mixinId;
_mixins.putIfAbsent(mixinId, () => {});
}
}
if (!baseType.type.isObject) generator.addParent(_typeFor(type), baseId);
}
TypeIdentifier _typeFor(ClassElement type) =>
new TypeIdentifier(importUrlFor(type.library), type.displayName);
/// Adds any declaration and superclass information that is needed to answer a
/// query on [type] that matches [options].
void runQuery(ClassElement type, QueryOptions options) {
if (type.type.isObject) return; // We don't include Object in query results.
var id = _typeFor(type);
var parent = type.supertype != null ? type.supertype.element : null;
if (options.includeInherited && parent != null &&
parent != options.includeUpTo) {
lookupParent(type);
runQuery(parent, options);
var parentId = _typeFor(parent);
for (var m in type.mixins) {
var mixinClass = m.element;
var mixinId = _mixins[parentId][mixinClass];
_runQueryInternal(mixinClass, mixinId, options);
parentId = mixinId;
}
}
_runQueryInternal(type, id, options);
}
/// Helper for [runQuery]. This runs the query only on a specific [type],
/// which could be a class or a mixin labeled by [id].
// TODO(sigmund): currently we materialize mixins in smoke/static.dart,
// we should consider to include the mixin declaration information directly,
// and remove the duplication we have for mixins today.
void _runQueryInternal(ClassElement type, TypeIdentifier id,
QueryOptions options) {
skipBecauseOfAnnotations(Element e) {
if (options.withAnnotations == null) return false;
return !_matchesAnnotation(e.metadata, options.withAnnotations);
}
if (options.includeFields) {
for (var f in type.fields) {
if (f.isStatic) continue;
if (f.isSynthetic) continue; // exclude getters
if (options.excludeFinal && f.isFinal) continue;
if (skipBecauseOfAnnotations(f)) continue;
generator.addDeclaration(id, f.displayName, _typeFor(f.type.element),
isField: true, isFinal: f.isFinal,
annotations: _copyAnnotations(f));
}
}
if (options.includeProperties) {
for (var a in type.accessors) {
if (a is! PropertyAccessorElement) continue;
if (a.isStatic || !a.isGetter) continue;
var v = a.variable;
if (v is FieldElement && !v.isSynthetic) continue; // exclude fields
if (options.excludeFinal && v.isFinal) continue;
if (skipBecauseOfAnnotations(v)) continue;
generator.addDeclaration(id, v.displayName, _typeFor(v.type.element),
isProperty: true, isFinal: v.isFinal,
annotations: _copyAnnotations(a));
}
}
if (options.includeMethods) {
for (var m in type.methods) {
if (m.isStatic) continue;
if (skipBecauseOfAnnotations(m)) continue;
generator.addDeclaration(id, m.displayName,
new TypeIdentifier('dart:core', 'Function'), isMethod: true,
annotations: _copyAnnotations(m));
}
}
}
/// Adds the declaration of [name] if it was found in [type]. If [recursive]
/// is true, then we continue looking up [name] in the parent classes until we
/// find it or we reach Object.
void lookupMember(ClassElement type, String name, {bool recursive: false}) {
_lookupMemberInternal(type, _typeFor(type), name, recursive);
}
/// Helper for [lookupMember] that walks up the type hierarchy including mixin
/// classes.
bool _lookupMemberInternal(ClassElement type, TypeIdentifier id, String name,
bool recursive) {
// Exclude members from [Object].
if (type.type.isObject) return false;
generator.addEmptyDeclaration(id);
for (var f in type.fields) {
if (f.displayName != name) continue;
if (f.isSynthetic) continue; // exclude getters
generator.addDeclaration(id, f.displayName,
_typeFor(f.type.element), isField: true, isFinal: f.isFinal,
isStatic: f.isStatic, annotations: _copyAnnotations(f));
return true;
}
for (var a in type.accessors) {
if (a is! PropertyAccessorElement) continue;
// TODO(sigmund): support setters without getters.
if (!a.isGetter) continue;
if (a.displayName != name) continue;
var v = a.variable;
if (v is FieldElement && !v.isSynthetic) continue; // exclude fields
generator.addDeclaration(id, v.displayName,
_typeFor(v.type.element), isProperty: true, isFinal: v.isFinal,
isStatic: v.isStatic, annotations: _copyAnnotations(a));
return true;
}
for (var m in type.methods) {
if (m.displayName != name) continue;
generator.addDeclaration(id, m.displayName,
new TypeIdentifier('dart:core', 'Function'), isMethod: true,
isStatic: m.isStatic, annotations: _copyAnnotations(m));
return true;
}
if (recursive) {
lookupParent(type);
var parent = type.supertype != null ? type.supertype.element : null;
if (parent == null) return false;
var parentId = _typeFor(parent);
for (var m in type.mixins) {
var mixinClass = m.element;
var mixinId = _mixins[parentId][mixinClass];
if (_lookupMemberInternal(mixinClass, mixinId, name, false)) {
return true;
}
parentId = mixinId;
}
return _lookupMemberInternal(parent, parentId, name, true);
}
return false;
}
/// Copy metadata associated with the declaration of [target].
List<ConstExpression> _copyAnnotations(Element target) {
var node = target.node;
// [node] is the initialization expression, we walk up to get to the actual
// member declaration where the metadata is attached to.
while (node is! ClassMember) node = node.parent;
return node.metadata.map(_convertAnnotation).toList();
}
/// Converts annotations into [ConstExpression]s supported by the codegen
/// library.
ConstExpression _convertAnnotation(Annotation annotation) {
var element = annotation.element;
if (element is ConstructorElement) {
if (!element.name.isEmpty) {
throw new UnimplementedError(
'named constructors are not implemented in smoke.codegen.recorder');
}
var positionalArgs = [];
for (var arg in annotation.arguments.arguments) {
if (arg is NamedExpression) {
throw new UnimplementedError(
'named arguments in constructors are not implemented in '
'smoke.codegen.recorder');
}
positionalArgs.add(_convertExpression(arg));
}
return new ConstructorExpression(importUrlFor(element.library),
element.enclosingElement.name, positionalArgs, const {});
}
if (element is PropertyAccessorElement) {
return new TopLevelIdentifier(
importUrlFor(element.library), element.name);
}
throw new UnsupportedError('unsupported annotation $annotation');
}
/// Converts [expression] into a [ConstExpression].
ConstExpression _convertExpression(Expression expression) {
if (expression is StringLiteral) {
return new ConstExpression.string(expression.stringValue);
}
if (expression is BooleanLiteral || expression is DoubleLiteral ||
expression is IntegerLiteral || expression is NullLiteral) {
return new CodeAsConstExpression("${(expression as dynamic).value}");
}
if (expression is Identifier) {
var element = expression.bestElement;
if (element == null || !element.isPublic) {
throw new UnsupportedError('private constants are not supported');
}
var url = importUrlFor(element.library);
if (element is ClassElement) {
return new TopLevelIdentifier(url, element.name);
}
if (element is PropertyAccessorElement) {
var variable = element.variable;
if (variable is FieldElement) {
var cls = variable.enclosingElement;
return new TopLevelIdentifier(url, '${cls.name}.${variable.name}');
} else if (variable is TopLevelVariableElement) {
return new TopLevelIdentifier(url, variable.name);
}
}
}
throw new UnimplementedError('expression convertion not implemented in '
'smoke.codegen.recorder (${expression.runtimeType} $expression)');
}
}
/// Returns whether [metadata] contains any annotation that is either equal to
/// an annotation in [queryAnnotations] or whose type is a subclass of a type
/// listed in [queryAnnotations]. This is equivalent to the check done in
/// `src/common.dart#matchesAnnotation`, except that this is applied to
/// static metadata as it was provided by the analyzer.
bool _matchesAnnotation(Iterable<ElementAnnotation> metadata,
Iterable<Element> queryAnnotations) {
for (var meta in metadata) {
var element = meta.element;
var exp;
var type;
if (element is PropertyAccessorElement) {
exp = element.variable;
type = exp.evaluationResult.value.type;
} else if (element is ConstructorElement) {
exp = element;
type = element.enclosingElement.type;
} else {
throw new UnimplementedError('Unsupported annotation: ${meta}');
}
for (var queryMeta in queryAnnotations) {
if (exp == queryMeta) return true;
if (queryMeta is ClassElement && type.isSubtypeOf(queryMeta.type)) {
return true;
}
}
}
return false;
}
/// Options equivalent to `smoke.dart#QueryOptions`, except that type
/// information and annotations are denoted by resolver's elements.
class QueryOptions {
/// Whether to include fields (default is true).
final bool includeFields;
/// Whether to include getters and setters (default is true). Note that to
/// include fields you also need to enable [includeFields].
final bool includeProperties;
/// Whether to include symbols from the given type and its superclasses
/// (except [Object]).
final bool includeInherited;
/// If [includeInherited], walk up the type hierarchy up to this type
/// (defaults to [Object]).
final ClassElement includeUpTo;
/// Whether to include final fields and getter-only properties.
final bool excludeFinal;
/// Whether to include methods (default is false).
final bool includeMethods;
/// If [withAnnotation] is not null, then it should be a list of types, so
/// only symbols that are annotated with instances of those types are
/// included.
final List<Element> withAnnotations;
const QueryOptions({
this.includeFields: true,
this.includeProperties: true,
this.includeInherited: true,
this.includeUpTo: null,
this.excludeFinal: false,
this.includeMethods: false,
this.withAnnotations: null});
}