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:
|
||||
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'
|
||||
|
|
|
@ -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: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<AssetId, List<ClassDeclaration>> 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<Object> {
|
||||
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<Object> {
|
|||
/// The path to the file which we are parsing.
|
||||
final String importPath;
|
||||
|
||||
CreateNgDepsVisitor(PrintWriter writer, this.importPath)
|
||||
CreateNgDepsVisitor(PrintWriter writer, this.importPath,
|
||||
Map<AssetId, List<ClassDeclaration>> 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<AstNode> list, String separator) {
|
||||
if (list == null) return;
|
||||
|
@ -136,7 +140,7 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
|
|||
|
||||
@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> {
|
|||
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<String, ClassDeclaration> _classesByName;
|
||||
|
||||
_Tester(Map<AssetId, List<ClassDeclaration>> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
||||
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 = [
|
||||
|
|
Loading…
Reference in New Issue