refactor(compiler): refactor template binding parsing (#23460)

A long time ago Angular used to support both those attribute notations:
- `*attr='binding'`
- `template=`attr: binding`

Because the last notation has been dropped we can refactor the binding parsing.
Source maps will benefit from that as no `attr:` prefix is added artificialy any
more.

PR Close #23460
This commit is contained in:
Victor Berchet 2018-04-19 17:23:27 -07:00
parent ca776c59dd
commit 4662878a1f
7 changed files with 557 additions and 573 deletions

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CommonModule, NgForOf} from '@angular/common';
import {Component, Directive} from '@angular/core';
import {CommonModule} from '@angular/common';
import {Component} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers';

View File

@ -215,7 +215,7 @@ export class ASTWithSource extends AST {
export class TemplateBinding {
constructor(
public span: ParseSpan, public key: string, public keyIsVar: boolean, public name: string,
public expression: ASTWithSource) {}
public expression: ASTWithSource|null) {}
}
export interface AstVisitor {

View File

@ -98,19 +98,11 @@ export class Parser {
return new Quote(new ParseSpan(0, input.length), prefix, uninterpretedExpression, location);
}
parseTemplateBindings(prefixToken: string|null, input: string, location: any):
parseTemplateBindings(tplKey: string, tplValue: string, location: any):
TemplateBindingParseResult {
const tokens = this._lexer.tokenize(input);
if (prefixToken) {
// Prefix the tokens with the tokens from prefixToken but have them take no space (0 index).
const prefixTokens = this._lexer.tokenize(prefixToken).map(t => {
t.index = 0;
return t;
});
tokens.unshift(...prefixTokens);
}
return new _ParseAST(input, location, tokens, input.length, false, this.errors, 0)
.parseTemplateBindings();
const tokens = this._lexer.tokenize(tplValue);
return new _ParseAST(tplValue, location, tokens, tplValue.length, false, this.errors, 0)
.parseTemplateBindings(tplKey);
}
parseInterpolation(
@ -686,48 +678,49 @@ export class _ParseAST {
return result.toString();
}
parseTemplateBindings(): TemplateBindingParseResult {
// Parses the AST for `<some-tag *tplKey=AST>`
parseTemplateBindings(tplKey: string): TemplateBindingParseResult {
let firstBinding = true;
const bindings: TemplateBinding[] = [];
let prefix: string = null !;
const warnings: string[] = [];
while (this.index < this.tokens.length) {
do {
const start = this.inputIndex;
let keyIsVar: boolean = this.peekKeywordLet();
if (keyIsVar) {
this.advance();
}
let rawKey = this.expectTemplateBindingKey();
let key = rawKey;
if (!keyIsVar) {
if (prefix == null) {
prefix = key;
let rawKey: string;
let key: string;
let isVar: boolean = false;
if (firstBinding) {
rawKey = key = tplKey;
firstBinding = false;
} else {
key = prefix + key[0].toUpperCase() + key.substring(1);
}
}
isVar = this.peekKeywordLet();
if (isVar) this.advance()
rawKey = this.expectTemplateBindingKey();
key = isVar ? rawKey : tplKey + rawKey[0].toUpperCase() + rawKey.substring(1);
this.optionalCharacter(chars.$COLON);
}
let name: string = null !;
let expression: ASTWithSource = null !;
if (keyIsVar) {
let expression: ASTWithSource|null = null;
if (isVar) {
if (this.optionalOperator('=')) {
name = this.expectTemplateBindingKey();
} else {
name = '\$implicit';
}
} else if (this.peekKeywordAs()) {
const letStart = this.inputIndex;
this.advance(); // consume `as`
name = rawKey;
key = this.expectTemplateBindingKey(); // read local var name
keyIsVar = true;
isVar = true;
} else if (this.next !== EOF && !this.peekKeywordLet()) {
const start = this.inputIndex;
const ast = this.parsePipe();
const source = this.input.substring(start - this.offset, this.inputIndex - this.offset);
expression = new ASTWithSource(ast, source, this.location, this.errors);
}
bindings.push(new TemplateBinding(this.span(start), key, keyIsVar, name, expression));
if (this.peekKeywordAs() && !keyIsVar) {
bindings.push(new TemplateBinding(this.span(start), key, isVar, name, expression));
if (this.peekKeywordAs() && !isVar) {
const letStart = this.inputIndex;
this.advance(); // consume `as`
const letName = this.expectTemplateBindingKey(); // read local var name
@ -736,7 +729,8 @@ export class _ParseAST {
if (!this.optionalCharacter(chars.$SEMICOLON)) {
this.optionalCharacter(chars.$COMMA);
}
}
} while (this.index < this.tokens.length)
return new TemplateBindingParseResult(bindings, warnings, this.errors);
}

View File

@ -107,16 +107,16 @@ export class HtmlToTemplateTransform implements html.Visitor {
}
isTemplateBinding = true;
elementHasInlineTemplate = true;
const templateBindingsSource = attribute.value;
const prefixToken = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length) + ':';
const templateValue = attribute.value;
const templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length);
const oldVariables: VariableAst[] = [];
inlineTemplateSourceSpan = attribute.valueSpan || attribute.sourceSpan;
this.bindingParser.parseInlineTemplateBinding(
prefixToken !, templateBindingsSource !, attribute.sourceSpan,
templateMatchableAttributes, templateBoundProperties, oldVariables);
templateKey, templateValue, attribute.sourceSpan, templateMatchableAttributes,
templateBoundProperties, oldVariables);
templateVariables.push(
...oldVariables.map(v => new t.Variable(v.name, v.value, v.sourceSpan)));

View File

@ -130,11 +130,12 @@ export class BindingParser {
}
}
// Parse an inline template binding. ie `<tag *prefixToken="<value>">`
// Parse an inline template binding. ie `<tag *tplKey="<tplValue>">`
parseInlineTemplateBinding(
prefixToken: string, value: string, sourceSpan: ParseSourceSpan,
tplKey: string, tplValue: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetProps: BoundProperty[], targetVars: VariableAst[]) {
const bindings = this._parseTemplateBindings(prefixToken, value, sourceSpan);
const bindings = this._parseTemplateBindings(tplKey, tplValue, sourceSpan);
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i];
if (binding.keyIsVar) {
@ -149,12 +150,12 @@ export class BindingParser {
}
}
private _parseTemplateBindings(prefixToken: string, value: string, sourceSpan: ParseSourceSpan):
private _parseTemplateBindings(tplKey: string, tplValue: string, sourceSpan: ParseSourceSpan):
TemplateBinding[] {
const sourceInfo = sourceSpan.start.toString();
try {
const bindingsResult = this._exprParser.parseTemplateBindings(prefixToken, value, sourceInfo);
const bindingsResult = this._exprParser.parseTemplateBindings(tplKey, tplValue, sourceInfo);
this._reportExpressionParserErrors(bindingsResult.errors, sourceSpan);
bindingsResult.templateBindings.forEach((binding) => {
if (binding.expression) {

View File

@ -289,16 +289,16 @@ class TemplateParseVisitor implements html.Visitor {
isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, events,
elementOrDirectiveRefs, elementVars);
let templateBindingsSource: string|undefined;
let prefixToken: string|undefined;
let templateValue: string|undefined;
let templateKey: string|undefined;
const normalizedName = this._normalizeAttributeName(attr.name);
if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) {
templateBindingsSource = attr.value;
prefixToken = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length) + ':';
templateValue = attr.value;
templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length);
}
const hasTemplateBinding = templateBindingsSource != null;
const hasTemplateBinding = templateValue != null;
if (hasTemplateBinding) {
if (hasInlineTemplates) {
this._reportError(
@ -307,7 +307,7 @@ class TemplateParseVisitor implements html.Visitor {
}
hasInlineTemplates = true;
this._bindingParser.parseInlineTemplateBinding(
prefixToken !, templateBindingsSource !, attr.sourceSpan, templateMatchableAttrs,
templateKey !, templateValue !, attr.sourceSpan, templateMatchableAttrs,
templateElementOrDirectiveProps, templateElementVars);
}

View File

@ -15,80 +15,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
import {unparse} from './unparser';
import {validate} from './validator';
(function() {
function createParser() { return new Parser(new Lexer()); }
function parseAction(text: string, location: any = null): ASTWithSource {
return createParser().parseAction(text, location);
}
function parseBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseBinding(text, location);
}
function parseTemplateBindingsResult(
text: string, location: any = null, prefix?: string): TemplateBindingParseResult {
return createParser().parseTemplateBindings(prefix || null, text, location);
}
function parseTemplateBindings(
text: string, location: any = null, prefix?: string): TemplateBinding[] {
return parseTemplateBindingsResult(text, location, prefix).templateBindings;
}
function parseInterpolation(text: string, location: any = null): ASTWithSource|null {
return createParser().parseInterpolation(text, location);
}
function splitInterpolation(text: string, location: any = null): SplitInterpolation|null {
return createParser().splitInterpolation(text, location);
}
function parseSimpleBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseSimpleBinding(text, location);
}
function checkInterpolation(exp: string, expected?: string) {
const ast = parseInterpolation(exp) !;
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkBinding(exp: string, expected?: string) {
const ast = parseBinding(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkAction(exp: string, expected?: string) {
const ast = parseAction(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function expectError(ast: {errors: ParserError[]}, message: string) {
for (const error of ast.errors) {
if (error.message.indexOf(message) >= 0) {
return;
}
}
const errMsgs = ast.errors.map(err => err.message).join('\n');
throw Error(
`Expected an error containing "${message}" to be reported, but got the errors:\n` +
errMsgs);
}
function expectActionError(text: string, message: string) {
expectError(validate(parseAction(text)), message);
}
function expectBindingError(text: string, message: string) {
expectError(validate(parseBinding(text)), message);
}
describe('parser', () => {
describe('parser', () => {
describe('parseAction', () => {
it('should parse numbers', () => { checkAction('1'); });
@ -321,9 +248,7 @@ import {validate} from './validator';
describe('parseTemplateBindings', () => {
function keys(templateBindings: any[]) {
return templateBindings.map(binding => binding.key);
}
function keys(templateBindings: any[]) { return templateBindings.map(binding => binding.key); }
function keyValues(templateBindings: any[]) {
return templateBindings.map(binding => {
@ -345,131 +270,126 @@ import {validate} from './validator';
binding => binding.expression != null ? binding.expression.source : null);
}
it('should parse an empty string', () => { expect(parseTemplateBindings('')).toEqual([]); });
it('should parse a key without a value',
() => { expect(keys(parseTemplateBindings('a', ''))).toEqual(['a']); });
it('should parse a string without a value',
() => { expect(keys(parseTemplateBindings('a'))).toEqual(['a']); });
it('should only allow identifier, string, or keyword including dashes as keys', () => {
let bindings = parseTemplateBindings('a:\'b\'');
it('should allow string including dashes as keys', () => {
let bindings = parseTemplateBindings('a', 'b');
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings('\'a\':\'b\'');
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings('"a":\'b\'');
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings('a-b:\'c\'');
bindings = parseTemplateBindings('a-b', 'c');
expect(keys(bindings)).toEqual(['a-b']);
expectError(parseTemplateBindingsResult('(:0'), 'expected identifier, keyword, or string');
expectError(
parseTemplateBindingsResult('1234:0'), 'expected identifier, keyword, or string');
});
it('should detect expressions as value', () => {
let bindings = parseTemplateBindings('a:b');
let bindings = parseTemplateBindings('a', 'b');
expect(exprSources(bindings)).toEqual(['b']);
bindings = parseTemplateBindings('a:1+1');
bindings = parseTemplateBindings('a', '1+1');
expect(exprSources(bindings)).toEqual(['1+1']);
});
it('should detect names as value', () => {
const bindings = parseTemplateBindings('a:let b');
expect(keyValues(bindings)).toEqual(['a', 'let b=\$implicit']);
const bindings = parseTemplateBindings('a', 'let b');
expect(keyValues(bindings)).toEqual(['a', 'let b=$implicit']);
});
it('should allow space and colon as separators', () => {
let bindings = parseTemplateBindings('a:b');
expect(keys(bindings)).toEqual(['a']);
expect(exprSources(bindings)).toEqual(['b']);
bindings = parseTemplateBindings('a b');
let bindings = parseTemplateBindings('a', 'b');
expect(keys(bindings)).toEqual(['a']);
expect(exprSources(bindings)).toEqual(['b']);
});
it('should allow multiple pairs', () => {
const bindings = parseTemplateBindings('a 1 b 2');
const bindings = parseTemplateBindings('a', '1 b 2');
expect(keys(bindings)).toEqual(['a', 'aB']);
expect(exprSources(bindings)).toEqual(['1 ', '2']);
});
it('should store the sources in the result', () => {
const bindings = parseTemplateBindings('a 1,b 2');
expect(bindings[0].expression.source).toEqual('1');
expect(bindings[1].expression.source).toEqual('2');
const bindings = parseTemplateBindings('a', '1,b 2');
expect(bindings[0].expression !.source).toEqual('1');
expect(bindings[1].expression !.source).toEqual('2');
});
it('should store the passed-in location', () => {
const bindings = parseTemplateBindings('a 1,b 2', 'location');
expect(bindings[0].expression.location).toEqual('location');
const bindings = parseTemplateBindings('a', '1,b 2', 'location');
expect(bindings[0].expression !.location).toEqual('location');
});
it('should support let notation', () => {
let bindings = parseTemplateBindings('let i');
expect(keyValues(bindings)).toEqual(['let i=\$implicit']);
let bindings = parseTemplateBindings('key', 'let i');
expect(keyValues(bindings)).toEqual(['key', 'let i=$implicit']);
bindings = parseTemplateBindings('let i');
expect(keyValues(bindings)).toEqual(['let i=\$implicit']);
bindings = parseTemplateBindings('let a; let b');
expect(keyValues(bindings)).toEqual(['let a=\$implicit', 'let b=\$implicit']);
bindings = parseTemplateBindings('let a; let b;');
expect(keyValues(bindings)).toEqual(['let a=\$implicit', 'let b=\$implicit']);
bindings = parseTemplateBindings('let i-a = k-a');
expect(keyValues(bindings)).toEqual(['let i-a=k-a']);
bindings = parseTemplateBindings('keyword let item; let i = k');
expect(keyValues(bindings)).toEqual(['keyword', 'let item=\$implicit', 'let i=k']);
bindings = parseTemplateBindings('keyword: let item; let i = k');
expect(keyValues(bindings)).toEqual(['keyword', 'let item=\$implicit', 'let i=k']);
bindings = parseTemplateBindings('directive: let item in expr; let a = b', 'location');
bindings = parseTemplateBindings('key', 'let a; let b');
expect(keyValues(bindings)).toEqual([
'directive', 'let item=\$implicit', 'directiveIn=expr in location', 'let a=b'
'key',
'let a=$implicit',
'let b=$implicit',
]);
bindings = parseTemplateBindings('key', 'let a; let b;');
expect(keyValues(bindings)).toEqual([
'key',
'let a=$implicit',
'let b=$implicit',
]);
bindings = parseTemplateBindings('key', 'let i-a = k-a');
expect(keyValues(bindings)).toEqual([
'key',
'let i-a=k-a',
]);
bindings = parseTemplateBindings('key', 'let item; let i = k');
expect(keyValues(bindings)).toEqual([
'key',
'let item=$implicit',
'let i=k',
]);
bindings = parseTemplateBindings('directive', 'let item in expr; let a = b', 'location');
expect(keyValues(bindings)).toEqual([
'directive',
'let item=$implicit',
'directiveIn=expr in location',
'let a=b',
]);
});
it('should support as notation', () => {
let bindings = parseTemplateBindings('ngIf exp as local', 'location');
let bindings = parseTemplateBindings('ngIf', 'exp as local', 'location');
expect(keyValues(bindings)).toEqual(['ngIf=exp in location', 'let local=ngIf']);
bindings = parseTemplateBindings('ngFor let item of items as iter; index as i', 'L');
bindings = parseTemplateBindings('ngFor', 'let item of items as iter; index as i', 'L');
expect(keyValues(bindings)).toEqual([
'ngFor', 'let item=$implicit', 'ngForOf=items in L', 'let iter=ngForOf', 'let i=index'
]);
});
it('should parse pipes', () => {
const bindings = parseTemplateBindings('key value|pipe');
const ast = bindings[0].expression.ast;
const bindings = parseTemplateBindings('key', 'value|pipe');
const ast = bindings[0].expression !.ast;
expect(ast).toBeAnInstanceOf(BindingPipe);
});
describe('spans', () => {
it('should should support let', () => {
const source = 'let i';
expect(keySpans(source, parseTemplateBindings(source))).toEqual(['let i']);
expect(keySpans(source, parseTemplateBindings('key', 'let i'))).toEqual(['', 'let i']);
});
it('should support multiple lets', () => {
const source = 'let item; let i=index; let e=even;';
expect(keySpans(source, parseTemplateBindings(source))).toEqual([
'let item', 'let i=index', 'let e=even'
expect(keySpans(source, parseTemplateBindings('key', source))).toEqual([
'', 'let item', 'let i=index', 'let e=even'
]);
});
it('should support a prefix', () => {
const source = 'let person of people';
const prefix = 'ngFor';
const bindings = parseTemplateBindings(source, null, prefix);
const bindings = parseTemplateBindings(prefix, source);
expect(keyValues(bindings)).toEqual([
'ngFor', 'let person=$implicit', 'ngForOf=people in null'
]);
@ -506,8 +426,7 @@ import {validate} from './validator';
'Parser Error: Blank expressions are not allowed in interpolated strings');
});
it('should parse conditional expression',
() => { checkInterpolation('{{ a < b ? a : b }}'); });
it('should parse conditional expression', () => { checkInterpolation('{{ a < b ? a : b }}'); });
it('should parse expression with newline characters', () => {
checkInterpolation(`{{ 'foo' +\n 'bar' +\r 'baz' }}`, `{{ "foo" + "bar" + "baz" }}`);
@ -515,8 +434,7 @@ import {validate} from './validator';
it('should support custom interpolation', () => {
const parser = new Parser(new Lexer());
const ast =
parser.parseInterpolation('{% a %}', null, {start: '{%', end: '%}'}) !.ast as any;
const ast = parser.parseInterpolation('{% a %}', null, {start: '{%', end: '%}'}) !.ast as any;
expect(ast.strings).toEqual(['', '']);
expect(ast.expressions.length).toEqual(1);
expect(ast.expressions[0].name).toEqual('a');
@ -574,8 +492,7 @@ import {validate} from './validator';
describe('wrapLiteralPrimitive', () => {
it('should wrap a literal primitive', () => {
expect(unparse(validate(createParser().wrapLiteralPrimitive('foo', null))))
.toEqual('"foo"');
expect(unparse(validate(createParser().wrapLiteralPrimitive('foo', null)))).toEqual('"foo"');
});
});
@ -605,5 +522,77 @@ import {validate} from './validator';
expect(interpolation.expressions.map(e => e.span.start)).toEqual([2, 9, 16]);
});
});
});
})();
});
function createParser() {
return new Parser(new Lexer());
}
function parseAction(text: string, location: any = null): ASTWithSource {
return createParser().parseAction(text, location);
}
function parseBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseBinding(text, location);
}
function parseTemplateBindingsResult(
key: string, value: string, location: any = null): TemplateBindingParseResult {
return createParser().parseTemplateBindings(key, value, location);
}
function parseTemplateBindings(
key: string, value: string, location: any = null): TemplateBinding[] {
return parseTemplateBindingsResult(key, value, location).templateBindings;
}
function parseInterpolation(text: string, location: any = null): ASTWithSource|null {
return createParser().parseInterpolation(text, location);
}
function splitInterpolation(text: string, location: any = null): SplitInterpolation|null {
return createParser().splitInterpolation(text, location);
}
function parseSimpleBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseSimpleBinding(text, location);
}
function checkInterpolation(exp: string, expected?: string) {
const ast = parseInterpolation(exp) !;
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkBinding(exp: string, expected?: string) {
const ast = parseBinding(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkAction(exp: string, expected?: string) {
const ast = parseAction(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function expectError(ast: {errors: ParserError[]}, message: string) {
for (const error of ast.errors) {
if (error.message.indexOf(message) >= 0) {
return;
}
}
const errMsgs = ast.errors.map(err => err.message).join('\n');
throw Error(
`Expected an error containing "${message}" to be reported, but got the errors:\n` + errMsgs);
}
function expectActionError(text: string, message: string) {
expectError(validate(parseAction(text)), message);
}
function expectBindingError(text: string, message: string) {
expectError(validate(parseBinding(text)), message);
}