feat(dart/transform): Add a parser for `.ng_deps.dart` files and use.

Use the parser in `BindGenerator`
This checkin also removes types from `registerSetters` calls since they
can cause runtime failures (see #886). We will resolve this by
generating change detector classes in the future.
This commit is contained in:
Tim Blasi 2015-03-12 14:04:41 -07:00
parent 92b22d24d0
commit 4b12c19560
7 changed files with 102 additions and 89 deletions

View File

@ -1,93 +1,53 @@
library angular2.src.transform.bind_generator.generator;
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/visitor_mixin.dart';
import 'dart:async';
String createNgSetters(String code, String path) {
if (_noSettersPresent(code)) return code;
import 'package:angular2/src/transform/common/asset_reader.dart';
import 'package:angular2/src/transform/common/parser.dart';
import 'package:barback/barback.dart';
var writer = new PrintStringWriter();
parseCompilationUnit(
code, name: path).accept(new CreateNgSettersVisitor(writer));
return writer.toString();
import 'visitor.dart';
Future<String> createNgSetters(AssetReader reader, AssetId entryPoint) async {
var parser = new Parser(reader);
NgDeps ngDeps = await parser.parse(entryPoint);
String code = ngDeps.code;
var setters = _generateSetters(_createBindMap(ngDeps));
if (setters.length == 0) return code;
var codeInjectIdx = ngDeps.registeredTypes.last.registerMethod.end;
return '${code.substring(0, codeInjectIdx)}'
'..registerSetters({${setters.join(', ')}})'
'${code.substring(codeInjectIdx)}';
}
bool _noSettersPresent(String code) => code.indexOf('bind') < 0;
class CreateNgSettersVisitor extends ToSourceVisitor with VisitorMixin {
final PrintWriter writer;
final _ExtractSettersVisitor extractVisitor = new _ExtractSettersVisitor();
CreateNgSettersVisitor(PrintWriter writer)
: this.writer = writer,
super(writer);
@override
Object visitMethodInvocation(MethodInvocation node) {
var isRegisterType =
node.methodName.toString() == REGISTER_TYPE_METHOD_NAME;
// The first argument to a `registerType` call is the type.
extractVisitor.currentName = node.argumentList.arguments[0] is Identifier
? node.argumentList.arguments[0]
: null;
extractVisitor.bindPieces.clear();
var retVal = super.visitMethodInvocation(node);
if (isRegisterType) {
if (extractVisitor.bindPieces.isNotEmpty) {
writer.print('..${REGISTER_SETTERS_METHOD_NAME}({');
writer.print(extractVisitor.bindPieces.join(','));
writer.print('})');
}
}
return retVal;
}
@override
Object visitMapLiteralEntry(MapLiteralEntry node) {
if (node.key is StringLiteral &&
stringLiteralToString(node.key) == 'annotations') {
node.value.accept(extractVisitor);
}
return super.visitMapLiteralEntry(node);
}
/// Consumes the map generated by [_createBindMap] to codegen setters.
List<String> _generateSetters(Map<String, String> bindMap) {
var setters = [];
// TODO(kegluneq): Include types for receivers. See #886.
bindMap.forEach((prop, type) {
setters.add('\'$prop\': (o, String v) => o.$prop = v');
});
return setters;
}
/// Visitor responsible for crawling the "annotations" value in a
/// `registerType` call and generating setters from any "bind" values found.
class _ExtractSettersVisitor extends Object
with RecursiveAstVisitor<Object>, VisitorMixin {
final List<String> bindPieces = [];
Identifier currentName = null;
void _extractFromMapLiteral(MapLiteral map) {
if (currentName == null) {
logger.error('Unexpected code path: `currentName` should never be null');
}
map.entries.forEach((entry) {
// TODO(kegluneq): Remove this restriction
if (entry.key is SimpleStringLiteral) {
var propName = entry.key.value;
bindPieces.add('\'${propName}\': ('
'${currentName} o, String value) => o.${propName} = value');
/// Collapses all `bindProperties` in [ngDeps] into a map where the keys are
/// the bind properties and the values are either the one and only type
/// binding to that property or the empty string.
Map<String, String> _createBindMap(NgDeps ngDeps) {
var visitor = new ExtractSettersVisitor();
var bindMap = {};
ngDeps.registeredTypes.forEach((RegisteredType t) {
visitor.bindMappings.clear();
t.annotations.accept(visitor);
visitor.bindMappings.forEach((String prop, _) {
if (bindMap.containsKey(prop)) {
bindMap[prop] = '';
} else {
logger.error('`bind` currently only supports string literals');
bindMap[prop] = '${t.typeName}';
}
});
}
@override
Object visitNamedExpression(NamedExpression node) {
if (node.name.label.toString() == 'bind') {
// TODO(kegluneq): Remove this restriction.
if (node.expression is MapLiteral) {
_extractFromMapLiteral(node.expression);
}
return null;
}
return super.visitNamedExpression(node);
}
});
return bindMap;
}

View File

@ -1,6 +1,7 @@
library angular2.src.transform.bind_generator.transformer;
import 'dart:async';
import 'package:angular2/src/transform/common/asset_reader.dart';
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';
@ -28,11 +29,11 @@ class BindGenerator extends Transformer {
log.init(transform);
try {
var assetCode = await transform.primaryInput.readAsString();
var assetPath = transform.primaryInput.id.path;
var transformedCode = createNgSetters(assetCode, assetPath);
transform.addOutput(new Asset.fromString(transform.primaryInput.id,
formatter.format(transformedCode, uri: assetPath)));
var id = transform.primaryInput.id;
var reader = new AssetReader.fromTransform(transform);
var transformedCode = await createNgSetters(reader, id);
transform.addOutput(new Asset.fromString(
id, formatter.format(transformedCode, uri: id.path)));
} catch (ex, stackTrace) {
log.logger.error('Creating ng setters failed.\n'
'Exception: $ex\n'

View File

@ -0,0 +1,39 @@
library angular2.src.transform.bind_generator.visitor;
import 'package:analyzer/analyzer.dart';
import 'package:angular2/src/transform/common/logging.dart';
/// Visitor responsible for crawling the "annotations" value in a
/// `registerType` call and pulling out the properties of any "bind"
/// values found.
class ExtractSettersVisitor extends Object with RecursiveAstVisitor<Object> {
final Map<String, String> bindMappings = {};
void _extractFromMapLiteral(MapLiteral map) {
map.entries.forEach((entry) {
// TODO(kegluneq): Remove this restriction
if (entry.key is SimpleStringLiteral &&
entry.value is SimpleStringLiteral) {
bindMappings[stringLiteralToString(entry.key)] =
stringLiteralToString(entry.value);
} else {
logger.error('`bind` currently only supports string literals '
'(${entry})');
}
});
}
@override
Object visitNamedExpression(NamedExpression node) {
if ('${node.name.label}' == 'bind') {
// TODO(kegluneq): Remove this restriction.
if (node.expression is MapLiteral) {
_extractFromMapLiteral(node.expression);
} else {
logger.error('`bind` currently only supports map literals');
}
return null;
}
return super.visitNamedExpression(node);
}
}

View File

@ -5,6 +5,8 @@ import 'dart:convert';
import 'package:barback/barback.dart';
/// A class that allows fetching code using [AssetId]s without all the
/// additional baggage of a [Transform].
abstract class AssetReader {
Future<String> readAsString(AssetId id, {Encoding encoding});
Future<bool> hasInput(AssetId id);

View File

@ -13,16 +13,22 @@ import 'registered_type.dart';
export 'registered_type.dart';
/// A parser that reads `.ng_deps.dart` files (represented by [AssetId]s into
/// easier to manage [NgDeps] files.
class Parser {
final AssetReader _reader;
final _ParseNgDepsVisitor _visitor = new _ParseNgDepsVisitor();
Parser(AssetReader this._reader);
/// Parses the `.ng_deps.dart` file represented by `id` and all of the `
/// .ng_deps.dart` files that it imports.
Future<List<NgDeps>> parseRecursive(AssetId id) async {
return _recurse(id);
}
/// Parses only the `.ng_deps.dart` file represented by `id`.
/// See also [parseRecursive].
Future<NgDeps> parse(AssetId id) async {
if (!(await _reader.hasInput(id))) return null;
var ngDeps = new NgDeps(await _reader.readAsString(id));
@ -56,6 +62,7 @@ class Parser {
}
}
/// The contents of a `.ng_deps.dart` file.
class NgDeps {
final String code;
final List<ImportDirective> imports = [];

View File

@ -3,13 +3,17 @@ library angular2.src.transform.common.registered_type;
import 'package:analyzer/analyzer.dart';
import 'package:angular2/src/transform/common/names.dart';
/// Represents a call to `Reflector#registerType` generated by
/// `DirectiveProcessor`.
/// A call to `Reflector#registerType` generated by `DirectiveProcessor`.
class RegisteredType {
/// The type registered by this call.
final Identifier typeName;
/// The actual call to `Reflector#registerType`.
final MethodInvocation registerMethod;
/// The factory method registered.
final Expression factory;
/// The parameters registered.
final Expression parameters;
/// The annotations registered.
final Expression annotations;
RegisteredType._(this.typeName, this.registerMethod, this.factory,

View File

@ -23,5 +23,5 @@ void setupReflection(reflector) {
selector: '[tool-tip]', bind: const {'text': 'tool-tip'})
]
})
..registerSetters({'text': (ToolTip o, String value) => o.text = value});
..registerSetters({'text': (o, String v) => o.text = v});
}