perf(dart/transform) Restructure transform to independent phases
Update summary: - Removes the need for resolution, gaining transform speed at the cost of some precision and ability to detect errors - Generates type registrations in the package alongside their declarations - Ensures that line numbers do not change in transformed user code
This commit is contained in:
207
modules/angular2/src/transform/directive_processor/rewriter.dart
Normal file
207
modules/angular2/src/transform/directive_processor/rewriter.dart
Normal file
@ -0,0 +1,207 @@
|
||||
library angular2.src.transform.directive_processor;
|
||||
|
||||
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:angular2/src/transform/common/ngdata.dart';
|
||||
import 'package:angular2/src/transform/common/visitor_mixin.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'visitors.dart';
|
||||
|
||||
/// Generates a file registering all Angular 2 `Directive`s found in [code] in
|
||||
/// ngDeps format [TODO(kegluneq): documentation reference needed]. [path] is
|
||||
/// the path to the file (or asset) containing [code].
|
||||
///
|
||||
/// 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, {bool forceGenerate: false}) {
|
||||
// TODO(kegluneq): Shortcut if we can determine that there are no
|
||||
// [Directive]s present.
|
||||
var writer = new PrintStringWriter();
|
||||
var visitor = new CreateNgDepsVisitor(writer, path);
|
||||
parseCompilationUnit(code, name: path).accept(visitor);
|
||||
if (visitor.foundNgDirectives || forceGenerate) {
|
||||
return writer.toString();
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Visitor responsible for processing [CompilationUnit] and creating an
|
||||
/// associated .ngDeps.dart file.
|
||||
class CreateNgDepsVisitor extends Object
|
||||
with SimpleAstVisitor<Object>, VisitorMixin {
|
||||
final PrintWriter writer;
|
||||
final _Tester _tester = const _Tester();
|
||||
bool foundNgDirectives = false;
|
||||
bool wroteImport = false;
|
||||
final ToSourceVisitor _copyVisitor;
|
||||
final FactoryTransformVisitor _factoryVisitor;
|
||||
final ParameterTransformVisitor _paramsVisitor;
|
||||
final AnnotationsTransformVisitor _metaVisitor;
|
||||
final NgData _ngData = new NgData();
|
||||
|
||||
/// The path to the file which we are parsing.
|
||||
final String importPath;
|
||||
|
||||
CreateNgDepsVisitor(PrintWriter writer, this.importPath)
|
||||
: writer = writer,
|
||||
_copyVisitor = new ToSourceVisitor(writer),
|
||||
_factoryVisitor = new FactoryTransformVisitor(writer),
|
||||
_paramsVisitor = new ParameterTransformVisitor(writer),
|
||||
_metaVisitor = new AnnotationsTransformVisitor(writer);
|
||||
|
||||
@override
|
||||
void visitCompilationUnit(CompilationUnit node) {
|
||||
visitNodeListWithSeparator(node.directives, " ");
|
||||
_openFunctionWrapper();
|
||||
visitNodeListWithSeparator(node.declarations, " ");
|
||||
_closeFunctionWrapper();
|
||||
return null;
|
||||
}
|
||||
|
||||
void _writeImport() {
|
||||
writer.print('import \'${path.basename(importPath)}\';');
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitImportDirective(ImportDirective node) {
|
||||
if (!wroteImport) {
|
||||
_writeImport();
|
||||
wroteImport = true;
|
||||
}
|
||||
_ngData.imports.add(node.uri.stringValue);
|
||||
return node.accept(_copyVisitor);
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitExportDirective(ExportDirective node) {
|
||||
_ngData.imports.add(node.uri.stringValue);
|
||||
return node.accept(_copyVisitor);
|
||||
}
|
||||
|
||||
void _openFunctionWrapper() {
|
||||
// TODO(kegluneq): Use a [PrintWriter] with a length getter.
|
||||
_ngData.importOffset = writer.toString().length;
|
||||
writer.print('bool _visited = false;'
|
||||
'void ${SETUP_METHOD_NAME}(${REFLECTOR_VAR_NAME}) {'
|
||||
'if (_visited) return; _visited = true;');
|
||||
}
|
||||
|
||||
void _closeFunctionWrapper() {
|
||||
if (foundNgDirectives) {
|
||||
writer.print(';');
|
||||
}
|
||||
// TODO(kegluneq): Use a [PrintWriter] with a length getter.
|
||||
_ngData.registerOffset = writer.toString().length;
|
||||
writer.print('}');
|
||||
writer.print('// ${_ngData.toJson()}');
|
||||
}
|
||||
|
||||
ConstructorDeclaration _getCtor(ClassDeclaration node) {
|
||||
int numCtorsFound = 0;
|
||||
var ctor = null;
|
||||
|
||||
for (ClassMember classMember in node.members) {
|
||||
if (classMember is ConstructorDeclaration) {
|
||||
numCtorsFound++;
|
||||
ConstructorDeclaration constructor = classMember;
|
||||
|
||||
// Use the unnnamed constructor if it is present.
|
||||
// Otherwise, use the first encountered.
|
||||
if (ctor == null) {
|
||||
ctor = constructor;
|
||||
} else if (constructor.name == null) {
|
||||
ctor = constructor;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numCtorsFound > 1) {
|
||||
var ctorName = ctor.name;
|
||||
ctorName = ctorName == null
|
||||
? 'the unnamed constructor'
|
||||
: 'constructor "${ctorName}"';
|
||||
logger.warning('Found ${numCtorsFound} ctors for class ${node.name},'
|
||||
'Using ${ctorName}.');
|
||||
}
|
||||
return ctor;
|
||||
}
|
||||
|
||||
void _generateEmptyFactory(String typeName) {
|
||||
writer.print('() => new ${typeName}()');
|
||||
}
|
||||
|
||||
void _generateEmptyParams() => writer.print('const []');
|
||||
|
||||
@override
|
||||
Object visitClassDeclaration(ClassDeclaration node) {
|
||||
var shouldProcess = node.metadata.any(_tester._isDirective);
|
||||
|
||||
if (shouldProcess) {
|
||||
var ctor = _getCtor(node);
|
||||
|
||||
if (!foundNgDirectives) {
|
||||
// The receiver for cascaded calls.
|
||||
writer.print(REFLECTOR_VAR_NAME);
|
||||
foundNgDirectives = true;
|
||||
}
|
||||
writer.print('..registerType(');
|
||||
visitNode(node.name);
|
||||
writer.print(', {"factory": ');
|
||||
if (ctor == null) {
|
||||
_generateEmptyFactory(node.name.toString());
|
||||
} else {
|
||||
ctor.accept(_factoryVisitor);
|
||||
}
|
||||
writer.print(', "parameters": ');
|
||||
if (ctor == null) {
|
||||
_generateEmptyParams();
|
||||
} else {
|
||||
ctor.accept(_paramsVisitor);
|
||||
}
|
||||
writer.print(', "annotations": ');
|
||||
node.accept(_metaVisitor);
|
||||
writer.print('})');
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Object _nodeToSource(AstNode node) {
|
||||
if (node == null) return null;
|
||||
return node.accept(_copyVisitor);
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitLibraryDirective(LibraryDirective node) => _nodeToSource(node);
|
||||
|
||||
@override
|
||||
Object visitPartOfDirective(PartOfDirective node) {
|
||||
// TODO(kegluneq): Consider importing [node.libraryName].
|
||||
logger.warning('[${importPath}]: '
|
||||
'Found `part of` directive while generating ${DEPS_EXTENSION} file, '
|
||||
'Transform may fail due to missing imports in generated file.');
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitPrefixedIdentifier(PrefixedIdentifier node) =>
|
||||
_nodeToSource(node);
|
||||
|
||||
@override
|
||||
Object visitSimpleIdentifier(SimpleIdentifier node) => _nodeToSource(node);
|
||||
}
|
||||
|
||||
class _Tester {
|
||||
const _Tester();
|
||||
|
||||
bool _isDirective(Annotation meta) {
|
||||
var metaName = meta.name.toString();
|
||||
return metaName == 'Component' ||
|
||||
metaName == 'Decorator' ||
|
||||
metaName == 'Template';
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
library angular2.src.transform.directive_processor.transformer;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:angular2/src/transform/common/formatter.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';
|
||||
import 'package:barback/barback.dart';
|
||||
|
||||
import 'rewriter.dart';
|
||||
|
||||
/// Transformer responsible for processing all .dart assets and creating
|
||||
/// .ngDeps.dart files which register @Injectable annotated classes with the
|
||||
/// reflector.
|
||||
///
|
||||
/// This will also create .ngDeps.dart files for classes annotated
|
||||
/// with @Component, @Template, @Decorator, etc.
|
||||
///
|
||||
/// This transformer is the first phase in a two-phase transform. It should
|
||||
/// be followed by [DirectiveLinker].
|
||||
class DirectiveProcessor extends Transformer {
|
||||
final TransformerOptions options;
|
||||
|
||||
DirectiveProcessor(this.options);
|
||||
|
||||
@override
|
||||
bool isPrimary(AssetId id) => id.extension.endsWith('dart');
|
||||
|
||||
@override
|
||||
Future apply(Transform transform) async {
|
||||
log.init(transform);
|
||||
|
||||
try {
|
||||
var assetCode = await transform.primaryInput.readAsString();
|
||||
var ngDepsSrc = createNgDeps(assetCode, transform.primaryInput.id.path,
|
||||
forceGenerate: transform.primaryInput.id.path == options.entryPoint);
|
||||
if (ngDepsSrc != null && ngDepsSrc.isNotEmpty) {
|
||||
var ngDepsAssetId =
|
||||
transform.primaryInput.id.changeExtension(DEPS_EXTENSION);
|
||||
var exists = await transform.hasInput(ngDepsAssetId);
|
||||
if (exists) {
|
||||
log.logger.error('Clobbering ${ngDepsAssetId}. '
|
||||
'This probably will not end well');
|
||||
}
|
||||
transform.addOutput(new Asset.fromString(ngDepsAssetId, ngDepsSrc));
|
||||
}
|
||||
} catch (ex, stackTrace) {
|
||||
log.logger.warning('Processing ng directives failed.\n'
|
||||
'Exception: $ex\n'
|
||||
'Stack Trace: $stackTrace');
|
||||
}
|
||||
}
|
||||
}
|
181
modules/angular2/src/transform/directive_processor/visitors.dart
Normal file
181
modules/angular2/src/transform/directive_processor/visitors.dart
Normal file
@ -0,0 +1,181 @@
|
||||
library angular2.src.transform.directive_processor;
|
||||
|
||||
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/visitor_mixin.dart';
|
||||
|
||||
/// SourceVisitor designed to accept [ConstructorDeclaration] nodes.
|
||||
class _CtorTransformVisitor extends ToSourceVisitor with VisitorMixin {
|
||||
bool _withParameterTypes = true;
|
||||
bool _withParameterNames = true;
|
||||
final PrintWriter writer;
|
||||
|
||||
/// Maps field names to their declared types. This is populated whenever
|
||||
/// the listener visits a [ConstructorDeclaration] node.
|
||||
final Map<String, TypeName> _fieldNameToType = {};
|
||||
|
||||
_CtorTransformVisitor(PrintWriter writer)
|
||||
: this.writer = writer,
|
||||
super(writer);
|
||||
|
||||
/// If [_withParameterTypes] is true, this method outputs [node]'s type. If
|
||||
/// [_withParameterNames] is true, this method outputs [node]'s identifier.
|
||||
Object _visitNormalFormalParameter(TypeName type, SimpleIdentifier name) {
|
||||
if (_withParameterTypes) {
|
||||
visitNodeWithSuffix(type, ' ');
|
||||
}
|
||||
if (_withParameterNames) {
|
||||
visitNode(name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _buildFieldMap(ConstructorDeclaration node) {
|
||||
ClassDeclaration clazz =
|
||||
node.getAncestor((node) => node is ClassDeclaration);
|
||||
_fieldNameToType.clear();
|
||||
clazz.members.where((member) => member is FieldDeclaration).forEach(
|
||||
(FieldDeclaration field) {
|
||||
var type = field.fields.type;
|
||||
if (type != null) {
|
||||
field.fields.variables.forEach((VariableDeclaration decl) {
|
||||
_fieldNameToType[decl.name.toString()] = type;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitSimpleFormalParameter(SimpleFormalParameter node) {
|
||||
return _visitNormalFormalParameter(node.type, node.identifier);
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitFieldFormalParameter(FieldFormalParameter node) {
|
||||
if (node.parameters != null) {
|
||||
logger.error('Parameters in ctor not supported '
|
||||
'(${node.toSource()})');
|
||||
}
|
||||
var type = node.type;
|
||||
if (type == null) {
|
||||
type = _fieldNameToType[node.identifier.toString()];
|
||||
}
|
||||
return _visitNormalFormalParameter(type, node.identifier);
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) {
|
||||
logger.error('Function typed formal parameters not supported '
|
||||
'(${node.toSource()})');
|
||||
return _visitNormalFormalParameter(null, node.identifier);
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitDefaultFormalParameter(DefaultFormalParameter node) {
|
||||
visitNode(node.parameter);
|
||||
// Ignore the declared default value.
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
/// Overridden to avoid outputting grouping operators for default parameters.
|
||||
Object visitFormalParameterList(FormalParameterList node) {
|
||||
writer.print('(');
|
||||
NodeList<FormalParameter> parameters = node.parameters;
|
||||
int size = parameters.length;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (i > 0) {
|
||||
writer.print(', ');
|
||||
}
|
||||
parameters[i].accept(this);
|
||||
}
|
||||
writer.print(')');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// ToSourceVisitor designed to print 'parameters' values for Angular2's
|
||||
/// [registerType] calls.
|
||||
class ParameterTransformVisitor extends _CtorTransformVisitor {
|
||||
ParameterTransformVisitor(PrintWriter writer) : super(writer);
|
||||
|
||||
@override
|
||||
Object visitConstructorDeclaration(ConstructorDeclaration node) {
|
||||
_buildFieldMap(node);
|
||||
_withParameterNames = false;
|
||||
_withParameterTypes = true;
|
||||
writer.print('const [');
|
||||
visitNode(node.parameters);
|
||||
writer.print(']');
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitFormalParameterList(FormalParameterList node) {
|
||||
NodeList<FormalParameter> parameters = node.parameters;
|
||||
int size = parameters.length;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (i > 0) {
|
||||
writer.print(', ');
|
||||
}
|
||||
// TODO(kegluneq): Include annotations on parameters.
|
||||
writer.print('const [');
|
||||
parameters[i].accept(this);
|
||||
writer.print(']');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// ToSourceVisitor designed to print 'factory' values for Angular2's
|
||||
/// [registerType] calls.
|
||||
class FactoryTransformVisitor extends _CtorTransformVisitor {
|
||||
FactoryTransformVisitor(PrintWriter writer) : super(writer);
|
||||
|
||||
@override
|
||||
Object visitConstructorDeclaration(ConstructorDeclaration node) {
|
||||
_buildFieldMap(node);
|
||||
_withParameterNames = true;
|
||||
_withParameterTypes = true;
|
||||
visitNode(node.parameters);
|
||||
writer.print(' => new ');
|
||||
visitNode(node.returnType);
|
||||
visitNodeWithPrefix(".", node.name);
|
||||
_withParameterTypes = false;
|
||||
visitNode(node.parameters);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// ToSourceVisitor designed to print a [ClassDeclaration] node as a
|
||||
/// 'annotations' value for Angular2's [registerType] calls.
|
||||
class AnnotationsTransformVisitor extends ToSourceVisitor with VisitorMixin {
|
||||
final PrintWriter writer;
|
||||
AnnotationsTransformVisitor(PrintWriter writer)
|
||||
: this.writer = writer,
|
||||
super(writer);
|
||||
|
||||
@override
|
||||
Object visitClassDeclaration(ClassDeclaration node) {
|
||||
writer.print('const [');
|
||||
var size = node.metadata.length;
|
||||
for (var i = 0; i < size; ++i) {
|
||||
if (i > 0) {
|
||||
writer.print(', ');
|
||||
}
|
||||
node.metadata[i].accept(this);
|
||||
}
|
||||
writer.print(']');
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitAnnotation(Annotation node) {
|
||||
writer.print('const ');
|
||||
visitNode(node.name);
|
||||
visitNodeWithPrefix(".", node.constructorName);
|
||||
visitNode(node.arguments);
|
||||
return null;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user