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,556 lines (1,460 loc) 64.5 kB
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ import { AccessKeyedExpression, AccessMemberExpression, AccessScopeExpression, AccessThisExpression, ArrayBindingPattern, ArrayLiteralExpression, AssignExpression, BinaryExpression, BindingBehaviorExpression, BindingIdentifier, CallFunctionExpression, CallMemberExpression, CallScopeExpression, ConditionalExpression, CustomExpression, ForOfStatement, Interpolation, ObjectBindingPattern, ObjectLiteralExpression, PrimitiveLiteralExpression, TaggedTemplateExpression, TemplateExpression, UnaryExpression, ValueConverterExpression, AnyBindingExpression, BinaryOperator, BindingIdentifierOrPattern, IsAssign, IsAssignable, IsBinary, IsBindingBehavior, IsExpressionOrStatement, IsLeftHandSide, IsValueConverter, UnaryOperator, DestructuringAssignmentSingleExpression as DASE, DestructuringAssignmentExpression as DAE, ArrowFunction, AccessGlobalExpression, CallGlobalExpression, type ExpressionKind, ekAccessThis, ekAccessGlobal, ekAccessMember, ekAccessScope, ekArrayDestructuring, ekArrayBindingPattern, ekObjectBindingPattern, ekBindingIdentifier, ekObjectDestructuring, AccessBoundaryExpression, AssignmentOperator, NewExpression, } 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 PrimitiveLiteralExpression.$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 = PrimitiveLiteralExpression.$false; const $true = PrimitiveLiteralExpression.$true; const $null = PrimitiveLiteralExpression.$null; const $undefined = PrimitiveLiteralExpression.$undefined; const $this = new AccessThisExpression(0); const $parent = new AccessThisExpression(1); const boundary = new AccessBoundaryExpression(); 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 = new UnaryExpression(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 : new AccessThisExpression(ancestor); optionalThisTail = true; break primary; } break; default: if ($currentToken & Token.AccessScopeTerminal) { result = ancestor === 0 ? $this : ancestor === 1 ? $parent : new AccessThisExpression(ancestor); break primary; } throw invalidMemberExpression(); } } while ($currentToken === Token.ParentScope); // falls through case Token.Identifier: { // identifier const id = $tokenValue as string; if (expressionType === etIsIterator) { result = new BindingIdentifier(id); } else if ($accessGlobal && globalNames.includes(id as (typeof globalNames)[number])) { result = new AccessGlobalExpression(id); } else if ($accessGlobal && id === 'import') { throw unexpectedImportKeyword(); } else { result = new AccessScopeExpression(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 = new ArrowFunction([new BindingIdentifier(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 = new AccessThisExpression($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 = new TemplateExpression([$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 = new PrimitiveLiteralExpression($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 = new NewExpression(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 = new UnaryExpression(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 = new AccessScopeExpression($tokenValue as string, result.ancestor); nextToken(); } else if (($currentToken as Token) === Token.OpenParen) { result = new CallFunctionExpression(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 = new AccessScopeExpression($tokenValue as string, result.ancestor); nextToken(); break; case Token.DotDot: case Token.DotDotDot: throw expectedIdentifier(); case Token.OpenParen: result = new CallFunctionExpression(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 = new CallScopeExpression(result.name, parseArguments(), result.ancestor, false); } else if (result.$kind === ekAccessMember) { result = new CallMemberExpression(result.object, result.name, parseArguments(), result.optional, false); } else if (result.$kind === ekAccessGlobal) { result = new CallGlobalExpression(result.name, parseArguments()); } else { result = new CallFunctionExpression(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 = new BinaryExpression(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 = new ConditionalExpression(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 = new AssignExpression(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 = new ValueConverterExpression(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 = new BindingBehaviorExpression(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: DASE[] = []; const dae = new DAE(ekArrayDestructuring, items, void 0, void 0); let target: string = ''; 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 dae; function addItem() { if (target !== '') { items.push(new DASE(new AccessMemberExpression($this, target), new AccessKeyedExpression($this, new PrimitiveLiteralExpression(index++)), void 0)); target = ''; } 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 = new AccessKeyedExpression(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 new CallScopeExpression(lhs.name, parseArguments(), lhs.ancestor, true); } else if (lhs.$kind === ekAccessMember) { return new CallMemberExpression(lhs.object, lhs.name, parseArguments(), lhs.optional, true); } else { return new CallFunctionExpression(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 new CallMemberExpression(lhs, rhs, parseArguments(), optional, true); } $index = indexSave; $startIndex = startIndexSave; $currentToken = currentTokenSave; $currentChar = currentCharSave; $tokenValue = tokenValueSave; $assignable = assignableSave; $optional = optionalSave; return new AccessMemberExpression(lhs, rhs, optional); } case Token.OpenParen: { $assignable = false; return new CallMemberExpression(lhs, rhs, parseArguments(), optional, false); } default: { $assignable = !$optional; nextToken(); return new AccessMemberExpression(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(new BindingIdentifier($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 new ArrowFunction(arrowParams, body, true); } switch ($currentToken as Token) { case Token.Identifier: arrowParams.push(new BindingIdentifier($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 new ArrowFunction(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 new ArrayBindingPattern(elements); } else { $assignable = false; return new ArrayLiteralExpression(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 new ForOfStatement(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 new ObjectBindingPattern(keys, values); } else { $assignable = false; return new ObjectLiteralExpression(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 new Interpolation(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 new TaggedTemplateExpression(cooked, cooked, result, expressions); } else { nextToken(); return new TemplateExpression(cooked, expressions); } } function createTemplateTail(result: IsLeftHandSide) { $assignable = false; const strings = [$tokenValue as string]; nextToken(); return new TaggedTemplateExpression(strings, strings, result); } function nextToken(): void { while ($index < $length) { $startIndex = $index; if (($currentToken = (CharScanners[$currentChar]()) as Token) != null) { // a null token means the character must be skipped return; } } $currentToken = Token.EOF; } function nextChar(): number { return $currentChar = $charCodeAt(++$index); } function scanIdentifier(): Token { // run to the next non-idPart while (IdParts[nextChar()]); const token: Token|undefined = KeywordLookup[$tokenValue = $tokenRaw()]; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return token === undefined ? Token.Identifier : token; } function scanNumber(isFloat: boolean): Token { let char