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:
Tim Blasi
2015-02-27 14:42:21 -08:00
parent 08bd3a4443
commit d0aceef4e0
52 changed files with 1530 additions and 318 deletions

View 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';
}
}

View File

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

View 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;
}
}