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:
parent
6600ac7031
commit
c65fd31e86
|
@ -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'
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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');
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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()]
|
||||||
|
});
|
||||||
|
}
|
|
@ -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()]
|
||||||
|
});
|
||||||
|
}
|
|
@ -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()]
|
||||||
|
});
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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 = [
|
||||||
|
|
Loading…
Reference in New Issue