Ayaz Hafiz ddc229b69b fix(ivy): record correct absolute source span for ngForOf expressions (#31813)
Expressions in an inline template binding are improperly recorded as
spaning an offset calculated from the start of the template binding
attribute key, whereas they should be calculated from the start of the
attribute value, which contains the actual binding AST.

PR Close #31813
2019-12-16 08:11:48 -08:00

878 lines
31 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
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 {EOF, Lexer, Token, TokenType, isIdentifier, isQuote} from './lexer';
export class SplitInterpolation {
constructor(public strings: string[], public expressions: string[], public offsets: number[]) {}
}
export class TemplateBindingParseResult {
constructor(
public templateBindings: TemplateBinding[], public warnings: string[],
public errors: ParserError[]) {}
}
const defaultInterpolateRegExp = _createInterpolateRegExp(DEFAULT_INTERPOLATION_CONFIG);
function _getInterpolateRegExp(config: InterpolationConfig): RegExp {
if (config === DEFAULT_INTERPOLATION_CONFIG) {
return defaultInterpolateRegExp;
} else {
return _createInterpolateRegExp(config);
}
}
function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
const pattern = escapeRegExp(config.start) + '([\\s\\S]*?)' + escapeRegExp(config.end);
return new RegExp(pattern, 'g');
}
export class Parser {
private errors: ParserError[] = [];
constructor(private _lexer: Lexer) {}
parseAction(
input: string, location: any, absoluteOffset: number,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
this._checkNoInterpolation(input, location, interpolationConfig);
const sourceToLex = this._stripComments(input);
const tokens = this._lexer.tokenize(this._stripComments(input));
const ast = new _ParseAST(
input, location, absoluteOffset, tokens, sourceToLex.length, true, this.errors,
input.length - sourceToLex.length)
.parseChain();
return new ASTWithSource(ast, input, location, absoluteOffset, this.errors);
}
parseBinding(
input: string, location: any, absoluteOffset: number,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
const ast = this._parseBindingAst(input, location, absoluteOffset, interpolationConfig);
return new ASTWithSource(ast, input, location, absoluteOffset, this.errors);
}
parseSimpleBinding(
input: string, location: string, absoluteOffset: number,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
const ast = this._parseBindingAst(input, location, absoluteOffset, interpolationConfig);
const errors = SimpleExpressionChecker.check(ast);
if (errors.length > 0) {
this._reportError(
`Host binding expression cannot contain ${errors.join(' ')}`, input, location);
}
return new ASTWithSource(ast, input, location, absoluteOffset, this.errors);
}
private _reportError(message: string, input: string, errLocation: string, ctxLocation?: any) {
this.errors.push(new ParserError(message, input, errLocation, ctxLocation));
}
private _parseBindingAst(
input: string, location: string, absoluteOffset: number,
interpolationConfig: InterpolationConfig): AST {
// Quotes expressions use 3rd-party expression language. We don't want to use
// our lexer or parser for that, so we check for that ahead of time.
const quote = this._parseQuote(input, location, absoluteOffset);
if (quote != null) {
return quote;
}
this._checkNoInterpolation(input, location, interpolationConfig);
const sourceToLex = this._stripComments(input);
const tokens = this._lexer.tokenize(sourceToLex);
return new _ParseAST(
input, location, absoluteOffset, tokens, sourceToLex.length, false, this.errors,
input.length - sourceToLex.length)
.parseChain();
}
private _parseQuote(input: string|null, location: any, absoluteOffset: number): AST|null {
if (input == null) return null;
const prefixSeparatorIndex = input.indexOf(':');
if (prefixSeparatorIndex == -1) return null;
const prefix = input.substring(0, prefixSeparatorIndex).trim();
if (!isIdentifier(prefix)) return null;
const uninterpretedExpression = input.substring(prefixSeparatorIndex + 1);
const span = new ParseSpan(0, input.length);
return new Quote(
span, span.toAbsolute(absoluteOffset), prefix, uninterpretedExpression, location);
}
parseTemplateBindings(tplKey: string, tplValue: string, location: any, absoluteOffset: number):
TemplateBindingParseResult {
const tokens = this._lexer.tokenize(tplValue);
return new _ParseAST(
tplValue, location, absoluteOffset, tokens, tplValue.length, false, this.errors, 0)
.parseTemplateBindings(tplKey);
}
parseInterpolation(
input: string, location: any, absoluteOffset: number,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource|null {
const split = this.splitInterpolation(input, location, interpolationConfig);
if (split == null) return null;
const expressions: AST[] = [];
for (let i = 0; i < split.expressions.length; ++i) {
const expressionText = split.expressions[i];
const sourceToLex = this._stripComments(expressionText);
const tokens = this._lexer.tokenize(sourceToLex);
const ast = new _ParseAST(
input, location, absoluteOffset, tokens, sourceToLex.length, false,
this.errors, split.offsets[i] + (expressionText.length - sourceToLex.length))
.parseChain();
expressions.push(ast);
}
const span = new ParseSpan(0, input == null ? 0 : input.length);
return new ASTWithSource(
new Interpolation(span, span.toAbsolute(absoluteOffset), split.strings, expressions), input,
location, absoluteOffset, this.errors);
}
splitInterpolation(
input: string, location: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation
|null {
const regexp = _getInterpolateRegExp(interpolationConfig);
const parts = input.split(regexp);
if (parts.length <= 1) {
return null;
}
const strings: string[] = [];
const expressions: string[] = [];
const offsets: number[] = [];
let offset = 0;
for (let i = 0; i < parts.length; i++) {
const part: string = parts[i];
if (i % 2 === 0) {
// fixed string
strings.push(part);
offset += part.length;
} else if (part.trim().length > 0) {
offset += interpolationConfig.start.length;
expressions.push(part);
offsets.push(offset);
offset += part.length + interpolationConfig.end.length;
} else {
this._reportError(
'Blank expressions are not allowed in interpolated strings', input,
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
location);
expressions.push('$implicit');
offsets.push(offset);
}
}
return new SplitInterpolation(strings, expressions, offsets);
}
wrapLiteralPrimitive(input: string|null, location: any, absoluteOffset: number): ASTWithSource {
const span = new ParseSpan(0, input == null ? 0 : input.length);
return new ASTWithSource(
new LiteralPrimitive(span, span.toAbsolute(absoluteOffset), input), input, location,
absoluteOffset, this.errors);
}
private _stripComments(input: string): string {
const i = this._commentStart(input);
return i != null ? input.substring(0, i).trim() : input;
}
private _commentStart(input: string): number|null {
let outerQuote: number|null = null;
for (let i = 0; i < input.length - 1; i++) {
const char = input.charCodeAt(i);
const nextChar = input.charCodeAt(i + 1);
if (char === chars.$SLASH && nextChar == chars.$SLASH && outerQuote == null) return i;
if (outerQuote === char) {
outerQuote = null;
} else if (outerQuote == null && isQuote(char)) {
outerQuote = char;
}
}
return null;
}
private _checkNoInterpolation(
input: string, location: any, interpolationConfig: InterpolationConfig): void {
const regexp = _getInterpolateRegExp(interpolationConfig);
const parts = input.split(regexp);
if (parts.length > 1) {
this._reportError(
`Got interpolation (${interpolationConfig.start}${interpolationConfig.end}) where expression was expected`,
input,
`at column ${this._findInterpolationErrorColumn(parts, 1, interpolationConfig)} in`,
location);
}
}
private _findInterpolationErrorColumn(
parts: string[], partInErrIdx: number, interpolationConfig: InterpolationConfig): number {
let errLocation = '';
for (let j = 0; j < partInErrIdx; j++) {
errLocation += j % 2 === 0 ?
parts[j] :
`${interpolationConfig.start}${parts[j]}${interpolationConfig.end}`;
}
return errLocation.length;
}
}
export class _ParseAST {
private rparensExpected = 0;
private rbracketsExpected = 0;
private rbracesExpected = 0;
// Cache of expression start and input indeces to the absolute source span they map to, used to
// prevent creating superfluous source spans in `sourceSpan`.
// A serial of the expression start and input index is used for mapping because both are stateful
// and may change for subsequent expressions visited by the parser.
private sourceSpanCache = new Map<string, AbsoluteSourceSpan>();
index: number = 0;
constructor(
public input: string, public location: any, public absoluteOffset: number,
public tokens: Token[], public inputLength: number, public parseAction: boolean,
private errors: ParserError[], private offset: number) {}
peek(offset: number): Token {
const i = this.index + offset;
return i < this.tokens.length ? this.tokens[i] : EOF;
}
get next(): Token { return this.peek(0); }
get inputIndex(): number {
return (this.index < this.tokens.length) ? this.next.index + this.offset :
this.inputLength + this.offset;
}
span(start: number) { return new ParseSpan(start, this.inputIndex); }
sourceSpan(start: number): AbsoluteSourceSpan {
const serial = `${start}@${this.inputIndex}`;
if (!this.sourceSpanCache.has(serial)) {
this.sourceSpanCache.set(serial, this.span(start).toAbsolute(this.absoluteOffset));
}
return this.sourceSpanCache.get(serial) !;
}
advance() { this.index++; }
optionalCharacter(code: number): boolean {
if (this.next.isCharacter(code)) {
this.advance();
return true;
} else {
return false;
}
}
peekKeywordLet(): boolean { return this.next.isKeywordLet(); }
peekKeywordAs(): boolean { return this.next.isKeywordAs(); }
expectCharacter(code: number) {
if (this.optionalCharacter(code)) return;
this.error(`Missing expected ${String.fromCharCode(code)}`);
}
optionalOperator(op: string): boolean {
if (this.next.isOperator(op)) {
this.advance();
return true;
} else {
return false;
}
}
expectOperator(operator: string) {
if (this.optionalOperator(operator)) return;
this.error(`Missing expected operator ${operator}`);
}
expectIdentifierOrKeyword(): string {
const n = this.next;
if (!n.isIdentifier() && !n.isKeyword()) {
this.error(`Unexpected token ${n}, expected identifier or keyword`);
return '';
}
this.advance();
return n.toString() as string;
}
expectIdentifierOrKeywordOrString(): string {
const n = this.next;
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
this.error(`Unexpected token ${n}, expected identifier, keyword, or string`);
return '';
}
this.advance();
return n.toString() as string;
}
parseChain(): AST {
const exprs: AST[] = [];
const start = this.inputIndex;
while (this.index < this.tokens.length) {
const expr = this.parsePipe();
exprs.push(expr);
if (this.optionalCharacter(chars.$SEMICOLON)) {
if (!this.parseAction) {
this.error('Binding expression cannot contain chained expression');
}
while (this.optionalCharacter(chars.$SEMICOLON)) {
} // read all semicolons
} else if (this.index < this.tokens.length) {
this.error(`Unexpected token '${this.next}'`);
}
}
if (exprs.length == 0) return new EmptyExpr(this.span(start), this.sourceSpan(start));
if (exprs.length == 1) return exprs[0];
return new Chain(this.span(start), this.sourceSpan(start), exprs);
}
parsePipe(): AST {
let result = this.parseExpression();
if (this.optionalOperator('|')) {
if (this.parseAction) {
this.error('Cannot have a pipe in an action expression');
}
do {
const nameStart = this.inputIndex;
const name = this.expectIdentifierOrKeyword();
const nameSpan = this.span(nameStart);
const args: AST[] = [];
while (this.optionalCharacter(chars.$COLON)) {
args.push(this.parseExpression());
}
const {start} = result.span;
result =
new BindingPipe(this.span(start), this.sourceSpan(start), result, name, args, nameSpan);
} while (this.optionalOperator('|'));
}
return result;
}
parseExpression(): AST { return this.parseConditional(); }
parseConditional(): AST {
const start = this.inputIndex;
const result = this.parseLogicalOr();
if (this.optionalOperator('?')) {
const yes = this.parsePipe();
let no: AST;
if (!this.optionalCharacter(chars.$COLON)) {
const end = this.inputIndex;
const expression = this.input.substring(start, end);
this.error(`Conditional expression ${expression} requires all 3 expressions`);
no = new EmptyExpr(this.span(start), this.sourceSpan(start));
} else {
no = this.parsePipe();
}
return new Conditional(this.span(start), this.sourceSpan(start), result, yes, no);
} else {
return result;
}
}
parseLogicalOr(): AST {
// '||'
let result = this.parseLogicalAnd();
while (this.optionalOperator('||')) {
const right = this.parseLogicalAnd();
const {start} = result.span;
result = new Binary(this.span(start), this.sourceSpan(start), '||', result, right);
}
return result;
}
parseLogicalAnd(): AST {
// '&&'
let result = this.parseEquality();
while (this.optionalOperator('&&')) {
const right = this.parseEquality();
const {start} = result.span;
result = new Binary(this.span(start), this.sourceSpan(start), '&&', result, right);
}
return result;
}
parseEquality(): AST {
// '==','!=','===','!=='
let result = this.parseRelational();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '==':
case '===':
case '!=':
case '!==':
this.advance();
const right = this.parseRelational();
const {start} = result.span;
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
continue;
}
break;
}
return result;
}
parseRelational(): AST {
// '<', '>', '<=', '>='
let result = this.parseAdditive();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '<':
case '>':
case '<=':
case '>=':
this.advance();
const right = this.parseAdditive();
const {start} = result.span;
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
continue;
}
break;
}
return result;
}
parseAdditive(): AST {
// '+', '-'
let result = this.parseMultiplicative();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '+':
case '-':
this.advance();
let right = this.parseMultiplicative();
const {start} = result.span;
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
continue;
}
break;
}
return result;
}
parseMultiplicative(): AST {
// '*', '%', '/'
let result = this.parsePrefix();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '*':
case '%':
case '/':
this.advance();
let right = this.parsePrefix();
const {start} = result.span;
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
continue;
}
break;
}
return result;
}
parsePrefix(): AST {
if (this.next.type == TokenType.Operator) {
const start = this.inputIndex;
const operator = this.next.strValue;
const literalSpan = new ParseSpan(start, start);
const literalSourceSpan = literalSpan.toAbsolute(this.absoluteOffset);
let result: AST;
switch (operator) {
case '+':
this.advance();
result = this.parsePrefix();
return new Binary(
this.span(start), this.sourceSpan(start), '-', result,
new LiteralPrimitive(literalSpan, literalSourceSpan, 0));
case '-':
this.advance();
result = this.parsePrefix();
return new Binary(
this.span(start), this.sourceSpan(start), operator,
new LiteralPrimitive(literalSpan, literalSourceSpan, 0), result);
case '!':
this.advance();
result = this.parsePrefix();
return new PrefixNot(this.span(start), this.sourceSpan(start), result);
}
}
return this.parseCallChain();
}
parseCallChain(): AST {
let result = this.parsePrimary();
const resultStart = result.span.start;
while (true) {
if (this.optionalCharacter(chars.$PERIOD)) {
result = this.parseAccessMemberOrMethodCall(result, false);
} else if (this.optionalOperator('?.')) {
result = this.parseAccessMemberOrMethodCall(result, true);
} else if (this.optionalCharacter(chars.$LBRACKET)) {
this.rbracketsExpected++;
const key = this.parsePipe();
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
if (this.optionalOperator('=')) {
const value = this.parseConditional();
result = new KeyedWrite(
this.span(resultStart), this.sourceSpan(resultStart), result, key, value);
} else {
result = new KeyedRead(this.span(resultStart), this.sourceSpan(resultStart), result, key);
}
} else if (this.optionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const args = this.parseCallArguments();
this.rparensExpected--;
this.expectCharacter(chars.$RPAREN);
result =
new FunctionCall(this.span(resultStart), this.sourceSpan(resultStart), result, args);
} else if (this.optionalOperator('!')) {
result = new NonNullAssert(this.span(resultStart), this.sourceSpan(resultStart), result);
} else {
return result;
}
}
}
parsePrimary(): AST {
const start = this.inputIndex;
if (this.optionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const result = this.parsePipe();
this.rparensExpected--;
this.expectCharacter(chars.$RPAREN);
return result;
} else if (this.next.isKeywordNull()) {
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), null);
} else if (this.next.isKeywordUndefined()) {
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), void 0);
} else if (this.next.isKeywordTrue()) {
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), true);
} else if (this.next.isKeywordFalse()) {
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), false);
} else if (this.next.isKeywordThis()) {
this.advance();
return new ImplicitReceiver(this.span(start), this.sourceSpan(start));
} else if (this.optionalCharacter(chars.$LBRACKET)) {
this.rbracketsExpected++;
const elements = this.parseExpressionList(chars.$RBRACKET);
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
return new LiteralArray(this.span(start), this.sourceSpan(start), elements);
} else if (this.next.isCharacter(chars.$LBRACE)) {
return this.parseLiteralMap();
} else if (this.next.isIdentifier()) {
return this.parseAccessMemberOrMethodCall(
new ImplicitReceiver(this.span(start), this.sourceSpan(start)), false);
} else if (this.next.isNumber()) {
const value = this.next.toNumber();
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), value);
} else if (this.next.isString()) {
const literalValue = this.next.toString();
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), literalValue);
} else if (this.index >= this.tokens.length) {
this.error(`Unexpected end of expression: ${this.input}`);
return new EmptyExpr(this.span(start), this.sourceSpan(start));
} else {
this.error(`Unexpected token ${this.next}`);
return new EmptyExpr(this.span(start), this.sourceSpan(start));
}
}
parseExpressionList(terminator: number): AST[] {
const result: AST[] = [];
if (!this.next.isCharacter(terminator)) {
do {
result.push(this.parsePipe());
} while (this.optionalCharacter(chars.$COMMA));
}
return result;
}
parseLiteralMap(): LiteralMap {
const keys: LiteralMapKey[] = [];
const values: AST[] = [];
const start = this.inputIndex;
this.expectCharacter(chars.$LBRACE);
if (!this.optionalCharacter(chars.$RBRACE)) {
this.rbracesExpected++;
do {
const quoted = this.next.isString();
const key = this.expectIdentifierOrKeywordOrString();
keys.push({key, quoted});
this.expectCharacter(chars.$COLON);
values.push(this.parsePipe());
} while (this.optionalCharacter(chars.$COMMA));
this.rbracesExpected--;
this.expectCharacter(chars.$RBRACE);
}
return new LiteralMap(this.span(start), this.sourceSpan(start), keys, values);
}
parseAccessMemberOrMethodCall(receiver: AST, isSafe: boolean = false): AST {
const start = receiver.span.start;
const id = this.expectIdentifierOrKeyword();
if (this.optionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const args = this.parseCallArguments();
this.expectCharacter(chars.$RPAREN);
this.rparensExpected--;
const span = this.span(start);
const sourceSpan = this.sourceSpan(start);
return isSafe ? new SafeMethodCall(span, sourceSpan, receiver, id, args) :
new MethodCall(span, sourceSpan, receiver, id, args);
} else {
if (isSafe) {
if (this.optionalOperator('=')) {
this.error('The \'?.\' operator cannot be used in the assignment');
return new EmptyExpr(this.span(start), this.sourceSpan(start));
} else {
return new SafePropertyRead(this.span(start), this.sourceSpan(start), receiver, id);
}
} else {
if (this.optionalOperator('=')) {
if (!this.parseAction) {
this.error('Bindings cannot contain assignments');
return new EmptyExpr(this.span(start), this.sourceSpan(start));
}
const value = this.parseConditional();
return new PropertyWrite(this.span(start), this.sourceSpan(start), receiver, id, value);
} else {
const span = this.span(start);
return new PropertyRead(this.span(start), this.sourceSpan(start), receiver, id);
}
}
}
}
parseCallArguments(): BindingPipe[] {
if (this.next.isCharacter(chars.$RPAREN)) return [];
const positionals: AST[] = [];
do {
positionals.push(this.parsePipe());
} while (this.optionalCharacter(chars.$COMMA));
return positionals as BindingPipe[];
}
/**
* An identifier, a keyword, a string with an optional `-` in between.
*/
expectTemplateBindingKey(): string {
let result = '';
let operatorFound = false;
do {
result += this.expectIdentifierOrKeywordOrString();
operatorFound = this.optionalOperator('-');
if (operatorFound) {
result += '-';
}
} while (operatorFound);
return result.toString();
}
// Parses the AST for `<some-tag *tplKey=AST>`
parseTemplateBindings(tplKey: string): TemplateBindingParseResult {
let firstBinding = true;
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;
} 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();
} else {
name = '\$implicit';
}
} 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);
}
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);
}
error(message: string, index: number|null = null) {
this.errors.push(new ParserError(message, this.input, this.locationText(index), this.location));
this.skip();
}
private locationText(index: number|null = null) {
if (index == null) index = this.index;
return (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` :
`at the end of the expression`;
}
// Error recovery should skip tokens until it encounters a recovery point. skip() treats
// the end of input and a ';' as unconditionally a recovery point. It also treats ')',
// '}' and ']' as conditional recovery points if one of calling productions is expecting
// one of these symbols. This allows skip() to recover from errors such as '(a.) + 1' allowing
// more of the AST to be retained (it doesn't skip any tokens as the ')' is retained because
// of the '(' begins an '(' <expr> ')' production). The recovery points of grouping symbols
// must be conditional as they must be skipped if none of the calling productions are not
// expecting the closing token else we will never make progress in the case of an
// extraneous group closing symbol (such as a stray ')'). This is not the case for ';' because
// parseChain() is always the root production and it expects a ';'.
// If a production expects one of these token it increments the corresponding nesting count,
// and then decrements it just prior to checking if the token is in the input.
private skip() {
let n = this.next;
while (this.index < this.tokens.length && !n.isCharacter(chars.$SEMICOLON) &&
(this.rparensExpected <= 0 || !n.isCharacter(chars.$RPAREN)) &&
(this.rbracesExpected <= 0 || !n.isCharacter(chars.$RBRACE)) &&
(this.rbracketsExpected <= 0 || !n.isCharacter(chars.$RBRACKET))) {
if (this.next.isError()) {
this.errors.push(new ParserError(
this.next.toString() !, this.input, this.locationText(), this.location));
}
this.advance();
n = this.next;
}
}
}
class SimpleExpressionChecker implements AstVisitor {
static check(ast: AST): string[] {
const s = new SimpleExpressionChecker();
ast.visit(s);
return s.errors;
}
errors: string[] = [];
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
visitInterpolation(ast: Interpolation, context: any) {}
visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {}
visitPropertyRead(ast: PropertyRead, context: any) {}
visitPropertyWrite(ast: PropertyWrite, context: any) {}
visitSafePropertyRead(ast: SafePropertyRead, context: any) {}
visitMethodCall(ast: MethodCall, context: any) {}
visitSafeMethodCall(ast: SafeMethodCall, context: any) {}
visitFunctionCall(ast: FunctionCall, context: any) {}
visitLiteralArray(ast: LiteralArray, context: any) { this.visitAll(ast.expressions); }
visitLiteralMap(ast: LiteralMap, context: any) { this.visitAll(ast.values); }
visitBinary(ast: Binary, context: any) {}
visitPrefixNot(ast: PrefixNot, context: any) {}
visitNonNullAssert(ast: NonNullAssert, context: any) {}
visitConditional(ast: Conditional, context: any) {}
visitPipe(ast: BindingPipe, context: any) { this.errors.push('pipes'); }
visitKeyedRead(ast: KeyedRead, context: any) {}
visitKeyedWrite(ast: KeyedWrite, context: any) {}
visitAll(asts: any[]): any[] { return asts.map(node => node.visit(this)); }
visitChain(ast: Chain, context: any) {}
visitQuote(ast: Quote, context: any) {}
}