Skip to content

Commit 64e8f93

Browse files
author
Tim Blasi
committed
feat(dart/transform): Record property metadata
Update the transformer to generate code registering annotations on class properties, getters, and setters. Closes angular#1800, angular#3267, angular#4003
1 parent cc1d758 commit 64e8f93

File tree

8 files changed

+257
-8
lines changed

8 files changed

+257
-8
lines changed

modules_dart/transform/lib/src/transform/common/code/reflection_info_code.dart

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:angular2/src/transform/common/annotation_matcher.dart';
55
import 'package:angular2/src/transform/common/logging.dart';
66
import 'package:angular2/src/transform/common/model/reflection_info_model.pb.dart';
77
import 'package:angular2/src/transform/common/names.dart';
8+
import 'package:angular2/src/transform/common/property_utils.dart';
89
import 'package:barback/barback.dart' show AssetId;
910

1011
import 'annotation_code.dart';
@@ -16,20 +17,26 @@ class ReflectionInfoVisitor extends RecursiveAstVisitor<ReflectionInfoModel> {
1617
/// The file we are processing.
1718
final AssetId assetId;
1819

20+
/// Responsible for testing whether [Annotation]s are those recognized by
21+
/// Angular 2, for example `@Component`.
22+
final AnnotationMatcher _annotationMatcher;
23+
1924
final AnnotationVisitor _annotationVisitor;
2025
final ParameterVisitor _parameterVisitor = new ParameterVisitor();
26+
final _PropertyMetadataVisitor _propMetadataVisitor;
2127

2228
/// Whether an Angular 2 `Reflection` has been found.
2329
bool _foundNgReflection = false;
2430

25-
/// Responsible for testing whether [Annotation]s are those recognized by
26-
/// Angular 2, for example `@Component`.
27-
final AnnotationMatcher _annotationMatcher;
31+
ReflectionInfoVisitor._(this.assetId, this._annotationMatcher,
32+
this._annotationVisitor, this._propMetadataVisitor);
2833

29-
ReflectionInfoVisitor(AssetId assetId, AnnotationMatcher annotationMatcher)
30-
: this.assetId = assetId,
31-
_annotationMatcher = annotationMatcher,
32-
_annotationVisitor = new AnnotationVisitor(assetId, annotationMatcher);
34+
factory ReflectionInfoVisitor(
35+
AssetId assetId, AnnotationMatcher annotationMatcher) {
36+
var annotationVisitor = new AnnotationVisitor(assetId, annotationMatcher);
37+
return new ReflectionInfoVisitor._(assetId, annotationMatcher,
38+
annotationVisitor, new _PropertyMetadataVisitor(annotationVisitor));
39+
}
3340

3441
bool get shouldCreateNgDeps => _foundNgReflection;
3542

@@ -92,9 +99,44 @@ class ReflectionInfoVisitor extends RecursiveAstVisitor<ReflectionInfoModel> {
9299
model.interfaces.addAll(node.implementsClause.interfaces
93100
.map((interface) => '${interface.name}'));
94101
}
102+
103+
// Record annotations attached to properties.
104+
for (var member in node.members) {
105+
var propMetaList = member.accept(_propMetadataVisitor);
106+
if (propMetaList != null) {
107+
model.propertyMetadata.addAll(propMetaList);
108+
}
109+
}
110+
_coalesce(model.propertyMetadata);
111+
95112
return model;
96113
}
97114

115+
// If a class has a getter & a setter with the same name and each has
116+
// individual metadata, collapse to a single entry.
117+
void _coalesce(List<PropertyMetadataModel> propertyMetadata) {
118+
if (propertyMetadata.isEmpty) return;
119+
120+
var firstSeenIdxMap = <String, int>{};
121+
firstSeenIdxMap[propertyMetadata[0].name] = 0;
122+
var i = 1;
123+
while (i < propertyMetadata.length) {
124+
var propName = propertyMetadata[i].name;
125+
if (firstSeenIdxMap.containsKey(propName)) {
126+
var propNameIdx = firstSeenIdxMap[propName];
127+
// We have seen this name before, combine the metadata lists.
128+
propertyMetadata[propNameIdx]
129+
.annotations
130+
.addAll(propertyMetadata[i].annotations);
131+
// Remove the higher index, okay since we directly check `length` above.
132+
propertyMetadata.removeAt(i);
133+
} else {
134+
firstSeenIdxMap[propName] = i;
135+
++i;
136+
}
137+
}
138+
}
139+
98140
@override
99141
ReflectionInfoModel visitFunctionDeclaration(FunctionDeclaration node) {
100142
if (!node.metadata
@@ -126,6 +168,53 @@ class ReflectionInfoVisitor extends RecursiveAstVisitor<ReflectionInfoModel> {
126168
}
127169
}
128170

171+
/// Visitor responsible for parsing [ClassMember]s into
172+
/// [PropertyMetadataModel]s.
173+
class _PropertyMetadataVisitor
174+
extends SimpleAstVisitor<List<PropertyMetadataModel>> {
175+
final AnnotationVisitor _annotationVisitor;
176+
177+
_PropertyMetadataVisitor(this._annotationVisitor);
178+
179+
@override
180+
List<PropertyMetadataModel> visitFieldDeclaration(FieldDeclaration node) {
181+
var retVal = null;
182+
for (var variable in node.fields.variables) {
183+
var propModel = new PropertyMetadataModel()..name = '${variable.name}';
184+
for (var meta in node.metadata) {
185+
var annotationModel = meta.accept(_annotationVisitor);
186+
if (annotationModel != null) {
187+
propModel.annotations.add(annotationModel);
188+
}
189+
}
190+
if (propModel.annotations.isNotEmpty) {
191+
if (retVal == null) {
192+
retVal = <PropertyMetadataModel>[];
193+
}
194+
retVal.add(propModel);
195+
}
196+
}
197+
return retVal;
198+
}
199+
200+
@override
201+
List<PropertyMetadataModel> visitMethodDeclaration(MethodDeclaration node) {
202+
if (node.isGetter || node.isSetter) {
203+
var propModel = new PropertyMetadataModel()..name = '${node.name}';
204+
for (var meta in node.metadata) {
205+
var annotationModel = meta.accept(_annotationVisitor);
206+
if (annotationModel != null) {
207+
propModel.annotations.add(annotationModel);
208+
}
209+
}
210+
if (propModel.annotations.isNotEmpty) {
211+
return <PropertyMetadataModel>[propModel];
212+
}
213+
}
214+
return null;
215+
}
216+
}
217+
129218
/// Defines the format in which an [ReflectionInfoModel] is expressed as Dart
130219
/// code in a `.ng_deps.dart` file.
131220
abstract class ReflectionWriterMixin
@@ -171,9 +260,25 @@ abstract class ReflectionWriterMixin
171260
_writeListWithSeparator(model.parameters, writeParameterModelForImpl,
172261
prefix: '(', suffix: ')');
173262
// Interfaces
263+
var hasPropertyMetadata =
264+
model.propertyMetadata != null && model.propertyMetadata.isNotEmpty;
174265
if (model.interfaces != null && model.interfaces.isNotEmpty) {
175266
_writeListWithSeparator(model.interfaces, buffer.write,
176267
prefix: ',\nconst [', suffix: ']');
268+
} else if (hasPropertyMetadata) {
269+
buffer.write(',\nconst []');
270+
}
271+
// Property Metadata
272+
if (hasPropertyMetadata) {
273+
buffer.write(',\nconst {');
274+
for (var propMeta in model.propertyMetadata) {
275+
if (propMeta != model.propertyMetadata.first) {
276+
buffer.write(', ');
277+
}
278+
_writeListWithSeparator(propMeta.annotations, writeAnnotationModel,
279+
prefix: "\n'${sanitize(propMeta.name)}': const [", suffix: ']');
280+
}
281+
buffer.write('}');
177282
}
178283
}
179284
buffer.writeln(')\n)');

modules_dart/transform/lib/src/transform/common/model/reflection_info_model.pb.dart

Lines changed: 45 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

modules_dart/transform/lib/src/transform/common/model/reflection_info_model.proto

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import "parameter_model.proto";
55

66
package angular2.src.transform.common.model.proto;
77

8+
message PropertyMetadataModel {
9+
// The name of the property with metadata attached.
10+
required string name = 1;
11+
12+
// The metadata attached to the property.
13+
repeated AnnotationModel annotations = 2;
14+
}
15+
816
message ReflectionInfoModel {
917
// The (potentially prefixed) name of this Injectable.
1018
// This can be a `Type` or a function name.
@@ -21,4 +29,7 @@ message ReflectionInfoModel {
2129
repeated ParameterModel parameters = 5;
2230

2331
repeated string interfaces = 6;
32+
33+
// Entries for all properties with associated metadata.
34+
repeated PropertyMetadataModel propertyMetadata = 7;
2435
}

modules_dart/transform/test/transform/directive_processor/all_tests.dart

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,61 @@ void allTests() {
292292
});
293293
});
294294

295+
describe('property metadata', () {
296+
it('should be recorded on fields', () async {
297+
var model = await _testCreateModel('prop_metadata_files/fields.dart');
298+
299+
expect(model.reflectables.first.propertyMetadata).toBeNotNull();
300+
expect(model.reflectables.first.propertyMetadata.isNotEmpty).toBeTrue();
301+
expect(model.reflectables.first.propertyMetadata.first.name)
302+
.toEqual('field');
303+
expect(model.reflectables.first.propertyMetadata.first.annotations
304+
.firstWhere((a) => a.name == 'FieldDecorator',
305+
orElse: () => null)).toBeNotNull();
306+
});
307+
308+
it('should be recorded on getters', () async {
309+
var model = await _testCreateModel('prop_metadata_files/getters.dart');
310+
311+
expect(model.reflectables.first.propertyMetadata).toBeNotNull();
312+
expect(model.reflectables.first.propertyMetadata.isNotEmpty).toBeTrue();
313+
expect(model.reflectables.first.propertyMetadata.first.name)
314+
.toEqual('getVal');
315+
expect(model.reflectables.first.propertyMetadata.first.annotations
316+
.firstWhere((a) => a.name == 'GetDecorator', orElse: () => null))
317+
.toBeNotNull();
318+
});
319+
320+
it('should be recorded on setters', () async {
321+
var model = await _testCreateModel('prop_metadata_files/setters.dart');
322+
323+
expect(model.reflectables.first.propertyMetadata).toBeNotNull();
324+
expect(model.reflectables.first.propertyMetadata.isNotEmpty).toBeTrue();
325+
expect(model.reflectables.first.propertyMetadata.first.name)
326+
.toEqual('setVal');
327+
expect(model.reflectables.first.propertyMetadata.first.annotations
328+
.firstWhere((a) => a.name == 'SetDecorator', orElse: () => null))
329+
.toBeNotNull();
330+
});
331+
332+
it('should be coalesced when getters and setters have the same name',
333+
() async {
334+
var model = await _testCreateModel(
335+
'prop_metadata_files/getters_and_setters.dart');
336+
337+
expect(model.reflectables.first.propertyMetadata).toBeNotNull();
338+
expect(model.reflectables.first.propertyMetadata.length).toBe(1);
339+
expect(model.reflectables.first.propertyMetadata.first.name)
340+
.toEqual('myVal');
341+
expect(model.reflectables.first.propertyMetadata.first.annotations
342+
.firstWhere((a) => a.name == 'GetDecorator', orElse: () => null))
343+
.toBeNotNull();
344+
expect(model.reflectables.first.propertyMetadata.first.annotations
345+
.firstWhere((a) => a.name == 'SetDecorator', orElse: () => null))
346+
.toBeNotNull();
347+
});
348+
});
349+
295350
it('should not throw/hang on invalid urls', () async {
296351
var logger = new RecordingLogger();
297352
var model =
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
library fields;
2+
3+
import 'package:angular2/src/core/metadata.dart';
4+
5+
@Component(selector: '[fields]')
6+
class FieldComponent {
7+
@FieldDecorator("field") String field;
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
library fields;
2+
3+
import 'package:angular2/src/core/metadata.dart';
4+
5+
@Component(selector: '[getters]')
6+
class FieldComponent {
7+
@GetDecorator("get") String get getVal => 'a';
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
library fields;
2+
3+
import 'package:angular2/src/core/metadata.dart';
4+
5+
@Component(selector: '[getters-and-setters]')
6+
class FieldComponent {
7+
String _val;
8+
@GetDecorator("get") String get myVal => _val;
9+
@SetDecorator("set") String set myVal(val) => _val = val;
10+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
library fields;
2+
3+
import 'package:angular2/src/core/metadata.dart';
4+
5+
@Component(selector: '[setters]')
6+
class FieldComponent {
7+
@SetDecorator("set") String set setVal(val) => null;
8+
}

0 commit comments

Comments
 (0)