refactor(compiler): Break up parseTemplateBindings() for microsyntax (#35812)

This commit is purely a refactoring of the logic in
`parseTemplateBindings` method for parsing the microsyntax expression.
This is done to enable the introduction of `keySpan` and `valueSpan` in
subsequent PR.

For a detailed explanation of this work and the subsequent work items,
please see https://docs.google.com/document/d/1mEVF2pSSMSnOloqOPQTYNiAJO0XQxA1H0BZyESASOrE/edit?usp=sharing

PR Close #35812
This commit is contained in:
Keen Yee Liau 2020-03-02 16:38:55 -08:00 committed by Matias Niemelä
parent 876aa5a78a
commit 716d50aa21
2 changed files with 256 additions and 60 deletions

View File

@ -118,12 +118,38 @@ export class Parser {
span, span.toAbsolute(absoluteOffset), prefix, uninterpretedExpression, location);
}
parseTemplateBindings(tplKey: string, tplValue: string, location: any, absoluteOffset: number):
TemplateBindingParseResult {
const tokens = this._lexer.tokenize(tplValue);
/**
* Parse microsyntax template expression and return a list of bindings or
* parsing errors in case the given expression is invalid.
*
* For example,
* ```
* <div *ngFor="let item of items">
* ^ `absoluteOffset` for `tplValue`
* ```
* contains three bindings:
* 1. ngFor -> null
* 2. item -> NgForOfContext.$implicit
* 3. ngForOf -> items
*
* This is apparent from the de-sugared template:
* ```
* <ng-template ngFor let-item [ngForOf]="items">
* ```
*
* @param templateKey name of directive, without the * prefix. For example: ngIf, ngFor
* @param templateValue RHS of the microsyntax attribute
* @param templateUrl template filename if it's external, component filename if it's inline
* @param absoluteOffset absolute offset of the `tplValue`
*/
parseTemplateBindings(
templateKey: string, templateValue: string, templateUrl: string,
absoluteOffset: number): TemplateBindingParseResult {
const tokens = this._lexer.tokenize(templateValue);
return new _ParseAST(
tplValue, location, absoluteOffset, tokens, tplValue.length, false, this.errors, 0)
.parseTemplateBindings(tplKey);
templateValue, templateUrl, absoluteOffset, tokens, templateValue.length,
false /* parseAction */, this.errors, 0 /* relative offset */)
.parseTemplateBindings(templateKey);
}
parseInterpolation(
@ -721,11 +747,12 @@ export class _ParseAST {
}
/**
* An identifier, a keyword, a string with an optional `-` in between.
* Parses an identifier, a keyword, a string with an optional `-` in between.
*/
expectTemplateBindingKey(): string {
expectTemplateBindingKey(): {key: string, keySpan: ParseSpan} {
let result = '';
let operatorFound = false;
const start = this.inputIndex;
do {
result += this.expectIdentifierOrKeywordOrString();
operatorFound = this.optionalOperator('-');
@ -733,67 +760,190 @@ export class _ParseAST {
result += '-';
}
} while (operatorFound);
return result.toString();
return {
key: result,
keySpan: new ParseSpan(start, start + result.length),
};
}
// Parses the AST for `<some-tag *tplKey=AST>`
parseTemplateBindings(tplKey: string): TemplateBindingParseResult {
let firstBinding = true;
/**
* Parse microsyntax template expression and return a list of bindings or
* parsing errors in case the given expression is invalid.
*
* For example,
* ```
* <div *ngFor="let item of items; index as i; trackBy: func">
* ```
* contains five bindings:
* 1. ngFor -> null
* 2. item -> NgForOfContext.$implicit
* 3. ngForOf -> items
* 4. i -> NgForOfContext.index
* 5. ngForTrackBy -> func
*
* For a full description of the microsyntax grammar, see
* https://gist.github.com/mhevery/d3530294cff2e4a1b3fe15ff75d08855
*
* @param templateKey name of the microsyntax directive, like ngIf, ngFor, without the *
*/
parseTemplateBindings(templateKey: string): TemplateBindingParseResult {
const bindings: TemplateBinding[] = [];
const warnings: string[] = [];
do {
const start = this.inputIndex;
let rawKey: string;
let key: string;
let isVar: boolean = false;
if (firstBinding) {
rawKey = key = tplKey;
firstBinding = false;
// The first binding is for the template key itself
// In *ngFor="let item of items", key = "ngFor", value = null
// In *ngIf="cond | pipe", key = "ngIf", value = "cond | pipe"
bindings.push(...this.parseDirectiveKeywordBindings(
templateKey, new ParseSpan(0, templateKey.length), this.absoluteOffset));
while (this.index < this.tokens.length) {
// If it starts with 'let', then this must be variable declaration
const letBinding = this.parseLetBinding();
if (letBinding) {
bindings.push(letBinding);
} else {
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 = null;
if (isVar) {
if (this.optionalOperator('=')) {
name = this.expectTemplateBindingKey();
// Two possible cases here, either `value "as" key` or
// "directive-keyword expression". We don't know which case, but both
// "value" and "directive-keyword" are template binding key, so consume
// the key first.
const {key, keySpan} = this.expectTemplateBindingKey();
// Peek at the next token, if it is "as" then this must be variable
// declaration.
const binding = this.parseAsBinding(key, keySpan, this.absoluteOffset);
if (binding) {
bindings.push(binding);
} else {
name = '\$implicit';
// Otherwise the key must be a directive keyword, like "of". Transform
// the key to actual key. Eg. of -> ngForOf, trackBy -> ngForTrackBy
const actualKey = templateKey + key[0].toUpperCase() + key.substring(1);
bindings.push(
...this.parseDirectiveKeywordBindings(actualKey, keySpan, this.absoluteOffset));
}
} else if (this.peekKeywordAs()) {
this.advance(); // consume `as`
name = rawKey;
key = this.expectTemplateBindingKey(); // read local var name
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.absoluteOffset + start, this.errors);
}
this.consumeStatementTerminator();
}
bindings.push(new TemplateBinding(
this.span(start), this.sourceSpan(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
bindings.push(new TemplateBinding(
this.span(letStart), this.sourceSpan(letStart), letName, true, key, null !));
}
if (!this.optionalCharacter(chars.$SEMICOLON)) {
this.optionalCharacter(chars.$COMMA);
}
} while (this.index < this.tokens.length);
return new TemplateBindingParseResult(bindings, [] /* warnings */, this.errors);
}
return new TemplateBindingParseResult(bindings, warnings, this.errors);
/**
* Parse a directive keyword, followed by a mandatory expression.
* For example, "of items", "trackBy: func".
* The bindings are: ngForOf -> items, ngForTrackBy -> func
* There could be an optional "as" binding that follows the expression.
* For example,
* ```
* *ngFor="let item of items | slice:0:1 as collection".`
* ^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
* keyword bound target optional 'as' binding
* ```
*
* @param key binding key, for example, ngFor, ngIf, ngForOf
* @param keySpan span of the key in the expression. keySpan might be different
* from `key.length`. For example, the span for key "ngForOf" is "of".
* @param absoluteOffset absolute offset of the attribute value
*/
private parseDirectiveKeywordBindings(key: string, keySpan: ParseSpan, absoluteOffset: number):
TemplateBinding[] {
const bindings: TemplateBinding[] = [];
this.optionalCharacter(chars.$COLON); // trackBy: trackByFunction
const valueExpr = this.getDirectiveBoundTarget();
const span = new ParseSpan(keySpan.start, this.inputIndex);
bindings.push(new TemplateBinding(
span, span.toAbsolute(absoluteOffset), key, false /* keyIsVar */, valueExpr?.source || '', valueExpr));
// The binding could optionally be followed by "as". For example,
// *ngIf="cond | pipe as x". In this case, the key in the "as" binding
// is "x" and the value is the template key itself ("ngIf"). Note that the
// 'key' in the current context now becomes the "value" in the next binding.
const asBinding = this.parseAsBinding(key, keySpan, absoluteOffset);
if (asBinding) {
bindings.push(asBinding);
}
this.consumeStatementTerminator();
return bindings;
}
/**
* Return the expression AST for the bound target of a directive keyword
* binding. For example,
* ```
* *ngIf="condition | pipe".
* ^^^^^^^^^^^^^^^^ bound target for "ngIf"
* *ngFor="let item of items"
* ^^^^^ bound target for "ngForOf"
* ```
*/
private getDirectiveBoundTarget(): ASTWithSource|null {
if (this.next === EOF || this.peekKeywordAs() || this.peekKeywordLet()) {
return null;
}
const ast = this.parsePipe(); // example: "condition | async"
const {start, end} = ast.span;
const value = this.input.substring(start, end);
return new ASTWithSource(ast, value, this.location, this.absoluteOffset + start, this.errors);
}
/**
* Return the binding for a variable declared using `as`. Note that the order
* of the key-value pair in this declaration is reversed. For example,
* ```
* *ngFor="let item of items; index as i"
* ^^^^^ ^
* value key
* ```
*
* @param value name of the value in the declaration, "ngIf" in the example above
* @param valueSpan span of the value in the declaration
* @param absoluteOffset absolute offset of `value`
*/
private parseAsBinding(value: string, valueSpan: ParseSpan, absoluteOffset: number):
TemplateBinding|null {
if (!this.peekKeywordAs()) {
return null;
}
this.advance(); // consume the 'as' keyword
const {key} = this.expectTemplateBindingKey();
const valueAst = new AST(valueSpan, valueSpan.toAbsolute(absoluteOffset));
const valueExpr = new ASTWithSource(
valueAst, value, this.location, absoluteOffset + valueSpan.start, this.errors);
const span = new ParseSpan(valueSpan.start, this.inputIndex);
return new TemplateBinding(
span, span.toAbsolute(absoluteOffset), key, true /* keyIsVar */, value, valueExpr);
}
/**
* Return the binding for a variable declared using `let`. For example,
* ```
* *ngFor="let item of items; let i=index;"
* ^^^^^^^^ ^^^^^^^^^^^
* ```
* In the first binding, `item` is bound to `NgForOfContext.$implicit`.
* In the second binding, `i` is bound to `NgForOfContext.index`.
*/
private parseLetBinding(): TemplateBinding|null {
if (!this.peekKeywordLet()) {
return null;
}
const spanStart = this.inputIndex;
this.advance(); // consume the 'let' keyword
const {key} = this.expectTemplateBindingKey();
let valueExpr: ASTWithSource|null = null;
if (this.optionalOperator('=')) {
const {key: value, keySpan: valueSpan} = this.expectTemplateBindingKey();
const ast = new AST(valueSpan, valueSpan.toAbsolute(this.absoluteOffset));
valueExpr = new ASTWithSource(
ast, value, this.location, this.absoluteOffset + valueSpan.start, this.errors);
}
const spanEnd = this.inputIndex;
const span = new ParseSpan(spanStart, spanEnd);
return new TemplateBinding(
span, span.toAbsolute(this.absoluteOffset), key, true /* keyIsVar */, valueExpr?.source || '$implicit', valueExpr);
}
/**
* Consume the optional statement terminator: semicolon or comma.
*/
private consumeStatementTerminator() {
this.optionalCharacter(chars.$SEMICOLON) || this.optionalCharacter(chars.$COMMA);
}
error(message: string, index: number|null = null) {
@ -896,4 +1046,4 @@ class IvySimpleExpressionChecker extends SimpleExpressionChecker {
}
visitPrefixNot(ast: PrefixNot, context: any) { ast.expression.visit(this); }
}
}

View File

@ -270,6 +270,14 @@ describe('parser', () => {
binding => binding.expression != null ? binding.expression.source : null);
}
function humanize(bindings: TemplateBinding[]): Array<[string, string | null, boolean]> {
return bindings.map(binding => {
const {key, expression, name, keyIsVar} = binding;
const value = keyIsVar ? name : (expression ? expression.source : expression);
return [key, value, keyIsVar];
});
}
it('should parse a key without a value',
() => { expect(keys(parseTemplateBindings('a', ''))).toEqual(['a']); });
@ -317,6 +325,44 @@ describe('parser', () => {
expect(bindings[0].expression !.location).toEqual('location');
});
it('should support common usage of ngIf', () => {
const bindings = parseTemplateBindings('ngIf', 'cond | pipe as foo, let x; ngIf as y');
expect(humanize(bindings)).toEqual([
// [ key, value, keyIsVar ]
['ngIf', 'cond | pipe ', false],
['foo', 'ngIf', true],
['x', '$implicit', true],
['y', 'ngIf', true],
]);
});
it('should support common usage of ngFor', () => {
let bindings: TemplateBinding[];
bindings = parseTemplateBindings(
'ngFor', 'let item; of items | slice:0:1 as collection, trackBy: func; index as i');
expect(humanize(bindings)).toEqual([
// [ key, value, keyIsVar ]
['ngFor', null, false],
['item', '$implicit', true],
['ngForOf', 'items | slice:0:1 ', false],
['collection', 'ngForOf', true],
['ngForTrackBy', 'func', false],
['i', 'index', true],
]);
bindings = parseTemplateBindings(
'ngFor', 'let item, of: [1,2,3] | pipe as items; let i=index, count as len');
expect(humanize(bindings)).toEqual([
// [ key, value, keyIsVar ]
['ngFor', null, false],
['item', '$implicit', true],
['ngForOf', '[1,2,3] | pipe ', false],
['items', 'ngForOf', true],
['i', 'index', true],
['len', 'count', true],
]);
});
it('should support let notation', () => {
let bindings = parseTemplateBindings('key', 'let i');
expect(keyValues(bindings)).toEqual(['key', 'let i=$implicit']);