@angular/compiler
Version:
Angular - the compiler library
1,099 lines • 155 kB
JavaScript
/**
* @license
* Copyright Google LLC 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 } from '../ml_parser/interpolation_config';
import { escapeRegExp } from '../util';
import { AbsoluteSourceSpan, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, ThisReceiver, Unary, VariableBinding } from './ast';
import { EOF, isIdentifier, isQuote, TokenType } from './lexer';
export class SplitInterpolation {
constructor(strings, stringSpans, expressions, expressionsSpans, offsets) {
this.strings = strings;
this.stringSpans = stringSpans;
this.expressions = expressions;
this.expressionsSpans = expressionsSpans;
this.offsets = offsets;
}
}
export class TemplateBindingParseResult {
constructor(templateBindings, warnings, errors) {
this.templateBindings = templateBindings;
this.warnings = warnings;
this.errors = errors;
}
}
const defaultInterpolateRegExp = _createInterpolateRegExp(DEFAULT_INTERPOLATION_CONFIG);
function _getInterpolateRegExp(config) {
if (config === DEFAULT_INTERPOLATION_CONFIG) {
return defaultInterpolateRegExp;
}
else {
return _createInterpolateRegExp(config);
}
}
function _createInterpolateRegExp(config) {
const pattern = escapeRegExp(config.start) + '([\\s\\S]*?)' + escapeRegExp(config.end);
return new RegExp(pattern, 'g');
}
export class Parser {
constructor(_lexer) {
this._lexer = _lexer;
this.errors = [];
this.simpleExpressionChecker = SimpleExpressionChecker;
}
parseAction(input, location, absoluteOffset, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
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, location, absoluteOffset, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
const ast = this._parseBindingAst(input, location, absoluteOffset, interpolationConfig);
return new ASTWithSource(ast, input, location, absoluteOffset, this.errors);
}
checkSimpleExpression(ast) {
const checker = new this.simpleExpressionChecker();
ast.visit(checker);
return checker.errors;
}
parseSimpleBinding(input, location, absoluteOffset, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
const ast = this._parseBindingAst(input, location, absoluteOffset, interpolationConfig);
const errors = this.checkSimpleExpression(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);
}
_reportError(message, input, errLocation, ctxLocation) {
this.errors.push(new ParserError(message, input, errLocation, ctxLocation));
}
_parseBindingAst(input, location, absoluteOffset, interpolationConfig) {
// 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();
}
_parseQuote(input, location, absoluteOffset) {
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);
}
/**
* 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">
* ^ ^ absoluteValueOffset for `templateValue`
* absoluteKeyOffset for `templateKey`
* ```
* 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 absoluteKeyOffset start of the `templateKey`
* @param absoluteValueOffset start of the `templateValue`
*/
parseTemplateBindings(templateKey, templateValue, templateUrl, absoluteKeyOffset, absoluteValueOffset) {
const tokens = this._lexer.tokenize(templateValue);
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(input, location, absoluteOffset, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
const split = this.splitInterpolation(input, location, interpolationConfig);
if (split == null)
return null;
const expressions = [];
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);
}
return this.createInterpolationAst(split.strings, expressions, input, location, absoluteOffset);
}
/**
* Similar to `parseInterpolation`, but treats the provided string as a single expression
* element that would normally appear within the interpolation prefix and suffix (`{{` and `}}`).
* This is used for parsing the switch expression in ICUs.
*/
parseInterpolationExpression(expression, location, absoluteOffset) {
const sourceToLex = this._stripComments(expression);
const tokens = this._lexer.tokenize(sourceToLex);
const ast = new _ParseAST(expression, location, absoluteOffset, tokens, sourceToLex.length,
/* parseAction */ false, this.errors, 0)
.parseChain();
const strings = ['', '']; // The prefix and suffix strings are both empty
return this.createInterpolationAst(strings, [ast], expression, location, absoluteOffset);
}
createInterpolationAst(strings, expressions, input, location, absoluteOffset) {
const span = new ParseSpan(0, input.length);
const interpolation = new Interpolation(span, span.toAbsolute(absoluteOffset), strings, expressions);
return new ASTWithSource(interpolation, input, location, absoluteOffset, this.errors);
}
/**
* Splits a string of text into "raw" text segments and expressions present in interpolations in
* the string.
* Returns `null` if there are no interpolations, otherwise a
* `SplitInterpolation` with splits that look like
* <raw text> <expression> <raw text> ... <raw text> <expression> <raw text>
*/
splitInterpolation(input, location, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
const strings = [];
const expressions = [];
const offsets = [];
const stringSpans = [];
const expressionSpans = [];
let i = 0;
let atInterpolation = false;
let extendLastString = false;
let { start: interpStart, end: interpEnd } = interpolationConfig;
while (i < input.length) {
if (!atInterpolation) {
// parse until starting {{
const start = i;
i = input.indexOf(interpStart, i);
if (i === -1) {
i = input.length;
}
const part = input.substring(start, i);
strings.push(part);
stringSpans.push({ start, end: i });
atInterpolation = true;
}
else {
// parse from starting {{ to ending }}
const fullStart = i;
const exprStart = fullStart + interpStart.length;
const exprEnd = input.indexOf(interpEnd, exprStart);
if (exprEnd === -1) {
// Could not find the end of the interpolation; do not parse an expression.
// Instead we should extend the content on the last raw string.
atInterpolation = false;
extendLastString = true;
break;
}
const fullEnd = exprEnd + interpEnd.length;
const part = input.substring(exprStart, exprEnd);
if (part.trim().length > 0) {
expressions.push(part);
}
else {
this._reportError('Blank expressions are not allowed in interpolated strings', input, `at column ${i} in`, location);
expressions.push('$implicit');
}
offsets.push(exprStart);
expressionSpans.push({ start: fullStart, end: fullEnd });
i = fullEnd;
atInterpolation = false;
}
}
if (!atInterpolation) {
// If we are now at a text section, add the remaining content as a raw string.
if (extendLastString) {
strings[strings.length - 1] += input.substring(i);
stringSpans[stringSpans.length - 1].end = input.length;
}
else {
strings.push(input.substring(i));
stringSpans.push({ start: i, end: input.length });
}
}
return expressions.length === 0 ?
null :
new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
}
wrapLiteralPrimitive(input, location, absoluteOffset) {
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);
}
_stripComments(input) {
const i = this._commentStart(input);
return i != null ? input.substring(0, i).trim() : input;
}
_commentStart(input) {
let outerQuote = 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;
}
_checkNoInterpolation(input, location, interpolationConfig) {
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);
}
}
_findInterpolationErrorColumn(parts, partInErrIdx, interpolationConfig) {
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 IvyParser extends Parser {
constructor() {
super(...arguments);
this.simpleExpressionChecker = IvySimpleExpressionChecker; //
}
}
/** Describes a stateful context an expression parser is in. */
var ParseContextFlags;
(function (ParseContextFlags) {
ParseContextFlags[ParseContextFlags["None"] = 0] = "None";
/**
* A Writable context is one in which a value may be written to an lvalue.
* For example, after we see a property access, we may expect a write to the
* property via the "=" operator.
* prop
* ^ possible "=" after
*/
ParseContextFlags[ParseContextFlags["Writable"] = 1] = "Writable";
})(ParseContextFlags || (ParseContextFlags = {}));
export class _ParseAST {
constructor(input, location, absoluteOffset, tokens, inputLength, parseAction, errors, offset) {
this.input = input;
this.location = location;
this.absoluteOffset = absoluteOffset;
this.tokens = tokens;
this.inputLength = inputLength;
this.parseAction = parseAction;
this.errors = errors;
this.offset = offset;
this.rparensExpected = 0;
this.rbracketsExpected = 0;
this.rbracesExpected = 0;
this.context = ParseContextFlags.None;
// 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.
this.sourceSpanCache = new Map();
this.index = 0;
}
peek(offset) {
const i = this.index + offset;
return i < this.tokens.length ? this.tokens[i] : EOF;
}
get next() {
return this.peek(0);
}
/** Whether all the parser input has been processed. */
get atEOF() {
return this.index >= this.tokens.length;
}
/**
* Index of the next token to be processed, or the end of the last token if all have been
* processed.
*/
get inputIndex() {
return this.atEOF ? this.currentEndIndex : this.next.index + this.offset;
}
/**
* End index of the last processed token, or the start of the first token if none have been
* processed.
*/
get currentEndIndex() {
if (this.index > 0) {
const curToken = this.peek(-1);
return curToken.end + this.offset;
}
// No tokens have been processed yet; return the next token's start or the length of the input
// if there is no token.
if (this.tokens.length === 0) {
return this.inputLength + this.offset;
}
return this.next.index + this.offset;
}
/**
* Returns the absolute offset of the start of the current token.
*/
get currentAbsoluteOffset() {
return this.absoluteOffset + this.inputIndex;
}
span(start) {
return new ParseSpan(start, this.currentEndIndex);
}
sourceSpan(start) {
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++;
}
/**
* Executes a callback in the provided context.
*/
withContext(context, cb) {
this.context |= context;
const ret = cb();
this.context ^= context;
return ret;
}
consumeOptionalCharacter(code) {
if (this.next.isCharacter(code)) {
this.advance();
return true;
}
else {
return false;
}
}
peekKeywordLet() {
return this.next.isKeywordLet();
}
peekKeywordAs() {
return this.next.isKeywordAs();
}
/**
* Consumes an expected character, otherwise emits an error about the missing expected character
* and skips over the token stream until reaching a recoverable point.
*
* See `this.error` and `this.skip` for more details.
*/
expectCharacter(code) {
if (this.consumeOptionalCharacter(code))
return;
this.error(`Missing expected ${String.fromCharCode(code)}`);
}
consumeOptionalOperator(op) {
if (this.next.isOperator(op)) {
this.advance();
return true;
}
else {
return false;
}
}
expectOperator(operator) {
if (this.consumeOptionalOperator(operator))
return;
this.error(`Missing expected operator ${operator}`);
}
prettyPrintToken(tok) {
return tok === EOF ? 'end of input' : `token ${tok}`;
}
expectIdentifierOrKeyword() {
const n = this.next;
if (!n.isIdentifier() && !n.isKeyword()) {
this.error(`Unexpected ${this.prettyPrintToken(n)}, expected identifier or keyword`);
return '';
}
this.advance();
return n.toString();
}
expectIdentifierOrKeywordOrString() {
const n = this.next;
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
this.error(`Unexpected ${this.prettyPrintToken(n)}, expected identifier, keyword, or string`);
return '';
}
this.advance();
return n.toString();
}
parseChain() {
const exprs = [];
const start = this.inputIndex;
while (this.index < this.tokens.length) {
const expr = this.parsePipe();
exprs.push(expr);
if (this.consumeOptionalCharacter(chars.$SEMICOLON)) {
if (!this.parseAction) {
this.error('Binding expression cannot contain chained expression');
}
while (this.consumeOptionalCharacter(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() {
let result = this.parseExpression();
if (this.consumeOptionalOperator('|')) {
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.sourceSpan(nameStart);
const args = [];
while (this.consumeOptionalCharacter(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.consumeOptionalOperator('|'));
}
return result;
}
parseExpression() {
return this.parseConditional();
}
parseConditional() {
const start = this.inputIndex;
const result = this.parseLogicalOr();
if (this.consumeOptionalOperator('?')) {
const yes = this.parsePipe();
let no;
if (!this.consumeOptionalCharacter(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() {
// '||'
let result = this.parseLogicalAnd();
while (this.consumeOptionalOperator('||')) {
const right = this.parseLogicalAnd();
const { start } = result.span;
result = new Binary(this.span(start), this.sourceSpan(start), '||', result, right);
}
return result;
}
parseLogicalAnd() {
// '&&'
let result = this.parseEquality();
while (this.consumeOptionalOperator('&&')) {
const right = this.parseEquality();
const { start } = result.span;
result = new Binary(this.span(start), this.sourceSpan(start), '&&', result, right);
}
return result;
}
parseEquality() {
// '==','!=','===','!=='
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() {
// '<', '>', '<=', '>='
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() {
// '+', '-'
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() {
// '*', '%', '/'
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() {
if (this.next.type == TokenType.Operator) {
const start = this.inputIndex;
const operator = this.next.strValue;
let result;
switch (operator) {
case '+':
this.advance();
result = this.parsePrefix();
return Unary.createPlus(this.span(start), this.sourceSpan(start), result);
case '-':
this.advance();
result = this.parsePrefix();
return Unary.createMinus(this.span(start), this.sourceSpan(start), result);
case '!':
this.advance();
result = this.parsePrefix();
return new PrefixNot(this.span(start), this.sourceSpan(start), result);
}
}
return this.parseCallChain();
}
parseCallChain() {
let result = this.parsePrimary();
const resultStart = result.span.start;
while (true) {
if (this.consumeOptionalCharacter(chars.$PERIOD)) {
result = this.parseAccessMemberOrMethodCall(result, false);
}
else if (this.consumeOptionalOperator('?.')) {
result = this.parseAccessMemberOrMethodCall(result, true);
}
else if (this.consumeOptionalCharacter(chars.$LBRACKET)) {
this.withContext(ParseContextFlags.Writable, () => {
this.rbracketsExpected++;
const key = this.parsePipe();
if (key instanceof EmptyExpr) {
this.error(`Key access cannot be empty`);
}
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
if (this.consumeOptionalOperator('=')) {
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.consumeOptionalCharacter(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.consumeOptionalOperator('!')) {
result = new NonNullAssert(this.span(resultStart), this.sourceSpan(resultStart), result);
}
else {
return result;
}
}
}
parsePrimary() {
const start = this.inputIndex;
if (this.consumeOptionalCharacter(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 ThisReceiver(this.span(start), this.sourceSpan(start));
}
else if (this.consumeOptionalCharacter(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) {
const result = [];
if (!this.next.isCharacter(terminator)) {
do {
result.push(this.parsePipe());
} while (this.consumeOptionalCharacter(chars.$COMMA));
}
return result;
}
parseLiteralMap() {
const keys = [];
const values = [];
const start = this.inputIndex;
this.expectCharacter(chars.$LBRACE);
if (!this.consumeOptionalCharacter(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.consumeOptionalCharacter(chars.$COMMA));
this.rbracesExpected--;
this.expectCharacter(chars.$RBRACE);
}
return new LiteralMap(this.span(start), this.sourceSpan(start), keys, values);
}
parseAccessMemberOrMethodCall(receiver, isSafe = false) {
const start = receiver.span.start;
const nameStart = this.inputIndex;
const id = this.expectIdentifierOrKeyword();
const nameSpan = this.sourceSpan(nameStart);
if (this.consumeOptionalCharacter(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, nameSpan, receiver, id, args) :
new MethodCall(span, sourceSpan, nameSpan, receiver, id, args);
}
else {
if (isSafe) {
if (this.consumeOptionalOperator('=')) {
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), nameSpan, receiver, id);
}
}
else {
if (this.consumeOptionalOperator('=')) {
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), nameSpan, receiver, id, value);
}
else {
return new PropertyRead(this.span(start), this.sourceSpan(start), nameSpan, receiver, id);
}
}
}
}
parseCallArguments() {
if (this.next.isCharacter(chars.$RPAREN))
return [];
const positionals = [];
do {
positionals.push(this.parsePipe());
} while (this.consumeOptionalCharacter(chars.$COMMA));
return positionals;
}
/**
* Parses an identifier, a keyword, a string with an optional `-` in between,
* and returns the string along with its absolute source span.
*/
expectTemplateBindingKey() {
let result = '';
let operatorFound = false;
const start = this.currentAbsoluteOffset;
do {
result += this.expectIdentifierOrKeywordOrString();
operatorFound = this.consumeOptionalOperator('-');
if (operatorFound) {
result += '-';
}
} while (operatorFound);
return {
source: result,
span: new AbsoluteSourceSpan(start, start + result.length),
};
}
/**
* 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 *, along with its absolute span.
*/
parseTemplateBindings(templateKey) {
const bindings = [];
// 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));
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 {
// 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 = this.expectTemplateBindingKey();
// Peek at the next token, if it is "as" then this must be variable
// declaration.
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
key.source = templateKey.source + key.source[0].toUpperCase() + key.source.substring(1);
bindings.push(...this.parseDirectiveKeywordBindings(key));
}
}
this.consumeStatementTerminator();
}
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, along with its
* absolute span.
*/
parseDirectiveKeywordBindings(key) {
const bindings = [];
this.consumeOptionalCharacter(chars.$COLON); // trackBy: trackByFunction
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);
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);
}
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"
* ```
*/
getDirectiveBoundTarget() {
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, along with its absolute span.
*/
parseAsBinding(value) {
if (!this.peekKeywordAs()) {
return null;
}
this.advance(); // consume the 'as' keyword
const key = this.expectTemplateBindingKey();
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;"
* ^^^^^^^^ ^^^^^^^^^^^
* ```
* In the first binding, `item` is bound to `NgForOfContext.$implicit`.
* In the second binding, `i` is bound to `NgForOfContext.index`.
*/
parseLetBinding() {
if (!this.peekKeywordLet()) {
return null;
}
const spanStart = this.currentAbsoluteOffset;
this.advance(); // consume the 'let' keyword
const key = this.expectTemplateBindingKey();
let value = null;
if (this.consumeOptionalOperator('=')) {
value = this.expectTemplateBindingKey();
}
this.consumeStatementTerminator();
const sourceSpan = new AbsoluteSourceSpan(spanStart, this.currentAbsoluteOffset);
return new VariableBinding(sourceSpan, key, value);
}
/**
* Consume the optional statement terminator: semicolon or comma.
*/
consumeStatementTerminator() {
this.consumeOptionalCharacter(chars.$SEMICOLON) || this.consumeOptionalCharacter(chars.$COMMA);
}
/**
* Records an error and skips over the token stream until reaching a recoverable point. See
* `this.skip` for more details on token skipping.
*/
error(message, index = null) {
this.errors.push(new ParserError(message, this.input, this.locationText(index), this.location));
this.skip();
}
locationText(index = 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 ';'.
*
* Furthermore, the presence of a stateful context can add more recovery points.
* - in a `Writable` context, we are able to recover after seeing the `=` operator, which
* signals the presence of an independent rvalue expression following the `=` operator.
*
* 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.
*/
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)) &&
(!(this.context & ParseContextFlags.Writable) || !n.isOperator('='))) {
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 {
constructor() {
this.errors = [];
}
visitImplicitReceiver(ast, context) { }
visitThisReceiver(ast, context) { }
visitInterpolation(ast, context) { }
visitLiteralPrimitive(ast, context) { }
visitPropertyRead(ast, context) { }
visitPropertyWrite(ast, context) { }
visitSafePropertyRead(ast, context) { }
visitMethodCall(ast, context) { }
visitSafeMethodCall(ast, context) { }
visitFunctionCall(ast, context) { }
visitLiteralArray(ast, context) {
this.visitAll(ast.expressions, context);
}
visitLiteralMap(ast, context) {
this.visitAll(ast.values, context);
}
visitUnary(ast, context) { }
visitBinary(ast, context) { }
visitPrefixNot(ast, context) { }
visitNonNullAssert(ast, context) { }
visitConditional(ast, context) { }
visitPipe(ast, context) {
this.errors.push('pipes');
}
visitKeyedRead(ast, context) { }
visitKeyedWrite(ast, context) { }
visitAll(asts, context) {
return asts.map(node => node.visit(this, context));
}
visitChain(ast, context) { }
visitQuote(ast, context) { }
}
/**
* This class implements SimpleExpressionChecker used in View Engine and performs more strict checks
* to make sure host bindings do not contain pipes. In View Engine, having pipes in host bindings is
* not supported as well, but in some cases (like `!(value | async)`) the error is not triggered at
* compile time. In order to preserve View Engine behavior, more strict checks are introduced for
* Ivy mode only.
*/
class IvySimpleExpressionChecker extends RecursiveAstVisitor {
constructor() {
super(...arguments);
this.errors = [];
}
visitPipe() {
this.errors.push('pipes');
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGFyc2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vLi4vLi4vcGFja2FnZXMvY29tcGlsZXIvc3JjL2V4cHJlc3Npb25fcGFyc2VyL3BhcnNlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFFSCxPQUFPLEtBQUssS0FBSyxNQUFNLFVBQVUsQ0FBQztBQUNsQyxPQUFPLEVBQUMsNEJBQTRCLEVBQXNCLE1BQU0sbUNBQW1DLENBQUM7QUFDcEcsT0FBTyxFQUFDLFlBQVksRUFBQyxNQUFNLFNBQVMsQ0FBQztBQUVyQyxPQUFPLEVBQUMsa0JBQWtCLEVBQW1CLGFBQWEsRUFBRSxNQUFNLEVBQUUsV0FBVyxFQUFFLEtBQUssRUFBRSxXQUFXLEVBQUUsU0FBUyxFQUFFLGlCQUFpQixFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxhQUFhLEVBQUUsU0FBUyxFQUFFLFVBQVUsRUFBRSxZQUFZLEVBQUUsVUFBVSxFQUFpQixnQkFBZ0IsRUFBRSxVQUFVLEVBQUUsYUFBYSxFQUFFLFdBQVcsRUFBRSxTQUFTLEVBQUUsU0FBUyxFQUFFLFlBQVksRUFBRSxhQUFhLEVBQUUsS0FBSyxFQUFFLG1CQUFtQixFQUFFLGNBQWMsRUFBRSxnQkFBZ0IsRUFBOEMsWUFBWSxFQUFFLEtBQUssRUFBRSxlQUFlLEVBQUMsTUFBTSxPQUFPLENBQUM7QUFDNWYsT0FBTyxFQUFDLEdBQUcsRUFBRSxZQUFZLEVBQUUsT0FBTyxFQUFnQixTQUFTLEVBQUMsTUFBTSxTQUFTLENBQUM7QUFFNUUsTUFBTSxPQUFPLGtCQUFrQjtJQUM3QixZQUNXLE9BQWlCLEVBQVMsV0FBMkMsRUFDckUsV0FBcUIsRUFBUyxnQkFBZ0QsRUFDOUUsT0FBaUI7UUFGakIsWUFBTyxHQUFQLE9BQU8sQ0FBVTtRQUFTLGdCQUFXLEdBQVgsV0FBVyxDQUFnQztRQUNyRSxnQkFBVyxHQUFYLFdBQVcsQ0FBVTtRQUFTLHFCQUFnQixHQUFoQixnQkFB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