/* MIT License http://www.opensource.org/licenses/mit-license.php Author Alexander Akait @alexander-akait */ "use strict"; // cspell:ignore yuku binop prec Prec const { Parser: BaseParser, tokTypes } = require("acorn"); // acorn exports its token-context table but leaves it out of its public types const tokContexts = /** @type {Record} */ ( /** @type {{ tokContexts: Record }} */ (/** @type {unknown} */ (require("acorn"))).tokContexts ); /** @typedef {{ token: string, isExpr: boolean, preserveSpace?: boolean, override?: unknown }} TokContextShim acorn TokContext fields read by the owned tokenizer */ // acorn's token contexts used by the inlined finishToken context updates const CTX_B_STAT = /** @type {TokContextShim} */ (tokContexts.b_stat); const CTX_B_EXPR = /** @type {TokContextShim} */ (tokContexts.b_expr); const CTX_P_STAT = /** @type {TokContextShim} */ (tokContexts.p_stat); const CTX_P_EXPR = /** @type {TokContextShim} */ (tokContexts.p_expr); const CTX_F_STAT = /** @type {TokContextShim} */ (tokContexts.f_stat); const CTX_F_EXPR = /** @type {TokContextShim} */ (tokContexts.f_expr); // acorn exports its keyword→TokenType map but leaves it out of its public // types; used by the word-classification lookups below. const keywordTypes = /** @type {Record} */ ( /** @type {{ keywordTypes: Record }} */ (/** @type {unknown} */ (require("acorn"))).keywordTypes ); // acorn exports these Unicode/character helpers at runtime but leaves them out // of its public types; the ported cold-path readers below reuse them so their // classification stays byte-identical to acorn's tokenizer. const { isIdentifierChar, isIdentifierStart, isNewLine, lineBreak, nonASCIIwhitespace } = /** @type {{ isIdentifierStart: (code: number, astral?: boolean) => boolean, isIdentifierChar: (code: number, astral?: boolean) => boolean, isNewLine: (code: number) => boolean, lineBreak: RegExp, nonASCIIwhitespace: RegExp }} */ (/** @type {unknown} */ (require("acorn"))); /** * @param {number} code code point * @returns {string} the string for a single code point */ const codePointToString = (code) => String.fromCodePoint(code); /** * acorn's `stringToNumber`: legacy octal parses in base 8, everything else via * `parseFloat` after dropping `_` separators. * @param {string} str numeric literal text (may contain `_` separators) * @param {boolean} isLegacyOctal whether it is a legacy octal literal * @returns {number} numeric value */ const stringToNumber = (str, isLegacyOctal) => isLegacyOctal ? Number.parseInt(str, 8) : Number.parseFloat(str.replace(/_/g, "")); /** * acorn's `stringToBigInt`, minus the pre-BigInt fallback (every supported * Node has `BigInt`). * @param {string} str numeric literal text (may contain `_` separators) * @returns {bigint} bigint value */ const stringToBigInt = (str) => BigInt(str.replace(/_/g, "")); /** * @import { * Options, * Position, * Node, * Identifier, * ImportAttribute, * ImportDefaultSpecifier, * ImportExpression, * Expression, * TokenType * } from "acorn" */ /** @typedef {import("acorn").ImportSpecifier | import("acorn").ImportDefaultSpecifier | import("acorn").ImportNamespaceSpecifier} AnyImportSpecifier */ /** @typedef {TokenType & { beforeExpr: boolean, isAssign?: boolean, prefix?: boolean, postfix?: boolean, binop: number | null, updateContext: ((prevType: TokenType) => void) | null }} TokenTypeInternal acorn's internal TokenType fields, absent from its public types */ /** @typedef {[number, number]} Range */ /** @typedef {"defer" | "source"} ImportPhase */ /** @typedef {import("estree").Comment & { start: number, end: number }} CollectedComment comment as JavascriptParser exposes it */ // Symbol-keyed so they stay out of for-in, Object.keys and JSON.stringify // over AST nodes. // Per-TokContext "the owned token loop must step aside" flag, cached on the // context itself so the hot guard reads one slot instead of two. const kSlowContext = Symbol("slow context"); const kSource = Symbol("source"); const kRange = Symbol("range"); const kText = Symbol("text"); const kTextStart = Symbol("text start"); // Marks import attributes parsed from the legacy `assert {...}` syntax. const LEGACY_ASSERT_ATTRIBUTES = Symbol("assert"); // acorn's binding types and scope flags, stable across acorn 8 const BIND_NONE = 0; const BIND_VAR = 1; const BIND_LEXICAL = 2; const BIND_OUTSIDE = 5; const SCOPE_TOP = 1; const SCOPE_FUNCTION = 2; const SCOPE_ASYNC = 4; const SCOPE_GENERATOR = 8; const SCOPE_ARROW = 16; const SCOPE_SIMPLE_CATCH = 32; // SCOPE_TOP | SCOPE_FUNCTION | SCOPE_CLASS_STATIC_BLOCK const SCOPE_SWITCH = 1024; const SCOPE_VAR = 0b100000011; // acorn's parseFunction statement bit flags const FUNC_STATEMENT = 1; const FUNC_HANGING_STATEMENT = 2; const FUNC_NULLABLE_ID = 4; // ASCII identifier-continuation chars ($ 0-9 A-Z _ a-z); css/html-style // Uint8Array table so the tokenizer fast path is one load per char const IDENT_CHAR = new Uint8Array(128); IDENT_CHAR[36] = 1; IDENT_CHAR[95] = 1; for (let i = 48; i <= 57; i++) IDENT_CHAR[i] = 1; for (let i = 65; i <= 90; i++) IDENT_CHAR[i] = 1; for (let i = 97; i <= 122; i++) IDENT_CHAR[i] = 1; // ASCII identifier-start chars (IDENT_CHAR minus 0-9), for token dispatch in // the owned `nextToken` loop. const IDENT_START = new Uint8Array(128); IDENT_START[36] = 1; IDENT_START[95] = 1; for (let i = 65; i <= 90; i++) IDENT_START[i] = 1; for (let i = 97; i <= 122; i++) IDENT_START[i] = 1; // Single-char punctuators that acorn's `getTokenFromCode` reads as just // `++pos; finishToken(type)` (no value, no operator state machine). Dispatching // them from `nextToken`'s char table skips the extra `getTokenFromCode` call and // its switch for the commonest tokens in JS ( ) { } [ ] ; , : — `0` is "not a // simple punctuator" since token types are truthy objects. const SIMPLE_PUNCT = Array.from({ length: 128 }); SIMPLE_PUNCT[40] = tokTypes.parenL; SIMPLE_PUNCT[41] = tokTypes.parenR; SIMPLE_PUNCT[59] = tokTypes.semi; SIMPLE_PUNCT[44] = tokTypes.comma; SIMPLE_PUNCT[91] = tokTypes.bracketL; SIMPLE_PUNCT[93] = tokTypes.bracketR; SIMPLE_PUNCT[123] = tokTypes.braceL; SIMPLE_PUNCT[125] = tokTypes.braceR; SIMPLE_PUNCT[58] = tokTypes.colon; // Characters that can only start a token which cannot continue an expression, // so an atom directly followed by one is the whole expression (see // `parseMaybeAssign`'s fast path). `}` included: it only ever closes a block, // an object or a template substitution. const EXPRESSION_END_CHAR = new Uint8Array(128); EXPRESSION_END_CHAR[41] = 1; // ) EXPRESSION_END_CHAR[44] = 1; // , EXPRESSION_END_CHAR[58] = 1; // : EXPRESSION_END_CHAR[59] = 1; // ; EXPRESSION_END_CHAR[93] = 1; // ] EXPRESSION_END_CHAR[125] = 1; // } // Char classification for the owned `nextToken` (yuku's ws_class): one table // load steers both the whitespace skip loop and the token dispatch. Token // classes sort below CLS_SPACE so the skip loop exits on a single compare. const CLS_OTHER = 0; const CLS_IDENT = 1; const CLS_PUNCT = 2; const CLS_DOT = 3; const CLS_EQ = 4; const CLS_UNICODE = 5; const CLS_SPACE = 6; const CLS_NEWLINE = 7; const CLS_SLASH = 8; // Full `charCodeAt` range so the scan loop needs no `code > 127` branch per // character: every non-ASCII code unit classifies as CLS_UNICODE (which sorts // below CLS_SPACE, so the loop exits on the same single compare) and the // dispatch delegates it to acorn's unicode-aware paths. const CHAR_CLASS = new Uint8Array(0x10000).fill(CLS_UNICODE, 128); for (let i = 0; i < 128; i++) { if (IDENT_START[i] === 1 || i === 92) CHAR_CLASS[i] = CLS_IDENT; else if (SIMPLE_PUNCT[i] !== undefined) CHAR_CLASS[i] = CLS_PUNCT; } CHAR_CLASS[46] = CLS_DOT; CHAR_CLASS[61] = CLS_EQ; CHAR_CLASS[32] = CLS_SPACE; CHAR_CLASS[9] = CLS_SPACE; CHAR_CLASS[11] = CLS_SPACE; CHAR_CLASS[12] = CLS_SPACE; CHAR_CLASS[10] = CLS_NEWLINE; CHAR_CLASS[13] = CLS_NEWLINE; CHAR_CLASS[47] = CLS_SLASH; /** * Drop-in replacement for acorn's `Node` that materializes `loc` and `range` * on first access instead of allocating them during parsing. Most nodes never * get either read, which saves three objects and an array per node. */ class LazyLocNode { /** * @param {number} pos start offset */ constructor(pos) { this.type = ""; this.start = pos; this.end = 0; } /** * Memoized in a symbol slot — a plain store is far cheaper than making the * property own via defineProperty, and the slot stays invisible to for-in, * Object.keys and JSON.stringify. No `loc` is served at all — locations * are derived from offsets via `JavascriptParser#getLocation`. * @returns {Range} source range */ get range() { const cached = this[kRange]; if (cached !== undefined) return cached; /** @type {Range} */ const range = [this.start, this.end]; if (this.end > 0) this[kRange] = range; return range; } /** * @param {Range} value source range */ set range(value) { this[kRange] = value; } } /** * Single-shape `Identifier`, the most common node: all fields are assigned in * one constructor, so every instance is born on its final hidden class instead * of transitioning through acorn's start-empty-then-mutate construction. */ class IdentifierNode { /** * @param {number} start start offset * @param {number} end end offset * @param {string} name identifier name */ constructor(start, end, name) { this.type = "Identifier"; this.start = start; this.end = end; this.name = name; } } /** * Single-shape `Literal`; `bigint` and `regex` stay post-construction * additions since both are rare. */ class LiteralNode { /** * @param {number} start start offset * @param {number} end end offset * @param {unknown} value literal value * @param {string} raw literal source text */ constructor(start, end, value, raw) { this.type = "Literal"; this.start = start; this.end = end; this.value = value; this.raw = raw; } } /** * Single-shape `MemberExpression`. `optional` is a real field on every * instance since webpack always parses with `ecmaVersion >= 11`. */ class MemberExpressionNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression} object object expression * @param {Node} property property node * @param {boolean} computed whether the access is computed (`a[b]`) * @param {boolean} optional whether the access is optional (`a?.b`) */ constructor(start, end, object, property, computed, optional) { this.type = "MemberExpression"; this.start = start; this.end = end; this.object = object; this.property = property; this.computed = computed; this.optional = optional; } } /** * Single-shape `CallExpression`; `optional` as in `MemberExpressionNode`. */ class CallExpressionNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression} callee callee expression * @param {Node[]} args call arguments * @param {boolean} optional whether the call is optional (`a?.()`) */ constructor(start, end, callee, args, optional) { this.type = "CallExpression"; this.start = start; this.end = end; this.callee = callee; this.arguments = args; this.optional = optional; } } /** * Single-shape `ThisExpression`. */ class ThisNode { /** * @param {number} start start offset * @param {number} end end offset */ constructor(start, end) { this.type = "ThisExpression"; this.start = start; this.end = end; } } /** * Single-shape `BinaryExpression`/`LogicalExpression` — identical field sets, * so both node types share one hidden class. */ class BinaryNode { /** * @param {number} start start offset * @param {number} end end offset * @param {"BinaryExpression" | "LogicalExpression"} type node type * @param {Expression} left left operand * @param {string} operator operator text * @param {Expression} right right operand */ constructor(start, end, type, left, operator, right) { this.type = type; this.start = start; this.end = end; this.left = left; this.operator = operator; this.right = right; } } /** * Single-shape `AssignmentExpression`. */ class AssignmentNode { /** * @param {number} start start offset * @param {number} end end offset * @param {string} operator assignment operator text * @param {Node} left assignment target * @param {Expression} right assigned value */ constructor(start, end, operator, left, right) { this.type = "AssignmentExpression"; this.start = start; this.end = end; this.operator = operator; this.left = left; this.right = right; } } /** * Single-shape `UnaryExpression`/`UpdateExpression` — identical field sets, * so both node types share one hidden class. */ class UnaryNode { /** * @param {number} start start offset * @param {number} end end offset * @param {"UnaryExpression" | "UpdateExpression"} type node type * @param {string} operator operator text * @param {boolean} prefix whether the operator is prefixed * @param {Expression} argument operand */ constructor(start, end, type, operator, prefix, argument) { this.type = type; this.start = start; this.end = end; this.operator = operator; this.prefix = prefix; this.argument = argument; } } /** * Single-shape `VariableDeclaration` (statement position; `for` heads keep the * generic node since their caller finishes them). */ class VariableDeclarationNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Node[]} declarations declarators * @param {string} kind declaration kind (`var`/`let`/`const`/`using`) */ constructor(start, end, declarations, kind) { this.type = "VariableDeclaration"; this.start = start; this.end = end; this.declarations = declarations; this.kind = kind; } } /** * Single-shape `VariableDeclarator`. */ class VariableDeclaratorNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Node} id binding target * @param {Expression | null} init initializer */ constructor(start, end, id, init) { this.type = "VariableDeclarator"; this.start = start; this.end = end; this.id = id; this.init = init; } } /** * Single-shape `ExpressionStatement`. */ class ExpressionStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression} expression the statement's expression */ constructor(start, end, expression) { this.type = "ExpressionStatement"; this.start = start; this.end = end; this.expression = expression; } } /** * Single-shape `BlockStatement`. */ class BlockStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Node[]} body statements */ constructor(start, end, body) { this.type = "BlockStatement"; this.start = start; this.end = end; this.body = body; } } /** * Single-shape `IfStatement`. */ class IfStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression} test condition * @param {Node} consequent then-branch * @param {Node | null} alternate else-branch */ constructor(start, end, test, consequent, alternate) { this.type = "IfStatement"; this.start = start; this.end = end; this.test = test; this.consequent = consequent; this.alternate = alternate; } } /** * Single-shape `ReturnStatement`. */ class ReturnStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression | null} argument returned expression */ constructor(start, end, argument) { this.type = "ReturnStatement"; this.start = start; this.end = end; this.argument = argument; } } /** * Single-shape `ConditionalExpression`. */ class ConditionalExpressionNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression} test condition * @param {Expression} consequent then-value * @param {Expression} alternate else-value */ constructor(start, end, test, consequent, alternate) { this.type = "ConditionalExpression"; this.start = start; this.end = end; this.test = test; this.consequent = consequent; this.alternate = alternate; } } /** * Single-shape `NewExpression`. */ class NewExpressionNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression} callee constructed expression * @param {Expression[]} args constructor arguments */ constructor(start, end, callee, args) { this.type = "NewExpression"; this.start = start; this.end = end; this.callee = callee; this.arguments = args; } } /** * Single-shape `ArrayExpression`. */ class ArrayExpressionNode { /** * @param {number} start start offset * @param {number} end end offset * @param {(Expression | null)[]} elements array elements (`null` for holes) */ constructor(start, end, elements) { this.type = "ArrayExpression"; this.start = start; this.end = end; this.elements = elements; } } /** * Single-shape `TemplateLiteral`. */ class TemplateLiteralNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression[]} expressions substitution expressions * @param {Node[]} quasis template chunks */ constructor(start, end, expressions, quasis) { this.type = "TemplateLiteral"; this.start = start; this.end = end; this.expressions = expressions; this.quasis = quasis; } } /** * Single-shape `TemplateElement`. */ class TemplateElementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {{ raw: string, cooked: string | null }} value chunk text * @param {boolean} tail whether this is the closing chunk */ constructor(start, end, value, tail) { this.type = "TemplateElement"; this.start = start; this.end = end; this.value = value; this.tail = tail; } } /** * Single-shape `ObjectExpression`/`ObjectPattern` — identical field sets, so * both node types share one hidden class. */ class ObjectNode { /** * @param {number} start start offset * @param {number} end end offset * @param {"ObjectExpression" | "ObjectPattern"} type node type * @param {Node[]} properties properties */ constructor(start, end, type, properties) { this.type = type; this.start = start; this.end = end; this.properties = properties; } } /** * Pre-shaped `Property`: acorn fills property nodes through shared * subroutines (`parsePropertyName`/`parsePropertyValue`), so instead of * rebuilding that flow the fields are all declared up-front and acorn's * writes land in existing slots — one hidden class, no transitions (yuku's * decoder emits `Property` with this fixed shape). Every non-throwing acorn * branch assigns `computed`, `key`, `value` and `kind`; `finishNode` sets * `type` and `end`. */ class PropertyNode { /** * @param {number} start start offset */ constructor(start) { this.type = ""; this.start = start; this.end = 0; this.method = false; this.shorthand = false; this.computed = false; /** @type {Node | null} */ this.key = null; /** @type {Node | null} */ this.value = null; this.kind = ""; } } /** * Single-shape `ForStatement`. */ class ForStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Node | null} init init statement or expression * @param {Expression | null} test loop condition * @param {Expression | null} update update expression * @param {Node} body loop body */ constructor(start, end, init, test, update, body) { this.type = "ForStatement"; this.start = start; this.end = end; this.init = init; this.test = test; this.update = update; this.body = body; } } /** * Single-shape `ForInStatement` (no `await` slot, matching acorn). */ class ForInStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Node} left loop target * @param {Expression} right iterated expression * @param {Node} body loop body */ constructor(start, end, left, right, body) { this.type = "ForInStatement"; this.start = start; this.end = end; this.left = left; this.right = right; this.body = body; } } /** * Single-shape `ForOfStatement`; `await` leads, matching acorn's write order. */ class ForOfStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {boolean} isAwait whether this is `for await` * @param {Node} left loop target * @param {Expression} right iterated expression * @param {Node} body loop body */ constructor(start, end, isAwait, left, right, body) { this.type = "ForOfStatement"; this.start = start; this.end = end; this.await = isAwait; this.left = left; this.right = right; this.body = body; } } /** * Single-shape `WhileStatement`. */ class WhileStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression} test loop condition * @param {Node} body loop body */ constructor(start, end, test, body) { this.type = "WhileStatement"; this.start = start; this.end = end; this.test = test; this.body = body; } } /** * Single-shape `SwitchStatement`. */ class SwitchStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression} discriminant switched expression * @param {Node[]} cases case clauses */ constructor(start, end, discriminant, cases) { this.type = "SwitchStatement"; this.start = start; this.end = end; this.discriminant = discriminant; this.cases = cases; } } /** * Pre-shaped `SwitchCase`: filled in acorn's write order (`consequent` before * `test`), finished via `finishNode` like acorn's. */ class SwitchCaseNode { /** * @param {number} start start offset */ constructor(start) { this.type = ""; this.start = start; this.end = 0; /** @type {Node[] | null} */ this.consequent = null; /** @type {Expression | null} */ this.test = null; } } /** * Single-shape `ThrowStatement`. */ class ThrowStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Expression} argument thrown expression */ constructor(start, end, argument) { this.type = "ThrowStatement"; this.start = start; this.end = end; this.argument = argument; } } /** * Single-shape `BreakStatement`/`ContinueStatement` — identical field sets, so * both node types share one hidden class. */ class BreakContinueNode { /** * @param {number} start start offset * @param {number} end end offset * @param {"BreakStatement" | "ContinueStatement"} type node type * @param {Identifier | null} label target label */ constructor(start, end, type, label) { this.type = type; this.start = start; this.end = end; this.label = label; } } /** * Single-shape `TryStatement`. */ class TryStatementNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Node} block try block * @param {Node | null} handler catch clause * @param {Node | null} finalizer finally block */ constructor(start, end, block, handler, finalizer) { this.type = "TryStatement"; this.start = start; this.end = end; this.block = block; this.handler = handler; this.finalizer = finalizer; } } /** * Single-shape `CatchClause`. */ class CatchClauseNode { /** * @param {number} start start offset * @param {number} end end offset * @param {Node | null} param catch parameter * @param {Node} body catch block */ constructor(start, end, param, body) { this.type = "CatchClause"; this.start = start; this.end = end; this.param = param; this.body = body; } } /** * Pre-shaped `FunctionDeclaration`/`FunctionExpression`/ * `ArrowFunctionExpression`: acorn fills function nodes through shared * subroutines (`parseFunctionParams`/`parseFunctionBody`), so like * `PropertyNode` the fields are all declared up-front — in acorn's exact write * order (`initFunction` assigns `expression` before `generator`) so JSON key * order is unchanged — and acorn's writes land in existing slots: one hidden * class for all three function node types, no transitions. */ class FunctionNode { /** * @param {number} start start offset */ constructor(start) { this.type = ""; this.start = start; this.end = 0; /** @type {Identifier | null} */ this.id = null; this.expression = false; this.generator = false; this.async = false; /** @type {Node[] | null} */ this.params = null; /** @type {Node | null} */ this.body = null; } } /** * Single-shape `SpreadElement`/`RestElement` — identical field sets, so both * node types share one hidden class. */ class RestSpreadNode { /** * @param {number} start start offset * @param {number} end end offset * @param {"SpreadElement" | "RestElement"} type node type * @param {Node} argument spread/rest argument */ constructor(start, end, type, argument) { this.type = type; this.start = start; this.end = end; this.argument = argument; } } // Mirrors of acorn's module-level `loopLabel`/`switchLabel`. const LOOP_LABEL = { kind: "loop" }; const SWITCH_LABEL = { kind: "switch" }; // Shared zero-length arguments array for `new X` without parens, mirroring // acorn's module-level `empty`. /** @type {Expression[]} */ const EMPTY_NEW_ARGS = []; /** * Mirror of acorn's module-level `isLocalVariableAccess`. * @param {Node} node checked node * @returns {boolean} whether the node reads a local variable */ const isLocalVariableAccess = (node) => node.type === "Identifier" || (node.type === "ParenthesizedExpression" && isLocalVariableAccess( /** @type {Node} */ ( /** @type {Node & { expression?: Node }} */ (node).expression ) )); /** * Mirror of acorn's module-level `isPrivateFieldAccess`. * @param {Node} node checked node * @returns {boolean} whether the node accesses a private field */ const isPrivateFieldAccess = (node) => (node.type === "MemberExpression" && /** @type {Node} */ ( /** @type {Node & { property?: Node }} */ (node).property ).type === "PrivateIdentifier") || (node.type === "ChainExpression" && isPrivateFieldAccess( /** @type {Node} */ ( /** @type {Node & { expression?: Node }} */ (node).expression ) )) || (node.type === "ParenthesizedExpression" && isPrivateFieldAccess( /** @type {Node} */ ( /** @type {Node & { expression?: Node }} */ (node).expression ) )); // the dedicated node classes serve `range` exactly like LazyLocNode for (const NodeClass of [ IdentifierNode, LiteralNode, MemberExpressionNode, CallExpressionNode, ThisNode, BinaryNode, AssignmentNode, UnaryNode, VariableDeclarationNode, VariableDeclaratorNode, ExpressionStatementNode, BlockStatementNode, IfStatementNode, ReturnStatementNode, ConditionalExpressionNode, NewExpressionNode, ArrayExpressionNode, TemplateLiteralNode, TemplateElementNode, ObjectNode, PropertyNode, RestSpreadNode, FunctionNode, ForStatementNode, ForInStatementNode, ForOfStatementNode, WhileStatementNode, SwitchStatementNode, SwitchCaseNode, ThrowStatementNode, BreakContinueNode, TryStatementNode, CatchClauseNode ]) { for (const key of ["range"]) { Object.defineProperty( NodeClass.prototype, key, /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(LazyLocNode.prototype, key)) ); } } /** * Comment collected without slicing its text out of the source: only magic * comments and pure annotations ever get their text read, and only binary * searches around magic-comment sites read `range`, so the text slice and the * range array are both deferred to first access and memoized like `loc`. */ class LazyComment { /** * @param {boolean} block whether this is a block comment * @param {number} textStart offset right after the comment opener * @param {number} start start offset * @param {number} end end offset * @param {string} source full source text for the lazy `value` slice */ constructor(block, textStart, start, end, source) { /** @type {"Block" | "Line"} */ this.type = block ? "Block" : "Line"; this.start = start; this.end = end; this[kSource] = source; this[kTextStart] = textStart; } /** * Memoized like `LazyLocNode#range`; offsets are final at construction so * the slot is cached unconditionally. * @returns {Range} source range */ get range() { const cached = this[kRange]; if (cached !== undefined) return cached; /** @type {Range} */ const range = [this.start, this.end]; return (this[kRange] = range); } /** * @param {Range} value source range */ set range(value) { this[kRange] = value; } /** * @returns {string} comment text without the delimiters */ get value() { const cached = this[kText]; if (cached !== undefined) return cached; return (this[kText] = this[kSource].slice( this[kTextStart], this.type === "Block" ? this.end - 2 : this.end )); } /** * @param {string} value comment text */ set value(value) { this[kText] = value; } } /** * Replaces acorn's array-backed `Scope`: membership checks in `declareName` * are `indexOf` there, which goes quadratic on files with thousands of * bindings per scope (bundled or minified inputs). The three Sets are * allocated lazily — most scopes declare into only one (module `functions` is * always empty), so ~⅔ of the Sets are never needed. */ class Scope { /** * @param {number} flags scope flags */ constructor(flags) { this.flags = flags; /** @type {Set | undefined} */ this.var = undefined; /** @type {Set | undefined} */ this.lexical = undefined; /** @type {Set | undefined} */ this.functions = undefined; // first lexically-declared name; stands in for acorn's `lexical[0]` // (the catch parameter of a simple catch scope) /** @type {string | undefined} */ this.firstLexical = undefined; } } /** * Acorn's methods and state used by `WebpackParser` but missing from its * public types, plus `WebpackParser`'s own fields, so overridden methods can * declare `this` precisely. * @typedef {import("acorn").Parser & { * type: TokenType, * value: unknown, * start: number, * startLoc?: Position, * containsEsc: boolean, * exprAllowed: boolean, * options: Options, * end: number, * lastTokEnd: number, * canInsertSemicolon: () => boolean, * nextToken: () => void, * next: (ignoreEscapeSequenceInKeyword?: boolean) => void, * eat: (type: TokenType) => boolean, * expect: (type: TokenType) => void, * afterTrailingComma: (type: TokenType, notNext?: boolean) => boolean, * unexpected: (pos?: number) => never, * raise: (pos: number, message: string) => never, * raiseRecoverable: (pos: number, message: string) => void, * isContextual: (name: string) => boolean, * parseIdent: (liberal?: boolean) => Identifier, * parseLiteral: (value: unknown) => Node, * awaitIdentPos: number, * lastTokStart: number, * lastTokStartLoc?: Position, * yieldPos: number, * awaitPos: number, * parseExpression: (forInit?: boolean | string, refDestructuringErrors?: DestructuringErrorsShim | null) => Expression, * parseSpread: (refDestructuringErrors?: DestructuringErrorsShim | null) => Node, * braceIsBlock: (prevType: TokenType) => boolean, * _gapHasNewline: () => boolean, * parseExprList: (close: TokenType, allowTrailingComma: boolean, allowEmpty: boolean, refDestructuringErrors?: DestructuringErrorsShim | null) => Expression[], * parsePrivateIdent: () => Node, * parseTemplate: (opts: { isTagged: boolean }) => Node, * shouldParseAsyncArrow: () => boolean, * parseSubscriptAsyncArrow: (startPos: number, startLoc: Position | undefined, exprList: Expression[], forInit: boolean | string) => Expression, * checkPatternErrors: (refDestructuringErrors: DestructuringErrorsShim, isAssign: boolean) => void, * checkYieldAwaitInDefaultParams: () => void, * checkExpressionErrors: (refDestructuringErrors?: DestructuringErrorsShim | null, andThrow?: boolean) => boolean, * parseSubscript: (base: Expression, startPos: number, startLoc: Position | undefined, noCalls: boolean | undefined, maybeAsyncArrow: boolean, optionalChained: boolean, forInit?: boolean | string) => Expression, * parseExprAtom: (refDestructuringErrors?: DestructuringErrorsShim | null, forInit?: boolean | string, forNew?: boolean) => Expression, * buildBinary: (startPos: number, startLoc: Position | undefined, left: Expression, right: Expression, op: string, logical: boolean) => Expression, * parseMaybeAssign: (forInit?: boolean | string, refDestructuringErrors?: DestructuringErrorsShim | null, afterLeftParse?: (this: unknown, left: Expression, startPos: number, startLoc?: Position) => Expression) => Expression, * _parseTrivialAtom: (forInit?: boolean | string) => Expression | null, * parseMaybeConditional: (forInit?: boolean | string, refDestructuringErrors?: DestructuringErrorsShim | null) => Expression, * parseMaybeUnary: (refDestructuringErrors: DestructuringErrorsShim | null, sawUnary: boolean, incDec: boolean, forInit?: boolean | string) => Expression, * parseExprSubscripts: (refDestructuringErrors?: DestructuringErrorsShim | null, forInit?: boolean | string) => Expression, * parseAwait: (forInit?: boolean | string) => Expression, * canAwait: boolean, * privateNameStack: unknown[], * semicolon: () => void, * exitScope: () => void, * parseStatement: (context: string | null, topLevel?: boolean, exports?: unknown) => Node, * parseBindingAtom: () => Node, * parseVarStatement: (node: Node, kind: string, allowMissingInitializer?: boolean) => Node, * parseVar: (node: Node, isFor: boolean, kind: string, allowMissingInitializer?: boolean) => Node, * parseExpressionStatement: (node: Node, expr: Expression) => Node, * parseParenExpression: () => Expression, * parseIfStatement: (node: Node) => Node, * parseReturnStatement: (node: Node) => Node, * insertSemicolon: () => boolean, * allowReturn: boolean, * allowNewDotTarget: boolean, * parseExprOps: (forInit?: boolean | string, refDestructuringErrors?: DestructuringErrorsShim | null) => Expression, * parseExprOp: (left: Expression, leftStartPos: number, leftStartLoc: Position | undefined, minPrec: number, forInit?: boolean | string) => Expression, * _deStack: DestructuringErrorsShim[], * _deDepth: number, * _ecmaVersion: number, * _noLocations: boolean, * _validRegexpFlags: string, * _propHashFastPath: boolean, * _propHashStack: { proto: boolean }[], * _propHashDepth: number, * _acquireDestructuringErrors: () => DestructuringErrorsShim, * _releaseDestructuringErrors: () => void, * _arrStack: EXPECTED_ANY[][], * _arrDepth: number, * _acquireScratch: () => EXPECTED_ANY[], * _releaseScratch: (scratch: EXPECTED_ANY[], count: number) => EXPECTED_ANY[], * parseRestBinding: () => Node, * parseBindingListItem: (param: Node) => Node, * parseAssignableListItem: (allowModifiers?: boolean) => Node, * parseParenItem: (item: Expression | Node) => Expression, * shouldParseArrow: (exprList: Expression[]) => boolean, * parseParenArrowList: (startPos: number, startLoc: Position | undefined, exprList: Expression[], forInit?: boolean | string) => Expression, * finishNodeAt: (node: Node, type: string, pos: number, loc?: Position) => Node, * parseParenAndDistinguishExpression: (canBeArrow: boolean, forInit?: boolean | string) => Expression, * parseSubscripts: (base: Expression, startPos: number, startLoc: Position | undefined, noCalls?: boolean, forInit?: boolean | string) => Expression, * parseNew: () => Expression, * parseTemplateElement: (opts: { isTagged: boolean }) => Node, * parseBlock: (createNewLexicalScope?: boolean, node?: Node, exitStrict?: boolean) => Node, * parseYield: (forInit?: boolean | string) => Expression, * toAssignable: (node: Node, isBinding?: boolean, refDestructuringErrors?: DestructuringErrorsShim | null) => Node, * checkLValPattern: (expr: Node, bindingType?: number, checkClashes?: unknown) => void, * checkUnreserved: (ref: Identifier) => void, * enterScope: (flags: number) => void, * readRegexp: () => void, * potentialArrowAt: number, * potentialArrowInForAwait: boolean, * overrideContext: (tokenCtx: unknown) => void, * parseFunction: (node: Node, statement: number, allowExpressionBody?: boolean, isAsync?: boolean, forInit?: boolean | string) => Expression, * parseArrowExpression: (node: Node, params: Node[], isAsync: boolean, forInit?: boolean | string) => Expression, * _subscriptFastPath: boolean, * checkLValSimple: (expr: Node, bindingType?: number, checkClashes?: Record | null) => void, * checkLValInnerPattern: (expr: Node, bindingType?: number, checkClashes?: Record | null) => void, * declareName: (name: string, bindingType: number, pos: number) => void, * startNode: () => Node, * startNodeAt: (pos: number, loc?: Position) => Node, * finishNode: (node: Node, type: string) => Node, * readWord1: () => string, * readWord: () => void, * readToken: (code: number) => void, * getTokenFromCode: (code: number) => void, * fullCharCodeAtPos: () => number, * skipSpace: () => void, * skipLineComment: (startSkip: number) => void, * skipBlockComment: () => void, * readString: (quote: number) => void, * readNumber: (startsWithDot: boolean) => void, * readRadixNumber: (radix: number) => void, * readTmplToken: () => void, * invalidStringToken: (position: number, message: string) => void, * _readInt: (radix: number, len?: number, maybeLegacyOctal?: boolean) => number | null, * _readCodePoint: () => number, * _readHexChar: (len: number) => number, * _readEscapedChar: (inTemplate: boolean) => string, * _readStringCold: (quote: number) => void, * _readTmplTokenCold: () => void, * _readNumberCold: (startsWithDot: boolean) => void, * _readRadixNumber: (radix: number) => void, * _readWord1Cold: () => string, * _finishWordSlow: (word: string) => void, * _skipSpaceCold: () => void, * _getUnknownOrPrivate: (code: number) => void, * finishToken: (type: TokenType, value?: unknown) => void, * context: TokContextShim[], * pos: number, * input: string, * scopeStack: Scope[], * currentScope: () => Scope, * currentThisScope: () => Scope, * currentVarScope: () => Scope, * keywords: RegExp, * reservedWords: RegExp, * reservedWordsStrict: RegExp, * reservedWordsStrictBind: RegExp, * strict: boolean, * inGenerator: boolean, * inGeneratorContext: () => boolean, * inAsync: boolean, * inClassStaticBlock: boolean, * _wordLookups: WordLookups, * treatFunctionsAsVar: boolean, * treatFunctionsAsVarInScope: (scope: Scope) => boolean, * inModule: boolean, * undefinedExports: Record, * parseObj: (isPattern: boolean, refDestructuringErrors?: DestructuringErrorsShim | null) => Node, * parseProperty: (isPattern: boolean, refDestructuringErrors?: DestructuringErrorsShim | null) => Node, * parsePropertyName: (prop: Node) => Node, * parsePropertyValue: (prop: Node, isPattern: boolean, isGenerator: boolean, isAsync: boolean, startPos: number | undefined, startLoc: Position | undefined, refDestructuringErrors: DestructuringErrorsShim | null | undefined, containsEsc: boolean) => void, * isAsyncProp: (prop: Node) => boolean, * checkPropClash: (prop: Node, propHash: Record, refDestructuringErrors?: DestructuringErrorsShim | null) => void, * parseImport: (node: Node) => Node, * parseExport: (node: Node, exports: unknown) => Node, * parseImportSpecifiers: () => AnyImportSpecifier[], * parseImportAttribute: () => ImportAttribute, * parseExprImport: (forNew: boolean) => Expression, * parseImportMeta: (node: Node) => Expression, * parseDynamicImport: (node: Node) => Expression, * updateContext: (prevType: TokenType) => void, * finishOp: (type: TokenType, size: number) => void, * readToken_dot: () => void, * readToken_slash: () => void, * readToken_mult_modulo_exp: (code: number) => void, * readToken_pipe_amp: (code: number) => void, * readToken_caret: () => void, * readToken_plus_min: (code: number) => void, * readToken_lt_gt: (code: number) => void, * readToken_eq_excl: (code: number) => void, * readToken_question: () => void, * readToken_numberSign: () => void, * _tokenFastPath: boolean, * _inlineFinish: boolean, * _lazy: boolean, * _importPhase: ImportPhase | null, * _importPhasesEnabled: boolean, * _lazyComments: CollectedComment[] | undefined, * _newlineBefore: 0 | 1 | 2, * _fullTokenFastPath: boolean, * _stmtFastPath: boolean, * _lastArrow: Expression | null, * _arrowFastPath: boolean, * labels: { kind?: string | null, name?: string, statementStart?: number }[], * initFunction: (node: Node) => void, * parseFunctionStatement: (node: Node, isAsync: boolean, declarationPosition: boolean) => Node, * parseFunctionBody: (node: Node, isArrowFunction: boolean, isMethod: boolean, forInit?: boolean | string) => void, * parseFunctionParams: (node: Node) => void, * isSimpleParamList: (params: Node[]) => boolean, * checkParams: (node: Node, allowDuplicates: boolean) => void, * adaptDirectivePrologue: (statements: Node[]) => void, * toAssignableList: (exprList: Node[], isBinding: boolean) => Node[], * parseBindingList: (close: TokenType, allowEmpty: boolean, allowTrailingComma: boolean, allowModifiers?: boolean) => Node[], * strictDirective: (start: number) => boolean, * _funcFastPath: boolean, * _funcStmtOwn: boolean, * _parseFunctionAt: (start: number, statement: number, allowExpressionBody: boolean, isAsync: boolean, forInit?: boolean | string) => Expression, * isLet: (context?: string | null) => boolean, * parseLabeledStatement: (node: Node, maybeName: string, expr: Identifier, context: string | null) => Node, * _parseVarInto: (declarations: Node[], isFor: boolean, kind: string, allowMissingInitializer?: boolean) => number, * _parseVarStatementAt: (start: number, kind: string, allowMissingInitializer?: boolean) => Node, * _parseIfStatementAt: (start: number) => Node, * _parseReturnStatementAt: (start: number) => Node, * _parseExpressionStatementAt: (start: number, expr: Expression) => Node, * eatContextual: (name: string) => boolean, * isUsing: (isFor: boolean) => boolean, * isAwaitUsing: (isFor: boolean) => boolean, * parseForStatement: (node: Node) => Node, * parseFor: (node: Node, init: Node | null) => Node, * parseForIn: (node: Node, init: Node) => Node, * parseForAfterInit: (node: Node, init: Node, awaitAt: number) => Node, * parseWhileStatement: (node: Node) => Node, * parseSwitchStatement: (node: Node) => Node, * parseThrowStatement: (node: Node) => Node, * parseTryStatement: (node: Node) => Node, * parseBreakContinueStatement: (node: Node, keyword: string) => Node, * parseCatchClauseParam: () => Node, * _stmt2FastPath: boolean, * _exprFastPath: boolean, * _parseForStatementAt: (start: number) => Node, * _parseForAt: (start: number, init: Node | null) => Node, * _parseForInAt: (start: number, isAwait: boolean, init: Node) => Node, * _parseForAfterInitAt: (start: number, init: Node, awaitAt: number) => Node, * _parseWhileStatementAt: (start: number) => Node, * _parseSwitchStatementAt: (start: number) => Node, * _parseThrowStatementAt: (start: number) => Node, * _parseTryStatementAt: (start: number) => Node, * _parseBreakContinueStatementAt: (start: number, keyword: string) => Node, * _moduleFallback: boolean, * _moduleSyntaxSeen: boolean, * _tryModuleFallback: () => boolean, * }} ParserInternals */ // internal methods are absent from acorn's types, so super calls do not // type-check; call through a typed view of the base prototype instead const base = /** @type {ParserInternals} */ ( /** @type {unknown} */ (BaseParser.prototype) ); /** * Acorn's internal destructuring-errors record; the class itself is not * exported. Owned methods must create records with the same hidden class the * rest of the expression parser reads, or every record field access there * turns polymorphic. * @typedef {{ shorthandAssign: number, trailingComma: number, parenthesizedAssign: number, parenthesizedBind: number, doubleProto: number }} DestructuringErrorsShim */ // Capture the class at module load: parse one expression through a probe // whose `checkExpressionErrors` sees the record the base parser created. /** @type {{ new (): DestructuringErrorsShim } | null} */ const DestructuringErrorsClass = (() => { /** @type {{ new (): DestructuringErrorsShim } | null} */ let captured = null; class Probe extends BaseParser { /** * @param {DestructuringErrorsShim | null} refDestructuringErrors record to inspect * @param {boolean=} andThrow whether to throw on error * @returns {boolean} whether an error position was set */ checkExpressionErrors(refDestructuringErrors, andThrow) { if (refDestructuringErrors) { captured = /** @type {{ new (): DestructuringErrorsShim }} */ (refDestructuringErrors.constructor); } return /** @type {ParserInternals} */ ( /** @type {unknown} */ (base) ).checkExpressionErrors.call(this, refDestructuringErrors, andThrow); } } Probe.parse("a", { ecmaVersion: 2020 }); // cast: the closure assignment above is invisible to control-flow analysis return /** @type {{ new (): DestructuringErrorsShim } | null} */ (captured); })(); /** * @returns {DestructuringErrorsShim} fresh destructuring-errors record on acorn's own class (plain-object fallback if the capture ever fails) */ const createDestructuringErrors = () => { const DestructuringErrors = DestructuringErrorsClass; return DestructuringErrors !== null ? new DestructuringErrors() : { shorthandAssign: -1, trailingComma: -1, parenthesizedAssign: -1, parenthesizedBind: -1, doubleProto: -1 }; }; /** * Reserved-word classification for `checkUnreserved`'s single lookup: * `1` keyword, `2` reserved in sloppy and strict mode, `3` reserved in strict * mode only. * @typedef {1 | 2 | 3} ReservedKind */ /** * @typedef {object} WordLookups * @property {Map} keywords keyword name → token type * @property {Map} reservedKinds identifier name → reserved kind * @property {number} reservedMaxLen longest key in `reservedKinds` * @property {{ test: (name: string) => boolean }} reservedBindTest strict-mode binding check, a Set-backed stand-in for acorn's `reservedWordsStrictBind` regexp * @property {Set} reservedBindSet strict-mode binding-reserved names * @property {number} reservedBindMinLen shortest key in `reservedBindSet` * @property {number} reservedBindMaxLen longest key in `reservedBindSet` * @property {number} id owner tag for the shared `WORD_TYPES` classification memo */ // One entry per distinct keyword/reserved-word set; webpack parses with a // single option set, making this effectively a one-time build shared across // every parse. /** @type {Map} */ const wordLookupsCache = new Map(); // Direct-mapped identifier cache for `readWord1`: one slot per hash, verified // by char compare, overwritten on collision. Shared across parses — hits are // content-checked, so a stale entry is merely a miss. Only words short enough // to be flat V8 strings (never slices retaining their whole source) are stored. const WORD_CACHE_MASK = 0x1fff; /** @type {(string | null)[]} */ const WORD_CACHE = Array.from({ length: WORD_CACHE_MASK + 1 }, () => null); const WORD_CACHE_MAX_LEN = 12; // Classification memo parallel to `WORD_CACHE`, same slot: every cache write // also writes the word's token type plus the id of the keyword set that // classified it, so a hit skips `classifyWord` and another option set (or a // `readWord1` write, owner 0) invalidates instead of mis-classifying. Keying // by the cache's own slot adds no extra string references — the memo can // never retain a word the cache itself dropped. /** @type {(TokenType | null)[]} */ const WORD_TYPES = Array.from({ length: WORD_CACHE_MASK + 1 }, () => null); const WORD_TYPE_OWNERS = new Int32Array(WORD_CACHE_MASK + 1); let nextWordLookupsId = 1; /** * Keyword-or-name classification: pure in (word, keywords), so it can be * memoized per `WORD_CACHE` slot. * @param {string} word word * @param {Map} keywords keyword name → token type * @returns {TokenType} token type for `word` */ const classifyWord = (word, keywords) => { const len = word.length; // every acorn keyword is 2-10 lowercase ASCII chars (`do`…`instanceof`) if (len >= 2 && len <= 10) { const first = word.charCodeAt(0); if (first >= 97 && first <= 122) return keywords.get(word) || tokTypes.name; } return tokTypes.name; }; // Multi-char operator strings for `finishOp` (`=>`, `===`, `&&=`, …), keyed by // their char codes packed 7 bits apart — collision-free for ASCII operators up // to acorn's maximum of 4 chars (`>>>=`), so the set stays ~40 entries. /** @type {Map} */ const OP_CACHE = new Map(); // The regexp flag whitelist depends only on the ecmaVersion, so build one // string per version instead of a fresh one in every parser constructor. /** @type {Map} */ const VALID_REGEXP_FLAGS = new Map(); /** * @param {number} ecmaVersion normalized acorn ecmaVersion * @returns {string} the regexp flags allowed at `ecmaVersion` */ const getValidRegexpFlags = (ecmaVersion) => { let flags = VALID_REGEXP_FLAGS.get(ecmaVersion); if (flags === undefined) { flags = `gim${ecmaVersion >= 6 ? "uy" : ""}${ecmaVersion >= 9 ? "s" : ""}${ ecmaVersion >= 13 ? "d" : "" }${ecmaVersion >= 15 ? "v" : ""}`; VALID_REGEXP_FLAGS.set(ecmaVersion, flags); } return flags; }; // Sticky mirrors of acorn's `skipWhiteSpace` / string-literal / `lineBreak` // regexes for the owned `strictDirective` (they scan at an offset, no slice). const STRICT_SKIP_WS = /(?:\s|\/\/.*|\/\*[^]*?\*\/)*/g; const STRICT_LITERAL = /(?:'((?:\\[^]|[^'\\])*?)'|"((?:\\[^]|[^"\\])*?)")/y; const STRICT_LINE_BREAK = /\r\n?|\n|\u2028|\u2029/; /** * @param {RegExp} re acorn `wordsRegexp` output (`^(?:a|b|c)$`) * @returns {Set} the alternation's words */ const wordsRegexpToSet = (re) => { const match = /^\^\(\?:(.*)\)\$$/.exec(re.source); const body = match ? match[1] : ""; return new Set(body ? body.split("|") : []); }; // One-entry identity memo in front of the string-keyed cache: acorn's // `wordsRegexp` interns its regexps, so identity captures the whole word set, // and builds construct thousands of parsers with one option set — this makes // the per-construction lookup three compares instead of a long key concat. /** @type {RegExp | undefined} */ let lastKeywordsRe; /** @type {RegExp | undefined} */ let lastReservedRe; /** @type {RegExp | undefined} */ let lastReservedStrictRe; /** @type {WordLookups | undefined} */ let lastWordLookups; /** * Mirrors acorn's `keywords` / `reservedWords` / `reservedWordsStrict` regexps * as Map/Set lookups. Membership is the hot per-word test in `readWord` and * `checkUnreserved`, and a hash lookup beats an anchored alternation regexp. * @param {ParserInternals} parser parser instance * @returns {WordLookups} lookups for this parser's keyword set */ const getWordLookups = (parser) => { if ( parser.keywords === lastKeywordsRe && parser.reservedWords === lastReservedRe && parser.reservedWordsStrict === lastReservedStrictRe ) { return /** @type {WordLookups} */ (lastWordLookups); } // module vs script share a keyword set but differ in reserved words, so the // key must cover all three regexps const key = `${parser.keywords.source}\n${parser.reservedWords.source}\n${parser.reservedWordsStrict.source}`; lastKeywordsRe = parser.keywords; lastReservedRe = parser.reservedWords; lastReservedStrictRe = parser.reservedWordsStrict; const cached = wordLookupsCache.get(key); if (cached !== undefined) { lastWordLookups = cached; return cached; } /** @type {Map} */ const keywords = new Map(); // acorn's keyword regexp is a subset of keywordTypes for the ecmaVersion for (const name of Object.keys(keywordTypes)) { if (parser.keywords.test(name)) keywords.set(name, keywordTypes[name]); } const reserved = wordsRegexpToSet(parser.reservedWords); /** @type {Map} */ const reservedKinds = new Map(); for (const name of reserved) reservedKinds.set(name, 2); for (const name of wordsRegexpToSet(parser.reservedWordsStrict)) { if (!reserved.has(name)) reservedKinds.set(name, 3); } // keyword classification wins, matching acorn's keyword-first check for (const name of keywords.keys()) reservedKinds.set(name, 1); const reservedBind = wordsRegexpToSet(parser.reservedWordsStrictBind); // length bounds so the owned checkLValSimple can skip the Set probe for // most identifiers let reservedBindMinLen = 0x7fffffff; let reservedBindMaxLen = 0; for (const name of reservedBind) { if (name.length < reservedBindMinLen) reservedBindMinLen = name.length; if (name.length > reservedBindMaxLen) reservedBindMaxLen = name.length; } let reservedMaxLen = 0; for (const name of reservedKinds.keys()) { if (name.length > reservedMaxLen) reservedMaxLen = name.length; } /** @type {WordLookups} */ const lookups = { keywords, reservedKinds, reservedMaxLen, reservedBindTest: { test: (name) => reservedBind.has(name) }, reservedBindSet: reservedBind, reservedBindMinLen, reservedBindMaxLen, id: nextWordLookupsId++ }; wordLookupsCache.set(key, lookups); lastWordLookups = lookups; return lookups; }; /** * webpack's parser: acorn plus lazy `range` (no `loc` at all), Set-based scopes, * tokenizer fast paths, import attributes and import phases (with acorn's * `!forNew` guard, unlike the former `acorn-import-phases` package). */ class WebpackParser extends BaseParser { /** * @param {Options & { lazyNodes?: boolean, lazyComments?: CollectedComment[], importPhases?: boolean, moduleFallback?: boolean }} options options * @param {string} input source code * @param {number=} startPos start position */ constructor(options, input, startPos) { const lazy = options.lazyNodes === true; // JavascriptParser._parse pre-disables acorn's tracking, so the // defensive copy only runs for direct callers if (lazy && (options.locations || options.ranges)) { options = { ...options, locations: false, ranges: false }; } super(options, input, startPos); // acorn sets this.keywords/reservedWords in its constructor; parsing // (and thus readWord) only starts later in parse(), so this is ready this._wordLookups = getWordLookups( /** @type {ParserInternals} */ (/** @type {unknown} */ (this)) ); // acorn only calls `.test()` on reservedWordsStrictBind (in // checkLValSimple); swap its regexp for the Set-backed check /** @type {{ reservedWordsStrictBind: { test: (name: string) => boolean } }} */ (/** @type {unknown} */ (this)).reservedWordsStrictBind = this._wordLookups.reservedBindTest; // per-token option probes cached once: acorn normalizes options in // `getOptions` before the constructor body runs and never mutates them const normalizedOptions = /** @type {ParserInternals} */ ( /** @type {unknown} */ (this) ).options; this._ecmaVersion = /** @type {number} */ (normalizedOptions.ecmaVersion); this._noLocations = !normalizedOptions.locations; // lazy mode: nodes get only offsets, gating the owned tokenizer and // statement fast paths this._lazy = lazy; // lazy comment collection must not race a user-provided onComment /** @type {CollectedComment[] | undefined} */ this._lazyComments = lazy && !options.onComment ? options.lazyComments : undefined; // acorn skips a hashbang inside its constructor, before `_lazyComments` // above exists — reconstruct the comment the override missed if ( this._lazyComments !== undefined && !startPos && this.options.allowHashBang && input.startsWith("#!") ) { this._lazyComments.push( new LazyComment( false, 2, 0, /** @type {ParserInternals} */ (/** @type {unknown} */ (this)).pos, input ) ); } /** @type {ImportPhase | null} */ this._importPhase = null; this._importPhasesEnabled = options.importPhases === true; // auto source type: parse as module first, downgrade to script in place // (instead of a second full parse) when script-only syntax is hit this._moduleFallback = options.moduleFallback === true; // set once a module-only construct is parsed; blocks the downgrade this._moduleSyntaxSeen = false; // the owned parseSubscript assumes optional chaining exists (it bakes // `optional` into the node shape), so gate it on the normalized version this._subscriptFastPath = lazy && this._ecmaVersion >= 11; const proto = WebpackParser.prototype; const self = /** @type {ParserInternals} */ (/** @type {unknown} */ (this)); // the owned per-token loop (nextToken/finishToken/next) also serves the // public non-lazy tokenizer(): locations off, no onToken, and none of the // acorn tokenizer methods it inlines or skips overridden by a plugin // (`nextToken`'s punct/dot/eq shortcuts bypass `getTokenFromCode`, // `finishOp`, `readToken_dot` and `readToken_eq_excl` even when the full // token path below is off, so their overrides must gate this loop too) this._tokenFastPath = lazy || (this._noLocations && !normalizedOptions.onToken && this.nextToken === proto.nextToken && this.finishToken === proto.finishToken && this.next === proto.next && this.skipSpace === proto.skipSpace && this.getTokenFromCode === proto.getTokenFromCode && this.finishOp === proto.finishOp && self.readToken === base.readToken && self.updateContext === base.updateContext && self.readToken_dot === base.readToken_dot && self.readToken_eq_excl === base.readToken_eq_excl); // the owned getTokenFromCode bakes in every ES2021 operator (?., ??=, // &&=, ...), so it needs at least that version; outside lazy mode it also // bypasses acorn's readToken_* family, so any override there turns it off this._fullTokenFastPath = this._ecmaVersion >= 12 && (lazy || (this._tokenFastPath && self.readToken_slash === base.readToken_slash && self.readToken_mult_modulo_exp === base.readToken_mult_modulo_exp && self.readToken_pipe_amp === base.readToken_pipe_amp && self.readToken_caret === base.readToken_caret && self.readToken_plus_min === base.readToken_plus_min && self.readToken_lt_gt === base.readToken_lt_gt && self.readToken_question === base.readToken_question && self.readToken_numberSign === base.readToken_numberSign && self.readRadixNumber === base.readRadixNumber)); // the owned nextToken/readWord finish the commonest tokens in place, // which skips this.finishToken: needs the fast token loop (readWord runs // in every mode) and no finishToken override by a plugin this._inlineFinish = this._tokenFastPath && this.finishToken === WebpackParser.prototype.finishToken; // whether the gap before the current token holds a line terminator: // 0 no, 1 yes, 2 unknown (canInsertSemicolon then scans the gap) /** @type {0 | 1 | 2} */ this._newlineBefore = 2; // LIFO pool for call-scoped destructuring-errors records; depth resets // implicitly since a raise aborts the whole parse /** @type {DestructuringErrorsShim[]} */ this._deStack = []; this._deDepth = 0; // LIFO pool for `parseObj`'s prop-clash records: acorn's ES6+ // `checkPropClash` only ever touches `.proto`, so one record per nesting // depth suffices; an overriding subclass gets the fresh `{}` acorn expects this._propHashFastPath = /** @type {ParserInternals} */ (/** @type {unknown} */ (this)) .checkPropClash === base.checkPropClash; /** @type {{ proto: boolean }[]} */ this._propHashStack = []; this._propHashDepth = 0; // LIFO pool of scratch arrays for list productions: elements are written // by index, then copied out exactly sized — a push-grown array retains // ~17 slots of capacity slack per list otherwise /** @type {unknown[][]} */ this._arrStack = []; this._arrDepth = 0; // `readRegexp`'s flag whitelist depends only on the ecmaVersion this._validRegexpFlags = getValidRegexpFlags(this._ecmaVersion); // the owned parseStatement inlines these methods, so a parser plugin // overriding any of them turns the statement fast path off this._stmtFastPath = lazy && this.parseVarStatement === proto.parseVarStatement && this.parseVar === proto.parseVar && this.parseIfStatement === proto.parseIfStatement && this.parseReturnStatement === proto.parseReturnStatement && this.parseExpressionStatement === proto.parseExpressionStatement; // last arrow finished by parseArrowExpression: `expr === this._lastArrow` // replaces the megamorphic `expr.type === "ArrowFunctionExpression"` // probes on the expression spine (arrows are created in exactly one place // and never backtracked, so identity captures the type test) /** @type {Expression | null} */ this._lastArrow = null; // arrows must flow through parseArrowExpression for the identity probe; // a plugin overriding it falls back to the type-based probes this._arrowFastPath = lazy && this.parseArrowExpression === proto.parseArrowExpression; // the owned parseFunction/parseFunctionBody inline initFunction and // isSimpleParamList; a plugin overriding either turns the fast path off const internals = /** @type {ParserInternals} */ ( /** @type {unknown} */ (this) ); this._funcFastPath = lazy && this._ecmaVersion >= 9 && internals.initFunction === base.initFunction && internals.isSimpleParamList === base.isSimpleParamList; // the owned parseStatement inlines parseFunctionStatement's body this._funcStmtOwn = this._funcFastPath && internals.parseFunctionStatement === base.parseFunctionStatement && this.parseFunction === proto.parseFunction; // the owned parseStatement inlines these statement parsers too; any // override falls back to acorn's dispatch for these heads this._stmt2FastPath = lazy && this._ecmaVersion >= 9 && internals.parseForStatement === base.parseForStatement && internals.parseFor === base.parseFor && internals.parseForIn === base.parseForIn && internals.parseForAfterInit === base.parseForAfterInit && this.parseVar === proto.parseVar && internals.parseWhileStatement === base.parseWhileStatement && internals.parseSwitchStatement === base.parseSwitchStatement && internals.parseThrowStatement === base.parseThrowStatement && internals.parseTryStatement === base.parseTryStatement && internals.parseBreakContinueStatement === base.parseBreakContinueStatement; // `parseMaybeAssign`'s trivial-atom fast path returns the atom without // descending the seven-layer expression chain, so every layer it skips // must be the owned one this._exprFastPath = lazy && this.parseMaybeConditional === proto.parseMaybeConditional && this.parseExprOps === proto.parseExprOps && this.parseExprOp === proto.parseExprOp && this.parseMaybeUnary === proto.parseMaybeUnary && this.parseExprSubscripts === proto.parseExprSubscripts && this.parseSubscripts === proto.parseSubscripts && this.parseSubscript === proto.parseSubscript && this.parseExprAtom === proto.parseExprAtom && this.checkExpressionErrors === proto.checkExpressionErrors && internals.isContextual === base.isContextual; } /** * Fetches a destructuring-errors record from the pool: acorn allocates one * per expression parse and drops it at the end of the call, so strictly * call-scoped users can reuse records instead. Pair every acquire with a * `_releaseDestructuringErrors` on each non-throwing exit. * @returns {DestructuringErrorsShim} reset record * @this {ParserInternals} */ _acquireDestructuringErrors() { const stack = this._deStack; const depth = this._deDepth++; const cached = stack[depth]; if (cached !== undefined) { cached.shorthandAssign = cached.trailingComma = cached.parenthesizedAssign = cached.parenthesizedBind = cached.doubleProto = -1; return cached; } return (stack[depth] = createDestructuringErrors()); } /** * @returns {void} * @this {ParserInternals} */ _releaseDestructuringErrors() { this._deDepth--; } /** * Fetches a scratch array for a list production: write elements by index, * then materialize with `_releaseScratch`. Entries past the caller's write * index are stale and meaningless. * @returns {EXPECTED_ANY[]} scratch array * @this {ParserInternals} */ _acquireScratch() { const stack = this._arrStack; const depth = this._arrDepth++; const cached = stack[depth]; if (cached !== undefined) return cached; return (stack[depth] = []); } /** * @param {EXPECTED_ANY[]} scratch scratch array from `_acquireScratch` * @param {number} count number of elements written * @returns {EXPECTED_ANY[]} exactly-sized copy of the first `count` entries * @this {ParserInternals} */ _releaseScratch(scratch, count) { this._arrDepth--; return scratch.slice(0, count); } // ----- tokenizer fast paths ----- /** * Owned per-token loop: acorn's `nextToken` chains `skipSpace` → * `fullCharCodeAtPos` → `readToken` → `isIdentifierStart` with a dead * `locations` check at each step. For the common non-template context of * `_tokenFastPath` mode (lazy, or locations-off with no tokenizer overrides) * this folds whitespace and comment skipping and the ASCII token dispatch * into one function so nothing re-enters acorn's per-step option checks. * Template/`preserveSpace` contexts and other modes use acorn's tokenizer. * acorn source: https://github.com/acornjs/acorn/blob/8.17.0/acorn/src/tokenize.js * @returns {void} * @this {ParserInternals} */ nextToken() { const context = this.context; const curContext = context[context.length - 1]; let slow; if (curContext !== undefined) { const flagged = /** @type {TokContextShim & { [kSlowContext]?: boolean }} */ (curContext); slow = flagged[kSlowContext]; if (slow === undefined) { slow = flagged[kSlowContext] = Boolean( curContext.preserveSpace || curContext.override ); } } if (!this._tokenFastPath || curContext === undefined || slow) { this._newlineBefore = 2; return base.nextToken.call(this); } const input = this.input; const len = input.length; let pos = this.pos; // line terminators are flagged while skipping (yuku's // `line_terminator_before`), so ASI checks need no gap re-scan. A // delegated path (acorn's html-comment handling) may have consumed part // of the gap before re-entering — start at "unknown" then. /** @type {0 | 1 | 2} */ let newline = pos === this.lastTokEnd ? 0 : 2; // one CHAR_CLASS load classifies each char for both the skip loop and // the token dispatch below (yuku's ws_class/ident/punct tables in one) let code = 0; let cls = CLS_OTHER; while (pos < len) { code = input.charCodeAt(pos); cls = CHAR_CLASS[code]; if (cls < CLS_SPACE) { if (cls === CLS_UNICODE) { // unicode whitespace / line terminators: the cold reader consumes them this.pos = pos; this._skipSpaceCold(); pos = this.pos; if (newline === 0) newline = 2; code = pos < len ? input.charCodeAt(pos) : 0; cls = CHAR_CLASS[code]; } break; } if (cls === CLS_SPACE) { // space, tab, VT, FF (no CRLF/line bookkeeping in lazy mode) pos++; } else if (cls === CLS_NEWLINE) { newline = 1; pos++; } else { const next = input.charCodeAt(pos + 1); if (next === 42) { this.pos = pos; this.skipBlockComment(); pos = this.pos; // the comment body may hold a line terminator if (newline === 0) newline = 2; } else if (next === 47) { this.pos = pos; this.skipLineComment(2); pos = this.pos; } else { // a division/regexp token, not a comment cls = CLS_OTHER; break; } } } this._newlineBefore = newline; this.pos = pos; this.start = pos; if (pos >= len) return this.finishToken(tokTypes.eof); switch (cls) { case CLS_IDENT: return this.readWord(); case CLS_PUNCT: { const type = /** @type {TokenType} */ (SIMPLE_PUNCT[code]); this.pos = pos + 1; if (!this._inlineFinish) return this.finishToken(type); // finishToken inlined for the simple punctuators (no value, and // their context updates are per-char static) this.end = pos + 1; const prevType = this.type; this.type = type; this.value = undefined; switch (code) { case 41: case 125: { // parenR/braceR.updateContext, inlined if (context.length === 1) { this.exprAllowed = true; break; } let out = /** @type {TokContextShim} */ (context.pop()); if ( out === CTX_B_STAT && /** @type {TokContextShim} */ (context[context.length - 1]) .token === "function" ) { out = /** @type {TokContextShim} */ (context.pop()); } this.exprAllowed = !out.isExpr; break; } case 123: // braceL.updateContext, inlined context.push(this.braceIsBlock(prevType) ? CTX_B_STAT : CTX_B_EXPR); this.exprAllowed = true; break; case 40: { // parenL.updateContext, inlined const statementParens = prevType === tokTypes._if || prevType === tokTypes._for || prevType === tokTypes._with || prevType === tokTypes._while; context.push(statementParens ? CTX_P_STAT : CTX_P_EXPR); this.exprAllowed = true; break; } case 58: // colon.updateContext, inlined if ( /** @type {TokContextShim} */ (context[context.length - 1]) .token === "function" ) { context.pop(); } this.exprAllowed = true; break; case 93: // bracketR: no context hook, beforeExpr is false this.exprAllowed = false; break; default: // semi, comma, bracketL: no context hook, beforeExpr is true this.exprAllowed = true; } return; } case CLS_DOT: { // `.` not starting `.5` or `...`: skip readToken_dot's re-dispatch const next = input.charCodeAt(pos + 1); if ((next < 48 || next > 57) && next !== 46) { this.pos = pos + 1; return this.finishToken(tokTypes.dot); } return this.getTokenFromCode(code); } case CLS_EQ: { // `=` not starting `==` or `=>`: skip readToken_eq_excl + finishOp slice const next = input.charCodeAt(pos + 1); if (next !== 61 && next !== 62) { this.pos = pos + 1; return this.finishToken(tokTypes.eq, "="); } return this.getTokenFromCode(code); } case CLS_UNICODE: return this.readToken(this.fullCharCodeAtPos()); default: return this.getTokenFromCode(code); } } /** * `_tokenFastPath` `finishToken`: acorn probes `options.locations` for a * dead `endLoc` write on every token and reaches `updateContext` through an * extra method call. Skip the probe and inline acorn's `updateContext` body — * this runs once per token. Other modes use acorn's. * acorn source: https://github.com/acornjs/acorn/blob/8.17.0/acorn/src/tokenize.js * @param {TokenType} type token type * @param {unknown=} value token value * @returns {void} * @this {ParserInternals} */ finishToken(type, value) { if (!this._tokenFastPath) { return base.finishToken.call(this, type, value); } this.end = this.pos; const prevType = this.type; this.type = type; this.value = value; const internal = /** @type {TokenTypeInternal} */ (type); // acorn's updateContext, inlined: keyword-after-dot forbids an expression, // else the token type's own context hook runs, else `exprAllowed` follows // the type's `beforeExpr` (the branch that makes `/` after a value divide) if (type === tokTypes.name) { // name.updateContext, inlined for the commonest token: only `of` / // `yield` (outside a `.` access, ES6+) can re-allow an expression this.exprAllowed = ((value === "of" && !this.exprAllowed) || (value === "yield" && this.inGeneratorContext())) && prevType !== tokTypes.dot && this._ecmaVersion >= 6; } else { const update = internal.updateContext; if (update === null) { // no context hook (most punctuation, operators, literals and // keywords): acorn's keyword-after-dot probe, else `beforeExpr` — // checked first so this majority skips the per-type compares below this.exprAllowed = prevType === tokTypes.dot && internal.keyword !== undefined ? false : internal.beforeExpr; } else if (type === tokTypes.parenR || type === tokTypes.braceR) { // parenR/braceR.updateContext, inlined const context = this.context; if (context.length === 1) { this.exprAllowed = true; } else { let out = /** @type {TokContextShim} */ (context.pop()); if ( out === CTX_B_STAT && /** @type {TokContextShim} */ (context[context.length - 1]) .token === "function" ) { out = /** @type {TokContextShim} */ (context.pop()); } this.exprAllowed = !out.isExpr; } } else if (type === tokTypes.braceL) { // braceL.updateContext, inlined this.context.push( this.braceIsBlock(prevType) ? CTX_B_STAT : CTX_B_EXPR ); this.exprAllowed = true; } else if (type === tokTypes.parenL) { // parenL.updateContext, inlined const statementParens = prevType === tokTypes._if || prevType === tokTypes._for || prevType === tokTypes._with || prevType === tokTypes._while; this.context.push(statementParens ? CTX_P_STAT : CTX_P_EXPR); this.exprAllowed = true; } else if (internal.keyword !== undefined && prevType === tokTypes.dot) { // `.function` etc.: keyword-after-dot wins over the type's own hook this.exprAllowed = false; } else { update.call(this, prevType); } } } /** * Owned `braceIsBlock`, acorn's verbatim except the line-terminator probe: * acorn slices the inter-token gap and runs a regexp; `_gapHasNewline` * answers from the tokenizer's newline flag (scanning only when unknown). * acorn source: https://github.com/acornjs/acorn/blob/8.17.0/acorn/src/tokencontext.js * @param {TokenType} prevType type of the previous token * @returns {boolean} whether a `{` opens a block in this context * @this {ParserInternals} */ braceIsBlock(prevType) { const context = this.context; const parent = /** @type {TokContextShim} */ (context[context.length - 1]); if (parent === CTX_F_EXPR || parent === CTX_F_STAT) return true; if ( prevType === tokTypes.colon && (parent === CTX_B_STAT || parent === CTX_B_EXPR) ) { return !parent.isExpr; } // after `return`, or after `yield`/`of` (name with exprAllowed), a line // terminator decides between block and expression if ( prevType === tokTypes._return || (prevType === tokTypes.name && this.exprAllowed) ) { return this._gapHasNewline(); } if ( prevType === tokTypes._else || prevType === tokTypes.semi || prevType === tokTypes.eof || prevType === tokTypes.parenR || prevType === tokTypes.arrow ) { return true; } if (prevType === tokTypes.braceL) return parent === CTX_B_STAT; if ( prevType === tokTypes._var || prevType === tokTypes._const || prevType === tokTypes.name ) { return false; } return !this.exprAllowed; } /** * Whether the gap before the current token holds a line terminator, served * from the owned tokenizer's flag when known. * @returns {boolean} whether a line terminator precedes the current token * @this {ParserInternals} */ _gapHasNewline() { const newlineBefore = this._newlineBefore; if (newlineBefore !== 2) return newlineBefore === 1; const input = this.input; const end = this.start; for (let i = this.lastTokEnd; i < end; i++) { const ch = input.charCodeAt(i); // LF, CR, LS, PS — acorn's `lineBreak` alternation if (ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029) { // memoize: the gap is fixed until the next token is read, and // `nextToken` rewrites the flag — ASI probes often repeat per token // (e.g. name atoms behind a /*#__PURE__*/ comment) this._newlineBefore = 1; return true; } } this._newlineBefore = 0; return false; } /** * Owned per-token advance: acorn's `next` writes `lastTokEndLoc`/ * `lastTokStartLoc` and probes `options.onToken` on every token, both dead in * `_tokenFastPath` mode (locations off, no token stream), leaving only the * two offset writes and the keyword-escape guard. Other modes use acorn's. * acorn source: https://github.com/acornjs/acorn/blob/8.17.0/acorn/src/tokenize.js * @param {boolean=} ignoreEscapeSequenceInKeyword whether an escape in a keyword is allowed here * @returns {void} * @this {ParserInternals} */ next(ignoreEscapeSequenceInKeyword) { if (!this._tokenFastPath) { return base.next.call(this, ignoreEscapeSequenceInKeyword); } const type = this.type; // `containsEsc` is a parser field and almost always false; testing it first // keeps the TokenType load off the common path. if (this.containsEsc && !ignoreEscapeSequenceInKeyword && type.keyword) { this.raiseRecoverable( this.start, `Escape sequence in keyword ${type.keyword}` ); } this.lastTokEnd = this.end; this.lastTokStart = this.start; this.nextToken(); } /** * Owned `finishOp`: acorn slices the operator text out of the source for * every operator token, allocating a fresh 2-4 char string per `=>`, `===`, * `&&` etc. Serve those from `OP_CACHE` instead; single-char operators keep * the direct slice, which V8 serves from its single-character table. * acorn source: https://github.com/acornjs/acorn/blob/8.17.0/acorn/src/tokenize.js * @param {TokenType} type token type * @param {number} size operator length * @returns {void} * @this {ParserInternals} */ finishOp(type, size) { const pos = this.pos; const input = this.input; if (size === 1) { this.pos = pos + 1; return this.finishToken(type, input.slice(pos, pos + 1)); } let key = input.charCodeAt(pos) | (input.charCodeAt(pos + 1) << 7); if (size > 2) { key |= input.charCodeAt(pos + 2) << 14; if (size > 3) key |= input.charCodeAt(pos + 3) << 21; } let str = OP_CACHE.get(key); if (str === undefined) { str = input.slice(pos, pos + size); OP_CACHE.set(key, str); } this.pos = pos + size; return this.finishToken(type, str); } /** * Owned `getTokenFromCode`: acorn dispatches operators through per-family * `readToken_*` methods that each end in `finishOp`'s source slice. Resolve * every operator by direct char peeks to a static string instead (yuku's * `scanPunctuation`) — no method chain, no slice, no `OP_CACHE` probe. The * HTML-comment forms (``) inline acorn's line-comment handling; * `#` and unknown chars use the owned cold reader. * acorn source: https://github.com/acornjs/acorn/blob/8.17.0/acorn/src/tokenize.js * @param {number} code current char code * @returns {void} * @this {ParserInternals} */ getTokenFromCode(code) { if (!this._fullTokenFastPath) { return base.getTokenFromCode.call(this, code); } const input = this.input; const pos = this.pos; switch (code) { case 46: { // '.': number, ellipsis or plain dot const next = input.charCodeAt(pos + 1); if (next >= 48 && next <= 57) return this.readNumber(true); if (next === 46 && input.charCodeAt(pos + 2) === 46) { this.pos = pos + 3; return this.finishToken(tokTypes.ellipsis); } this.pos = pos + 1; return this.finishToken(tokTypes.dot); } case 47: { // '/': regexp in expression position, otherwise /= or / if (this.exprAllowed) { this.pos = pos + 1; return this.readRegexp(); } if (input.charCodeAt(pos + 1) === 61) { this.pos = pos + 2; return this.finishToken(tokTypes.assign, "/="); } this.pos = pos + 1; return this.finishToken(tokTypes.slash, "/"); } case 37: { // '%': %= or % if (input.charCodeAt(pos + 1) === 61) { this.pos = pos + 2; return this.finishToken(tokTypes.assign, "%="); } this.pos = pos + 1; return this.finishToken(tokTypes.modulo, "%"); } case 42: { // '*': **=, **, *= or * const next = input.charCodeAt(pos + 1); if (next === 42) { if (input.charCodeAt(pos + 2) === 61) { this.pos = pos + 3; return this.finishToken(tokTypes.assign, "**="); } this.pos = pos + 2; return this.finishToken(tokTypes.starstar, "**"); } if (next === 61) { this.pos = pos + 2; return this.finishToken(tokTypes.assign, "*="); } this.pos = pos + 1; return this.finishToken(tokTypes.star, "*"); } case 124: { // '|': ||=, ||, |= or | const next = input.charCodeAt(pos + 1); if (next === 124) { if (input.charCodeAt(pos + 2) === 61) { this.pos = pos + 3; return this.finishToken(tokTypes.assign, "||="); } this.pos = pos + 2; return this.finishToken(tokTypes.logicalOR, "||"); } if (next === 61) { this.pos = pos + 2; return this.finishToken(tokTypes.assign, "|="); } this.pos = pos + 1; return this.finishToken(tokTypes.bitwiseOR, "|"); } case 38: { // '&': &&=, &&, &= or & const next = input.charCodeAt(pos + 1); if (next === 38) { if (input.charCodeAt(pos + 2) === 61) { this.pos = pos + 3; return this.finishToken(tokTypes.assign, "&&="); } this.pos = pos + 2; return this.finishToken(tokTypes.logicalAND, "&&"); } if (next === 61) { this.pos = pos + 2; return this.finishToken(tokTypes.assign, "&="); } this.pos = pos + 1; return this.finishToken(tokTypes.bitwiseAND, "&"); } case 94: { // '^': ^= or ^ if (input.charCodeAt(pos + 1) === 61) { this.pos = pos + 2; return this.finishToken(tokTypes.assign, "^="); } this.pos = pos + 1; return this.finishToken(tokTypes.bitwiseXOR, "^"); } case 43: { // '+': ++, += or + const next = input.charCodeAt(pos + 1); if (next === 43) { this.pos = pos + 2; return this.finishToken(tokTypes.incDec, "++"); } if (next === 61) { this.pos = pos + 2; return this.finishToken(tokTypes.assign, "+="); } this.pos = pos + 1; return this.finishToken(tokTypes.plusMin, "+"); } case 45: { // '-': --, -= or -; `-->` may open an HTML line comment const next = input.charCodeAt(pos + 1); if (next === 45) { // `-->` opens an HTML line comment only at the start of a line // (acorn readToken_plus_min): https://github.com/acornjs/acorn/blob/8.15.0/acorn/src/tokenize.js#L599-L618 if ( input.charCodeAt(pos + 2) === 62 && !this.inModule && (this.lastTokEnd === 0 || lineBreak.test(input.slice(this.lastTokEnd, pos))) ) { this.skipLineComment(3); this.skipSpace(); return this.nextToken(); } this.pos = pos + 2; return this.finishToken(tokTypes.incDec, "--"); } if (next === 61) { this.pos = pos + 2; return this.finishToken(tokTypes.assign, "-="); } this.pos = pos + 1; return this.finishToken(tokTypes.plusMin, "-"); } case 60: { // '<': <<=, <<, <= or <; `