UNPKG

@aurelia/expression-parser

Version:

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![CircleCI](https://circleci.com/

1,550 lines (1,457 loc) 65.2 kB
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ import { ArrayBindingPattern, ArrayLiteralExpression, BindingIdentifier, CustomExpression, ForOfStatement, Interpolation, ObjectBindingPattern, ObjectLiteralExpression, PrimitiveLiteralExpression, TaggedTemplateExpression, TemplateExpression, AnyBindingExpression, BinaryOperator, BindingIdentifierOrPattern, IsAssign, IsAssignable, IsBinary, IsBindingBehavior, IsExpressionOrStatement, IsLeftHandSide, IsValueConverter, UnaryOperator, DestructuringAssignmentExpression as DAE, type ExpressionKind, ekAccessThis, ekAccessGlobal, ekAccessMember, ekAccessScope, ekArrayDestructuring, ekArrayBindingPattern, ekObjectBindingPattern, ekBindingIdentifier, ekObjectDestructuring, AssignmentOperator, createAccessThisExpression, createAccessBoundaryExpression, createAccessGlobalExpression, createAccessScopeExpression, createAccessMemberExpression, createAccessKeyedExpression, createNewExpression, createCallScopeExpression, createCallMemberExpression, createCallFunctionExpression, createCallGlobalExpression, createUnaryExpression, createTemplateExpression, createPrimitiveLiteralExpression, createBinaryExpression, createConditionalExpression, createAssignExpression, createValueConverterExpression, createBindingBehaviorExpression, createArrayLiteralExpression, createObjectLiteralExpression, createTaggedTemplateExpression, createBindingIdentifier, createArrayBindingPattern, createObjectBindingPattern, createForOfStatement, createInterpolation as createInterpolationAst, createDestructuringAssignmentExpression, createDestructuringAssignmentSingleExpression, createArrowFunction, PrimitiveLiteral, } from './ast'; import { createLookup } from './utilities'; import { ErrorNames, createMappedError } from './errors'; import { createImplementationRegister, DI } from '@aurelia/kernel'; export interface IExpressionParser<TCustom extends CustomExpression = CustomExpression> { parse(expression: string, expressionType: 'IsIterator'): ForOfStatement; parse(expression: string, expressionType: 'Interpolation'): Interpolation; parse(expression: string, expressionType: Exclude<ExpressionType, 'IsIterator' | 'Interpolation'>): IsBindingBehavior; parse(expression: string, expressionType: ExpressionType): AnyBindingExpression<TCustom>; } export const IExpressionParser = /*@__PURE__*/DI.createInterface<IExpressionParser>('IExpressionParser'); /** * A default implementation of the IExpressionParser interface */ export class ExpressionParser<TCustom extends CustomExpression = CustomExpression> implements IExpressionParser<TCustom> { public static readonly register = createImplementationRegister(IExpressionParser); /** @internal */ private readonly _expressionLookup: Record<string, IsBindingBehavior> = createLookup(); /** @internal */ private readonly _forOfLookup: Record<string, ForOfStatement> = createLookup(); /** @internal */ private readonly _interpolationLookup: Record<string, Interpolation> = createLookup(); public parse(expression: string, expressionType: 'IsIterator'): ForOfStatement; public parse(expression: string, expressionType: 'Interpolation'): Interpolation; public parse(expression: string, expressionType: Exclude<ExpressionType, 'IsIterator' | 'Interpolation'>): IsBindingBehavior; public parse(expression: string, expressionType: ExpressionType): AnyBindingExpression; public parse(expression: string, expressionType: ExpressionType): AnyBindingExpression { let found: AnyBindingExpression; switch (expressionType) { case etIsCustom: return new CustomExpression(expression) as AnyBindingExpression; case etInterpolation: found = this._interpolationLookup[expression]; if (found === void 0) { found = this._interpolationLookup[expression] = this.$parse(expression, expressionType); } return found; case etIsIterator: found = this._forOfLookup[expression]; if (found === void 0) { found = this._forOfLookup[expression] = this.$parse(expression, expressionType); } return found; default: { if (expression.length === 0) { if (expressionType === etIsFunction || expressionType === etIsProperty) { return PrimitiveLiteral.$empty; } throw invalidEmptyExpression(); } found = this._expressionLookup[expression]; if (found === void 0) { found = this._expressionLookup[expression] = this.$parse(expression, expressionType); } return found; } } } /** @internal */ private $parse(expression: string, expressionType: 'IsIterator'): ForOfStatement; /** @internal */ private $parse(expression: string, expressionType: 'Interpolation'): Interpolation; /** @internal */ private $parse(expression: string, expressionType: Exclude<ExpressionType, 'IsIterator' | 'Interpolation'>): IsBindingBehavior; /** @internal */ private $parse(expression: string, expressionType: ExpressionType): AnyBindingExpression { $input = expression; $index = 0; $length = expression.length; $scopeDepth = 0; $startIndex = 0; $currentToken = Token.EOF; $tokenValue = ''; $currentChar = $charCodeAt(0); $assignable = true; $optional = false; $accessGlobal = true; $semicolonIndex = -1; return parse(Precedence.Variadic, expressionType === void 0 ? etIsProperty : expressionType); } } _START_CONST_ENUM(); const enum Char { Null = 0x00, Backspace = 0x08, Tab = 0x09, LineFeed = 0x0A, VerticalTab = 0x0B, FormFeed = 0x0C, CarriageReturn = 0x0D, Space = 0x20, Exclamation = 0x21, DoubleQuote = 0x22, Dollar = 0x24, Percent = 0x25, Ampersand = 0x26, SingleQuote = 0x27, OpenParen = 0x28, CloseParen = 0x29, Asterisk = 0x2A, Plus = 0x2B, Comma = 0x2C, Minus = 0x2D, Dot = 0x2E, Slash = 0x2F, Semicolon = 0x3B, Backtick = 0x60, OpenBracket = 0x5B, Backslash = 0x5C, CloseBracket = 0x5D, Caret = 0x5E, Underscore = 0x5F, OpenBrace = 0x7B, Bar = 0x7C, CloseBrace = 0x7D, Colon = 0x3A, LessThan = 0x3C, Equals = 0x3D, GreaterThan = 0x3E, Question = 0x3F, Zero = 0x30, One = 0x31, Two = 0x32, Three = 0x33, Four = 0x34, Five = 0x35, Six = 0x36, Seven = 0x37, Eight = 0x38, Nine = 0x39, UpperA = 0x41, UpperB = 0x42, UpperC = 0x43, UpperD = 0x44, UpperE = 0x45, UpperF = 0x46, UpperG = 0x47, UpperH = 0x48, UpperI = 0x49, UpperJ = 0x4A, UpperK = 0x4B, UpperL = 0x4C, UpperM = 0x4D, UpperN = 0x4E, UpperO = 0x4F, UpperP = 0x50, UpperQ = 0x51, UpperR = 0x52, UpperS = 0x53, UpperT = 0x54, UpperU = 0x55, UpperV = 0x56, UpperW = 0x57, UpperX = 0x58, UpperY = 0x59, UpperZ = 0x5A, LowerA = 0x61, LowerB = 0x62, LowerC = 0x63, LowerD = 0x64, LowerE = 0x65, LowerF = 0x66, LowerG = 0x67, LowerH = 0x68, LowerI = 0x69, LowerJ = 0x6A, LowerK = 0x6B, LowerL = 0x6C, LowerM = 0x6D, LowerN = 0x6E, LowerO = 0x6F, LowerP = 0x70, LowerQ = 0x71, LowerR = 0x72, LowerS = 0x73, LowerT = 0x74, LowerU = 0x75, LowerV = 0x76, LowerW = 0x77, LowerX = 0x78, LowerY = 0x79, LowerZ = 0x7A } _END_CONST_ENUM(); function unescapeCode(code: number): number { switch (code) { case Char.LowerB: return Char.Backspace; case Char.LowerT: return Char.Tab; case Char.LowerN: return Char.LineFeed; case Char.LowerV: return Char.VerticalTab; case Char.LowerF: return Char.FormFeed; case Char.LowerR: return Char.CarriageReturn; case Char.DoubleQuote: return Char.DoubleQuote; case Char.SingleQuote: return Char.SingleQuote; case Char.Backslash: return Char.Backslash; default: return code; } } _START_CONST_ENUM(); const enum Precedence { Variadic = 0b0000_111101, Assign = 0b0000_111110, Conditional = 0b0000_111111, Assignment = 0b0001_000000, NullishCoalescing = 0b0010_000000, LogicalOR = 0b0011_000000, LogicalAND = 0b0100_000000, Equality = 0b0101_000000, Relational = 0b0110_000000, Additive = 0b0111_000000, Multiplicative = 0b1000_000000, Exponentiation = 0b1001_000000, Binary = 0b1001_000001, Member = 0b1001_000010, LeftHandSide = 0b1001_000011, Primary = 0b1001_000100, Unary = 0b1001_000101, } _END_CONST_ENUM(); _START_CONST_ENUM(); const enum Token { EOF = 0b1100000000000_0000_000000, ExpressionTerminal = 0b1000000000000_0000_000000, AccessScopeTerminal = 0b0100000000000_0000_000000, ClosingToken = 0b0010000000000_0000_000000, OpeningToken = 0b0001000000000_0000_000000, BinaryOp = 0b0000100000000_0000_000000, UnaryOp = 0b0000010000000_0000_000000, LeftHandSide = 0b0000001000000_0000_000000, StringOrNumericLiteral = 0b0000000110000_0000_000000, NumericLiteral = 0b0000000100000_0000_000000, StringLiteral = 0b0000000010000_0000_000000, IdentifierName = 0b0000000001100_0000_000000, // Keyword = 0b0000000001000_0000_000000, Identifier = 0b0000000000100_0000_000000, Contextual = 0b0000000000010_0000_000000, OptionalSuffix = 0b0000000001101_0000_000000, Precedence = 0b0000000000000_1111_000000, Type = 0b0000000000000_0000_111111, FalseKeyword = 0b0000000001000_0000_000000, TrueKeyword = 0b0000000001000_0000_000001, NullKeyword = 0b0000000001000_0000_000010, UndefinedKeyword = 0b0000000001000_0000_000011, NewKeyword = 0b0000000001000_0000_000100, ThisScope = 0b0000000001100_0000_000101, AccessBoundary = 0b0000000001100_0000_000110, // HostScope = 0b0000000001100_0000_000111, ParentScope = 0b0000000001100_0000_001000, OpenParen = 0b0101001000001_0000_001001, OpenBrace = 0b0001000000000_0000_001010, Dot = 0b0000001000000_0000_001011, DotDot = 0b0000000000000_0000_001100, DotDotDot = 0b0000000000000_0000_001101, QuestionDot = 0b0100001000000_0000_001110, CloseBrace = 0b1110000000000_0000_001111, CloseParen = 0b1110000000000_0000_010000, Comma = 0b1100000000000_0000_010011, OpenBracket = 0b0101001000001_0000_010100, CloseBracket = 0b1110000000000_0000_010101, Colon = 0b1100000000000_0000_010110, Semicolon = 0b1100000000000_0000_010111, Question = 0b1100000000000_0000_011000, Ampersand = 0b1100000000000_0000_011001, Bar = 0b1100000000000_0000_011010, QuestionQuestion = 0b1100100000000_0010_011011, BarBar = 0b1100100000000_0011_011100, AmpersandAmpersand = 0b1100100000000_0100_011101, EqualsEquals = 0b1100100000000_0101_011110, ExclamationEquals = 0b1100100000000_0101_011111, EqualsEqualsEquals = 0b1100100000000_0101_100000, ExclamationEqualsEquals = 0b1100100000000_0101_100001, LessThan = 0b1100100000000_0110_100010, GreaterThan = 0b1100100000000_0110_100011, LessThanEquals = 0b1100100000000_0110_100100, GreaterThanEquals = 0b1100100000000_0110_100101, InKeyword = 0b1100100001000_0110_100110, InstanceOfKeyword = 0b1100100001000_0110_100111, Plus = 0b0100110000000_0111_101000, Minus = 0b0100110000000_0111_101001, TypeofKeyword = 0b0000010001000_0000_101010, VoidKeyword = 0b0000010001000_0000_101011, Asterisk = 0b1100100000000_1000_101100, Percent = 0b1100100000000_1000_101101, Slash = 0b1100100000000_1000_101110, AsteriskAsterisk = 0b1100100000000_1001_101111, Equals = 0b1000000000000_0000_110000, Exclamation = 0b0000010000000_0000_110001, TemplateTail = 0b0100001000001_0000_110010, TemplateContinuation = 0b0100001000001_0000_110011, OfKeyword = 0b1000000001010_0000_110100, Arrow = 0b0000000000000_0000_110101, PlusEquals = 0b1000000000000_0000_110110, MinusEquals = 0b1000000000000_0000_110111, AsteriskEquals = 0b1000000000000_0000_111000, SlashEquals = 0b1000000000000_0000_111001, PlusPlus = 0b0100010000000_0000_111010, MinusMinus = 0b0100010000000_0000_111011, } _END_CONST_ENUM(); const $false = PrimitiveLiteral.$false; const $true = PrimitiveLiteral.$true; const $null = PrimitiveLiteral.$null; const $undefined = PrimitiveLiteral.$undefined; const $this = createAccessThisExpression(0); const $parent = createAccessThisExpression(1); const boundary = createAccessBoundaryExpression(); const etNone = 'None'; const etInterpolation = 'Interpolation'; const etIsIterator = 'IsIterator'; const etIsChainable = 'IsChainable'; const etIsFunction = 'IsFunction'; const etIsProperty = 'IsProperty'; const etIsCustom = 'IsCustom'; export type ExpressionType = 'None' | 'Interpolation' | 'IsIterator' | 'IsChainable' | 'IsFunction' | 'IsProperty' | 'IsCustom'; let $input: string = ''; let $index: number = 0; let $length: number = 0; let $scopeDepth: number = 0; let $startIndex: number = 0; let $currentToken: Token = Token.EOF; let $tokenValue: string | number = ''; let $currentChar: number; let $assignable: boolean = true; let $optional: boolean = false; let $accessGlobal: boolean = true; let $semicolonIndex: number = -1; const stringFromCharCode = String.fromCharCode; const $charCodeAt = (index: number) => $input.charCodeAt(index); const $tokenRaw = (): string => $input.slice($startIndex, $index); const globalNames = ('Infinity NaN isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent' + ' Array BigInt Boolean Date Map Number Object RegExp Set String JSON Math Intl').split(' '); export function parseExpression(input: string, expressionType?: ExpressionType): AnyBindingExpression { $input = input; $index = 0; $length = input.length; $scopeDepth = 0; $startIndex = 0; $currentToken = Token.EOF; $tokenValue = ''; $currentChar = $charCodeAt(0); $assignable = true; $optional = false; $accessGlobal = true; $semicolonIndex = -1; return parse(Precedence.Variadic, expressionType === void 0 ? etIsProperty : expressionType); } // This is performance-critical code which follows a subset of the well-known ES spec. // Knowing the spec, or parsers in general, will help with understanding this code and it is therefore not the // single source of information for being able to figure it out. // It generally does not need to change unless the spec changes or spec violations are found, or optimization // opportunities are found (which would likely not fix these warnings in any case). // It's therefore not considered to have any tangible impact on the maintainability of the code base. // For reference, most of the parsing logic is based on: https://tc39.github.io/ecma262/#sec-ecmascript-language-expressions // eslint-disable-next-line max-lines-per-function export function parse(minPrecedence: Precedence, expressionType: ExpressionType): AnyBindingExpression { if (expressionType === etIsCustom) { return new CustomExpression($input); } if ($index === 0) { if (expressionType === etInterpolation) { return parseInterpolation(); } nextToken(); if ($currentToken & Token.ExpressionTerminal) { throw invalidStartOfExpression(); } } $assignable = Precedence.Binary > minPrecedence; $optional = false; $accessGlobal = Precedence.LeftHandSide > minPrecedence; let optionalThisTail = false; let result = void 0 as unknown as IsExpressionOrStatement; let ancestor = 0; if ($currentToken & Token.UnaryOp) { /** * parseUnaryExpression * * https://tc39.github.io/ecma262/#sec-unary-operators * * UnaryExpression : * 1. LeftHandSideExpression * 2. void UnaryExpression * 3. typeof UnaryExpression * 4. + UnaryExpression * 5. - UnaryExpression * 6. ! UnaryExpression * 7. ++ UnaryExpression * 8. -- UnaryExpression * * IsValidAssignmentTarget * 2,3,4,5,6,7,8 = false * 1 = see parseLeftHandSideExpression * * Note: technically we should throw on +++ / ---, but there's nothing to gain from that */ const op = TokenValues[$currentToken & Token.Type] as UnaryOperator; nextToken(); result = createUnaryExpression(op, parse(Precedence.LeftHandSide, expressionType) as IsLeftHandSide); $assignable = false; } else { /** * parsePrimaryExpression * * https://tc39.github.io/ecma262/#sec-primary-expression * * PrimaryExpression : * 1. this * 2. IdentifierName * 3. Literal * 4. ArrayLiteralExpression * 5. ObjectLiteralExpression * 6. TemplateLiteral * 7. ParenthesizedExpression * * Literal : * NullLiteral * BooleanLiteral * NumericLiteral * StringLiteral * * ParenthesizedExpression : * ( AssignmentExpression ) * * IsValidAssignmentTarget * 1,3,4,5,6,7 = false * 2 = true */ primary: switch ($currentToken) { case Token.ParentScope: // $parent ancestor = $scopeDepth; $assignable = false; $accessGlobal = false; do { nextToken(); ++ancestor; switch (($currentToken as Token)) { case Token.Dot: nextToken(); if (($currentToken & Token.IdentifierName) === 0) { throw expectedIdentifier(); } break; case Token.DotDot: case Token.DotDotDot: throw expectedIdentifier(); case Token.QuestionDot: $optional = true; nextToken(); if (($currentToken & Token.IdentifierName) === 0) { result = ancestor === 0 ? $this : ancestor === 1 ? $parent : createAccessThisExpression(ancestor); optionalThisTail = true; break primary; } break; default: if ($currentToken & Token.AccessScopeTerminal) { result = ancestor === 0 ? $this : ancestor === 1 ? $parent : createAccessThisExpression(ancestor); break primary; } throw invalidMemberExpression(); } } while ($currentToken === Token.ParentScope); // falls through case Token.Identifier: { // identifier const id = $tokenValue as string; if (expressionType === etIsIterator) { result = createBindingIdentifier(id); } else if ($accessGlobal && globalNames.includes(id as (typeof globalNames)[number])) { result = createAccessGlobalExpression(id); } else if ($accessGlobal && id === 'import') { throw unexpectedImportKeyword(); } else { result = createAccessScopeExpression(id, ancestor); } $assignable = !$optional; nextToken(); if (consumeOpt(Token.Arrow)) { if (($currentToken as Token) === Token.OpenBrace) { throw functionBodyInArrowFn(); } const _optional = $optional; const _scopeDepth = $scopeDepth; ++$scopeDepth; const body = parse(Precedence.Assign, etNone) as IsAssign; $optional = _optional; $scopeDepth = _scopeDepth; $assignable = false; result = createArrowFunction([createBindingIdentifier(id)], body); } break; } case Token.DotDot: throw unexpectedDoubleDot(); case Token.DotDotDot: throw invalidSpreadOp(); case Token.ThisScope: // $this $assignable = false; nextToken(); switch ($scopeDepth) { case 0: result = $this; break; case 1: result = $parent; break; default: result = createAccessThisExpression($scopeDepth); break; } break; case Token.AccessBoundary: // this $assignable = false; nextToken(); result = boundary; break; case Token.OpenParen: result = parseCoverParenthesizedExpressionAndArrowParameterList(expressionType); break; case Token.OpenBracket: result = $input.search(/\s+of\s+/) > $index ? parseArrayDestructuring() : parseArrayLiteralExpression(expressionType); break; case Token.OpenBrace: result = parseObjectLiteralExpression(expressionType); break; case Token.TemplateTail: result = createTemplateExpression([$tokenValue as string]); $assignable = false; nextToken(); break; case Token.TemplateContinuation: result = parseTemplate(expressionType, result as IsLeftHandSide, false); break; case Token.StringLiteral: case Token.NumericLiteral: result = createPrimitiveLiteralExpression($tokenValue); $assignable = false; nextToken(); break; case Token.NullKeyword: case Token.UndefinedKeyword: case Token.TrueKeyword: case Token.FalseKeyword: result = TokenValues[$currentToken & Token.Type] as PrimitiveLiteralExpression; $assignable = false; nextToken(); break; case Token.NewKeyword: { nextToken(); const callee = parse(Precedence.Member, expressionType) as IsLeftHandSide; let args: IsAssign[]; if (($currentToken as Token) === Token.OpenParen) { args = parseArguments(); } else { args = []; nextToken(); } result = createNewExpression(callee, args); $assignable = false; break; } default: if ($index >= $length) { throw unexpectedEndOfExpression(); } else { throw unconsumedToken(); } } if (expressionType === etIsIterator) { return parseForOfStatement(result as BindingIdentifierOrPattern); } switch ($currentToken as Token) { case Token.PlusPlus: case Token.MinusMinus: result = createUnaryExpression(TokenValues[$currentToken & Token.Type] as UnaryOperator, result as IsLeftHandSide, 1); nextToken(); $assignable = false; break; } if (Precedence.LeftHandSide < minPrecedence) { return result as any; } if (($currentToken as Token) === Token.DotDot || ($currentToken as Token) === Token.DotDotDot) { throw expectedIdentifier(); } if (result.$kind === ekAccessThis) { switch ($currentToken as Token) { case Token.QuestionDot: $optional = true; $assignable = false; nextToken(); if (($currentToken & Token.OptionalSuffix) === 0) { throw unexpectedTokenInOptionalChain(); } if ($currentToken & Token.IdentifierName) { result = createAccessScopeExpression($tokenValue as string, result.ancestor); nextToken(); } else if (($currentToken as Token) === Token.OpenParen) { result = createCallFunctionExpression(result as IsLeftHandSide, parseArguments(), true); } else if (($currentToken as Token) === Token.OpenBracket) { result = parseKeyedExpression(result, true); } else { throw invalidTaggedTemplateOnOptionalChain(); } break; case Token.Dot: $assignable = !$optional; nextToken(); if (($currentToken & Token.IdentifierName) === 0) { throw expectedIdentifier(); } result = createAccessScopeExpression($tokenValue as string, result.ancestor); nextToken(); break; case Token.DotDot: case Token.DotDotDot: throw expectedIdentifier(); case Token.OpenParen: result = createCallFunctionExpression(result as IsLeftHandSide, parseArguments(), optionalThisTail); break; case Token.OpenBracket: result = parseKeyedExpression(result, optionalThisTail); break; case Token.TemplateTail: result = createTemplateTail(result as IsLeftHandSide); break; case Token.TemplateContinuation: result = parseTemplate(expressionType, result as IsLeftHandSide, true); break; } } /** * parseMemberExpression (Token.Dot, Token.OpenBracket, Token.TemplateContinuation) * * MemberExpression : * 1. PrimaryExpression * 2. MemberExpression [ AssignmentExpression ] * 3. MemberExpression . IdentifierName * 4. MemberExpression TemplateLiteral * * IsValidAssignmentTarget * 1,4 = false * 2,3 = true * * * parseCallExpression (Token.OpenParen) * CallExpression : * 1. MemberExpression Arguments * 2. CallExpression Arguments * 3. CallExpression [ AssignmentExpression ] * 4. CallExpression . IdentifierName * 5. CallExpression TemplateLiteral * * IsValidAssignmentTarget * 1,2,5 = false * 3,4 = true */ while (($currentToken & Token.LeftHandSide) > 0) { switch (($currentToken as Token)) { case Token.QuestionDot: result = parseOptionalChainLHS(result as IsLeftHandSide); break; case Token.Dot: nextToken(); if (($currentToken & Token.IdentifierName) === 0) { throw expectedIdentifier(); } result = parseMemberExpressionLHS(result as IsLeftHandSide, false); break; case Token.DotDot: case Token.DotDotDot: throw expectedIdentifier(); case Token.OpenParen: if (Precedence.Member === minPrecedence) { return result as any; } if (result.$kind === ekAccessScope) { result = createCallScopeExpression(result.name, parseArguments(), result.ancestor, false); } else if (result.$kind === ekAccessMember) { result = createCallMemberExpression(result.object, result.name, parseArguments(), result.optional, false); } else if (result.$kind === ekAccessGlobal) { result = createCallGlobalExpression(result.name, parseArguments()); } else { result = createCallFunctionExpression(result as IsLeftHandSide, parseArguments(), false); } break; case Token.OpenBracket: result = parseKeyedExpression(result as IsLeftHandSide, false); break; case Token.TemplateTail: if ($optional) { throw invalidTaggedTemplateOnOptionalChain(); } result = createTemplateTail(result as IsLeftHandSide); break; case Token.TemplateContinuation: if ($optional) { throw invalidTaggedTemplateOnOptionalChain(); } result = parseTemplate(expressionType, result as IsLeftHandSide, true); break; } } } if (($currentToken as Token) === Token.DotDot || ($currentToken as Token) === Token.DotDotDot) { throw expectedIdentifier(); } if (Precedence.Binary < minPrecedence) { return result as any; } /** * parseBinaryExpression * * https://tc39.github.io/ecma262/#sec-multiplicative-operators * * MultiplicativeExpression : (local precedence 6) * UnaryExpression * MultiplicativeExpression * / % UnaryExpression * * AdditiveExpression : (local precedence 5) * MultiplicativeExpression * AdditiveExpression + - MultiplicativeExpression * * RelationalExpression : (local precedence 4) * AdditiveExpression * RelationalExpression < > <= >= instanceof in AdditiveExpression * * EqualityExpression : (local precedence 3) * RelationalExpression * EqualityExpression == != === !== RelationalExpression * * LogicalANDExpression : (local precedence 2) * EqualityExpression * LogicalANDExpression && EqualityExpression * * LogicalORExpression : (local precedence 1) * LogicalANDExpression * LogicalORExpression || LogicalANDExpression * * CoalesceExpression : * CoalesceExpressionHead ?? BitwiseORExpression * * CoalesceExpressionHead : * CoelesceExpression * BitwiseORExpression * * ShortCircuitExpression : * LogicalORExpression * CoalesceExpression */ while (($currentToken & Token.BinaryOp) > 0) { const opToken = $currentToken; if ((opToken & Token.Precedence) <= minPrecedence) { break; } nextToken(); result = createBinaryExpression(TokenValues[opToken & Token.Type] as BinaryOperator, result as IsBinary, parse(opToken & Token.Precedence, expressionType) as IsBinary); $assignable = false; } if (Precedence.Conditional < minPrecedence) { return result as any; } /** * parseConditionalExpression * https://tc39.github.io/ecma262/#prod-ConditionalExpression * * ConditionalExpression : * 1. ShortCircuitExpression * 2. ShortCircuitExpression ? AssignmentExpression : AssignmentExpression * * IsValidAssignmentTarget * 1,2 = false */ if (consumeOpt(Token.Question)) { const yes = parse(Precedence.Assign, expressionType) as IsAssign; consume(Token.Colon); result = createConditionalExpression(result as IsBinary, yes, parse(Precedence.Assign, expressionType) as IsAssign); $assignable = false; } if (Precedence.Assign < minPrecedence) { return result as any; } /** * parseAssignmentExpression * * https://tc39.github.io/ecma262/#prod-AssignmentExpression * Note: AssignmentExpression here is equivalent to ES Expression because we don't parse the comma operator * * AssignmentExpression : * 1. ConditionalExpression * 2. LeftHandSideExpression = AssignmentExpression * 3. LeftHandSideExpression AssignmentOperator AssignmentExpression * * IsValidAssignmentTarget * 1,2 = false */ switch ($currentToken as Token) { case Token.Equals: case Token.PlusEquals: case Token.MinusEquals: case Token.AsteriskEquals: case Token.SlashEquals: { if (!$assignable) { throw lhsNotAssignable(); } const op = TokenValues[$currentToken & Token.Type] as AssignmentOperator; nextToken(); result = createAssignExpression(result as IsAssignable, parse(Precedence.Assign, expressionType) as IsAssign, op); break; } } if (Precedence.Variadic < minPrecedence) { return result as any; } /** * parseValueConverter */ while (consumeOpt(Token.Bar)) { if ($currentToken === Token.EOF) { throw expectedValueConverterIdentifier(); } const name = $tokenValue as string; nextToken(); const args = new Array<IsAssign>(); while (consumeOpt(Token.Colon)) { args.push(parse(Precedence.Assign, expressionType) as IsAssign); } result = createValueConverterExpression(result as IsValueConverter, name, args); } /** * parseBindingBehavior */ while (consumeOpt(Token.Ampersand)) { if ($currentToken === Token.EOF) { throw expectedBindingBehaviorIdentifier(); } const name = $tokenValue as string; nextToken(); const args = new Array<IsAssign>(); while (consumeOpt(Token.Colon)) { args.push(parse(Precedence.Assign, expressionType) as IsAssign); } result = createBindingBehaviorExpression(result as IsBindingBehavior, name, args); } if ($currentToken !== Token.EOF) { if (expressionType === etInterpolation && $currentToken === Token.CloseBrace) { return result as any; } if (expressionType === etIsChainable && $currentToken === Token.Semicolon) { if ($index === $length) { throw unconsumedToken(); } $semicolonIndex = $index - 1; return result as any; } if ($tokenRaw() === 'of') { throw unexpectedOfKeyword(); } throw unconsumedToken(); } return result as any; } /** * [key,] * [key] * [,value] * [key,value] */ function parseArrayDestructuring(): DAE { const items: DAE['list'][number][] = []; let target: string | null = null; let $continue = true; let index = 0; while ($continue) { nextToken(); switch ($currentToken) { case Token.CloseBracket: $continue = false; addItem(); break; case Token.Comma: addItem(); break; case Token.Identifier: target = $tokenRaw(); break; default: throw unexpectedTokenInDestructuring(); } } consume(Token.CloseBracket); return createDestructuringAssignmentExpression(ekArrayDestructuring, items, void 0, void 0); function addItem() { if (target !== null) { items.push( createDestructuringAssignmentSingleExpression( createAccessMemberExpression($this, target), createAccessKeyedExpression($this, createPrimitiveLiteralExpression(index++)), void 0, ), ); target = null; } else { index++; } } } function parseArguments() { const _optional = $optional; nextToken(); const args: IsAssign[] = []; while (($currentToken as Token) !== Token.CloseParen) { args.push(parse(Precedence.Assign, etNone) as IsAssign); if (!consumeOpt(Token.Comma)) { break; } } consume(Token.CloseParen); $assignable = false; $optional = _optional; return args; } function parseKeyedExpression(result: IsLeftHandSide, optional: boolean) { const _optional = $optional; nextToken(); result = createAccessKeyedExpression(result, parse(Precedence.Assign, etNone) as IsAssign, optional); consume(Token.CloseBracket); $assignable = !_optional; $optional = _optional; return result; } function parseOptionalChainLHS(lhs: IsLeftHandSide) { $optional = true; $assignable = false; nextToken(); if (($currentToken & Token.OptionalSuffix) === 0) { throw unexpectedTokenInOptionalChain(); } if ($currentToken & Token.IdentifierName) { return parseMemberExpressionLHS(lhs, true); } if (($currentToken as Token) === Token.OpenParen) { if (lhs.$kind === ekAccessScope) { return createCallScopeExpression(lhs.name, parseArguments(), lhs.ancestor, true); } else if (lhs.$kind === ekAccessMember) { return createCallMemberExpression(lhs.object, lhs.name, parseArguments(), lhs.optional, true); } else { return createCallFunctionExpression(lhs, parseArguments(), true); } } if (($currentToken as Token) === Token.OpenBracket) { return parseKeyedExpression(lhs, true); } throw invalidTaggedTemplateOnOptionalChain(); } function parseMemberExpressionLHS(lhs: IsLeftHandSide, optional: boolean) { const rhs = $tokenValue as string; switch (($currentToken as Token)) { case Token.QuestionDot: { $optional = true; $assignable = false; const indexSave = $index; const startIndexSave = $startIndex; const currentTokenSave = $currentToken; const currentCharSave = $currentChar; const tokenValueSave = $tokenValue; const assignableSave = $assignable; const optionalSave = $optional; nextToken(); if (($currentToken & Token.OptionalSuffix) === 0) { throw unexpectedTokenInOptionalChain(); } if (($currentToken as Token) === Token.OpenParen) { return createCallMemberExpression(lhs, rhs, parseArguments(), optional, true); } $index = indexSave; $startIndex = startIndexSave; $currentToken = currentTokenSave; $currentChar = currentCharSave; $tokenValue = tokenValueSave; $assignable = assignableSave; $optional = optionalSave; return createAccessMemberExpression(lhs, rhs, optional); } case Token.OpenParen: { $assignable = false; return createCallMemberExpression(lhs, rhs, parseArguments(), optional, false); } default: { $assignable = !$optional; nextToken(); return createAccessMemberExpression(lhs, rhs, optional); } } } _START_CONST_ENUM(); const enum ArrowFnParams { Valid = 1, Invalid = 2, Default = 3, Destructuring = 4, } _END_CONST_ENUM(); /** * https://tc39.es/ecma262/#prod-CoverParenthesizedExpressionAndArrowParameterList * CoverParenthesizedExpressionAndArrowParameterList : * ( Expression ) * ( ) * ( BindingIdentifier ) * ( Expression , BindingIdentifier ) */ function parseCoverParenthesizedExpressionAndArrowParameterList(expressionType: ExpressionType): IsAssign { nextToken(); const indexSave = $index; const startIndexSave = $startIndex; const currentTokenSave = $currentToken; const currentCharSave = $currentChar; const tokenValueSave = $tokenValue; const optionalSave = $optional; const arrowParams: BindingIdentifier[] = []; let paramsState = ArrowFnParams.Valid; let isParamList = false; // eslint-disable-next-line no-constant-condition loop: while (true) { if (($currentToken as Token) === Token.DotDotDot) { nextToken(); if (($currentToken as Token) !== Token.Identifier) { throw expectedIdentifier(); } arrowParams.push(createBindingIdentifier($tokenValue as string)); nextToken(); if (($currentToken as Token) === Token.Comma) { throw restParamsMustBeLastParam(); } if (($currentToken as Token) !== Token.CloseParen) { throw invalidSpreadOp(); } nextToken(); if (($currentToken as Token) !== Token.Arrow) { throw invalidSpreadOp(); } nextToken(); const _optional = $optional; const _scopeDepth = $scopeDepth; ++$scopeDepth; const body = parse(Precedence.Assign, etNone) as IsAssign; $optional = _optional; $scopeDepth = _scopeDepth; $assignable = false; return createArrowFunction(arrowParams, body, true); } switch ($currentToken as Token) { case Token.Identifier: arrowParams.push(createBindingIdentifier($tokenValue as string)); nextToken(); break; case Token.CloseParen: // () - only valid if followed directly by an arrow nextToken(); break loop; /* eslint-disable */ case Token.OpenBrace: // ({ - may be a valid parenthesized expression case Token.OpenBracket: // ([ - may be a valid parenthesized expression nextToken(); paramsState = ArrowFnParams.Destructuring; break; /* eslint-enable */ case Token.Comma: // (, - never valid // (a,, - never valid paramsState = ArrowFnParams.Invalid; isParamList = true; break loop; case Token.OpenParen: // (( - may be a valid nested parenthesized expression or arrow fn // (a,( - never valid paramsState = ArrowFnParams.Invalid; break loop; default: nextToken(); paramsState = ArrowFnParams.Invalid; break; } switch ($currentToken) { case Token.Comma: nextToken(); isParamList = true; if (paramsState === ArrowFnParams.Valid) { break; } // ([something invalid], - treat as arrow fn / invalid arrow params break loop; case Token.CloseParen: nextToken(); break loop; case Token.Equals: // (a=a - may be a valid parenthesized expression if (paramsState === ArrowFnParams.Valid) { paramsState = ArrowFnParams.Default; } break loop; case Token.Arrow: // (a,a=> - never valid if (isParamList) { throw invalidArrowParameterList(); } // (a=> - may be a valid parenthesized expression with nested arrow fn nextToken(); paramsState = ArrowFnParams.Invalid; break loop; default: if (paramsState === ArrowFnParams.Valid) { paramsState = ArrowFnParams.Invalid; } break loop; } } if ($currentToken === Token.Arrow) { if (paramsState === ArrowFnParams.Valid) { nextToken(); if (($currentToken as Token) === Token.OpenBrace) { throw functionBodyInArrowFn(); } const _optional = $optional; const _scopeDepth = $scopeDepth; ++$scopeDepth; const body = parse(Precedence.Assign, etNone) as IsAssign; $optional = _optional; $scopeDepth = _scopeDepth; $assignable = false; return createArrowFunction(arrowParams, body); } throw invalidArrowParameterList(); } else if (paramsState === ArrowFnParams.Valid && arrowParams.length === 0) { // () - never valid as a standalone expression throw missingExpectedToken(Token.Arrow); } if (isParamList) { // ([something invalid], - treat as arrow fn / invalid arrow params switch (paramsState) { case ArrowFnParams.Invalid: throw invalidArrowParameterList(); case ArrowFnParams.Default: throw defaultParamsInArrowFn(); case ArrowFnParams.Destructuring: throw destructuringParamsInArrowFn(); } } $index = indexSave; $startIndex = startIndexSave; $currentToken = currentTokenSave; $currentChar = currentCharSave; $tokenValue = tokenValueSave; $optional = optionalSave; const _optional = $optional; const expr = parse(Precedence.Assign, expressionType) as IsAssign; $optional = _optional; consume(Token.CloseParen); if ($currentToken === Token.Arrow) { // we only get here if there was a valid parenthesized expression which was not valid as arrow fn params switch (paramsState) { case ArrowFnParams.Invalid: throw invalidArrowParameterList(); case ArrowFnParams.Default: throw defaultParamsInArrowFn(); case ArrowFnParams.Destructuring: throw destructuringParamsInArrowFn(); } } return expr; } /** * parseArrayLiteralExpression * https://tc39.github.io/ecma262/#prod-ArrayLiteralExpression * * ArrayLiteralExpression : * [ Elision(opt) ] * [ ElementList ] * [ ElementList, Elision(opt) ] * * ElementList : * Elision(opt) AssignmentExpression * ElementList, Elision(opt) AssignmentExpression * * Elision : * , * Elision , */ function parseArrayLiteralExpression(expressionType: ExpressionType): ArrayBindingPattern | ArrayLiteralExpression { const _optional = $optional; nextToken(); const elements = new Array<IsAssign>(); while ($currentToken !== Token.CloseBracket) { if (consumeOpt(Token.Comma)) { elements.push($undefined); if (($currentToken as Token) === Token.CloseBracket) { break; } } else { elements.push(parse(Precedence.Assign, expressionType === etIsIterator ? etNone : expressionType) as IsAssign); if (consumeOpt(Token.Comma)) { if (($currentToken as Token) === Token.CloseBracket) { break; } } else { break; } } } $optional = _optional; consume(Token.CloseBracket); if (expressionType === etIsIterator) { return createArrayBindingPattern(elements); } else { $assignable = false; return createArrayLiteralExpression(elements); } } const allowedForExprKinds: ExpressionKind[] = [ekArrayBindingPattern, ekObjectBindingPattern, ekBindingIdentifier, ekArrayDestructuring, ekObjectDestructuring]; function parseForOfStatement(result: BindingIdentifierOrPattern): ForOfStatement { if (!allowedForExprKinds.includes(result.$kind)) { throw invalidLHSBindingIdentifierInForOf(result.$kind); } if ($currentToken !== Token.OfKeyword) { throw invalidLHSBindingIdentifierInForOf(result.$kind); } nextToken(); const declaration = result; const statement = parse(Precedence.Variadic, etIsChainable); return createForOfStatement(declaration, statement as IsBindingBehavior, $semicolonIndex); } /** * parseObjectLiteralExpression * https://tc39.github.io/ecma262/#prod-Literal * * ObjectLiteralExpression : * { } * { PropertyDefinitionList } * * PropertyDefinitionList : * PropertyDefinition * PropertyDefinitionList, PropertyDefinition * * PropertyDefinition : * IdentifierName * PropertyName : AssignmentExpression * * PropertyName : * IdentifierName * StringLiteral * NumericLiteral */ function parseObjectLiteralExpression(expressionType: ExpressionType): ObjectBindingPattern | ObjectLiteralExpression { const _optional = $optional; const keys = new Array<string | number>(); const values = new Array<IsAssign>(); nextToken(); while ($currentToken !== Token.CloseBrace) { keys.push($tokenValue); // Literal = mandatory colon if ($currentToken & Token.StringOrNumericLiteral) { nextToken(); consume(Token.Colon); values.push(parse(Precedence.Assign, expressionType === etIsIterator ? etNone : expressionType) as IsAssign); } else if ($currentToken & Token.IdentifierName) { // IdentifierName = optional colon const currentChar = $currentChar; const currentToken = $currentToken; const index = $index; nextToken(); if (consumeOpt(Token.Colon)) { values.push(parse(Precedence.Assign, expressionType === etIsIterator ? etNone : expressionType) as IsAssign); } else { // Shorthand $currentChar = currentChar; $currentToken = currentToken; $index = index; values.push(parse(Precedence.Primary, expressionType === etIsIterator ? etNone : expressionType) as IsAssign); } } else { throw invalidPropDefInObjLiteral(); } if (($currentToken as Token) !== Token.CloseBrace) { consume(Token.Comma); } } $optional = _optional; consume(Token.CloseBrace); if (expressionType === etIsIterator) { return createObjectBindingPattern(keys, values); } else { $assignable = false; return createObjectLiteralExpression(keys, values); } } function parseInterpolation(): Interpolation { const parts = []; const expressions: (IsBindingBehavior | Interpolation)[] = []; const length = $length; let result = ''; while ($index < length) { switch ($currentChar) { case Char.Dollar: if ($charCodeAt($index + 1) === Char.OpenBrace) { parts.push(result); result = ''; $index += 2; $currentChar = $charCodeAt($index); nextToken(); const expression = parse(Precedence.Variadic, etInterpolation) as IsBindingBehavior | Interpolation; expressions.push(expression); continue; } else { result += '$'; } break; case Char.Backslash: result += stringFromCharCode(unescapeCode(nextChar())); break; default: result += stringFromCharCode($currentChar); } nextChar(); } if (expressions.length) { parts.push(result); return createInterpolationAst(parts, expressions as IsBindingBehavior[]); } return null!; } /** * parseTemplateLiteralExpression * https://tc39.github.io/ecma262/#prod-Literal * * TemplateExpression : * NoSubstitutionTemplate * TemplateHead * * NoSubstitutionTemplate : * ` TemplateCharacters(opt) ` * * TemplateHead : * ` TemplateCharacters(opt) ${ * * TemplateSubstitutionTail : * TemplateMiddle * TemplateTail * * TemplateMiddle : * } TemplateCharacters(opt) ${ * * TemplateTail : * } TemplateCharacters(opt) ` * * TemplateCharacters : * TemplateCharacter TemplateCharacters(opt) * * TemplateCharacter : * $ [lookahead ≠ {] * \ EscapeSequence * SourceCharacter (but not one of ` or \ or $) */ function parseTemplate(expressionType: ExpressionType, result: IsLeftHandSide, tagged: boolean): TaggedTemplateExpression | TemplateExpression { const _optional = $optional; const cooked = [$tokenValue as string]; // TODO: properly implement raw parts / decide whether we want this consume(Token.TemplateContinuation); const expressions = [parse(Precedence.Assign, expressionType) as IsAssign]; while (($currentToken = scanTemplateTail()) !== Token.TemplateTail) { cooked.push($tokenValue as string); consume(Token.TemplateContinuation); expressions.push(parse(Precedence.Assign, expressionType) as IsAssign); } cooked.push($tokenValue as string); $assignable = false; $optional = _optional; if (tagged) { nextToken(); return createTaggedTemplateExpression(cooked, cooked, result, expressions); } else { nextToken(); return createTemplateExpression(cooked, expressions); } } function createTemplateTail(result: IsLeftHandSide) { $assignable = false; const strings = [$tokenValue as string]; nextToken(); return createTaggedTemplateExpression(strings, strings, result); } function nextToken(): void {