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:
@@ -0,0 +1,36 @@
|
||||
library angular2.src.transform.reflection_remover.ast_tester;
|
||||
|
||||
import 'package:analyzer/src/generated/ast.dart';
|
||||
import 'package:analyzer/src/generated/element.dart';
|
||||
|
||||
/// An object that checks for [ReflectionCapabilities] syntactically, that is,
|
||||
/// without resolution information.
|
||||
class AstTester {
|
||||
static const REFLECTION_CAPABILITIES_NAME = 'ReflectionCapabilities';
|
||||
|
||||
const AstTester();
|
||||
|
||||
bool isNewReflectionCapabilities(InstanceCreationExpression node) =>
|
||||
'${node.constructorName.type.name}' == REFLECTION_CAPABILITIES_NAME;
|
||||
|
||||
bool isReflectionCapabilitiesImport(ImportDirective node) {
|
||||
return node.uri.stringValue.endsWith("reflection_capabilities.dart");
|
||||
}
|
||||
}
|
||||
|
||||
/// An object that checks for [ReflectionCapabilities] using a fully resolved
|
||||
/// Ast.
|
||||
class ResolvedTester implements AstTester {
|
||||
final ClassElement _forbiddenClass;
|
||||
|
||||
ResolvedTester(this._forbiddenClass);
|
||||
|
||||
bool isNewReflectionCapabilities(InstanceCreationExpression node) {
|
||||
var typeElement = node.constructorName.type.name.bestElement;
|
||||
return typeElement != null && typeElement == _forbiddenClass;
|
||||
}
|
||||
|
||||
bool isReflectionCapabilitiesImport(ImportDirective node) {
|
||||
return node.uriElement == _forbiddenClass.library;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
library angular2.src.transform.reflection_remover.codegen;
|
||||
|
||||
import 'package:analyzer/src/generated/ast.dart';
|
||||
import 'package:barback/barback.dart';
|
||||
import 'package:code_transformers/resolver.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:angular2/src/transform/common/names.dart';
|
||||
|
||||
class Codegen {
|
||||
static const _PREFIX_BASE = 'ngStaticInit';
|
||||
|
||||
/// The prefix used to import our generated file.
|
||||
final String prefix;
|
||||
/// The import uri
|
||||
final String importUri;
|
||||
|
||||
Codegen(String reflectionEntryPointPath, String newEntryPointPath,
|
||||
{String prefix})
|
||||
: this.prefix = prefix == null ? _PREFIX_BASE : prefix,
|
||||
importUri = path.relative(newEntryPointPath,
|
||||
from: path.dirname(reflectionEntryPointPath)) {
|
||||
if (this.prefix.isEmpty) throw new ArgumentError.value('(empty)', 'prefix');
|
||||
}
|
||||
|
||||
factory Codegen.fromResolver(
|
||||
Resolver resolver, AssetId reflectionEntryPoint, AssetId newEntryPoint) {
|
||||
var lib = resolver.getLibrary(reflectionEntryPoint);
|
||||
var prefix = _PREFIX_BASE;
|
||||
var idx = 0;
|
||||
while (lib.imports.any((import) {
|
||||
return import.prefix != null && import.prefix == prefix;
|
||||
})) {
|
||||
prefix = '${_PREFIX_BASE}${idx++}';
|
||||
}
|
||||
|
||||
return new Codegen(reflectionEntryPoint, newEntryPoint, prefix: prefix);
|
||||
}
|
||||
|
||||
/// Generates code to import the library containing the method which sets up
|
||||
/// Angular2 reflection statically.
|
||||
///
|
||||
/// The code generated here should follow the example of code generated for
|
||||
/// an [ImportDirective] node.
|
||||
String codegenImport() {
|
||||
return 'import \'${importUri}\' as ${prefix};';
|
||||
}
|
||||
|
||||
/// Generates code to call the method which sets up Angular2 reflection
|
||||
/// statically.
|
||||
///
|
||||
/// If [reflectorAssignment] is provided, it is expected to be the node
|
||||
/// representing the [ReflectionCapabilities] assignment, and we will
|
||||
/// attempt to parse the access of [reflector] from it so that [reflector] is
|
||||
/// properly prefixed if necessary.
|
||||
String codegenSetupReflectionCall(
|
||||
{AssignmentExpression reflectorAssignment}) {
|
||||
var reflectorExpression = null;
|
||||
if (reflectorAssignment != null) {
|
||||
reflectorExpression = reflectorAssignment.accept(new _ReflectorVisitor());
|
||||
}
|
||||
if (reflectorExpression == null) {
|
||||
reflectorExpression = 'reflector';
|
||||
}
|
||||
|
||||
return '${prefix}.${SETUP_METHOD_NAME}(${reflectorExpression});';
|
||||
}
|
||||
}
|
||||
|
||||
/// A visitor whose job it is to find the access of [reflector].
|
||||
class _ReflectorVisitor extends Object with SimpleAstVisitor<Expression> {
|
||||
@override
|
||||
Expression visitAssignmentExpression(AssignmentExpression node) {
|
||||
if (node == null || node.leftHandSide == null) return null;
|
||||
return node.leftHandSide.accept(this);
|
||||
}
|
||||
|
||||
@override
|
||||
Expression visitPropertyAccess(PropertyAccess node) {
|
||||
if (node == null || node.target == null) return;
|
||||
return node.target;
|
||||
}
|
||||
|
||||
@override
|
||||
Expression visitPrefixedIdentifier(PrefixedIdentifier node) {
|
||||
if (node == null || node.prefix == null) return null;
|
||||
return node.prefix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
library angular2.src.transform.reflection_remover.remove_reflection_capabilities;
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
|
||||
import 'codegen.dart';
|
||||
import 'rewriter.dart';
|
||||
|
||||
/// Finds the call to the Angular2 [ReflectionCapabilities] constructor
|
||||
/// in [code] and replaces it with a call to `setupReflection` in
|
||||
/// [newEntryPoint].
|
||||
///
|
||||
/// [reflectionEntryPointPath] is the path where [code] is defined and is
|
||||
/// used to display parsing errors.
|
||||
///
|
||||
/// This only searches [code] not `part`s, `import`s, `export`s, etc.
|
||||
String removeReflectionCapabilities(
|
||||
String code, String reflectionEntryPointPath, String newEntryPointPath) {
|
||||
var codegen = new Codegen(reflectionEntryPointPath, newEntryPointPath);
|
||||
return new Rewriter(code, codegen)
|
||||
.rewrite(parseCompilationUnit(code, name: reflectionEntryPointPath));
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
library angular2.src.transform.reflection_remover.rewriter;
|
||||
|
||||
import 'package:analyzer/src/generated/ast.dart';
|
||||
import 'package:angular2/src/transform/common/logging.dart';
|
||||
|
||||
import 'ast_tester.dart';
|
||||
import 'codegen.dart';
|
||||
|
||||
class Rewriter {
|
||||
final String _code;
|
||||
final Codegen _codegen;
|
||||
final AstTester _tester;
|
||||
|
||||
Rewriter(this._code, this._codegen, {AstTester tester})
|
||||
: _tester = tester == null ? const AstTester() : tester;
|
||||
|
||||
/// Rewrites the provided code removing imports of the
|
||||
/// [ReflectionCapabilities] library and instantiations of
|
||||
/// [ReflectionCapabilities], as detected by the (potentially) provided
|
||||
/// [AstTester].
|
||||
///
|
||||
/// To the extent possible, this method does not change line numbers or
|
||||
/// offsets in the provided code to facilitate debugging via source maps.
|
||||
String rewrite(CompilationUnit node) {
|
||||
if (node == null) throw new ArgumentError.notNull('node');
|
||||
|
||||
var visitor = new _FindReflectionCapabilitiesVisitor(_tester);
|
||||
node.accept(visitor);
|
||||
if (visitor.reflectionCapabilityImports.isEmpty) {
|
||||
logger.error(
|
||||
'Failed to find ${AstTester.REFLECTION_CAPABILITIES_NAME} import.');
|
||||
return _code;
|
||||
}
|
||||
if (visitor.reflectionCapabilityAssignments.isEmpty) {
|
||||
logger.error('Failed to find ${AstTester.REFLECTION_CAPABILITIES_NAME} '
|
||||
'instantiation.');
|
||||
return _code;
|
||||
}
|
||||
|
||||
var compare = (AstNode a, AstNode b) => b.offset - a.offset;
|
||||
visitor.reflectionCapabilityImports.sort(compare);
|
||||
visitor.reflectionCapabilityAssignments.sort(compare);
|
||||
|
||||
var importAdded = false;
|
||||
var buf = new StringBuffer();
|
||||
var idx = visitor.reflectionCapabilityImports.fold(0,
|
||||
(int lastIdx, ImportDirective node) {
|
||||
buf.write(_code.substring(lastIdx, node.offset));
|
||||
if ('${node.prefix}' == _codegen.prefix) {
|
||||
logger.warning(
|
||||
'Found import prefix "${_codegen.prefix}" in source file.'
|
||||
' Transform may not succeed.');
|
||||
}
|
||||
buf.write(_commentedNode(node));
|
||||
if (!importAdded) {
|
||||
buf.write(_codegen.codegenImport());
|
||||
importAdded = true;
|
||||
}
|
||||
return node.end;
|
||||
});
|
||||
|
||||
var setupAdded = false;
|
||||
idx = visitor.reflectionCapabilityAssignments.fold(idx,
|
||||
(int lastIdx, AssignmentExpression assignNode) {
|
||||
var node = assignNode;
|
||||
while (node.parent is ExpressionStatement) {
|
||||
node = node.parent;
|
||||
}
|
||||
buf.write(_code.substring(lastIdx, node.offset));
|
||||
buf.write(_commentedNode(node));
|
||||
if (!setupAdded) {
|
||||
buf.write(_codegen.codegenSetupReflectionCall(
|
||||
reflectorAssignment: assignNode));
|
||||
setupAdded = true;
|
||||
}
|
||||
return node.end;
|
||||
});
|
||||
if (idx < _code.length) buf.write(_code.substring(idx));
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
String _commentedNode(AstNode node) {
|
||||
// TODO(kegluneq): Return commented code once we generate all needed code.
|
||||
return _code.substring(node.offset, node.end);
|
||||
}
|
||||
}
|
||||
|
||||
/// Visitor responsible for rewriting the Angular 2 code which instantiates
|
||||
/// [ReflectionCapabilities] and removing its associated import.
|
||||
///
|
||||
/// This breaks our dependency on dart:mirrors, which enables smaller code
|
||||
/// size and better performance.
|
||||
class _FindReflectionCapabilitiesVisitor extends Object
|
||||
with RecursiveAstVisitor<Object> {
|
||||
final reflectionCapabilityImports = new List<ImportDirective>();
|
||||
final reflectionCapabilityAssignments = new List<AssignmentExpression>();
|
||||
final AstTester _tester;
|
||||
|
||||
_FindReflectionCapabilitiesVisitor(this._tester);
|
||||
|
||||
@override
|
||||
Object visitImportDirective(ImportDirective node) {
|
||||
if (_tester.isReflectionCapabilitiesImport(node)) {
|
||||
reflectionCapabilityImports.add(node);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitAssignmentExpression(AssignmentExpression node) {
|
||||
if (node.rightHandSide is InstanceCreationExpression &&
|
||||
_tester.isNewReflectionCapabilities(node.rightHandSide)) {
|
||||
reflectionCapabilityAssignments.add(node);
|
||||
}
|
||||
return super.visitAssignmentExpression(node);
|
||||
}
|
||||
|
||||
@override
|
||||
Object visitInstanceCreationExpression(InstanceCreationExpression node) {
|
||||
if (_tester.isNewReflectionCapabilities(node) &&
|
||||
!reflectionCapabilityAssignments.contains(node.parent)) {
|
||||
logger.error('Unexpected format in creation of '
|
||||
'${reflectionCapabilitiesTypeName}');
|
||||
}
|
||||
return super.visitInstanceCreationExpression(node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
library angular2.src.transform.reflection_remover.transformer;
|
||||
|
||||
import 'dart:async';
|
||||
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 'remove_reflection_capabilities.dart';
|
||||
|
||||
/// Transformer responsible for removing the import and instantiation of
|
||||
/// [ReflectionCapabilities].
|
||||
///
|
||||
/// The goal of this is to break the app's dependency on dart:mirrors.
|
||||
///
|
||||
/// This transformer assumes that [DirectiveProcessor] and [DirectiveLinker]
|
||||
/// have already been run and that a .ngDeps.dart file has been generated for
|
||||
/// [options.entryPoint]. The instantiation of [ReflectionCapabilities] is
|
||||
/// replaced by calling `setupReflection` in that .ngDeps.dart file.
|
||||
class ReflectionRemover extends Transformer {
|
||||
final TransformerOptions options;
|
||||
|
||||
ReflectionRemover(this.options);
|
||||
|
||||
@override
|
||||
bool isPrimary(AssetId id) => options.reflectionEntryPoint == id.path;
|
||||
|
||||
@override
|
||||
Future apply(Transform transform) async {
|
||||
log.init(transform);
|
||||
|
||||
try {
|
||||
var newEntryPoint = new AssetId(
|
||||
transform.primaryInput.id.package, options.entryPoint)
|
||||
.changeExtension(DEPS_EXTENSION);
|
||||
|
||||
var assetCode = await transform.primaryInput.readAsString();
|
||||
transform.addOutput(new Asset.fromString(transform.primaryInput.id,
|
||||
removeReflectionCapabilities(
|
||||
assetCode, transform.primaryInput.id.path, newEntryPoint.path)));
|
||||
} catch (ex, stackTrace) {
|
||||
log.logger.error('Removing reflection failed.\n'
|
||||
'Exception: $ex\n'
|
||||
'Stack Trace: $stackTrace');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user