From c65fd31e86718d8ea56250ea1108254173470543 Mon Sep 17 00:00:00 2001 From: Jacob MacDonald Date: Thu, 19 Mar 2015 15:47:10 -0700 Subject: [PATCH] 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. --- modules/angular2/pubspec.yaml | 2 +- .../src/transform/common/classdef_parser.dart | 86 +++++++++++++++++++ .../directive_processor/rewriter.dart | 59 +++++++++---- .../directive_processor/transformer.dart | 6 +- .../directive_processor/all_tests.dart | 45 +++++++--- .../custom_metadata/chicken_soup.dart | 19 ++++ .../expected/chicken_soup.ng_deps.dart | 17 ++++ .../expected/split_pea_soup.ng_deps.dart | 17 ++++ .../expected/tortilla_soup.ng_deps.dart | 17 ++++ .../custom_metadata/split_pea_soup.dart | 19 ++++ .../custom_metadata/tortilla_soup.dart | 19 ++++ .../test/transform/integration/all_tests.dart | 9 +- 12 files changed, 286 insertions(+), 29 deletions(-) create mode 100644 modules/angular2/src/transform/common/classdef_parser.dart create mode 100644 modules/angular2/test/transform/directive_processor/custom_metadata/chicken_soup.dart create mode 100644 modules/angular2/test/transform/directive_processor/custom_metadata/expected/chicken_soup.ng_deps.dart create mode 100644 modules/angular2/test/transform/directive_processor/custom_metadata/expected/split_pea_soup.ng_deps.dart create mode 100644 modules/angular2/test/transform/directive_processor/custom_metadata/expected/tortilla_soup.ng_deps.dart create mode 100644 modules/angular2/test/transform/directive_processor/custom_metadata/split_pea_soup.dart create mode 100644 modules/angular2/test/transform/directive_processor/custom_metadata/tortilla_soup.dart diff --git a/modules/angular2/pubspec.yaml b/modules/angular2/pubspec.yaml index 135a6b5127..b20da15e1c 100644 --- a/modules/angular2/pubspec.yaml +++ b/modules/angular2/pubspec.yaml @@ -9,7 +9,7 @@ homepage: <%= packageJson.homepage %> environment: sdk: '>=1.9.0-dev.8.0' dependencies: - analyzer: '>=0.22.4 <0.25.0' + analyzer: '^0.24.4' barback: '^0.15.2+2' code_transformers: '^0.2.5' dart_style: '^0.1.3' diff --git a/modules/angular2/src/transform/common/classdef_parser.dart b/modules/angular2/src/transform/common/classdef_parser.dart new file mode 100644 index 0000000000..bf10c5bcbf --- /dev/null +++ b/modules/angular2/src/transform/common/classdef_parser.dart @@ -0,0 +1,86 @@ +library angular2.src.transform.common.classdef_parser; + +import 'dart:async'; + +import 'package:analyzer/analyzer.dart'; +import 'package:angular2/src/transform/common/asset_reader.dart'; +import 'package:angular2/src/transform/common/logging.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/assets.dart'; + +/// Creates a mapping of [AssetId]s to the [ClassDeclaration]s which they +/// define. +Future>> createTypeMap( + AssetReader reader, AssetId id) { + return _recurse(reader, id); +} + +Future>> _recurse( + AssetReader reader, AssetId id, + [_ClassDefVisitor visitor, Set seen]) async { + if (seen == null) seen = new Set(); + if (visitor == null) visitor = new _ClassDefVisitor(); + + if (seen.contains(id)) return visitor.result; + seen.add(id); + + var hasAsset = await reader.hasInput(id); + if (!hasAsset) return visitor.result; + + var code = await reader.readAsString(id); + visitor.current = id; + parseCompilationUnit(code, + name: id.path, + parseFunctionBodies: false, + suppressErrors: true).accept(visitor); + var toWait = []; + visitor.dependencies[id] + .map((node) => stringLiteralToString(node.uri)) + .where(_isNotDartImport) + .forEach((uri) { + var nodeId = uriToAssetId(id, uri, logger, null); + toWait.add(_recurse(reader, nodeId, visitor, seen)); + }); + + await Future.wait(toWait); + return visitor.result; +} + +bool _isNotDartImport(String uri) => !uri.startsWith('dart:'); + +class _ClassDefVisitor extends Object with RecursiveAstVisitor { + final Map> result = {}; + final Map> dependencies = {}; + List _currentClass; + List _currentDependencies; + + void set current(AssetId val) { + _currentDependencies = dependencies.putIfAbsent(val, () => []); + _currentClass = result.putIfAbsent(val, () => []); + } + + // TODO(kegluneq): Handle `part` directives. + @override + Object visitPartDirective(PartDirective node) => null; + + @override + Object visitImportDirective(ImportDirective node) { + _currentDependencies.add(node); + return null; + } + + @override + Object visitExportDirective(ExportDirective node) { + _currentDependencies.add(node); + return null; + } + + @override + Object visitFunctionDeclaration(FunctionDeclaration node) => null; + + @override + Object visitClassDeclaration(ClassDeclaration node) { + _currentClass.add(node); + return null; + } +} diff --git a/modules/angular2/src/transform/directive_processor/rewriter.dart b/modules/angular2/src/transform/directive_processor/rewriter.dart index 42fb181c3b..94d05574ba 100644 --- a/modules/angular2/src/transform/directive_processor/rewriter.dart +++ b/modules/angular2/src/transform/directive_processor/rewriter.dart @@ -4,6 +4,7 @@ import 'package:analyzer/analyzer.dart'; import 'package:analyzer/src/generated/java_core.dart'; import 'package:angular2/src/transform/common/logging.dart'; import 'package:angular2/src/transform/common/names.dart'; +import 'package:barback/barback.dart' show AssetId; import 'package:path/path.dart' as path; import 'visitors.dart'; @@ -15,11 +16,12 @@ import 'visitors.dart'; /// If no Angular 2 `Directive`s are found in [code], returns the empty /// string unless [forceGenerate] is true, in which case an empty ngDeps /// file is created. -String createNgDeps(String code, String path) { +String createNgDeps(String code, String path, + Map> assetClasses) { // TODO(kegluneq): Shortcut if we can determine that there are no // [Directive]s present, taking into account `export`s. var writer = new PrintStringWriter(); - var visitor = new CreateNgDepsVisitor(writer, path); + var visitor = new CreateNgDepsVisitor(writer, path, assetClasses); parseCompilationUnit(code, name: path).accept(visitor); return '$writer'; } @@ -28,7 +30,7 @@ String createNgDeps(String code, String path) { /// associated .ng_deps.dart file. class CreateNgDepsVisitor extends Object with SimpleAstVisitor { final PrintWriter writer; - final _Tester _tester = const _Tester(); + final _Tester _tester; bool _foundNgDirectives = false; bool _wroteImport = false; final ToSourceVisitor _copyVisitor; @@ -39,12 +41,14 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor { /// The path to the file which we are parsing. final String importPath; - CreateNgDepsVisitor(PrintWriter writer, this.importPath) + CreateNgDepsVisitor(PrintWriter writer, this.importPath, + Map> assetClasses) : writer = writer, _copyVisitor = new ToSourceVisitor(writer), _factoryVisitor = new FactoryTransformVisitor(writer), _paramsVisitor = new ParameterTransformVisitor(writer), - _metaVisitor = new AnnotationsTransformVisitor(writer); + _metaVisitor = new AnnotationsTransformVisitor(writer), + _tester = new _Tester(assetClasses); void _visitNodeListWithSeparator(NodeList list, String separator) { if (list == null) return; @@ -136,7 +140,7 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor { @override Object visitClassDeclaration(ClassDeclaration node) { - var shouldProcess = node.metadata.any(_tester._isDirective); + var shouldProcess = node.metadata.any(_tester._shouldKeepMeta); if (shouldProcess) { var ctor = _getCtor(node); @@ -199,15 +203,40 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor { Object visitSimpleIdentifier(SimpleIdentifier node) => _nodeToSource(node); } -class _Tester { - const _Tester(); +const annotationNamesToKeep = const ['Injectable', 'Template']; - bool _isDirective(Annotation meta) { - var metaName = meta.name.toString(); - return metaName == 'Component' || - metaName == 'Decorator' || - metaName == 'Injectable' || - metaName == 'View' || - metaName == 'Viewport'; +class _Tester { + final Map _classesByName; + + _Tester(Map> assetClasses) + : _classesByName = new Map.fromIterables(assetClasses.values + .expand((classes) => classes.map((c) => c.name.toString())), + assetClasses.values.expand((list) => list)); + + bool _shouldKeepMeta(Annotation meta) => + _shouldKeepClass(_classesByName[meta.name.name]); + + bool _shouldKeepClass(ClassDeclaration next) { + while (next != null) { + if (annotationNamesToKeep.contains(next.name.name)) return true; + + // Check classes that this class implements. + if (next.implementsClause != null) { + for (var interface in next.implementsClause.interfaces) { + if (_shouldKeepClass(_classesByName[interface.name.name])) { + return true; + } + } + } + + // Check the class that this class extends. + if (next.extendsClause != null && next.extendsClause.superclass != null) { + next = _classesByName[next.extendsClause.superclass.name.name]; + } else { + break; + } + } + + return false; } } diff --git a/modules/angular2/src/transform/directive_processor/transformer.dart b/modules/angular2/src/transform/directive_processor/transformer.dart index 9e20341b11..147d1e8dff 100644 --- a/modules/angular2/src/transform/directive_processor/transformer.dart +++ b/modules/angular2/src/transform/directive_processor/transformer.dart @@ -2,6 +2,8 @@ library angular2.transform.directive_processor.transformer; import 'dart:async'; +import 'package:angular2/src/transform/common/asset_reader.dart'; +import 'package:angular2/src/transform/common/classdef_parser.dart'; import 'package:angular2/src/transform/common/logging.dart' as log; import 'package:angular2/src/transform/common/names.dart'; import 'package:angular2/src/transform/common/options.dart'; @@ -32,8 +34,10 @@ class DirectiveProcessor extends Transformer { try { var asset = transform.primaryInput; + var reader = new AssetReader.fromTransform(transform); + var defMap = await createTypeMap(reader, asset.id); var assetCode = await asset.readAsString(); - var ngDepsSrc = createNgDeps(assetCode, asset.id.path); + var ngDepsSrc = createNgDeps(assetCode, asset.id.path, defMap); if (ngDepsSrc != null && ngDepsSrc.isNotEmpty) { var ngDepsAssetId = transform.primaryInput.id.changeExtension(DEPS_EXTENSION); diff --git a/modules/angular2/test/transform/directive_processor/all_tests.dart b/modules/angular2/test/transform/directive_processor/all_tests.dart index dee04671b4..d41dbadd08 100644 --- a/modules/angular2/test/transform/directive_processor/all_tests.dart +++ b/modules/angular2/test/transform/directive_processor/all_tests.dart @@ -1,24 +1,47 @@ library angular2.test.transform.directive_processor.all_tests; +import 'package:barback/barback.dart'; import 'package:angular2/src/transform/directive_processor/rewriter.dart'; +import '../common/read_file.dart'; +import 'package:angular2/src/transform/common/classdef_parser.dart'; import 'package:dart_style/dart_style.dart'; import 'package:guinness/guinness.dart'; - -import '../common/read_file.dart'; +import 'package:path/path.dart' as path; var formatter = new DartFormatter(); +main() { + allTests(); +} + void allTests() { - it('should preserve parameter annotations as const instances.', () { - var inputPath = 'parameter_metadata/soup.dart'; - var expected = _readFile('parameter_metadata/expected/soup.ng_deps.dart'); - var output = - formatter.format(createNgDeps(_readFile(inputPath), inputPath)); + _testNgDeps('should preserve parameter annotations as const instances.', + 'parameter_metadata/soup.dart'); + + _testNgDeps('should recognize annotations which extend Injectable.', + 'custom_metadata/tortilla_soup.dart'); + + _testNgDeps('should recognize annotations which implement Injectable.', + 'custom_metadata/chicken_soup.dart'); + + _testNgDeps( + 'should recognize annotations which implement a class that extends ' + 'Injectable.', 'custom_metadata/chicken_soup.dart'); +} + +void _testNgDeps(String name, String inputPath) { + it(name, () async { + var inputId = _assetIdForPath(inputPath); + var reader = new TestAssetReader(); + var defMap = await createTypeMap(reader, inputId); + var input = await reader.readAsString(inputId); + var output = formatter.format(createNgDeps(input, inputPath, defMap)); + var expectedPath = path.join(path.dirname(inputPath), 'expected', + path.basename(inputPath).replaceFirst('.dart', '.ng_deps.dart')); + var expected = await reader.readAsString(_assetIdForPath(expectedPath)); expect(output).toEqual(expected); }); } -var pathBase = 'directive_processor'; - -/// Smooths over differences in CWD between IDEs and running tests in Travis. -String _readFile(String path) => readFile('$pathBase/$path'); +AssetId _assetIdForPath(String path) => + new AssetId('angular2', 'test/transform/directive_processor/$path'); diff --git a/modules/angular2/test/transform/directive_processor/custom_metadata/chicken_soup.dart b/modules/angular2/test/transform/directive_processor/custom_metadata/chicken_soup.dart new file mode 100644 index 0000000000..16c05f64c0 --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/custom_metadata/chicken_soup.dart @@ -0,0 +1,19 @@ +library dinner.chicken_soup; + +import 'package:angular2/di.dart' show Injectable; +import 'package:angular2/src/facade/lang.dart' show CONST; + +class Food implements Injectable { + @CONST() + const Food() : super(); +} + +class Soup extends Food { + @CONST() + const Soup() : super(); +} + +@Soup() +class ChickenSoup { + ChickenSoup(); +} diff --git a/modules/angular2/test/transform/directive_processor/custom_metadata/expected/chicken_soup.ng_deps.dart b/modules/angular2/test/transform/directive_processor/custom_metadata/expected/chicken_soup.ng_deps.dart new file mode 100644 index 0000000000..4387fc6f71 --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/custom_metadata/expected/chicken_soup.ng_deps.dart @@ -0,0 +1,17 @@ +library dinner.chicken_soup.ng_deps.dart; + +import 'chicken_soup.dart'; +import 'package:angular2/di.dart' show Injectable; +import 'package:angular2/src/facade/lang.dart' show CONST; + +bool _visited = false; +void initReflector(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(ChickenSoup, { + 'factory': () => new ChickenSoup(), + 'parameters': const [], + 'annotations': const [const Soup()] + }); +} diff --git a/modules/angular2/test/transform/directive_processor/custom_metadata/expected/split_pea_soup.ng_deps.dart b/modules/angular2/test/transform/directive_processor/custom_metadata/expected/split_pea_soup.ng_deps.dart new file mode 100644 index 0000000000..dfcbc71f59 --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/custom_metadata/expected/split_pea_soup.ng_deps.dart @@ -0,0 +1,17 @@ +library dinner.split_pea_soup.ng_deps.dart; + +import 'split_pea_soup.dart'; +import 'package:angular2/di.dart' show Injectable; +import 'package:angular2/src/facade/lang.dart' show CONST; + +bool _visited = false; +void initReflector(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(SplitPea, { + 'factory': () => new SplitPea(), + 'parameters': const [], + 'annotations': const [const Soup()] + }); +} diff --git a/modules/angular2/test/transform/directive_processor/custom_metadata/expected/tortilla_soup.ng_deps.dart b/modules/angular2/test/transform/directive_processor/custom_metadata/expected/tortilla_soup.ng_deps.dart new file mode 100644 index 0000000000..074a68dae7 --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/custom_metadata/expected/tortilla_soup.ng_deps.dart @@ -0,0 +1,17 @@ +library dinner.tortilla_soup.ng_deps.dart; + +import 'tortilla_soup.dart'; +import 'package:angular2/di.dart' show Injectable; +import 'package:angular2/src/facade/lang.dart' show CONST; + +bool _visited = false; +void initReflector(reflector) { + if (_visited) return; + _visited = true; + reflector + ..registerType(TortillaSoup, { + 'factory': () => new TortillaSoup(), + 'parameters': const [], + 'annotations': const [const Soup()] + }); +} diff --git a/modules/angular2/test/transform/directive_processor/custom_metadata/split_pea_soup.dart b/modules/angular2/test/transform/directive_processor/custom_metadata/split_pea_soup.dart new file mode 100644 index 0000000000..6accf12e6e --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/custom_metadata/split_pea_soup.dart @@ -0,0 +1,19 @@ +library dinner.split_pea_soup; + +import 'package:angular2/di.dart' show Injectable; +import 'package:angular2/src/facade/lang.dart' show CONST; + +class Food extends Injectable { + @CONST() + const Food() : super(); +} + +class Soup implements Food { + @CONST() + const Soup() : super(); +} + +@Soup() +class SplitPeaSoup { + SplitPeaSoup(); +} diff --git a/modules/angular2/test/transform/directive_processor/custom_metadata/tortilla_soup.dart b/modules/angular2/test/transform/directive_processor/custom_metadata/tortilla_soup.dart new file mode 100644 index 0000000000..165c3596d4 --- /dev/null +++ b/modules/angular2/test/transform/directive_processor/custom_metadata/tortilla_soup.dart @@ -0,0 +1,19 @@ +library dinner.tortilla_soup; + +import 'package:angular2/di.dart' show Injectable; +import 'package:angular2/src/facade/lang.dart' show CONST; + +class Food extends Injectable { + @CONST() + const Food() : super(); +} + +class Soup extends Food { + @CONST() + const Soup() : super(); +} + +@Soup() +class TortillaSoup { + TortillaSoup(); +} diff --git a/modules/angular2/test/transform/integration/all_tests.dart b/modules/angular2/test/transform/integration/all_tests.dart index 79a3821777..9c92f10d6e 100644 --- a/modules/angular2/test/transform/integration/all_tests.dart +++ b/modules/angular2/test/transform/integration/all_tests.dart @@ -8,6 +8,10 @@ import 'package:dart_style/dart_style.dart'; import '../common/read_file.dart'; +main() { + allTests(); +} + var formatter = new DartFormatter(); var transform = new AngularTransformerGroup(new TransformerOptions( ['web/index.dart'], @@ -39,7 +43,10 @@ void allTests() { '../../../lib/src/core/annotations/annotations.dart', 'angular2|lib/src/core/application.dart': '../common/application.dart', 'angular2|lib/src/reflection/reflection_capabilities.dart': - '../common/reflection_capabilities.dart' + '../common/reflection_capabilities.dart', + 'angular2|lib/di.dart': '../../../lib/di.dart', + 'angular2|lib/src/di/annotations.dart': + '../../../lib/src/di/annotations.dart', }; var tests = [