feat(dart/transform): Improve constant evaluation

Use `package:analyzer`'s `ConstantEvaluator` to read from the AST.
This cleanly builds values for us from adjacent strings, interpolations,
etc.
This commit is contained in:
Tim Blasi 2015-05-26 16:53:14 -07:00
parent a9be2ebf1b
commit 5d2af54730
11 changed files with 114 additions and 82 deletions

View File

@ -8,29 +8,20 @@ import 'package:angular2/src/transform/common/logging.dart';
/// values found. /// values found.
class ExtractSettersVisitor extends Object with RecursiveAstVisitor<Object> { class ExtractSettersVisitor extends Object with RecursiveAstVisitor<Object> {
final Map<String, String> bindMappings = {}; final Map<String, String> bindMappings = {};
final ConstantEvaluator _evaluator = new ConstantEvaluator();
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('`properties` currently only supports string literals '
'(${entry})');
}
});
}
@override @override
Object visitNamedExpression(NamedExpression node) { Object visitNamedExpression(NamedExpression node) {
if ('${node.name.label}' == 'properties') { if ('${node.name.label}' == 'properties') {
// TODO(kegluneq): Remove this restriction. var evaluated = node.expression.accept(_evaluator);
if (node.expression is MapLiteral) { if (evaluated is Map) {
_extractFromMapLiteral(node.expression); evaluated.forEach((key, value) {
if (value != null) {
bindMappings[key] = '$value';
}
});
} else { } else {
logger.error('`properties` currently only supports map literals'); logger.error('`properties` currently only supports Map values');
} }
return null; return null;
} }

View File

@ -49,6 +49,7 @@ num _getDirectiveType(String annotationName, Element element) {
class _DirectiveMetadataVisitor extends Object class _DirectiveMetadataVisitor extends Object
with RecursiveAstVisitor<Object> { with RecursiveAstVisitor<Object> {
DirectiveMetadata meta; DirectiveMetadata meta;
final ConstantEvaluator _evaluator = new ConstantEvaluator();
void _createEmptyMetadata(num type) { void _createEmptyMetadata(num type) {
assert(type >= 0); assert(type >= 0);
@ -130,13 +131,12 @@ class _DirectiveMetadataVisitor extends Object
} }
String _expressionToString(Expression node, String nodeDescription) { String _expressionToString(Expression node, String nodeDescription) {
// TODO(kegluneq): Accept more options. var value = node.accept(_evaluator);
if (node is! SimpleStringLiteral) { if (value is! String) {
throw new FormatException( throw new FormatException('Angular 2 could not understand the value '
'Angular 2 currently only supports string literals '
'in $nodeDescription.', '$node' /* source */); 'in $nodeDescription.', '$node' /* source */);
} }
return stringLiteralToString(node); return value;
} }
void _populateSelector(Expression selectorValue) { void _populateSelector(Expression selectorValue) {
@ -153,72 +153,52 @@ class _DirectiveMetadataVisitor extends Object
void _populateCompileChildren(Expression compileChildrenValue) { void _populateCompileChildren(Expression compileChildrenValue) {
_checkMeta(); _checkMeta();
if (compileChildrenValue is! BooleanLiteral) { var evaluated = compileChildrenValue.accept(_evaluator);
if (evaluated is! bool) {
throw new FormatException( throw new FormatException(
'Angular 2 currently only supports boolean literal values for ' 'Angular 2 expects a bool but could not understand the value for '
'Directive#compileChildren.', '$compileChildrenValue' /* source */); 'Directive#compileChildren.', '$compileChildrenValue' /* source */);
} }
meta.compileChildren = (compileChildrenValue as BooleanLiteral).value; meta.compileChildren = evaluated;
}
/// Evaluates the [Map] represented by `expression` and adds all `key`,
/// `value` pairs to `map`. If `expression` does not evaluate to a [Map],
/// throws a descriptive [FormatException].
void _populateMap(Expression expression, Map map, String propertyName) {
var evaluated = expression.accept(_evaluator);
if (evaluated is! Map) {
throw new FormatException(
'Angular 2 expects a Map but could not understand the value for '
'$propertyName.', '$expression' /* source */);
}
evaluated.forEach((key, value) {
if (value != null) {
map[key] = '$value';
}
});
} }
void _populateProperties(Expression propertiesValue) { void _populateProperties(Expression propertiesValue) {
_checkMeta(); _checkMeta();
if (propertiesValue is! MapLiteral) { _populateMap(propertiesValue, meta.properties, 'Directive#properties');
throw new FormatException(
'Angular 2 currently only supports map literal values for '
'Directive#properties.', '$propertiesValue' /* source */);
}
for (MapLiteralEntry entry in (propertiesValue as MapLiteral).entries) {
var sKey = _expressionToString(entry.key, 'Directive#properties keys');
var sVal = _expressionToString(entry.value, 'Direcive#properties values');
meta.properties[sKey] = sVal;
}
} }
void _populateHostListeners(Expression hostListenersValue) { void _populateHostListeners(Expression hostListenersValue) {
_checkMeta(); _checkMeta();
if (hostListenersValue is! MapLiteral) { _populateMap(
throw new FormatException( hostListenersValue, meta.hostListeners, 'Directive#hostListeners');
'Angular 2 currently only supports map literal values for '
'Directive#hostListeners.', '$hostListenersValue' /* source */);
}
for (MapLiteralEntry entry in (hostListenersValue as MapLiteral).entries) {
var sKey = _expressionToString(entry.key, 'Directive#hostListeners keys');
var sVal =
_expressionToString(entry.value, 'Directive#hostListeners values');
meta.hostListeners[sKey] = sVal;
}
} }
void _populateHostProperties(Expression hostPropertyValue) { void _populateHostProperties(Expression hostPropertyValue) {
_checkMeta(); _checkMeta();
if (hostPropertyValue is! MapLiteral) { _populateMap(
throw new FormatException( hostPropertyValue, meta.hostProperties, 'Directive#hostProperties');
'Angular 2 currently only supports map literal values for '
'Directive#hostProperties.', '$hostPropertyValue' /* source */);
}
for (MapLiteralEntry entry in (hostPropertyValue as MapLiteral).entries) {
var sKey =
_expressionToString(entry.key, 'Directive#hostProperties keys');
var sVal =
_expressionToString(entry.value, 'Directive#hostProperties values');
meta.hostProperties[sKey] = sVal;
}
} }
void _populateHostAttributes(Expression hostAttributeValue) { void _populateHostAttributes(Expression hostAttributeValue) {
_checkMeta(); _checkMeta();
if (hostAttributeValue is! MapLiteral) { _populateMap(
throw new FormatException( hostAttributeValue, meta.hostAttributes, 'Directive#hostAttributes');
'Angular 2 currently only supports map literal values for '
'Directive#hostAttributes.', '$hostAttributeValue' /* source */);
}
for (MapLiteralEntry entry in (hostAttributeValue as MapLiteral).entries) {
var sKey =
_expressionToString(entry.key, 'Directive#hostAttributes keys');
var sVal =
_expressionToString(entry.value, 'Directive#hostAttributes values');
meta.hostAttributes[sKey] = sVal;
}
} }
} }

View File

@ -210,6 +210,7 @@ bool _isViewAnnotation(Annotation node) => '${node.name}' == 'View';
class AnnotationsTransformVisitor extends ToSourceVisitor { class AnnotationsTransformVisitor extends ToSourceVisitor {
final AsyncStringWriter writer; final AsyncStringWriter writer;
final XHR _xhr; final XHR _xhr;
final ConstantEvaluator _evaluator = new ConstantEvaluator();
bool _processingView = false; bool _processingView = false;
AnnotationsTransformVisitor(AsyncStringWriter writer, this._xhr) AnnotationsTransformVisitor(AsyncStringWriter writer, this._xhr)
@ -257,14 +258,15 @@ class AnnotationsTransformVisitor extends ToSourceVisitor {
return super.visitNamedExpression(node); return super.visitNamedExpression(node);
} }
var keyString = '${node.name.label}'; var keyString = '${node.name.label}';
if (keyString == 'templateUrl' && node.expression is SimpleStringLiteral) { if (keyString == 'templateUrl') {
var url = stringLiteralToString(node.expression); var url = node.expression.accept(_evaluator);
writer.print("template: r'''"); if (url is String) {
writer.asyncPrint(_xhr.get(url)); writer.print("template: r'''");
writer.print("'''"); writer.asyncPrint(_xhr.get(url));
return null; writer.print("'''");
} else { return null;
return super.visitNamedExpression(node); }
} }
return super.visitNamedExpression(node);
} }
} }

View File

@ -17,7 +17,6 @@ class CompileStepFactory implements base.CompileStepFactory {
List<CompileStep> createSteps( List<CompileStep> createSteps(
ViewDefinition template, List<Future> subTaskPromises) { ViewDefinition template, List<Future> subTaskPromises) {
// TODO(kegluneq): Add other compile steps from default_steps.dart.
return [ return [
new ViewSplitter(_parser), new ViewSplitter(_parser),
new PropertyBindingParser(_parser), new PropertyBindingParser(_parser),

View File

@ -155,6 +155,7 @@ class _ViewDefinitionCreator {
class _TemplateExtractVisitor extends Object with RecursiveAstVisitor<Object> { class _TemplateExtractVisitor extends Object with RecursiveAstVisitor<Object> {
ViewDefinition viewDef = null; ViewDefinition viewDef = null;
final Map<String, DirectiveMetadata> _metadataMap; final Map<String, DirectiveMetadata> _metadataMap;
final ConstantEvaluator _evaluator = new ConstantEvaluator();
_TemplateExtractVisitor(this._metadataMap); _TemplateExtractVisitor(this._metadataMap);
@ -191,13 +192,13 @@ class _TemplateExtractVisitor extends Object with RecursiveAstVisitor<Object> {
// `templateUrl` property. // `templateUrl` property.
if (viewDef == null) return null; if (viewDef == null) return null;
if (node.expression is! SimpleStringLiteral) { var valueString = node.expression.accept(_evaluator);
if (valueString is! String) {
logger.error( logger.error(
'Angular 2 currently only supports string literals in directives.' 'Angular 2 currently only supports string literals in directives.'
' Source: ${node}'); ' Source: ${node}');
return null; return null;
} }
var valueString = stringLiteralToString(node.expression);
if (keyString == 'templateUrl') { if (keyString == 'templateUrl') {
if (viewDef.absUrl != null) { if (viewDef.absUrl != null) {
logger.error( logger.error(

View File

@ -0,0 +1,16 @@
library bar.ng_deps.dart;
import 'foo.dart';
import 'package:angular2/src/core/annotations/annotations.dart';
var _visited = false;
void initReflector(reflector) {
if (_visited) return;
_visited = true;
reflector
..registerType(FooComponent, {
'factory': () => new FooComponent(),
'parameters': const [],
'annotations': const [const Component(selector: '[fo' 'o]')]
});
}

View File

@ -101,6 +101,17 @@ void allTests() {
expect(extractedMeta.selector).toEqual('[foo]'); expect(extractedMeta.selector).toEqual('[foo]');
}); });
it('should generate `DirectiveMetadata` from .ng_deps.dart files that use '
'automatic adjacent string concatenation.', () async {
var extracted = await extractDirectiveMetadata(reader, new AssetId('a',
'directive_metadata_extractor/adjacent_strings_files/'
'foo.ng_deps.dart'));
expect(extracted).toContain('FooComponent');
var extractedMeta = extracted['FooComponent'];
expect(extractedMeta.selector).toEqual('[foo]');
});
it('should include `DirectiveMetadata` from exported files.', () async { it('should include `DirectiveMetadata` from exported files.', () async {
var extracted = await extractDirectiveMetadata(reader, new AssetId( var extracted = await extractDirectiveMetadata(reader, new AssetId(
'a', 'directive_metadata_extractor/export_files/foo.ng_deps.dart')); 'a', 'directive_metadata_extractor/export_files/foo.ng_deps.dart'));

View File

@ -39,6 +39,9 @@ void allTests() {
_testNgDeps( _testNgDeps(
'should inline `templateUrl` values.', 'url_expression_files/hello.dart'); 'should inline `templateUrl` values.', 'url_expression_files/hello.dart');
_testNgDeps('should inline `templateUrl`s expressed as adjacent strings.',
'split_url_expression_files/hello.dart');
} }
void _testNgDeps(String name, String inputPath, void _testNgDeps(String name, String inputPath,

View File

@ -0,0 +1,20 @@
library examples.src.hello_world.index_common_dart.ng_deps.dart;
import 'hello.dart';
import 'package:angular2/angular2.dart'
show bootstrap, Component, Directive, View, NgElement;
var _visited = false;
void initReflector(reflector) {
if (_visited) return;
_visited = true;
reflector
..registerType(HelloCmp, {
'factory': () => new HelloCmp(),
'parameters': const [],
'annotations': const [
const Component(selector: 'hello-app'),
const View(template: r'''{{greeting}}''')
]
});
}

View File

@ -0,0 +1,8 @@
library examples.src.hello_world.index_common_dart;
import 'package:angular2/angular2.dart'
show bootstrap, Component, Directive, View, NgElement;
@Component(selector: 'hello-app')
@View(templateUrl: 'templ' 'ate.html')
class HelloCmp {}