Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compiler): Add sourceSpan and keySpan to TemplateBinding #35897

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 51 additions & 3 deletions packages/compiler/src/expression_parser/ast.ts
Expand Up @@ -283,10 +283,58 @@ export class ASTWithSource extends AST {
toString(): string { return `${this.source} in ${this.location}`; }
}

export class TemplateBinding {
/**
* TemplateBinding refers to a particular key-value pair in a microsyntax
* expression. A few examples are:
*
* |---------------------|--------------|---------|--------------|
* | expression | key | value | binding type |
* |---------------------|--------------|---------|--------------|
* | 1. let item | item | null | variable |
* | 2. of items | ngForOf | items | expression |
* | 3. let x = y | x | y | variable |
* | 4. index as i | i | index | variable |
* | 5. trackBy: func | ngForTrackBy | func | expression |
* | 6. *ngIf="cond" | ngIf | cond | expression |
* |---------------------|--------------|---------|--------------|
*
* (6) is a notable exception because it is a binding from the template key in
* the LHS of a HTML attribute to the expression in the RHS. All other bindings
* in the example above are derived solely from the RHS.
*/
export type TemplateBinding = VariableBinding | ExpressionBinding;

export class VariableBinding {
/**
* @param sourceSpan entire span of the binding.
kyliau marked this conversation as resolved.
Show resolved Hide resolved
* @param key name of the LHS along with its span.
* @param value optional value for the RHS along with its span.
*/
constructor(
public span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public key: string,
public keyIsVar: boolean, public name: string, public value: ASTWithSource|null) {}
public readonly sourceSpan: AbsoluteSourceSpan,
public readonly key: TemplateBindingIdentifier,
public readonly value: TemplateBindingIdentifier|null) {}
}

export class ExpressionBinding {
/**
* @param sourceSpan entire span of the binding.
* @param key binding name, like ngForOf, ngForTrackBy, ngIf, along with its
* span. Note that the length of the span may not be the same as
* `key.source.length`. For example,
* 1. key.source = ngFor, key.span is for "ngFor"
* 2. key.source = ngForOf, key.span is for "of"
* 3. key.source = ngForTrackBy, key.span is for "trackBy"
* @param value optional expression for the RHS.
*/
constructor(
public readonly sourceSpan: AbsoluteSourceSpan,
public readonly key: TemplateBindingIdentifier, public readonly value: ASTWithSource|null) {}
}

export interface TemplateBindingIdentifier {
source: string;
span: AbsoluteSourceSpan;
}

export interface AstVisitor {
Expand Down
147 changes: 76 additions & 71 deletions packages/compiler/src/expression_parser/parser.ts
Expand Up @@ -10,7 +10,7 @@ import * as chars from '../chars';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
import {escapeRegExp} from '../util';

import {AST, ASTWithSource, AbsoluteSourceSpan, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, ParserError, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast';
import {AST, ASTWithSource, AbsoluteSourceSpan, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, ParserError, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, VariableBinding} from './ast';
import {EOF, Lexer, Token, TokenType, isIdentifier, isQuote} from './lexer';

export class SplitInterpolation {
Expand Down Expand Up @@ -125,7 +125,8 @@ export class Parser {
* For example,
* ```
* <div *ngFor="let item of items">
* ^ `absoluteOffset` for `tplValue`
* ^ ^ absoluteValueOffset for `templateValue`
* absoluteKeyOffset for `templateKey`
* ```
* contains three bindings:
* 1. ngFor -> null
Expand All @@ -140,16 +141,20 @@ export class Parser {
* @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`
* @param absoluteKeyOffset start of the `templateKey`
* @param absoluteValueOffset start of the `templateValue`
*/
parseTemplateBindings(
templateKey: string, templateValue: string, templateUrl: string,
absoluteOffset: number): TemplateBindingParseResult {
templateKey: string, templateValue: string, templateUrl: string, absoluteKeyOffset: number,
absoluteValueOffset: number): TemplateBindingParseResult {
const tokens = this._lexer.tokenize(templateValue);
return new _ParseAST(
templateValue, templateUrl, absoluteOffset, tokens, templateValue.length,
false /* parseAction */, this.errors, 0 /* relative offset */)
.parseTemplateBindings(templateKey);
const parser = new _ParseAST(
templateValue, templateUrl, absoluteValueOffset, tokens, templateValue.length,
false /* parseAction */, this.errors, 0 /* relative offset */);
return parser.parseTemplateBindings({
source: templateKey,
span: new AbsoluteSourceSpan(absoluteKeyOffset, absoluteKeyOffset + templateKey.length),
});
}

parseInterpolation(
Expand Down Expand Up @@ -302,6 +307,11 @@ export class _ParseAST {
this.inputLength + this.offset;
}

/**
* Returns the absolute offset of the start of the current token.
*/
get currentAbsoluteOffset(): number { return this.absoluteOffset + this.inputIndex; }

span(start: number) { return new ParseSpan(start, this.inputIndex); }

sourceSpan(start: number): AbsoluteSourceSpan {
Expand Down Expand Up @@ -747,12 +757,13 @@ export class _ParseAST {
}

/**
* Parses an identifier, a keyword, a string with an optional `-` in between.
* Parses an identifier, a keyword, a string with an optional `-` in between,
* and returns the string along with its absolute source span.
*/
expectTemplateBindingKey(): {key: string, keySpan: ParseSpan} {
expectTemplateBindingKey(): TemplateBindingIdentifier {
let result = '';
let operatorFound = false;
const start = this.inputIndex;
const start = this.currentAbsoluteOffset;
do {
result += this.expectIdentifierOrKeywordOrString();
operatorFound = this.consumeOptionalOperator('-');
Expand All @@ -761,8 +772,8 @@ export class _ParseAST {
}
} while (operatorFound);
return {
key: result,
keySpan: new ParseSpan(start, start + result.length),
source: result,
span: new AbsoluteSourceSpan(start, start + result.length),
};
}

Expand All @@ -784,16 +795,16 @@ export class _ParseAST {
* 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 *
* @param templateKey name of the microsyntax directive, like ngIf, ngFor,
* without the *, along with its absolute span.
*/
parseTemplateBindings(templateKey: string): TemplateBindingParseResult {
parseTemplateBindings(templateKey: TemplateBindingIdentifier): TemplateBindingParseResult {
const bindings: TemplateBinding[] = [];

// 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));
bindings.push(...this.parseDirectiveKeywordBindings(templateKey));

while (this.index < this.tokens.length) {
// If it starts with 'let', then this must be variable declaration
Expand All @@ -805,18 +816,17 @@ export class _ParseAST {
// "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();
const key = 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);
const binding = this.parseAsBinding(key);
if (binding) {
bindings.push(binding);
} else {
// 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));
key.source = templateKey.source + key.source[0].toUpperCase() + key.source.substring(1);
bindings.push(...this.parseDirectiveKeywordBindings(key));
}
}
this.consumeStatementTerminator();
Expand All @@ -832,52 +842,56 @@ export class _ParseAST {
* 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
* *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
* @param key binding key, for example, ngFor, ngIf, ngForOf, along with its
* absolute span.
*/
private parseDirectiveKeywordBindings(key: string, keySpan: ParseSpan, absoluteOffset: number):
TemplateBinding[] {
private parseDirectiveKeywordBindings(key: TemplateBindingIdentifier): TemplateBinding[] {
const bindings: TemplateBinding[] = [];
this.consumeOptionalCharacter(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));
const value = this.getDirectiveBoundTarget();
let spanEnd = this.currentAbsoluteOffset;
// 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);
const asBinding = this.parseAsBinding(key);
if (!asBinding) {
this.consumeStatementTerminator();
spanEnd = this.currentAbsoluteOffset;
}
const sourceSpan = new AbsoluteSourceSpan(key.span.start, spanEnd);
bindings.push(new ExpressionBinding(sourceSpan, key, value));
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"
* *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 {start} = ast.span;
// Getting the end of the last token removes trailing whitespace.
// If ast has the correct end span then no need to peek at last token.
// TODO(ayazhafiz): Remove this in https://github.com/angular/angular/pull/34690
const {end} = this.peek(-1);
const value = this.input.substring(start, end);
return new ASTWithSource(ast, value, this.location, this.absoluteOffset + start, this.errors);
}
Expand All @@ -886,35 +900,30 @@ export class _ParseAST {
* 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
* *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`
* @param value name of the value in the declaration, "ngIf" in the example
* above, along with its absolute span.
*/
private parseAsBinding(value: string, valueSpan: ParseSpan, absoluteOffset: number):
TemplateBinding|null {
private parseAsBinding(value: TemplateBindingIdentifier): 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);
const key = this.expectTemplateBindingKey();
kyliau marked this conversation as resolved.
Show resolved Hide resolved
this.consumeStatementTerminator();
const sourceSpan = new AbsoluteSourceSpan(value.span.start, this.currentAbsoluteOffset);
return new VariableBinding(sourceSpan, key, value);
}

/**
* Return the binding for a variable declared using `let`. For example,
* ```
* *ngFor="let item of items; let i=index;"
* ^^^^^^^^ ^^^^^^^^^^^
* *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`.
Expand All @@ -923,20 +932,16 @@ export class _ParseAST {
if (!this.peekKeywordLet()) {
return null;
}
const spanStart = this.inputIndex;
const spanStart = this.currentAbsoluteOffset;
this.advance(); // consume the 'let' keyword
const {key} = this.expectTemplateBindingKey();
let valueExpr: ASTWithSource|null = null;
const key = this.expectTemplateBindingKey();
let value: TemplateBindingIdentifier|null = null;
if (this.consumeOptionalOperator('=')) {
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);
value = this.expectTemplateBindingKey();
}
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);
this.consumeStatementTerminator();
const sourceSpan = new AbsoluteSourceSpan(spanStart, this.currentAbsoluteOffset);
return new VariableBinding(sourceSpan, key, value);
}

/**
Expand Down