perf(dart/transform) Restructure transform to independent phases

Update summary:
- Removes the need for resolution, gaining transform speed at the cost
  of some precision and ability to detect errors
- Generates type registrations in the package alongside their declarations
- Ensures that line numbers do not change in transformed user code
This commit is contained in:
Tim Blasi
2015-02-27 14:42:21 -08:00
parent 08bd3a4443
commit d0aceef4e0
52 changed files with 1530 additions and 318 deletions

View File

@ -0,0 +1,71 @@
library angular2.src.transform;
import 'dart:collection' show Queue;
import 'package:analyzer/src/generated/ast.dart';
import 'package:analyzer/src/generated/element.dart';
import 'resolvers.dart';
/// Provides a mechanism for checking an element for the provided
/// [_annotationClass] and reporting the resulting (element, annotation) pairs.
class AnnotationMatcher {
/// Queue for annotations.
final matchQueue = new Queue<AnnotationMatch>();
/// All the annotations we have seen for each element
final _seenAnnotations = new Map<Element, Set<ElementAnnotation>>();
/// The classes we are searching for to populate [matchQueue].
final Set<ClassElement> _annotationClasses;
AnnotationMatcher(this._annotationClasses);
/// Records all [_annotationClass] annotations and the [element]s they apply to.
/// Returns
List<AnnotationMatch> processAnnotations(ClassElement element) {
// Finding the node corresponding to [element] can be very expensive.
ClassDeclaration cachedNode = null;
var result = <AnnotationMatch>[];
element.metadata.where((ElementAnnotation meta) {
// TODO(tjblasi): Make this recognize non-ConstructorElement annotations.
return meta.element is ConstructorElement &&
_isAnnotationMatch(meta.element.returnType);
}).where((ElementAnnotation meta) {
// Only process ([element], [meta]) combinations we haven't seen previously.
return !_seenAnnotations
.putIfAbsent(element, () => new Set<ElementAnnotation>())
.contains(meta);
}).forEach((ElementAnnotation meta) {
if (cachedNode == null) {
cachedNode = element.node;
}
var match = new AnnotationMatch(cachedNode, meta);
_seenAnnotations[element].add(meta);
matchQueue.addLast(match);
result.add(match);
});
return result;
}
/// Whether [type], its superclass, or one of its interfaces matches [_annotationClass].
bool _isAnnotationMatch(InterfaceType type) {
return _annotationClasses.any((el) => isAnnotationMatch(type, el));
}
}
/// [ClassElement] / [ElementAnnotation] pair.
class AnnotationMatch {
/// The resolved element corresponding to [node].
final ClassElement element;
/// The Ast node corresponding to the class definition.
final ClassDeclaration node;
/// The resolved element for the matched annotation.
final ElementAnnotation annotation;
AnnotationMatch(ClassDeclaration node, this.annotation)
: node = node,
element = node.element;
}

View File

@ -0,0 +1,518 @@
library angular2.src.transform;
import 'package:analyzer/src/generated/ast.dart';
import 'package:analyzer/src/generated/element.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:barback/barback.dart' show AssetId, TransformLogger;
import 'package:dart_style/dart_style.dart';
import 'package:path/path.dart' as path;
import 'annotation_processor.dart';
import 'common/logging.dart';
/// Base class that maintains codegen state.
class Context {
/// Maps libraries to the import prefixes we will use in the newly
/// generated code.
final Map<LibraryElement, String> _libraryPrefixes = {};
/// Whether to generate constructor stubs for classes annotated
/// with [Component], [Decorator], [Template], and [Inject] (and subtypes).
bool generateCtorStubs = true;
/// Whether to generate setter stubs for classes annotated with
/// [Directive] subtypes. These setters depend on the value passed to the
/// annotation's `bind` value.
bool generateSetterStubs = true;
DirectiveRegistry _directiveRegistry;
/// Generates [registerType] calls for all [register]ed [AnnotationMatch]
/// objects.
DirectiveRegistry get directiveRegistry => _directiveRegistry;
Context() {
_directiveRegistry = new _DirectiveRegistryImpl(this);
}
/// If elements in [lib] should be prefixed in our generated code, returns
/// the appropriate prefix followed by a `.`. Future items from the same
/// library will use the same prefix.
/// If [lib] does not need a prefix, returns the empty string.
String _getPrefixDot(LibraryElement lib) {
if (lib == null || lib.isInSdk) return '';
var prefix =
_libraryPrefixes.putIfAbsent(lib, () => 'i${_libraryPrefixes.length}');
return '${prefix}.';
}
}
/// Object which [register]s [AnnotationMatch] objects for code generation.
abstract class DirectiveRegistry {
// Adds [entry] to the `registerType` calls which will be generated.
void register(AnnotationMatch entry);
}
const setupReflectionMethodName = 'setupReflection';
const _libraryDeclaration = '''
library angular2.src.transform.generated;
''';
const _reflectorImport = '''
import 'package:angular2/src/reflection/reflection.dart' show reflector;
''';
/// Default implementation to map from [LibraryElement] to [AssetId]. This
/// assumes that [el.source] has a getter called [assetId].
AssetId _assetIdFromLibraryElement(LibraryElement el) {
return (el.source as dynamic).assetId;
}
String codegenEntryPoint(Context context, {AssetId newEntryPoint}) {
if (newEntryPoint == null) {
throw new ArgumentError.notNull('newEntryPoint');
}
// TODO(kegluneq): copyright declaration
var outBuffer = new StringBuffer()
..write(_libraryDeclaration)
..write(_reflectorImport);
_codegenImports(context, newEntryPoint, outBuffer);
outBuffer
.write('${setupReflectionMethodName}() {${context.directiveRegistry}}');
return new DartFormatter().format(outBuffer.toString());
}
void _codegenImports(
Context context, AssetId newEntryPoint, StringBuffer buffer) {
context._libraryPrefixes.forEach((lib, prefix) {
buffer
..write(_codegenImport(
context, _assetIdFromLibraryElement(lib), newEntryPoint))
..writeln('as ${prefix};');
});
}
String _codegenImport(Context context, AssetId libraryId, AssetId entryPoint) {
if (libraryId.path.startsWith('lib/')) {
var packagePath = libraryId.path.replaceFirst('lib/', '');
return "import 'package:${libraryId.package}/${packagePath}'";
} else if (libraryId.package != entryPoint.package) {
logger.error("Can't import `${libraryId}` from `${entryPoint}`");
} else if (path.url.split(libraryId.path)[0] ==
path.url.split(entryPoint.path)[0]) {
var relativePath =
path.relative(libraryId.path, from: path.dirname(entryPoint.path));
return "import '${relativePath}'";
} else {
logger.error("Can't import `${libraryId}` from `${entryPoint}`");
}
}
// TODO(https://github.com/kegluneq/angular/issues/4): Remove calls to
// Element#node.
class _DirectiveRegistryImpl implements DirectiveRegistry {
final Context _context;
final PrintWriter _writer;
final Set<ClassDeclaration> _seen = new Set();
final _AnnotationsTransformVisitor _annotationsVisitor;
final _BindTransformVisitor _bindVisitor;
final _FactoryTransformVisitor _factoryVisitor;
final _ParameterTransformVisitor _parametersVisitor;
_DirectiveRegistryImpl._internal(Context context, PrintWriter writer)
: _writer = writer,
_context = context,
_annotationsVisitor = new _AnnotationsTransformVisitor(writer, context),
_bindVisitor = new _BindTransformVisitor(writer, context),
_factoryVisitor = new _FactoryTransformVisitor(writer, context),
_parametersVisitor = new _ParameterTransformVisitor(writer, context);
factory _DirectiveRegistryImpl(Context context) {
return new _DirectiveRegistryImpl._internal(
context, new PrintStringWriter());
}
@override
String toString() {
return _seen.isEmpty ? '' : 'reflector${_writer};';
}
// Adds [entry] to the `registerType` calls which will be generated.
void register(AnnotationMatch entry) {
if (_seen.contains(entry.node)) return;
_seen.add(entry.node);
if (_context.generateCtorStubs) {
_generateCtorStubs(entry);
}
if (_context.generateSetterStubs) {
_generateSetterStubs(entry);
}
}
void _generateSetterStubs(AnnotationMatch entry) {
// TODO(kegluneq): Remove these requirements for setter stub generation.
if (entry.element is! ClassElement) {
logger.error('Directives can only be applied to classes.');
return;
}
if (entry.node is! ClassDeclaration) {
logger.error('Unsupported annotation type for ctor stub generation. '
'Only class declarations are supported as Directives.');
return;
}
entry.node.accept(_bindVisitor);
}
void _generateCtorStubs(AnnotationMatch entry) {
var element = entry.element;
var annotation = entry.annotation;
// TODO(kegluneq): Remove these requirements for ctor stub generation.
if (annotation.element is! ConstructorElement) {
logger.error('Unsupported annotation type for ctor stub generation. '
'Only constructors are supported as Directives.');
return;
}
if (element is! ClassElement) {
logger.error('Directives can only be applied to classes.');
return;
}
if (entry.node is! ClassDeclaration) {
logger.error('Unsupported annotation type for ctor stub generation. '
'Only class declarations are supported as Directives.');
return;
}
var ctor = element.unnamedConstructor;
if (ctor == null) {
logger.error('No unnamed constructor found for ${element.name}');
return;
}
var ctorNode = ctor.node;
_writer.print('..registerType(');
_codegenClassTypeString(element);
_writer.print(', {"factory": ');
_codegenClassTypeString(element);
_writer.print('.ngFactory');
_writer.print(', "parameters": ');
_codegenClassTypeString(element);
_writer.print('.ngParameters');
_writer.print(', "annotations": ');
_codegenClassTypeString(element);
_writer.print('.ngAnnotations');
_writer.print('})');
}
void _codegenClassTypeString(ClassElement el) {
_writer.print('${_context._getPrefixDot(el.library)}${el.name}');
}
/// Creates the 'annotations' property for the Angular2 [registerType] call
/// for [node].
void _codegenAnnotationsProp(ClassDeclaration node) {
node.accept(_annotationsVisitor);
}
/// Creates the 'factory' property for the Angular2 [registerType] call
/// for [node]. [element] is necessary if [node] is null.
void _codegenFactoryProp(ConstructorDeclaration node, ClassElement element) {
if (node == null) {
// This occurs when the class does not declare a constructor.
var prefix = _context._getPrefixDot(element.library);
_writer.print('() => new ${prefix}${element.displayName}()');
} else {
node.accept(_factoryVisitor);
}
}
/// Creates the 'parameters' property for the Angular2 [registerType] call
/// for [node].
void _codegenParametersProp(ConstructorDeclaration node) {
if (node == null) {
// This occurs when the class does not declare a constructor.
_writer.print('const [const []]');
} else {
node.accept(_parametersVisitor);
}
}
}
/// Visitor providing common methods for concrete implementations.
class _TransformVisitorMixin {
final Context context;
final PrintWriter writer;
/// Safely visit [node].
void _visitNode(AstNode node) {
if (node != null) {
node.accept(this);
}
}
/// If [node] is null does nothing. Otherwise, prints [prefix], then
/// visits [node].
void _visitNodeWithPrefix(String prefix, AstNode node) {
if (node != null) {
writer.print(prefix);
node.accept(this);
}
}
/// If [node] is null does nothing. Otherwise, visits [node], then prints
/// [suffix].
void _visitNodeWithSuffix(AstNode node, String suffix) {
if (node != null) {
node.accept(this);
writer.print(suffix);
}
}
String prefixedSimpleIdentifier(SimpleIdentifier node) {
// Make sure the identifier is prefixed if necessary.
if (node.bestElement is ClassElementImpl ||
node.bestElement is PropertyAccessorElement) {
return context._getPrefixDot(node.bestElement.library) +
node.token.lexeme;
} else {
return node.token.lexeme;
}
}
}
class _TransformVisitor extends ToSourceVisitor with _TransformVisitorMixin {
final Context context;
final PrintWriter writer;
_TransformVisitor(PrintWriter writer, this.context)
: this.writer = writer,
super(writer);
@override
Object visitPrefixedIdentifier(PrefixedIdentifier node) {
// We add our own prefixes in [visitSimpleIdentifier], discard any used in
// the original source.
writer.print(super.prefixedSimpleIdentifier(node.identifier));
return null;
}
@override
Object visitSimpleIdentifier(SimpleIdentifier node) {
writer.print(super.prefixedSimpleIdentifier(node));
return null;
}
}
/// SourceVisitor designed to accept [ConstructorDeclaration] nodes.
class _CtorTransformVisitor extends _TransformVisitor {
bool _withParameterTypes = true;
bool _withParameterNames = true;
_CtorTransformVisitor(PrintWriter writer, Context _context)
: super(writer, _context);
/// If [_withParameterTypes] is true, this method outputs [node]'s type
/// (appropriately prefixed based on [_libraryPrefixes]. If
/// [_withParameterNames] is true, this method outputs [node]'s identifier.
Object _visitNormalFormalParameter(NormalFormalParameter node) {
if (_withParameterTypes) {
var paramType = node.element.type;
var prefix = context._getPrefixDot(paramType.element.library);
writer.print('${prefix}${paramType.displayName}');
if (_withParameterNames) {
_visitNodeWithPrefix(' ', node.identifier);
}
} else if (_withParameterNames) {
_visitNode(node.identifier);
}
return null;
}
@override
Object visitSimpleFormalParameter(SimpleFormalParameter node) {
return _visitNormalFormalParameter(node);
}
@override
Object visitFieldFormalParameter(FieldFormalParameter node) {
if (node.parameters != null) {
logger.error('Parameters in ctor not supported '
'(${super.visitFormalParameterList(node)}');
}
return _visitNormalFormalParameter(node);
}
@override
Object visitDefaultFormalParameter(DefaultFormalParameter node) {
_visitNode(node.parameter);
// Ignore the declared default value.
return null;
}
@override
/// Overridden to avoid outputting grouping operators for default parameters.
Object visitFormalParameterList(FormalParameterList node) {
writer.print('(');
NodeList<FormalParameter> parameters = node.parameters;
int size = parameters.length;
for (int i = 0; i < size; i++) {
if (i > 0) {
writer.print(', ');
}
parameters[i].accept(this);
}
writer.print(')');
return null;
}
}
/// ToSourceVisitor designed to print 'parameters' values for Angular2's
/// [registerType] calls.
class _ParameterTransformVisitor extends _CtorTransformVisitor {
_ParameterTransformVisitor(PrintWriter writer, Context _context)
: super(writer, _context);
@override
Object visitConstructorDeclaration(ConstructorDeclaration node) {
_withParameterNames = false;
_withParameterTypes = true;
writer.print('const [const [');
_visitNode(node.parameters);
writer.print(']]');
return null;
}
@override
Object visitFormalParameterList(FormalParameterList node) {
NodeList<FormalParameter> parameters = node.parameters;
int size = parameters.length;
for (int i = 0; i < size; i++) {
if (i > 0) {
writer.print(', ');
}
parameters[i].accept(this);
}
return null;
}
}
/// ToSourceVisitor designed to print 'factory' values for Angular2's
/// [registerType] calls.
class _FactoryTransformVisitor extends _CtorTransformVisitor {
_FactoryTransformVisitor(PrintWriter writer, Context _context)
: super(writer, _context);
@override
Object visitConstructorDeclaration(ConstructorDeclaration node) {
_withParameterNames = true;
_withParameterTypes = true;
_visitNode(node.parameters);
writer.print(' => new ');
_visitNode(node.returnType);
_visitNodeWithPrefix(".", node.name);
_withParameterTypes = false;
_visitNode(node.parameters);
return null;
}
}
/// ToSourceVisitor designed to print a [ClassDeclaration] node as a
/// 'annotations' value for Angular2's [registerType] calls.
class _AnnotationsTransformVisitor extends _TransformVisitor {
_AnnotationsTransformVisitor(PrintWriter writer, Context _context)
: super(writer, _context);
@override
Object visitClassDeclaration(ClassDeclaration node) {
writer.print('const [');
var size = node.metadata.length;
for (var i = 0; i < size; ++i) {
if (i > 0) {
writer.print(', ');
}
node.metadata[i].accept(this);
}
writer.print(']');
return null;
}
@override
Object visitAnnotation(Annotation node) {
writer.print('const ');
_visitNode(node.name);
// TODO(tjblasi): Do we need to handle named constructors for annotations?
// _visitNodeWithPrefix(".", node.constructorName);
_visitNode(node.arguments);
return null;
}
}
/// Visitor designed to print a [ClassDeclaration] node as a
/// `registerSetters` call for Angular2.
class _BindTransformVisitor extends Object
with SimpleAstVisitor<Object>, _TransformVisitorMixin {
final Context context;
final PrintWriter writer;
final List<String> _bindPieces = [];
SimpleIdentifier _currentName = null;
_BindTransformVisitor(this.writer, this.context);
@override
Object visitClassDeclaration(ClassDeclaration node) {
_currentName = node.name;
node.metadata.forEach((meta) => _visitNode(meta));
if (_bindPieces.isNotEmpty) {
writer.print('..registerSetters({${_bindPieces.join(', ')}})');
}
return null;
}
@override
Object visitAnnotation(Annotation node) {
// TODO(kegluneq): Remove this restriction.
if (node.element is ConstructorElement) {
if (node.element.returnType.element is ClassElement) {
// TODO(kegluneq): Check if this is actually a `directive`.
node.arguments.arguments.forEach((arg) => _visitNode(arg));
}
}
return null;
}
@override
Object visitNamedExpression(NamedExpression node) {
if (node.name.label.toString() == 'bind') {
// TODO(kegluneq): Remove this restriction.
if (node.expression is MapLiteral) {
node.expression.accept(this);
}
}
return null;
}
@override
Object visitMapLiteral(MapLiteral node) {
node.entries.forEach((entry) {
if (entry.key is SimpleStringLiteral) {
_visitNode(entry.key);
} else {
logger.error('`bind` currently only supports string literals');
}
});
return null;
}
@override
Object visitSimpleStringLiteral(SimpleStringLiteral node) {
if (_currentName == null) {
logger.error('Unexpected code path: `currentName` should never be null');
}
_bindPieces.add('"${node.value}": ('
'${super.prefixedSimpleIdentifier(_currentName)} o, String value) => '
'o.${node.value} = value');
return null;
}
}

View File

@ -0,0 +1,93 @@
library angular2.src.transform;
import 'package:analyzer/src/generated/ast.dart';
import 'package:analyzer/src/generated/element.dart';
import 'package:code_transformers/resolver.dart';
import 'resolvers.dart';
/// Finds all calls to the Angular2 [bootstrap] method defined in [library].
/// This only searches the code defined in the file
// represented by [library], not `part`s, `import`s, `export`s, etc.
Set<BootstrapCallInfo> findBootstrapCalls(
Resolver resolver, LibraryElement library) {
var types = new Angular2Types(resolver);
if (types.bootstrapMethod == null) {
throw new ArgumentError(
'Could not find symbol for ${bootstrapMethodName}.');
}
var visitor = new _FindFunctionVisitor(types.bootstrapMethod);
// TODO(kegluneq): Determine how to get nodes without querying Element#node.
// Root of file defining that library (main part).
library.definingCompilationUnit.node.accept(visitor);
return new Set.from(visitor.functionCalls.map((MethodInvocation mi) {
var visitor = new _ParseBootstrapTypeVisitor(types);
if (mi.argumentList.arguments.isEmpty) {
throw new ArgumentError('No arguments provided to `bootstrap`.');
}
mi.argumentList.arguments[0].accept(visitor);
if (visitor.bootstrapType == null) {
throw new UnsupportedError(
'Failed to parse `bootstrap` call: ${mi.toSource()}');
}
return new BootstrapCallInfo(mi, visitor.bootstrapType);
}));
}
/// Information about a single call to Angular2's [bootstrap] method.
class BootstrapCallInfo {
/// The [AstNode] representing the call to [bootstrap].
final MethodInvocation call;
/// The type, which should be annotated as a [Component], which is the root
/// of the Angular2 app.
final ClassElement bootstrapType;
BootstrapCallInfo(this.call, this.bootstrapType);
}
/// Visitor that finds the Angular2 bootstrap component given [bootstrap]'s
/// first argument.
///
/// This visitor does not recursively visit nodes in the Ast.
class _ParseBootstrapTypeVisitor extends SimpleAstVisitor<Object> {
ClassElement bootstrapType = null;
final Angular2Types _types;
_ParseBootstrapTypeVisitor(this._types);
// TODO(kegluneq): Allow non-SimpleIdentifier expressions.
@override
Object visitSimpleIdentifier(SimpleIdentifier node) {
bootstrapType = (node.bestElement as ClassElement);
if (!_types.isComponent(bootstrapType)) {
throw new ArgumentError('Class passed to `${bootstrapMethodName}` must '
'be a @${_types.componentAnnotation.name}');
}
}
}
/// Recursively visits all nodes in an Ast structure, recording all encountered
/// calls to the provided [FunctionElement].
class _FindFunctionVisitor extends RecursiveAstVisitor<Object> {
final FunctionElement _target;
_FindFunctionVisitor(this._target);
final Set<MethodInvocation> functionCalls = new Set();
bool _isDesiredMethod(MethodInvocation node) {
return node.methodName.bestElement == _target;
}
@override
Object visitMethodInvocation(MethodInvocation node) {
if (_isDesiredMethod(node)) {
functionCalls.add(node);
}
return super.visitMethodInvocation(node);
}
}

View File

@ -0,0 +1,152 @@
library angular2.src.transform;
import 'package:analyzer/src/generated/ast.dart';
import 'package:analyzer/src/generated/element.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:barback/barback.dart';
import 'package:code_transformers/resolver.dart';
import 'package:dart_style/dart_style.dart';
import 'package:path/path.dart' as path;
import 'codegen.dart';
import 'common/logging.dart';
import 'resolvers.dart';
/// Finds all calls to the Angular2 [ReflectionCapabilities] constructor
/// defined in [library].
/// This only searches the code defined in the file
// represented by [library], not `part`s, `import`s, `export`s, etc.
String findReflectionCapabilities(
Resolver resolver, AssetId reflectionEntryPoint, AssetId newEntryPoint) {
var types = new Angular2Types(resolver);
if (types.reflectionCapabilities == null) {
throw new ArgumentError(
'Could not find class for ${reflectionCapabilitiesTypeName}.');
}
var codegen = new _SetupReflectionCodegen(
resolver, reflectionEntryPoint, newEntryPoint);
var writer = new PrintStringWriter();
var visitor = new _RewriteReflectionEntryPointVisitor(
writer, types.reflectionCapabilities, codegen);
// TODO(kegluneq): Determine how to get nodes without querying Element#node.
// Root of file defining that library (main part).
resolver.getLibrary(reflectionEntryPoint).definingCompilationUnit.node
.accept(visitor);
return new DartFormatter().format(writer.toString());
}
class _SetupReflectionCodegen {
static const _prefixBase = 'ngStaticInit';
final String prefix;
final String importUri;
_SetupReflectionCodegen._internal(this.prefix, this.importUri);
factory _SetupReflectionCodegen(
Resolver resolver, AssetId reflectionEntryPoint, AssetId newEntryPoint) {
var lib = resolver.getLibrary(reflectionEntryPoint);
var prefix = _prefixBase;
var idx = 0;
while (lib.imports.any((import) {
return import.prefix != null && import.prefix == prefix;
})) {
prefix = '${_prefixBase}${idx++}';
}
var importPath = path.relative(newEntryPoint.path,
from: path.dirname(reflectionEntryPoint.path));
return new _SetupReflectionCodegen._internal(prefix, importPath);
}
/// Generates code to import the library containing the method which sets up
/// Angular2 reflection statically.
///
/// The code generated here should follow the example of code generated for
/// an [ImportDirective] node.
String codegenImport() {
return 'import \'${importUri}\' as ${prefix};';
}
/// Generates code to call the method which sets up Angular2 reflection
/// statically.
///
/// The code generated here should follow the example of code generated for
/// a [MethodInvocation] node, that is, it should be prefixed as necessary
/// and not be followed by a ';'.
String codegenSetupReflectionCall() {
return '${prefix}.${setupReflectionMethodName}()';
}
}
class _RewriteReflectionEntryPointVisitor extends ToSourceVisitor {
final PrintWriter _writer;
final ClassElement _forbiddenClass;
final _SetupReflectionCodegen _codegen;
_RewriteReflectionEntryPointVisitor(
PrintWriter writer, this._forbiddenClass, this._codegen)
: _writer = writer,
super(writer);
bool _isNewReflectionCapabilities(InstanceCreationExpression node) {
var typeElement = node.constructorName.type.name.bestElement;
return typeElement != null && typeElement == _forbiddenClass;
}
bool _isReflectionCapabilitiesImport(ImportDirective node) {
return node.uriElement == _forbiddenClass.library;
}
@override
Object visitImportDirective(ImportDirective node) {
if (_isReflectionCapabilitiesImport(node)) {
// TODO(kegluneq): Remove newlines once dart_style bug is fixed.
// https://github.com/dart-lang/dart_style/issues/178
// _writer.print('\n/* ReflectionCapabilities import removed */\n');
_writer.print(_codegen.codegenImport());
// TODO(kegluneq): Remove once we generate all needed code.
{
super.visitImportDirective(node);
}
return null;
}
return super.visitImportDirective(node);
}
@override
Object visitAssignmentExpression(AssignmentExpression node) {
if (node.rightHandSide is InstanceCreationExpression &&
_isNewReflectionCapabilities(node.rightHandSide)) {
// TODO(kegluneq): Remove newlines once dart_style bug is fixed.
// https://github.com/dart-lang/dart_style/issues/178
// _writer.print('/* Creation of ReflectionCapabilities removed */\n');
_writer.print(_codegen.codegenSetupReflectionCall());
// TODO(kegluneq): Remove once we generate all needed code.
{
_writer.print(';');
node.leftHandSide.accept(this);
_writer.print(' ${node.operator.lexeme} ');
super.visitInstanceCreationExpression(node.rightHandSide);
}
return null;
}
return super.visitAssignmentExpression(node);
}
@override
Object visitInstanceCreationExpression(InstanceCreationExpression node) {
if (_isNewReflectionCapabilities(node)) {
logger.error('Unexpected format in creation of '
'${reflectionCapabilitiesTypeName}');
} else {
return super.visitInstanceCreationExpression(node);
}
return null;
}
}

View File

@ -0,0 +1,140 @@
library angular2.src.transform;
import 'package:barback/barback.dart';
import 'package:analyzer/src/generated/element.dart';
import 'package:code_transformers/resolver.dart';
Resolvers createResolvers() {
return new Resolvers.fromMock({
// The list of types below is derived from:
// * types that are used internally by the resolver (see
// _initializeFrom in resolver.dart).
// TODO(jakemac): Move this into code_transformers so it can be shared.
'dart:core': '''
library dart.core;
class Object {}
class Function {}
class StackTrace {}
class Symbol {}
class Type {}
class String extends Object {}
class bool extends Object {}
class num extends Object {}
class int extends num {}
class double extends num {}
class DateTime extends Object {}
class Null extends Object {}
class Deprecated extends Object {
final String expires;
const Deprecated(this.expires);
}
const Object deprecated = const Deprecated("next release");
class _Override { const _Override(); }
const Object override = const _Override();
class _Proxy { const _Proxy(); }
const Object proxy = const _Proxy();
class List<V> extends Object {}
class Map<K, V> extends Object {}
''',
'dart:html': '''
library dart.html;
class HtmlElement {}
''',
});
}
const bootstrapMethodName = 'bootstrap';
const reflectionCapabilitiesTypeName = 'ReflectionCapabilities';
/// Provides resolved [Elements] for well-known Angular2 symbols.
class Angular2Types {
static Map<Resolver, Angular2Types> _cache = {};
static final _annotationsLibAssetId =
new AssetId('angular2', 'lib/src/core/annotations/annotations.dart');
static final _applicationLibAssetId =
new AssetId('angular2', 'lib/src/core/application.dart');
static final _templateLibAssetId =
new AssetId('angular2', 'lib/src/core/annotations/template.dart');
static final _reflectionCapabilitiesLibAssetId = new AssetId(
'angular2', 'lib/src/reflection/reflection_capabilities.dart');
final Resolver _resolver;
FunctionElement _bootstrapMethod;
Angular2Types._internal(this._resolver);
factory Angular2Types(Resolver resolver) {
return _cache.putIfAbsent(
resolver, () => new Angular2Types._internal(resolver));
}
LibraryElement get annotationsLib =>
_resolver.getLibrary(_annotationsLibAssetId);
ClassElement get directiveAnnotation =>
_getTypeSafe(annotationsLib, 'Directive');
ClassElement get componentAnnotation =>
_getTypeSafe(annotationsLib, 'Component');
ClassElement get decoratorAnnotation =>
_getTypeSafe(annotationsLib, 'Decorator');
LibraryElement get templateLib => _resolver.getLibrary(_templateLibAssetId);
ClassElement get templateAnnotation => _getTypeSafe(templateLib, 'Template');
LibraryElement get reflectionCapabilitiesLib =>
_resolver.getLibrary(_reflectionCapabilitiesLibAssetId);
ClassElement get reflectionCapabilities =>
_getTypeSafe(reflectionCapabilitiesLib, reflectionCapabilitiesTypeName);
LibraryElement get applicationLib =>
_resolver.getLibrary(_applicationLibAssetId);
FunctionElement get bootstrapMethod {
if (_bootstrapMethod == null) {
_bootstrapMethod = applicationLib.definingCompilationUnit.functions
.firstWhere((FunctionElement el) => el.name == bootstrapMethodName,
orElse: () => null);
}
return _bootstrapMethod;
}
/// Gets the type named [name] in library [lib]. Returns `null` if [lib] is
/// `null` or [name] cannot be found in [lib].
ClassElement _getTypeSafe(LibraryElement lib, String name) {
if (lib == null) return null;
return lib.getType(name);
}
/// Whether [clazz] is annotated as a [Component].
bool isComponent(ClassElement clazz) =>
hasAnnotation(clazz, componentAnnotation);
}
/// Whether [type], its superclass, or one of its interfaces matches [target].
bool isAnnotationMatch(InterfaceType type, ClassElement target) {
if (type == null || type.element == null) return false;
if (type.element.type == target.type) return true;
if (isAnnotationMatch(type.superclass, target)) return true;
for (var interface in type.interfaces) {
if (isAnnotationMatch(interface, target)) return true;
}
return false;
}
/// Determines whether [clazz] has at least one annotation that `is` a
/// [metaClazz].
bool hasAnnotation(ClassElement clazz, ClassElement metaClazz) {
if (clazz == null || metaClazz == null) return false;
return clazz.metadata.firstWhere((ElementAnnotation meta) {
// TODO(kegluneq): Make this recognize non-ConstructorElement annotations.
return meta.element is ConstructorElement &&
isAnnotationMatch(meta.element.returnType, metaClazz);
}, orElse: () => null) != null;
}

View File

@ -0,0 +1,83 @@
library angular2.src.transform;
import 'package:analyzer/src/generated/ast.dart';
import 'package:analyzer/src/generated/element.dart';
import 'annotation_processor.dart';
import 'common/logging.dart';
import 'resolvers.dart';
/// Walks through an Angular2 application, finding all classes matching the
/// provided [annotationMatcher].
class AngularVisibleTraversal {
final Angular2Types _types;
final _ComponentParsingAstVisitor _visitor;
AngularVisibleTraversal(this._types, AnnotationMatcher annotationMatcher)
: _visitor = new _ComponentParsingAstVisitor(annotationMatcher);
/// Walks an Angular2 application, starting with the class represented by
/// [entryPoint], which must be annotated as an Angular2 [Component].
///
/// We recursively check the entryPoint's annotations and constructor
/// arguments for types which match the provided [annotationMatcher].
void traverse(ClassElement entryPoint) {
if (!_types.isComponent(entryPoint)) {
throw new ArgumentError.value(entryPoint, 'entryPoint',
'Provided entryPoint must be annotated as a Component');
}
entryPoint.node.accept(_visitor);
}
}
class _ComponentParsingAstVisitor extends Object
with RecursiveAstVisitor<Object> {
final Set<ClassElement> _seen = new Set();
final AnnotationMatcher _matcher;
_ComponentParsingAstVisitor(this._matcher);
@override
Object visitClassDeclaration(ClassDeclaration node) {
if (node.element != null) {
if (_seen.contains(node.element)) return null;
_seen.add(node.element);
}
// Process the class itself.
node.name.accept(this);
// Process metadata information, ignoring [FieldDeclaration]s and
// [MethodDeclaration]s (see below).
node.metadata.forEach((Annotation meta) => meta.accept(this));
// Process constructor parameters, fields & methods are ignored below.
node.members.forEach((m) => m.accept(this));
return null;
}
@override
Object visitFieldDeclaration(FieldDeclaration node) => null;
@override
Object visitMethodDeclaration(MethodDeclaration node) => null;
@override
Object visitAnnotation(Annotation node) {
// TODO(kegluneq): Visit only Angular2 annotations & subtypes.
return super.visitAnnotation(node);
}
@override
Object visitSimpleIdentifier(SimpleIdentifier node) {
if (node.bestElement != null) {
if (node.bestElement is ClassElement) {
var matches = _matcher.processAnnotations(node.bestElement);
// If any of these types are matches, recurse on them.
matches.forEach((match) => match.node.accept(this));
}
}
return super.visitSimpleIdentifier(node);
}
}