feat(dart/transform): Reuse readDirectiveMetadata in plugin

Share code for parsing `DirectiveMetadata` values between the
transformer and the analyzer plugin.
This commit is contained in:
Tim Blasi 2015-05-08 14:02:47 -07:00
parent 75e9d3f634
commit abc8878547
6 changed files with 128 additions and 126 deletions

View File

@ -5,8 +5,7 @@ import 'types.dart';
import 'dart:mirrors'; import 'dart:mirrors';
class ReflectionCapabilities { class ReflectionCapabilities {
ReflectionCapabilities([metadataReader]) { ReflectionCapabilities([metadataReader]) {}
}
Function factory(Type type) { Function factory(Type type) {
ClassMirror classMirror = reflectType(type); ClassMirror classMirror = reflectType(type);

View File

@ -1,30 +1,47 @@
library angular2.transform.common.directive_metadata_reader; library angular2.transform.common.directive_metadata_reader;
import 'package:analyzer/analyzer.dart'; import 'package:analyzer/analyzer.dart';
import 'package:analyzer/src/generated/element.dart';
import 'package:angular2/src/render/api.dart'; import 'package:angular2/src/render/api.dart';
import 'logging.dart';
import 'parser.dart';
/// Reads [DirectiveMetadata] from the `attributes` of `t`. /// Reads [DirectiveMetadata] from the `node`. `node` is expected to be an
DirectiveMetadata readDirectiveMetadata(RegisteredType t) { /// instance of [Annotation], [NodeList<Annotation>], ListLiteral, or
/// [InstanceCreationExpression].
DirectiveMetadata readDirectiveMetadata(dynamic node) {
assert(node is Annotation ||
node is NodeList ||
node is InstanceCreationExpression ||
node is ListLiteral);
var visitor = new _DirectiveMetadataVisitor(); var visitor = new _DirectiveMetadataVisitor();
t.annotations.accept(visitor); node.accept(visitor);
if (visitor.meta != null) {
visitor.meta.id = '${t.typeName}';
}
return visitor.meta; return visitor.meta;
} }
num _getDirectiveType(String annotationName) { num _getDirectiveType(String annotationName, Element element) {
var byNameMatch = -1;
// TODO(kegluneq): Detect subtypes & implementations of `Directive`s. // TODO(kegluneq): Detect subtypes & implementations of `Directive`s.
switch (annotationName) { switch (annotationName) {
case 'Directive': case 'Directive':
return DirectiveMetadata.DIRECTIVE_TYPE; byNameMatch = DirectiveMetadata.DIRECTIVE_TYPE;
break;
case 'Component': case 'Component':
return DirectiveMetadata.COMPONENT_TYPE; byNameMatch = DirectiveMetadata.COMPONENT_TYPE;
break;
default: default:
return -1; return -1;
} }
if (element != null) {
var byResolvedAst = -1;
var libName = element.library.name;
// If we have resolved, ensure the library is correct.
if (libName == 'angular2.src.core.annotations.annotations' ||
libName == 'angular2.src.core.annotations_impl.annotations') {
byResolvedAst = byNameMatch;
}
// TODO(kegluneq): @keertip, can we expose this as a warning?
assert(byNameMatch == byResolvedAst);
}
return byNameMatch;
} }
/// Visitor responsible for processing the `annotations` property of a /// Visitor responsible for processing the `annotations` property of a
@ -33,22 +50,45 @@ class _DirectiveMetadataVisitor extends Object
with RecursiveAstVisitor<Object> { with RecursiveAstVisitor<Object> {
DirectiveMetadata meta; DirectiveMetadata meta;
@override void _createEmptyMetadata(num type) {
Object visitInstanceCreationExpression(InstanceCreationExpression node) { assert(type >= 0);
var directiveType = _getDirectiveType('${node.constructorName.type.name}');
if (directiveType >= 0) {
if (meta != null) {
logger.error('Only one Directive is allowed per class. '
'Found "$node" but already processed "$meta".');
}
meta = new DirectiveMetadata( meta = new DirectiveMetadata(
type: directiveType, type: type,
compileChildren: true, compileChildren: true,
properties: {}, properties: {},
hostListeners: {}, hostListeners: {},
hostProperties: {}, hostProperties: {},
hostAttributes: {}, hostAttributes: {},
readAttributes: []); readAttributes: []);
}
@override
Object visitAnnotation(Annotation node) {
var directiveType = _getDirectiveType('${node.name}', node.element);
if (directiveType >= 0) {
if (meta != null) {
throw new FormatException('Only one Directive is allowed per class. '
'Found "$node" but already processed "$meta".',
'$node' /* source */);
}
_createEmptyMetadata(directiveType);
super.visitAnnotation(node);
}
// Annotation we do not recognize - no need to visit.
return null;
}
@override
Object visitInstanceCreationExpression(InstanceCreationExpression node) {
var directiveType = _getDirectiveType(
'${node.constructorName.type.name}', node.staticElement);
if (directiveType >= 0) {
if (meta != null) {
throw new FormatException('Only one Directive is allowed per class. '
'Found "$node" but already processed "$meta".',
'$node' /* source */);
}
_createEmptyMetadata(directiveType);
super.visitInstanceCreationExpression(node); super.visitInstanceCreationExpression(node);
} }
// Annotation we do not recognize - no need to visit. // Annotation we do not recognize - no need to visit.
@ -59,10 +99,9 @@ class _DirectiveMetadataVisitor extends Object
Object visitNamedExpression(NamedExpression node) { Object visitNamedExpression(NamedExpression node) {
// TODO(kegluneq): Remove this limitation. // TODO(kegluneq): Remove this limitation.
if (node.name is! Label || node.name.label is! SimpleIdentifier) { if (node.name is! Label || node.name.label is! SimpleIdentifier) {
logger.error( throw new FormatException(
'Angular 2 currently only supports simple identifiers in directives.' 'Angular 2 currently only supports simple identifiers in directives.',
' Source: ${node}'); '$node' /* source */);
return null;
} }
var keyString = '${node.name.label}'; var keyString = '${node.name.label}';
// TODO(kegluneq): Populate the other values in [DirectiveMetadata] once // TODO(kegluneq): Populate the other values in [DirectiveMetadata] once
@ -93,9 +132,9 @@ class _DirectiveMetadataVisitor extends Object
String _expressionToString(Expression node, String nodeDescription) { String _expressionToString(Expression node, String nodeDescription) {
// TODO(kegluneq): Accept more options. // TODO(kegluneq): Accept more options.
if (node is! SimpleStringLiteral) { if (node is! SimpleStringLiteral) {
logger.error('Angular 2 currently only supports string literals ' throw new FormatException(
'in $nodeDescription. Source: ${node}'); 'Angular 2 currently only supports string literals '
return null; 'in $nodeDescription.', '$node' /* source */);
} }
return stringLiteralToString(node); return stringLiteralToString(node);
} }
@ -104,23 +143,30 @@ class _DirectiveMetadataVisitor extends Object
meta.selector = _expressionToString(selectorValue, 'Directive#selector'); meta.selector = _expressionToString(selectorValue, 'Directive#selector');
} }
void _checkMeta() {
if (meta == null) {
throw new ArgumentError(
'Incorrect value passed to readDirectiveMetadata. '
'Expected types are Annotation and InstanceCreationExpression');
}
}
void _populateCompileChildren(Expression compileChildrenValue) { void _populateCompileChildren(Expression compileChildrenValue) {
_checkMeta();
if (compileChildrenValue is! BooleanLiteral) { if (compileChildrenValue is! BooleanLiteral) {
logger.error( throw new FormatException(
'Angular 2 currently only supports boolean literal values for ' 'Angular 2 currently only supports boolean literal values for '
'Directive#compileChildren.' 'Directive#compileChildren.', '$compileChildrenValue' /* source */);
' Source: ${compileChildrenValue}');
return;
} }
meta.compileChildren = (compileChildrenValue as BooleanLiteral).value; meta.compileChildren = (compileChildrenValue as BooleanLiteral).value;
} }
void _populateProperties(Expression propertiesValue) { void _populateProperties(Expression propertiesValue) {
_checkMeta();
if (propertiesValue is! MapLiteral) { if (propertiesValue is! MapLiteral) {
logger.error('Angular 2 currently only supports map literal values for ' throw new FormatException(
'Directive#properties.' 'Angular 2 currently only supports map literal values for '
' Source: ${propertiesValue}'); 'Directive#properties.', '$propertiesValue' /* source */);
return;
} }
for (MapLiteralEntry entry in (propertiesValue as MapLiteral).entries) { for (MapLiteralEntry entry in (propertiesValue as MapLiteral).entries) {
var sKey = _expressionToString(entry.key, 'Directive#properties keys'); var sKey = _expressionToString(entry.key, 'Directive#properties keys');
@ -130,11 +176,11 @@ class _DirectiveMetadataVisitor extends Object
} }
void _populateHostListeners(Expression hostListenersValue) { void _populateHostListeners(Expression hostListenersValue) {
_checkMeta();
if (hostListenersValue is! MapLiteral) { if (hostListenersValue is! MapLiteral) {
logger.error('Angular 2 currently only supports map literal values for ' throw new FormatException(
'Directive#hostListeners.' 'Angular 2 currently only supports map literal values for '
' Source: ${hostListenersValue}'); 'Directive#hostListeners.', '$hostListenersValue' /* source */);
return;
} }
for (MapLiteralEntry entry in (hostListenersValue as MapLiteral).entries) { for (MapLiteralEntry entry in (hostListenersValue as MapLiteral).entries) {
var sKey = _expressionToString(entry.key, 'Directive#hostListeners keys'); var sKey = _expressionToString(entry.key, 'Directive#hostListeners keys');
@ -145,11 +191,11 @@ class _DirectiveMetadataVisitor extends Object
} }
void _populateHostProperties(Expression hostPropertyValue) { void _populateHostProperties(Expression hostPropertyValue) {
_checkMeta();
if (hostPropertyValue is! MapLiteral) { if (hostPropertyValue is! MapLiteral) {
logger.error('Angular 2 currently only supports map literal values for ' throw new FormatException(
'Directive#hostProperties.' 'Angular 2 currently only supports map literal values for '
' Source: ${hostPropertyValue}'); 'Directive#hostProperties.', '$hostPropertyValue' /* source */);
return;
} }
for (MapLiteralEntry entry in (hostPropertyValue as MapLiteral).entries) { for (MapLiteralEntry entry in (hostPropertyValue as MapLiteral).entries) {
var sKey = var sKey =
@ -161,11 +207,11 @@ class _DirectiveMetadataVisitor extends Object
} }
void _populateHostAttributes(Expression hostAttributeValue) { void _populateHostAttributes(Expression hostAttributeValue) {
_checkMeta();
if (hostAttributeValue is! MapLiteral) { if (hostAttributeValue is! MapLiteral) {
logger.error('Angular 2 currently only supports map literal values for ' throw new FormatException(
'Directive#hostAttributes.' 'Angular 2 currently only supports map literal values for '
' Source: ${hostAttributeValue}'); 'Directive#hostAttributes.', '$hostAttributeValue' /* source */);
return;
} }
for (MapLiteralEntry entry in (hostAttributeValue as MapLiteral).entries) { for (MapLiteralEntry entry in (hostAttributeValue as MapLiteral).entries) {
var sKey = var sKey =

View File

@ -1,6 +1,9 @@
library angular2.transform.common.registered_type; library angular2.transform.common.registered_type;
import 'package:analyzer/analyzer.dart'; import 'package:analyzer/analyzer.dart';
import 'package:angular2/src/render/api.dart';
import 'package:angular2/src/transform/common/directive_metadata_reader.dart';
import 'package:angular2/src/transform/common/logging.dart';
import 'package:angular2/src/transform/common/names.dart'; import 'package:angular2/src/transform/common/names.dart';
/// A call to `Reflector#registerType` generated by `DirectiveProcessor`. /// A call to `Reflector#registerType` generated by `DirectiveProcessor`.
@ -16,6 +19,8 @@ class RegisteredType {
/// The annotations registered. /// The annotations registered.
final Expression annotations; final Expression annotations;
DirectiveMetadata _directiveMetadata = null;
RegisteredType._(this.typeName, this.registerMethod, this.factoryFn, RegisteredType._(this.typeName, this.registerMethod, this.factoryFn,
this.parameters, this.annotations); this.parameters, this.annotations);
@ -27,6 +32,20 @@ class RegisteredType {
return new RegisteredType._(visitor.typeName, registerMethod, return new RegisteredType._(visitor.typeName, registerMethod,
visitor.factoryFn, visitor.parameters, visitor.annotations); visitor.factoryFn, visitor.parameters, visitor.annotations);
} }
DirectiveMetadata get directiveMetadata {
if (_directiveMetadata == null) {
try {
_directiveMetadata = readDirectiveMetadata(annotations);
if (_directiveMetadata != null) {
_directiveMetadata.id = '$typeName';
}
} on FormatException catch (ex) {
logger.error(ex.message);
}
}
return _directiveMetadata;
}
} }
class _ParseRegisterTypeVisitor extends Object class _ParseRegisterTypeVisitor extends Object

View File

@ -5,7 +5,6 @@ import 'dart:async';
import 'package:analyzer/analyzer.dart'; import 'package:analyzer/analyzer.dart';
import 'package:angular2/src/render/api.dart'; import 'package:angular2/src/render/api.dart';
import 'package:angular2/src/transform/common/asset_reader.dart'; import 'package:angular2/src/transform/common/asset_reader.dart';
import 'package:angular2/src/transform/common/directive_metadata_reader.dart';
import 'package:angular2/src/transform/common/logging.dart'; import 'package:angular2/src/transform/common/logging.dart';
import 'package:angular2/src/transform/common/names.dart'; import 'package:angular2/src/transform/common/names.dart';
import 'package:angular2/src/transform/common/parser.dart'; import 'package:angular2/src/transform/common/parser.dart';
@ -58,9 +57,8 @@ Map<String, DirectiveMetadata> _metadataMapFromNgDeps(NgDeps ngDeps) {
if (ngDeps == null || ngDeps.registeredTypes.isEmpty) return null; if (ngDeps == null || ngDeps.registeredTypes.isEmpty) return null;
var retVal = <String, DirectiveMetadata>{}; var retVal = <String, DirectiveMetadata>{};
ngDeps.registeredTypes.forEach((rType) { ngDeps.registeredTypes.forEach((rType) {
var meta = readDirectiveMetadata(rType); if (rType.directiveMetadata != null) {
if (meta != null) { retVal['${rType.typeName}'] = rType.directiveMetadata;
retVal['${rType.typeName}'] = meta;
} }
}); });
return retVal; return retVal;

View File

@ -24,7 +24,7 @@ void allTests() {
Future<DirectiveMetadata> readMetadata(inputPath) async { Future<DirectiveMetadata> readMetadata(inputPath) async {
var ngDeps = await parser.parse(new AssetId('a', inputPath)); var ngDeps = await parser.parse(new AssetId('a', inputPath));
return readDirectiveMetadata(ngDeps.registeredTypes.first); return ngDeps.registeredTypes.first.directiveMetadata;
} }
describe('readMetadata', () { describe('readMetadata', () {
@ -44,17 +44,17 @@ void allTests() {
// Unset value defaults to `true`. // Unset value defaults to `true`.
it.moveNext(); it.moveNext();
expect('${it.current.typeName}').toEqual('UnsetComp'); expect('${it.current.typeName}').toEqual('UnsetComp');
var unsetComp = readDirectiveMetadata(it.current); var unsetComp = it.current.directiveMetadata;
expect(unsetComp.compileChildren).toBeTrue(); expect(unsetComp.compileChildren).toBeTrue();
it.moveNext(); it.moveNext();
expect('${it.current.typeName}').toEqual('FalseComp'); expect('${it.current.typeName}').toEqual('FalseComp');
var falseComp = readDirectiveMetadata(it.current); var falseComp = it.current.directiveMetadata;
expect(falseComp.compileChildren).toBeFalse(); expect(falseComp.compileChildren).toBeFalse();
it.moveNext(); it.moveNext();
expect('${it.current.typeName}').toEqual('TrueComp'); expect('${it.current.typeName}').toEqual('TrueComp');
var trueComp = readDirectiveMetadata(it.current); var trueComp = it.current.directiveMetadata;
expect(trueComp.compileChildren).toBeTrue(); expect(trueComp.compileChildren).toBeTrue();
}); });
@ -85,8 +85,8 @@ void allTests() {
var ngDeps = await parser.parse(new AssetId('a', var ngDeps = await parser.parse(new AssetId('a',
'directive_metadata_extractor/' 'directive_metadata_extractor/'
'directive_metadata_files/too_many_directives.ng_deps.dart')); 'directive_metadata_files/too_many_directives.ng_deps.dart'));
expect(() => readDirectiveMetadata(ngDeps.registeredTypes.first)) expect(() => ngDeps.registeredTypes.first.directiveMetadata).toThrowWith(
.toThrowWith(anInstanceOf: PrintLoggerError); anInstanceOf: PrintLoggerError);
}); });
}); });

View File

@ -1,13 +1,12 @@
library angular2.src.analysis.analyzer_plugin.src.tasks; library angular2.src.analysis.analyzer_plugin.src.tasks;
import 'package:analyzer/src/generated/ast.dart' hide Directive; import 'package:analyzer/src/generated/ast.dart' hide Directive;
import 'package:analyzer/src/generated/element.dart';
import 'package:analyzer/src/generated/engine.dart'; import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/task/general.dart'; import 'package:analyzer/src/task/general.dart';
import 'package:analyzer/task/dart.dart'; import 'package:analyzer/task/dart.dart';
import 'package:analyzer/task/model.dart'; import 'package:analyzer/task/model.dart';
import 'package:angular2/src/core/annotations/annotations.dart';
import 'package:angular2/src/render/api.dart'; import 'package:angular2/src/render/api.dart';
import 'package:angular2/src/transform/common/directive_metadata_reader.dart';
/// The [DirectiveMetadata]s of a [LibrarySpecificUnit]. /// The [DirectiveMetadata]s of a [LibrarySpecificUnit].
final ListResultDescriptor<DirectiveMetadata> DIRECTIVES = final ListResultDescriptor<DirectiveMetadata> DIRECTIVES =
@ -33,74 +32,15 @@ class BuildUnitDirectivesTask extends SourceBasedAnalysisTask {
List<DirectiveMetadata> metaList = <DirectiveMetadata>[]; List<DirectiveMetadata> metaList = <DirectiveMetadata>[];
for (CompilationUnitMember unitMember in unit.declarations) { for (CompilationUnitMember unitMember in unit.declarations) {
if (unitMember is ClassDeclaration) { if (unitMember is ClassDeclaration) {
for (Annotation annotationNode in unitMember.metadata) { DirectiveMetadata meta = readDirectiveMetadata(unitMember.metadata);
Directive directive = _createDirective(annotationNode); if (meta != null) {
if (directive != null) {
DirectiveMetadata meta = new DirectiveMetadata(
type: _getDirectiveType(directive),
selector: directive.selector);
metaList.add(meta); metaList.add(meta);
} }
} }
} }
}
outputs[DIRECTIVES] = metaList; outputs[DIRECTIVES] = metaList;
} }
/// Returns an Angular [Directive] that corresponds to the given [node].
/// Returns `null` if not an Angular annotation.
Directive _createDirective(Annotation node) {
// TODO(scheglov) add support for all arguments
if (_isAngularAnnotation(node, 'Component')) {
String selector = _getNamedArgument(node, 'selector');
return new Component(selector: selector);
}
if (_isAngularAnnotation(node, 'Directive')) {
String selector = _getNamedArgument(node, 'selector');
return new Directive(selector: selector);
}
return null;
}
int _getDirectiveType(Directive directive) {
if (directive is Component) {
return DirectiveMetadata.COMPONENT_TYPE;
}
return DirectiveMetadata.DIRECTIVE_TYPE;
}
/// Returns the value of an argument with the given [name].
/// Returns `null` if not found or cannot be evaluated statically.
Object _getNamedArgument(Annotation node, String name) {
if (node.arguments != null) {
List<Expression> arguments = node.arguments.arguments;
for (Expression argument in arguments) {
if (argument is NamedExpression &&
argument.name != null &&
argument.name.label != null &&
argument.name.label.name == name) {
Expression expression = argument.expression;
if (expression is SimpleStringLiteral) {
return expression.value;
}
}
}
}
return null;
}
/// Returns `true` is the given [node] is resolved to a creation of an Angular
/// annotation class with the given [name].
bool _isAngularAnnotation(Annotation node, String name) {
if (node.element is ConstructorElement) {
ClassElement clazz = node.element.enclosingElement;
return clazz.library.name ==
'angular2.src.core.annotations.annotations' &&
clazz.name == name;
}
return null;
}
static Map<String, TaskInput> buildInputs(LibrarySpecificUnit target) { static Map<String, TaskInput> buildInputs(LibrarySpecificUnit target) {
return <String, TaskInput>{UNIT_INPUT: RESOLVED_UNIT.of(target)}; return <String, TaskInput>{UNIT_INPUT: RESOLVED_UNIT.of(target)};
} }