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.
This commit is contained in:
Jacob MacDonald 2015-03-19 15:47:10 -07:00
parent 6600ac7031
commit c65fd31e86
12 changed files with 286 additions and 29 deletions

View File

@ -9,7 +9,7 @@ homepage: <%= packageJson.homepage %>
environment: environment:
sdk: '>=1.9.0-dev.8.0' sdk: '>=1.9.0-dev.8.0'
dependencies: dependencies:
analyzer: '>=0.22.4 <0.25.0' analyzer: '^0.24.4'
barback: '^0.15.2+2' barback: '^0.15.2+2'
code_transformers: '^0.2.5' code_transformers: '^0.2.5'
dart_style: '^0.1.3' dart_style: '^0.1.3'

View File

@ -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<Map<AssetId, List<ClassDeclaration>>> createTypeMap(
AssetReader reader, AssetId id) {
return _recurse(reader, id);
}
Future<Map<AssetId, List<ClassDeclaration>>> _recurse(
AssetReader reader, AssetId id,
[_ClassDefVisitor visitor, Set<AssetId> seen]) async {
if (seen == null) seen = new Set<AssetId>();
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<Object> {
final Map<AssetId, List<ClassDeclaration>> result = {};
final Map<AssetId, List<NamespaceDirective>> dependencies = {};
List<ClassDeclaration> _currentClass;
List<NamespaceDirective> _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;
}
}

View File

@ -4,6 +4,7 @@ import 'package:analyzer/analyzer.dart';
import 'package:analyzer/src/generated/java_core.dart'; import 'package:analyzer/src/generated/java_core.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:barback/barback.dart' show AssetId;
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'visitors.dart'; import 'visitors.dart';
@ -15,11 +16,12 @@ import 'visitors.dart';
/// If no Angular 2 `Directive`s are found in [code], returns the empty /// If no Angular 2 `Directive`s are found in [code], returns the empty
/// string unless [forceGenerate] is true, in which case an empty ngDeps /// string unless [forceGenerate] is true, in which case an empty ngDeps
/// file is created. /// file is created.
String createNgDeps(String code, String path) { String createNgDeps(String code, String path,
Map<AssetId, List<ClassDeclaration>> assetClasses) {
// TODO(kegluneq): Shortcut if we can determine that there are no // TODO(kegluneq): Shortcut if we can determine that there are no
// [Directive]s present, taking into account `export`s. // [Directive]s present, taking into account `export`s.
var writer = new PrintStringWriter(); var writer = new PrintStringWriter();
var visitor = new CreateNgDepsVisitor(writer, path); var visitor = new CreateNgDepsVisitor(writer, path, assetClasses);
parseCompilationUnit(code, name: path).accept(visitor); parseCompilationUnit(code, name: path).accept(visitor);
return '$writer'; return '$writer';
} }
@ -28,7 +30,7 @@ String createNgDeps(String code, String path) {
/// associated .ng_deps.dart file. /// associated .ng_deps.dart file.
class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> { class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
final PrintWriter writer; final PrintWriter writer;
final _Tester _tester = const _Tester(); final _Tester _tester;
bool _foundNgDirectives = false; bool _foundNgDirectives = false;
bool _wroteImport = false; bool _wroteImport = false;
final ToSourceVisitor _copyVisitor; final ToSourceVisitor _copyVisitor;
@ -39,12 +41,14 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
/// The path to the file which we are parsing. /// The path to the file which we are parsing.
final String importPath; final String importPath;
CreateNgDepsVisitor(PrintWriter writer, this.importPath) CreateNgDepsVisitor(PrintWriter writer, this.importPath,
Map<AssetId, List<ClassDeclaration>> assetClasses)
: writer = writer, : writer = writer,
_copyVisitor = new ToSourceVisitor(writer), _copyVisitor = new ToSourceVisitor(writer),
_factoryVisitor = new FactoryTransformVisitor(writer), _factoryVisitor = new FactoryTransformVisitor(writer),
_paramsVisitor = new ParameterTransformVisitor(writer), _paramsVisitor = new ParameterTransformVisitor(writer),
_metaVisitor = new AnnotationsTransformVisitor(writer); _metaVisitor = new AnnotationsTransformVisitor(writer),
_tester = new _Tester(assetClasses);
void _visitNodeListWithSeparator(NodeList<AstNode> list, String separator) { void _visitNodeListWithSeparator(NodeList<AstNode> list, String separator) {
if (list == null) return; if (list == null) return;
@ -136,7 +140,7 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
@override @override
Object visitClassDeclaration(ClassDeclaration node) { Object visitClassDeclaration(ClassDeclaration node) {
var shouldProcess = node.metadata.any(_tester._isDirective); var shouldProcess = node.metadata.any(_tester._shouldKeepMeta);
if (shouldProcess) { if (shouldProcess) {
var ctor = _getCtor(node); var ctor = _getCtor(node);
@ -199,15 +203,40 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
Object visitSimpleIdentifier(SimpleIdentifier node) => _nodeToSource(node); Object visitSimpleIdentifier(SimpleIdentifier node) => _nodeToSource(node);
} }
class _Tester { const annotationNamesToKeep = const ['Injectable', 'Template'];
const _Tester();
bool _isDirective(Annotation meta) { class _Tester {
var metaName = meta.name.toString(); final Map<String, ClassDeclaration> _classesByName;
return metaName == 'Component' ||
metaName == 'Decorator' || _Tester(Map<AssetId, List<ClassDeclaration>> assetClasses)
metaName == 'Injectable' || : _classesByName = new Map.fromIterables(assetClasses.values
metaName == 'View' || .expand((classes) => classes.map((c) => c.name.toString())),
metaName == 'Viewport'; 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;
} }
} }

View File

@ -2,6 +2,8 @@ library angular2.transform.directive_processor.transformer;
import 'dart:async'; 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/logging.dart' as log;
import 'package:angular2/src/transform/common/names.dart'; import 'package:angular2/src/transform/common/names.dart';
import 'package:angular2/src/transform/common/options.dart'; import 'package:angular2/src/transform/common/options.dart';
@ -32,8 +34,10 @@ class DirectiveProcessor extends Transformer {
try { try {
var asset = transform.primaryInput; var asset = transform.primaryInput;
var reader = new AssetReader.fromTransform(transform);
var defMap = await createTypeMap(reader, asset.id);
var assetCode = await asset.readAsString(); 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) { if (ngDepsSrc != null && ngDepsSrc.isNotEmpty) {
var ngDepsAssetId = var ngDepsAssetId =
transform.primaryInput.id.changeExtension(DEPS_EXTENSION); transform.primaryInput.id.changeExtension(DEPS_EXTENSION);

View File

@ -1,24 +1,47 @@
library angular2.test.transform.directive_processor.all_tests; library angular2.test.transform.directive_processor.all_tests;
import 'package:barback/barback.dart';
import 'package:angular2/src/transform/directive_processor/rewriter.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:dart_style/dart_style.dart';
import 'package:guinness/guinness.dart'; import 'package:guinness/guinness.dart';
import 'package:path/path.dart' as path;
import '../common/read_file.dart';
var formatter = new DartFormatter(); var formatter = new DartFormatter();
main() {
allTests();
}
void allTests() { void allTests() {
it('should preserve parameter annotations as const instances.', () { _testNgDeps('should preserve parameter annotations as const instances.',
var inputPath = 'parameter_metadata/soup.dart'; 'parameter_metadata/soup.dart');
var expected = _readFile('parameter_metadata/expected/soup.ng_deps.dart');
var output = _testNgDeps('should recognize annotations which extend Injectable.',
formatter.format(createNgDeps(_readFile(inputPath), inputPath)); '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); expect(output).toEqual(expected);
}); });
} }
var pathBase = 'directive_processor'; AssetId _assetIdForPath(String path) =>
new AssetId('angular2', 'test/transform/directive_processor/$path');
/// Smooths over differences in CWD between IDEs and running tests in Travis.
String _readFile(String path) => readFile('$pathBase/$path');

View File

@ -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();
}

View File

@ -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()]
});
}

View File

@ -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()]
});
}

View File

@ -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()]
});
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -8,6 +8,10 @@ import 'package:dart_style/dart_style.dart';
import '../common/read_file.dart'; import '../common/read_file.dart';
main() {
allTests();
}
var formatter = new DartFormatter(); var formatter = new DartFormatter();
var transform = new AngularTransformerGroup(new TransformerOptions( var transform = new AngularTransformerGroup(new TransformerOptions(
['web/index.dart'], ['web/index.dart'],
@ -39,7 +43,10 @@ void allTests() {
'../../../lib/src/core/annotations/annotations.dart', '../../../lib/src/core/annotations/annotations.dart',
'angular2|lib/src/core/application.dart': '../common/application.dart', 'angular2|lib/src/core/application.dart': '../common/application.dart',
'angular2|lib/src/reflection/reflection_capabilities.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 = [ var tests = [