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; library angular2.src.transform.bind_generator.generator;
import 'package:analyzer/analyzer.dart'; import 'dart:async';
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';
String createNgSetters(String code, String path) { import 'package:angular2/src/transform/common/asset_reader.dart';
if (_noSettersPresent(code)) return code; import 'package:angular2/src/transform/common/parser.dart';
import 'package:barback/barback.dart';
var writer = new PrintStringWriter(); import 'visitor.dart';
parseCompilationUnit(
code, name: path).accept(new CreateNgSettersVisitor(writer)); Future<String> createNgSetters(AssetReader reader, AssetId entryPoint) async {
return writer.toString(); 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; /// Consumes the map generated by [_createBindMap] to codegen setters.
List<String> _generateSetters(Map<String, String> bindMap) {
class CreateNgSettersVisitor extends ToSourceVisitor with VisitorMixin { var setters = [];
final PrintWriter writer; // TODO(kegluneq): Include types for receivers. See #886.
final _ExtractSettersVisitor extractVisitor = new _ExtractSettersVisitor(); bindMap.forEach((prop, type) {
setters.add('\'$prop\': (o, String v) => o.$prop = v');
CreateNgSettersVisitor(PrintWriter writer) });
: this.writer = writer, return setters;
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);
}
} }
/// Visitor responsible for crawling the "annotations" value in a /// Collapses all `bindProperties` in [ngDeps] into a map where the keys are
/// `registerType` call and generating setters from any "bind" values found. /// the bind properties and the values are either the one and only type
class _ExtractSettersVisitor extends Object /// binding to that property or the empty string.
with RecursiveAstVisitor<Object>, VisitorMixin { Map<String, String> _createBindMap(NgDeps ngDeps) {
final List<String> bindPieces = []; var visitor = new ExtractSettersVisitor();
Identifier currentName = null; var bindMap = {};
ngDeps.registeredTypes.forEach((RegisteredType t) {
void _extractFromMapLiteral(MapLiteral map) { visitor.bindMappings.clear();
if (currentName == null) { t.annotations.accept(visitor);
logger.error('Unexpected code path: `currentName` should never be null'); visitor.bindMappings.forEach((String prop, _) {
} if (bindMap.containsKey(prop)) {
map.entries.forEach((entry) { bindMap[prop] = '';
// 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');
} else { } else {
logger.error('`bind` currently only supports string literals'); bindMap[prop] = '${t.typeName}';
} }
}); });
} });
return bindMap;
@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);
}
} }

View File

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

View File

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

View File

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

View File

@ -23,5 +23,5 @@ void setupReflection(reflector) {
selector: '[tool-tip]', bind: const {'text': 'tool-tip'}) 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});
} }