Skip to content

Commit c65fd31

Browse files
committed
feat(dart/transform): Detect annotations which extend Injectable or Template.
Create a method that recursively walks imports from an entry point and determines where classes are registered. Use this information to determine if a particular annotation implements or extends Injectable or Template.
1 parent 6600ac7 commit c65fd31

File tree

12 files changed

+286
-29
lines changed

12 files changed

+286
-29
lines changed

modules/angular2/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ homepage: <%= packageJson.homepage %>
99
environment:
1010
sdk: '>=1.9.0-dev.8.0'
1111
dependencies:
12-
analyzer: '>=0.22.4 <0.25.0'
12+
analyzer: '^0.24.4'
1313
barback: '^0.15.2+2'
1414
code_transformers: '^0.2.5'
1515
dart_style: '^0.1.3'
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
library angular2.src.transform.common.classdef_parser;
2+
3+
import 'dart:async';
4+
5+
import 'package:analyzer/analyzer.dart';
6+
import 'package:angular2/src/transform/common/asset_reader.dart';
7+
import 'package:angular2/src/transform/common/logging.dart';
8+
import 'package:barback/barback.dart';
9+
import 'package:code_transformers/assets.dart';
10+
11+
/// Creates a mapping of [AssetId]s to the [ClassDeclaration]s which they
12+
/// define.
13+
Future<Map<AssetId, List<ClassDeclaration>>> createTypeMap(
14+
AssetReader reader, AssetId id) {
15+
return _recurse(reader, id);
16+
}
17+
18+
Future<Map<AssetId, List<ClassDeclaration>>> _recurse(
19+
AssetReader reader, AssetId id,
20+
[_ClassDefVisitor visitor, Set<AssetId> seen]) async {
21+
if (seen == null) seen = new Set<AssetId>();
22+
if (visitor == null) visitor = new _ClassDefVisitor();
23+
24+
if (seen.contains(id)) return visitor.result;
25+
seen.add(id);
26+
27+
var hasAsset = await reader.hasInput(id);
28+
if (!hasAsset) return visitor.result;
29+
30+
var code = await reader.readAsString(id);
31+
visitor.current = id;
32+
parseCompilationUnit(code,
33+
name: id.path,
34+
parseFunctionBodies: false,
35+
suppressErrors: true).accept(visitor);
36+
var toWait = [];
37+
visitor.dependencies[id]
38+
.map((node) => stringLiteralToString(node.uri))
39+
.where(_isNotDartImport)
40+
.forEach((uri) {
41+
var nodeId = uriToAssetId(id, uri, logger, null);
42+
toWait.add(_recurse(reader, nodeId, visitor, seen));
43+
});
44+
45+
await Future.wait(toWait);
46+
return visitor.result;
47+
}
48+
49+
bool _isNotDartImport(String uri) => !uri.startsWith('dart:');
50+
51+
class _ClassDefVisitor extends Object with RecursiveAstVisitor<Object> {
52+
final Map<AssetId, List<ClassDeclaration>> result = {};
53+
final Map<AssetId, List<NamespaceDirective>> dependencies = {};
54+
List<ClassDeclaration> _currentClass;
55+
List<NamespaceDirective> _currentDependencies;
56+
57+
void set current(AssetId val) {
58+
_currentDependencies = dependencies.putIfAbsent(val, () => []);
59+
_currentClass = result.putIfAbsent(val, () => []);
60+
}
61+
62+
// TODO(kegluneq): Handle `part` directives.
63+
@override
64+
Object visitPartDirective(PartDirective node) => null;
65+
66+
@override
67+
Object visitImportDirective(ImportDirective node) {
68+
_currentDependencies.add(node);
69+
return null;
70+
}
71+
72+
@override
73+
Object visitExportDirective(ExportDirective node) {
74+
_currentDependencies.add(node);
75+
return null;
76+
}
77+
78+
@override
79+
Object visitFunctionDeclaration(FunctionDeclaration node) => null;
80+
81+
@override
82+
Object visitClassDeclaration(ClassDeclaration node) {
83+
_currentClass.add(node);
84+
return null;
85+
}
86+
}

modules/angular2/src/transform/directive_processor/rewriter.dart

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:analyzer/analyzer.dart';
44
import 'package:analyzer/src/generated/java_core.dart';
55
import 'package:angular2/src/transform/common/logging.dart';
66
import 'package:angular2/src/transform/common/names.dart';
7+
import 'package:barback/barback.dart' show AssetId;
78
import 'package:path/path.dart' as path;
89

910
import 'visitors.dart';
@@ -15,11 +16,12 @@ import 'visitors.dart';
1516
/// If no Angular 2 `Directive`s are found in [code], returns the empty
1617
/// string unless [forceGenerate] is true, in which case an empty ngDeps
1718
/// file is created.
18-
String createNgDeps(String code, String path) {
19+
String createNgDeps(String code, String path,
20+
Map<AssetId, List<ClassDeclaration>> assetClasses) {
1921
// TODO(kegluneq): Shortcut if we can determine that there are no
2022
// [Directive]s present, taking into account `export`s.
2123
var writer = new PrintStringWriter();
22-
var visitor = new CreateNgDepsVisitor(writer, path);
24+
var visitor = new CreateNgDepsVisitor(writer, path, assetClasses);
2325
parseCompilationUnit(code, name: path).accept(visitor);
2426
return '$writer';
2527
}
@@ -28,7 +30,7 @@ String createNgDeps(String code, String path) {
2830
/// associated .ng_deps.dart file.
2931
class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
3032
final PrintWriter writer;
31-
final _Tester _tester = const _Tester();
33+
final _Tester _tester;
3234
bool _foundNgDirectives = false;
3335
bool _wroteImport = false;
3436
final ToSourceVisitor _copyVisitor;
@@ -39,12 +41,14 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
3941
/// The path to the file which we are parsing.
4042
final String importPath;
4143

42-
CreateNgDepsVisitor(PrintWriter writer, this.importPath)
44+
CreateNgDepsVisitor(PrintWriter writer, this.importPath,
45+
Map<AssetId, List<ClassDeclaration>> assetClasses)
4346
: writer = writer,
4447
_copyVisitor = new ToSourceVisitor(writer),
4548
_factoryVisitor = new FactoryTransformVisitor(writer),
4649
_paramsVisitor = new ParameterTransformVisitor(writer),
47-
_metaVisitor = new AnnotationsTransformVisitor(writer);
50+
_metaVisitor = new AnnotationsTransformVisitor(writer),
51+
_tester = new _Tester(assetClasses);
4852

4953
void _visitNodeListWithSeparator(NodeList<AstNode> list, String separator) {
5054
if (list == null) return;
@@ -136,7 +140,7 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
136140

137141
@override
138142
Object visitClassDeclaration(ClassDeclaration node) {
139-
var shouldProcess = node.metadata.any(_tester._isDirective);
143+
var shouldProcess = node.metadata.any(_tester._shouldKeepMeta);
140144

141145
if (shouldProcess) {
142146
var ctor = _getCtor(node);
@@ -199,15 +203,40 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
199203
Object visitSimpleIdentifier(SimpleIdentifier node) => _nodeToSource(node);
200204
}
201205

206+
const annotationNamesToKeep = const ['Injectable', 'Template'];
207+
202208
class _Tester {
203-
const _Tester();
204-
205-
bool _isDirective(Annotation meta) {
206-
var metaName = meta.name.toString();
207-
return metaName == 'Component' ||
208-
metaName == 'Decorator' ||
209-
metaName == 'Injectable' ||
210-
metaName == 'View' ||
211-
metaName == 'Viewport';
209+
final Map<String, ClassDeclaration> _classesByName;
210+
211+
_Tester(Map<AssetId, List<ClassDeclaration>> assetClasses)
212+
: _classesByName = new Map.fromIterables(assetClasses.values
213+
.expand((classes) => classes.map((c) => c.name.toString())),
214+
assetClasses.values.expand((list) => list));
215+
216+
bool _shouldKeepMeta(Annotation meta) =>
217+
_shouldKeepClass(_classesByName[meta.name.name]);
218+
219+
bool _shouldKeepClass(ClassDeclaration next) {
220+
while (next != null) {
221+
if (annotationNamesToKeep.contains(next.name.name)) return true;
222+
223+
// Check classes that this class implements.
224+
if (next.implementsClause != null) {
225+
for (var interface in next.implementsClause.interfaces) {
226+
if (_shouldKeepClass(_classesByName[interface.name.name])) {
227+
return true;
228+
}
229+
}
230+
}
231+
232+
// Check the class that this class extends.
233+
if (next.extendsClause != null && next.extendsClause.superclass != null) {
234+
next = _classesByName[next.extendsClause.superclass.name.name];
235+
} else {
236+
break;
237+
}
238+
}
239+
240+
return false;
212241
}
213242
}

modules/angular2/src/transform/directive_processor/transformer.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ library angular2.transform.directive_processor.transformer;
22

33
import 'dart:async';
44

5+
import 'package:angular2/src/transform/common/asset_reader.dart';
6+
import 'package:angular2/src/transform/common/classdef_parser.dart';
57
import 'package:angular2/src/transform/common/logging.dart' as log;
68
import 'package:angular2/src/transform/common/names.dart';
79
import 'package:angular2/src/transform/common/options.dart';
@@ -32,8 +34,10 @@ class DirectiveProcessor extends Transformer {
3234

3335
try {
3436
var asset = transform.primaryInput;
37+
var reader = new AssetReader.fromTransform(transform);
38+
var defMap = await createTypeMap(reader, asset.id);
3539
var assetCode = await asset.readAsString();
36-
var ngDepsSrc = createNgDeps(assetCode, asset.id.path);
40+
var ngDepsSrc = createNgDeps(assetCode, asset.id.path, defMap);
3741
if (ngDepsSrc != null && ngDepsSrc.isNotEmpty) {
3842
var ngDepsAssetId =
3943
transform.primaryInput.id.changeExtension(DEPS_EXTENSION);
Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,47 @@
11
library angular2.test.transform.directive_processor.all_tests;
22

3+
import 'package:barback/barback.dart';
34
import 'package:angular2/src/transform/directive_processor/rewriter.dart';
5+
import '../common/read_file.dart';
6+
import 'package:angular2/src/transform/common/classdef_parser.dart';
47
import 'package:dart_style/dart_style.dart';
58
import 'package:guinness/guinness.dart';
6-
7-
import '../common/read_file.dart';
9+
import 'package:path/path.dart' as path;
810

911
var formatter = new DartFormatter();
1012

13+
main() {
14+
allTests();
15+
}
16+
1117
void allTests() {
12-
it('should preserve parameter annotations as const instances.', () {
13-
var inputPath = 'parameter_metadata/soup.dart';
14-
var expected = _readFile('parameter_metadata/expected/soup.ng_deps.dart');
15-
var output =
16-
formatter.format(createNgDeps(_readFile(inputPath), inputPath));
18+
_testNgDeps('should preserve parameter annotations as const instances.',
19+
'parameter_metadata/soup.dart');
20+
21+
_testNgDeps('should recognize annotations which extend Injectable.',
22+
'custom_metadata/tortilla_soup.dart');
23+
24+
_testNgDeps('should recognize annotations which implement Injectable.',
25+
'custom_metadata/chicken_soup.dart');
26+
27+
_testNgDeps(
28+
'should recognize annotations which implement a class that extends '
29+
'Injectable.', 'custom_metadata/chicken_soup.dart');
30+
}
31+
32+
void _testNgDeps(String name, String inputPath) {
33+
it(name, () async {
34+
var inputId = _assetIdForPath(inputPath);
35+
var reader = new TestAssetReader();
36+
var defMap = await createTypeMap(reader, inputId);
37+
var input = await reader.readAsString(inputId);
38+
var output = formatter.format(createNgDeps(input, inputPath, defMap));
39+
var expectedPath = path.join(path.dirname(inputPath), 'expected',
40+
path.basename(inputPath).replaceFirst('.dart', '.ng_deps.dart'));
41+
var expected = await reader.readAsString(_assetIdForPath(expectedPath));
1742
expect(output).toEqual(expected);
1843
});
1944
}
2045

21-
var pathBase = 'directive_processor';
22-
23-
/// Smooths over differences in CWD between IDEs and running tests in Travis.
24-
String _readFile(String path) => readFile('$pathBase/$path');
46+
AssetId _assetIdForPath(String path) =>
47+
new AssetId('angular2', 'test/transform/directive_processor/$path');
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
library dinner.chicken_soup;
2+
3+
import 'package:angular2/di.dart' show Injectable;
4+
import 'package:angular2/src/facade/lang.dart' show CONST;
5+
6+
class Food implements Injectable {
7+
@CONST()
8+
const Food() : super();
9+
}
10+
11+
class Soup extends Food {
12+
@CONST()
13+
const Soup() : super();
14+
}
15+
16+
@Soup()
17+
class ChickenSoup {
18+
ChickenSoup();
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
library dinner.chicken_soup.ng_deps.dart;
2+
3+
import 'chicken_soup.dart';
4+
import 'package:angular2/di.dart' show Injectable;
5+
import 'package:angular2/src/facade/lang.dart' show CONST;
6+
7+
bool _visited = false;
8+
void initReflector(reflector) {
9+
if (_visited) return;
10+
_visited = true;
11+
reflector
12+
..registerType(ChickenSoup, {
13+
'factory': () => new ChickenSoup(),
14+
'parameters': const [],
15+
'annotations': const [const Soup()]
16+
});
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
library dinner.split_pea_soup.ng_deps.dart;
2+
3+
import 'split_pea_soup.dart';
4+
import 'package:angular2/di.dart' show Injectable;
5+
import 'package:angular2/src/facade/lang.dart' show CONST;
6+
7+
bool _visited = false;
8+
void initReflector(reflector) {
9+
if (_visited) return;
10+
_visited = true;
11+
reflector
12+
..registerType(SplitPea, {
13+
'factory': () => new SplitPea(),
14+
'parameters': const [],
15+
'annotations': const [const Soup()]
16+
});
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
library dinner.tortilla_soup.ng_deps.dart;
2+
3+
import 'tortilla_soup.dart';
4+
import 'package:angular2/di.dart' show Injectable;
5+
import 'package:angular2/src/facade/lang.dart' show CONST;
6+
7+
bool _visited = false;
8+
void initReflector(reflector) {
9+
if (_visited) return;
10+
_visited = true;
11+
reflector
12+
..registerType(TortillaSoup, {
13+
'factory': () => new TortillaSoup(),
14+
'parameters': const [],
15+
'annotations': const [const Soup()]
16+
});
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
library dinner.split_pea_soup;
2+
3+
import 'package:angular2/di.dart' show Injectable;
4+
import 'package:angular2/src/facade/lang.dart' show CONST;
5+
6+
class Food extends Injectable {
7+
@CONST()
8+
const Food() : super();
9+
}
10+
11+
class Soup implements Food {
12+
@CONST()
13+
const Soup() : super();
14+
}
15+
16+
@Soup()
17+
class SplitPeaSoup {
18+
SplitPeaSoup();
19+
}

0 commit comments

Comments
 (0)