feat(dart/transform): Parse `directives` dependencies from the Dart ast

Previously, we parsed dependencies out of a the stringified value of
`directives`, which is brittle and error-prone.

Move this parsing into `DirectiveProcessor` where we have the full Dart
ast to help.
This commit is contained in:
Tim Blasi 2015-10-15 14:21:20 -07:00
parent ca5e31bc77
commit 26044026c9
9 changed files with 221 additions and 135 deletions

View File

@ -83,6 +83,10 @@ class ReflectionInfoVisitor extends RecursiveAstVisitor<ReflectionInfoModel> {
if (node.metadata != null) {
node.metadata.forEach((node) {
final extractedDirectives = _extractDirectives(node);
if (extractedDirectives != null) {
model.directives.addAll(extractedDirectives);
}
model.annotations.add(_annotationVisitor.visitAnnotation(node));
});
}
@ -137,6 +141,36 @@ class ReflectionInfoVisitor extends RecursiveAstVisitor<ReflectionInfoModel> {
}
}
Iterable<PrefixedDirective> _extractDirectives(Annotation node) {
var shouldProcess = _annotationMatcher.isComponent(node, assetId);
shouldProcess = shouldProcess || _annotationMatcher.isView(node, assetId);
if (node.arguments == null && node.arguments.arguments == null) return null;
final directivesNode = node.arguments.arguments.firstWhere((arg) {
return arg is NamedExpression && '${arg.name.label}' == 'directives';
}, orElse: () => null);
if (directivesNode == null) return null;
if (directivesNode.expression is! ListLiteral) {
logger.warning('Angular 2 expects a list literal for `directives` '
'but found a ${directivesNode.expression.runtimeType}');
return null;
}
final directives = <PrefixedDirective>[];
for (var dep in (directivesNode.expression as ListLiteral).elements) {
if (dep is PrefixedIdentifier) {
directives.add(new PrefixedDirective()
..prefix = '${dep.prefix}'
..name = '${dep.identifier}');
} else if (dep is Identifier) {
directives.add(new PrefixedDirective()..name = '${dep}');
} else {
logger.warning('Found unexpected value $dep in `directives`.');
}
}
return directives;
}
@override
ReflectionInfoModel visitFunctionDeclaration(FunctionDeclaration node) {
if (!node.metadata

View File

@ -52,6 +52,55 @@ class PropertyMetadataModel extends GeneratedMessage {
class _ReadonlyPropertyMetadataModel extends PropertyMetadataModel
with ReadonlyMessageMixin {}
class PrefixedDirective extends GeneratedMessage {
static final BuilderInfo _i = new BuilderInfo('PrefixedDirective')
..a(1, 'prefix', PbFieldType.OS)
..a(2, 'name', PbFieldType.OS)
..hasRequiredFields = false;
PrefixedDirective() : super();
PrefixedDirective.fromBuffer(List<int> i,
[ExtensionRegistry r = ExtensionRegistry.EMPTY])
: super.fromBuffer(i, r);
PrefixedDirective.fromJson(String i,
[ExtensionRegistry r = ExtensionRegistry.EMPTY])
: super.fromJson(i, r);
PrefixedDirective clone() => new PrefixedDirective()..mergeFromMessage(this);
BuilderInfo get info_ => _i;
static PrefixedDirective create() => new PrefixedDirective();
static PbList<PrefixedDirective> createRepeated() =>
new PbList<PrefixedDirective>();
static PrefixedDirective getDefault() {
if (_defaultInstance == null) _defaultInstance =
new _ReadonlyPrefixedDirective();
return _defaultInstance;
}
static PrefixedDirective _defaultInstance;
static void $checkItem(PrefixedDirective v) {
if (v is! PrefixedDirective) checkItemFailed(v, 'PrefixedDirective');
}
String get prefix => $_get(0, 1, '');
void set prefix(String v) {
$_setString(0, 1, v);
}
bool hasPrefix() => $_has(0, 1);
void clearPrefix() => clearField(1);
String get name => $_get(1, 2, '');
void set name(String v) {
$_setString(1, 2, v);
}
bool hasName() => $_has(1, 2);
void clearName() => clearField(2);
}
class _ReadonlyPrefixedDirective extends PrefixedDirective
with ReadonlyMessageMixin {}
class ReflectionInfoModel extends GeneratedMessage {
static final BuilderInfo _i = new BuilderInfo('ReflectionInfoModel')
..a(1, 'name', PbFieldType.QS)
@ -63,7 +112,9 @@ class ReflectionInfoModel extends GeneratedMessage {
ParameterModel.create)
..p(6, 'interfaces', PbFieldType.PS)
..pp(7, 'propertyMetadata', PbFieldType.PM,
PropertyMetadataModel.$checkItem, PropertyMetadataModel.create);
PropertyMetadataModel.$checkItem, PropertyMetadataModel.create)
..pp(8, 'directives', PbFieldType.PM, PrefixedDirective.$checkItem,
PrefixedDirective.create);
ReflectionInfoModel() : super();
ReflectionInfoModel.fromBuffer(List<int> i,
@ -120,6 +171,8 @@ class ReflectionInfoModel extends GeneratedMessage {
List<String> get interfaces => $_get(5, 6, null);
List<PropertyMetadataModel> get propertyMetadata => $_get(6, 7, null);
List<PrefixedDirective> get directives => $_get(7, 8, null);
}
class _ReadonlyReflectionInfoModel extends ReflectionInfoModel
@ -139,6 +192,14 @@ const PropertyMetadataModel$json = const {
],
};
const PrefixedDirective$json = const {
'1': 'PrefixedDirective',
'2': const [
const {'1': 'prefix', '3': 1, '4': 1, '5': 9},
const {'1': 'name', '3': 2, '4': 1, '5': 9},
],
};
const ReflectionInfoModel$json = const {
'1': 'ReflectionInfoModel',
'2': const [
@ -167,12 +228,19 @@ const ReflectionInfoModel$json = const {
'5': 11,
'6': '.angular2.src.transform.common.model.proto.PropertyMetadataModel'
},
const {
'1': 'directives',
'3': 8,
'4': 3,
'5': 11,
'6': '.angular2.src.transform.common.model.proto.PrefixedDirective'
},
],
};
/**
* Generated with:
* reflection_info_model.proto (71d723738054f1276f792a2672a956ef9be94a4c)
* reflection_info_model.proto (e81bf93b6872b2bd5fabc6625be2560bacc3d186)
* libprotoc 2.6.1
* dart-protoc-plugin (af5fc2bf1de367a434c3b1847ab260510878ffc0)
*/

View File

@ -13,6 +13,15 @@ message PropertyMetadataModel {
repeated AnnotationModel annotations = 2;
}
message PrefixedDirective {
// The prefix used to reference this Directive, if any.
optional string prefix = 1;
// The name of the Directive or directive alias.
// See https://goo.gl/d8XPt0 for info on directive aliases.
optional string name = 2;
}
message ReflectionInfoModel {
// The (potentially prefixed) name of this Injectable.
// This can be a `Type` or a function name.
@ -32,4 +41,8 @@ message ReflectionInfoModel {
// Entries for all properties with associated metadata.
repeated PropertyMetadataModel propertyMetadata = 7;
// Directive dependencies parsed from the @View or @Component `directives`
// parameter.
repeated PrefixedDirective directives = 8;
}

View File

@ -38,58 +38,6 @@ class CompileDataResults {
this.ngMeta, this.viewDefinitions, this.directiveMetadatas);
}
class _PrefixedDirectiveName {
final String prefix;
final String directiveName;
_PrefixedDirectiveName(String prefix, this.directiveName)
: this.prefix = prefix == null ? '' : prefix;
@override
String toString() {
if (prefix.isEmpty) {
return directiveName;
} else {
return '${prefix}.${directiveName}';
}
}
}
String _directivesValue(ReflectionInfoModel model, bool test(AnnotationModel)) {
final viewAnnotation = model.annotations.firstWhere(test, orElse: () => null);
if (viewAnnotation == null) return null;
final directivesParam = viewAnnotation.namedParameters
.firstWhere((p) => p.name == 'directives', orElse: () => null);
return directivesParam != null ? directivesParam.value : null;
}
// TODO(kegluneq): Parse this value when creating [NgDepsModel]?
/// Find the `directives` parameter on the @View annotation and make sure that
/// all necessary [CompileDirectiveMetadata] objects are available.
Iterable<_PrefixedDirectiveName> _getDirectiveDeps(ReflectionInfoModel model) {
var directives = _directivesValue(model, (m) => m.isView);
if (directives == null) {
directives = _directivesValue(model, (m) => m.isComponent);
}
if (directives == null) return const [];
directives = directives.trim();
for (var toTrim in ['const [', '[']) {
if (directives.startsWith(toTrim) && directives.endsWith(']')) {
directives =
directives.substring(toTrim.length, directives.length - 1).trim();
}
}
if (directives.length == 0) return const [];
return directives.split(',').map((p) {
var parts = p.trim().split('.');
if (parts.length == 1) {
return new _PrefixedDirectiveName(null, parts[0]);
} else {
return new _PrefixedDirectiveName(parts[0], parts[1]);
}
});
}
/// Creates [ViewDefinition] objects for all `View` `Directive`s defined in
/// `entryPoint`.
class _CompileDataCreator {
@ -127,7 +75,7 @@ class _CompileDataCreator {
if (compileDirectiveMetadata.template != null) {
final compileDatum = new NormalizedComponentWithViewDirectives(
compileDirectiveMetadata, <CompileDirectiveMetadata>[]);
for (var dep in _getDirectiveDeps(reflectable)) {
for (var dep in reflectable.directives) {
if (!ngMetaMap.containsKey(dep.prefix)) {
logger.warning(
'Missing prefix "${dep.prefix}" '
@ -137,11 +85,10 @@ class _CompileDataCreator {
}
final depNgMeta = ngMetaMap[dep.prefix];
if (depNgMeta.types.containsKey(dep.directiveName)) {
compileDatum.directives.add(depNgMeta.types[dep.directiveName]);
} else if (depNgMeta.aliases.containsKey(dep.directiveName)) {
compileDatum.directives
.addAll(depNgMeta.flatten(dep.directiveName));
if (depNgMeta.types.containsKey(dep.name)) {
compileDatum.directives.add(depNgMeta.types[dep.name]);
} else if (depNgMeta.aliases.containsKey(dep.name)) {
compileDatum.directives.addAll(depNgMeta.flatten(dep.name));
} else {
logger.warning('Could not find Directive entry for $dep. '
'Please be aware that Dart transformers have limited support for '

View File

@ -10,6 +10,7 @@ import 'package:angular2/src/transform/common/annotation_matcher.dart';
import 'package:angular2/src/transform/common/code/ng_deps_code.dart';
import 'package:angular2/src/transform/common/asset_reader.dart';
import 'package:angular2/src/transform/common/logging.dart' as log;
import 'package:angular2/src/transform/common/model/ng_deps_model.pb.dart';
import 'package:angular2/src/transform/common/model/reflection_info_model.pb.dart';
import 'package:angular2/src/transform/common/ng_meta.dart';
import 'package:barback/barback.dart';
@ -441,6 +442,58 @@ void allTests() {
.toContain('[soup]');
});
});
describe('directives', () {
final reflectableNamed = (NgDepsModel model, String name) {
return model.reflectables
.firstWhere((r) => r.name == name, orElse: () => null);
};
it('should populate `directives` from @View value specified second.',
() async {
var model =
(await _testCreateModel('directives_files/components.dart')).ngDeps;
final componentFirst = reflectableNamed(model, 'ComponentFirst');
expect(componentFirst).toBeNotNull();
expect(componentFirst.directives).toBeNotNull();
expect(componentFirst.directives.length).toEqual(2);
expect(componentFirst.directives.first)
.toEqual(new PrefixedDirective()..name = 'Dep');
expect(componentFirst.directives[1]).toEqual(new PrefixedDirective()
..name = 'Dep'
..prefix = 'dep2');
});
it('should populate `directives` from @View value specified first.',
() async {
var model =
(await _testCreateModel('directives_files/components.dart')).ngDeps;
final viewFirst = reflectableNamed(model, 'ViewFirst');
expect(viewFirst).toBeNotNull();
expect(viewFirst.directives).toBeNotNull();
expect(viewFirst.directives.length).toEqual(2);
expect(viewFirst.directives.first).toEqual(new PrefixedDirective()
..name = 'Dep'
..prefix = 'dep2');
expect(viewFirst.directives[1])
.toEqual(new PrefixedDirective()..name = 'Dep');
});
it('should populate `directives` from @Component value with no @View.',
() async {
var model =
(await _testCreateModel('directives_files/components.dart')).ngDeps;
final componentOnly = reflectableNamed(model, 'ComponentOnly');
expect(componentOnly).toBeNotNull();
expect(componentOnly.directives).toBeNotNull();
expect(componentOnly.directives.length).toEqual(2);
expect(componentOnly.directives.first)
.toEqual(new PrefixedDirective()..name = 'Dep');
expect(componentOnly.directives[1]).toEqual(new PrefixedDirective()
..name = 'Dep'
..prefix = 'dep2');
});
});
}
Future<NgMeta> _testCreateModel(String inputPath,

View File

@ -0,0 +1,20 @@
library angular2.test.transform.directive_processor.directive_files.components;
import 'package:angular2/angular2.dart'
show Component, Directive, View, NgElement;
import 'dep1.dart';
import 'dep2.dart' as dep2;
@Component(selector: 'component-first')
@View(template: '<dep1></dep1><dep2></dep2>', directives: [Dep, dep2.Dep])
class ComponentFirst {}
@View(template: '<dep1></dep1><dep2></dep2>', directives: [dep2.Dep, Dep])
@Component(selector: 'view-first')
class ViewFirst {}
@Component(
selector: 'component-only',
template: '<dep1></dep1><dep2></dep2>',
directives: [Dep, dep2.Dep])
class ComponentOnly {}

View File

@ -0,0 +1,8 @@
library angular2.test.transform.directive_processor.directive_files.dep1;
import 'package:angular2/angular2.dart'
show Component, Directive, View, NgElement;
@Component(selector: 'dep1')
@View(template: 'Dep1')
class Dep {}

View File

@ -0,0 +1,8 @@
library angular2.test.transform.directive_processor.directive_files.dep2;
import 'package:angular2/angular2.dart'
show Component, Directive, View, NgElement;
@Component(selector: 'dep2')
@View(template: 'Dep2')
class Dep {}

View File

@ -76,8 +76,7 @@ void allTests() {
Future<String> process(AssetId assetId) {
logger = new RecordingLogger();
return log.setZoned(logger,
() => processTemplates(reader, assetId));
return log.setZoned(logger, () => processTemplates(reader, assetId));
}
// TODO(tbosch): This is just a temporary test that makes sure that the dart
@ -88,10 +87,9 @@ void allTests() {
final viewAnnotation = new AnnotationModel()
..name = 'View'
..isView = true;
viewAnnotation.namedParameters.add(new NamedParameter()
..name = 'directives'
..value = 'const [NgFor]');
fooNgMeta.ngDeps.reflectables.first.annotations.add(viewAnnotation);
fooNgMeta.ngDeps.reflectables.first.directives
.add(new PrefixedDirective()..name = 'NgFor');
fooNgMeta.ngDeps.imports.add(
new ImportModel()..uri = 'package:angular2/src/directives/ng_for.dart');
@ -155,6 +153,8 @@ void allTests() {
..name = 'directives'
..value = 'const [${barComponentMeta.type.name}]');
fooNgMeta.ngDeps.reflectables.first.annotations.add(viewAnnotation);
fooNgMeta.ngDeps.reflectables.first.directives
.add(new PrefixedDirective()..name = barComponentMeta.type.name);
fooNgMeta.ngDeps.imports.add(new ImportModel()..uri = 'bar.dart');
barComponentMeta.template =
new CompileTemplateMetadata(template: 'BarTemplate');
@ -176,69 +176,6 @@ void allTests() {
..toContain("import 'bar.template.dart'");
});
it('should handle `directives` regardless of annotation ordering', () async {
fooComponentMeta.template =
new CompileTemplateMetadata(template: '<${barComponentMeta.selector}>');
final viewAnnotation = new AnnotationModel()
..name = 'View'
..isView = true;
final directivesParameter = new NamedParameter()
..name = 'directives'
..value = 'const [${barComponentMeta.type.name}]';
viewAnnotation.namedParameters.add(directivesParameter);
final componentAnnotation = new AnnotationModel()
..name = 'Component'
..isComponent = true;
fooNgMeta.ngDeps.reflectables.first.annotations
.addAll([viewAnnotation, componentAnnotation]);
fooNgMeta.ngDeps.imports.add(new ImportModel()..uri = 'bar.dart');
barComponentMeta.template =
new CompileTemplateMetadata(template: 'BarTemplate');
updateReader();
final viewFirstOutputs = await process(fooAssetId);
fooNgMeta.ngDeps.reflectables.first.annotations.clear();
fooNgMeta.ngDeps.reflectables.first.annotations
.addAll([componentAnnotation, viewAnnotation]);
updateReader();
final componentFirstOutputs = await process(fooAssetId);
expect(viewFirstOutputs.templatesCode).toEqual(componentFirstOutputs.templatesCode);
});
it('should handle `directives` on @Component or @View', () async {
fooComponentMeta.template =
new CompileTemplateMetadata(template: '<${barComponentMeta.selector}>');
final viewAnnotation = new AnnotationModel()
..name = 'View'
..isView = true;
final directivesParameter = new NamedParameter()
..name = 'directives'
..value = 'const [${barComponentMeta.type.name}]';
viewAnnotation.namedParameters.add(directivesParameter);
final componentAnnotation = new AnnotationModel()
..name = 'Component'
..isComponent = true;
fooNgMeta.ngDeps.reflectables.first.annotations
.addAll([viewAnnotation, componentAnnotation]);
fooNgMeta.ngDeps.imports.add(new ImportModel()..uri = 'bar.dart');
barComponentMeta.template =
new CompileTemplateMetadata(template: 'BarTemplate');
updateReader();
final onViewOutputs = await process(fooAssetId);
viewAnnotation.namedParameters.clear();
componentAnnotation.namedParameters.add(directivesParameter);
updateReader();
final onComponentOutputs = await process(fooAssetId);
expect(onComponentOutputs.templatesCode).toEqual(onViewOutputs.templatesCode);
});
it('should parse `View` directives with a single prefixed dependency.',
() async {
fooComponentMeta.template =
@ -246,10 +183,10 @@ void allTests() {
final componentAnnotation = new AnnotationModel()
..name = 'View'
..isView = true;
componentAnnotation.namedParameters.add(new NamedParameter()
..name = 'directives'
..value = 'const [prefix.${barComponentMeta.type.name}]');
fooNgMeta.ngDeps.reflectables.first.annotations.add(componentAnnotation);
fooNgMeta.ngDeps.reflectables.first.directives.add(new PrefixedDirective()
..name = barComponentMeta.type.name
..prefix = 'prefix');
fooNgMeta.ngDeps.imports.add(new ImportModel()
..uri = 'bar.dart'
..prefix = 'prefix');
@ -279,11 +216,9 @@ void allTests() {
final componentAnnotation = new AnnotationModel()
..name = 'View'
..isView = true;
final directivesParam = new NamedParameter()
..name = 'directives'
..value = 'const [directiveAlias]';
componentAnnotation.namedParameters.add(directivesParam);
fooNgMeta.ngDeps.reflectables.first.annotations.add(componentAnnotation);
fooNgMeta.ngDeps.reflectables.first.directives
.add(new PrefixedDirective()..name = 'directiveAlias');
fooNgMeta.ngDeps.imports.add(new ImportModel()..uri = 'bar.dart');
fooNgMeta.aliases['directiveAlias'] = [barComponentMeta.type.name];