Source code

Revision control

Copy as Markdown

Other Tools

/*
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<string, unknown>} */
(
/** @type {{ tokContexts: Record<string, unknown> }} */
(/** @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<string, TokenType>} */
(
/** @type {{ keywordTypes: Record<string, TokenType> }} */
(/** @type {unknown} */ (require("acorn"))).keywordTypes
);
/** @typedef {import("acorn").Options} Options */
/** @typedef {import("acorn").Position} Position */
/** @typedef {import("acorn").Node} Node */
/** @typedef {import("acorn").Identifier} Identifier */
/** @typedef {import("acorn").ImportAttribute} ImportAttribute */
/** @typedef {import("acorn").ImportDefaultSpecifier} ImportDefaultSpecifier */
/** @typedef {import("acorn").ImportExpression} ImportExpression */
/** @typedef {import("acorn").Expression} Expression */
/** @typedef {import("acorn").ImportSpecifier | import("acorn").ImportDefaultSpecifier | import("acorn").ImportNamespaceSpecifier} AnyImportSpecifier */
/** @typedef {import("acorn").TokenType} TokenType */
/** @typedef {TokenType & { beforeExpr: boolean, isAssign?: boolean, prefix?: boolean, postfix?: boolean, binop: number | null, updateContext?: (prevType: TokenType) => void }} TokenTypeInternal acorn's internal TokenType fields, absent from its public types */
/** @typedef {import("estree").SourceLocation} SourceLocation */
/** @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.
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_VAR = 1;
const BIND_LEXICAL = 2;
const SCOPE_TOP = 1;
const SCOPE_SIMPLE_CATCH = 32;
// SCOPE_TOP | SCOPE_FUNCTION | SCOPE_CLASS_STATIC_BLOCK
const SCOPE_VAR = 0b100000011;
// 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;
// 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 `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;
}
}
// 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
]) {
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, so the slice is
* deferred to the first `value` 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;
/** @type {Range} */
this.range = [start, end];
this[kSource] = source;
this[kTextStart] = textStart;
}
/**
* @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<string> | undefined} */
this.var = undefined;
/** @type {Set<string> | undefined} */
this.lexical = undefined;
/** @type {Set<string> | 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,
* yieldPos: number,
* awaitPos: number,
* parseExpression: () => 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,
* 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,
* 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) => 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,
* 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<string, Node>,
* 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<string, unknown>, 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,
* _lazy: boolean,
* _importPhase: ImportPhase | null,
* _importPhasesEnabled: boolean,
* _lazyComments: CollectedComment[] | undefined,
* _newlineBefore: 0 | 1 | 2,
* _fullTokenFastPath: boolean,
* _stmtFastPath: boolean,
* 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) => void,
* _parseVarStatementAt: (start: number, kind: string, allowMissingInitializer?: boolean) => Node,
* _parseIfStatementAt: (start: number) => Node,
* _parseReturnStatementAt: (start: number) => Node,
* _parseExpressionStatementAt: (start: number, expr: Expression) => 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<string, TokenType>} keywords keyword name → token type
* @property {Map<string, ReservedKind>} 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
*/
// 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<string, WordLookups>} */
const wordLookupsCache = new Map();
/**
* @param {string} word interned candidate
* @param {string} input source code
* @param {number} start word start offset
* @param {number} len word length (already known equal to `word.length`)
* @returns {boolean} whether the source span spells `word`
*/
const sameWord = (word, input, start, len) => {
for (let i = 0; i < len; i++) {
if (word.charCodeAt(i) !== input.charCodeAt(start + i)) return false;
}
return true;
};
// 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;
// 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<number, string>} */
const OP_CACHE = new Map();
// 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<string>} 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<string, TokenType>} */
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<string, ReservedKind>} */
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);
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) }
};
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;
// the owned getTokenFromCode bakes in every ES2021 operator (?., ??=,
// &&=, ...), so it needs at least that version
this._fullTokenFastPath = lazy && this._ecmaVersion >= 12;
// 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;
// `readRegexp`'s flag whitelist depends only on the ecmaVersion
this._validRegexpFlags = `gim${this._ecmaVersion >= 6 ? "uy" : ""}${
this._ecmaVersion >= 9 ? "s" : ""
}${this._ecmaVersion >= 13 ? "d" : ""}${this._ecmaVersion >= 15 ? "v" : ""}`;
// the owned parseStatement inlines these methods, so a parser plugin
// overriding any of them turns the statement fast path off
const proto = WebpackParser.prototype;
this._stmtFastPath =
lazy &&
this.parseVarStatement === proto.parseVarStatement &&
this.parseVar === proto.parseVar &&
this.parseIfStatement === proto.parseIfStatement &&
this.parseReturnStatement === proto.parseReturnStatement &&
this.parseExpressionStatement === proto.parseExpressionStatement;
}
/**
* 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--;
}
// ----- 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 lazy, non-template context
* 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 non-lazy mode use acorn's tokenizer.
* @returns {void}
* @this {ParserInternals}
*/
nextToken() {
const context = this.context;
const curContext = context[context.length - 1];
if (
!this._lazy ||
!curContext ||
curContext.preserveSpace ||
curContext.override
) {
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: acorn consumes the rest
this.pos = pos;
base.skipSpace.call(this);
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:
this.pos = pos + 1;
return this.finishToken(/** @type {TokenType} */ (SIMPLE_PUNCT[code]));
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);
}
}
/**
* Lazy-mode `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.
* @param {TokenType} type token type
* @param {unknown=} value token value
* @returns {void}
* @this {ParserInternals}
*/
finishToken(type, value) {
if (!this._lazy) {
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 if (type.keyword && prevType === tokTypes.dot) {
this.exprAllowed = false;
} 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.updateContext) {
internal.updateContext.call(this, prevType);
} else {
this.exprAllowed = internal.beforeExpr;
}
}
/**
* 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).
* @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
* lazy mode (locations off, no token stream), leaving only the two offset
* writes and the keyword-escape guard. Other modes use acorn's.
* @param {boolean=} ignoreEscapeSequenceInKeyword whether an escape in a keyword is allowed here
* @returns {void}
* @this {ParserInternals}
*/
next(ignoreEscapeSequenceInKeyword) {
if (!this._lazy) {
return base.next.call(this, ignoreEscapeSequenceInKeyword);
}
const type = this.type;
if (!ignoreEscapeSequenceInKeyword && type.keyword && this.containsEsc) {
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.
* @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 (`<!--`, `-->`), `#`, and unknown chars delegate.
* @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) {
if (input.charCodeAt(pos + 2) === 62 && !this.inModule) {
return base.getTokenFromCode.call(this, code);
}
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 <; `<!--` opens an HTML line comment
const next = input.charCodeAt(pos + 1);
if (next === 60) {
if (input.charCodeAt(pos + 2) === 61) {
this.pos = pos + 3;
return this.finishToken(tokTypes.assign, "<<=");
}
this.pos = pos + 2;
return this.finishToken(tokTypes.bitShift, "<<");
}
if (
next === 33 &&
!this.inModule &&
input.charCodeAt(pos + 2) === 45 &&
input.charCodeAt(pos + 3) === 45
) {
return base.getTokenFromCode.call(this, code);
}
if (next === 61) {
this.pos = pos + 2;
return this.finishToken(tokTypes.relational, "<=");
}
this.pos = pos + 1;
return this.finishToken(tokTypes.relational, "<");
}
case 62: {
// '>': >>>=, >>>, >>=, >>, >= or >
const next = input.charCodeAt(pos + 1);
if (next === 62) {
if (input.charCodeAt(pos + 2) === 62) {
if (input.charCodeAt(pos + 3) === 61) {
this.pos = pos + 4;
return this.finishToken(tokTypes.assign, ">>>=");
}
this.pos = pos + 3;
return this.finishToken(tokTypes.bitShift, ">>>");
}
if (input.charCodeAt(pos + 2) === 61) {
this.pos = pos + 3;
return this.finishToken(tokTypes.assign, ">>=");
}
this.pos = pos + 2;
return this.finishToken(tokTypes.bitShift, ">>");
}
if (next === 61) {
this.pos = pos + 2;
return this.finishToken(tokTypes.relational, ">=");
}
this.pos = pos + 1;
return this.finishToken(tokTypes.relational, ">");
}
case 61: {
// '=': ===, ==, => or =
const next = input.charCodeAt(pos + 1);
if (next === 61) {
if (input.charCodeAt(pos + 2) === 61) {
this.pos = pos + 3;
return this.finishToken(tokTypes.equality, "===");
}
this.pos = pos + 2;
return this.finishToken(tokTypes.equality, "==");
}
if (next === 62) {
this.pos = pos + 2;
return this.finishToken(tokTypes.arrow);
}
this.pos = pos + 1;
return this.finishToken(tokTypes.eq, "=");
}
case 33: {
// '!': !==, != or !
const next = input.charCodeAt(pos + 1);
if (next === 61) {
if (input.charCodeAt(pos + 2) === 61) {
this.pos = pos + 3;
return this.finishToken(tokTypes.equality, "!==");
}
this.pos = pos + 2;
return this.finishToken(tokTypes.equality, "!=");
}
this.pos = pos + 1;
return this.finishToken(tokTypes.prefix, "!");
}
case 63: {
// '?': ?. (not before a digit), ??=, ?? or ?
const next = input.charCodeAt(pos + 1);
if (next === 46) {
const next2 = input.charCodeAt(pos + 2);
if (next2 < 48 || next2 > 57) {
this.pos = pos + 2;
return this.finishToken(tokTypes.questionDot, "?.");
}
}
if (next === 63) {
if (input.charCodeAt(pos + 2) === 61) {
this.pos = pos + 3;
return this.finishToken(tokTypes.assign, "??=");
}
this.pos = pos + 2;
return this.finishToken(tokTypes.coalesce, "??");
}
this.pos = pos + 1;
return this.finishToken(tokTypes.question, "?");
}
case 126: {
// '~'
this.pos = pos + 1;
return this.finishToken(tokTypes.prefix, "~");
}
case 96: {
// '`'
this.pos = pos + 1;
return this.finishToken(tokTypes.backQuote);
}
case 48: {
// '0': radix literals split off before the decimal reader
const next = input.charCodeAt(pos + 1);
if (next === 120 || next === 88) return this.readRadixNumber(16);
if (next === 111 || next === 79) return this.readRadixNumber(8);
if (next === 98 || next === 66) return this.readRadixNumber(2);
return this.readNumber(false);
}
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57:
return this.readNumber(false);
case 34:
case 39:
return this.readString(code);
default:
return base.getTokenFromCode.call(this, code);
}
}
/**
* ASCII fast path for acorn's `readWord1`, which pays a surrogate-aware
* method call and a range-check helper per character. Escapes, non-ASCII
* and astral input restart the base implementation from the word start.
* Words are deduplicated through `WORD_CACHE` so repeated identifiers —
* which dominate real code — reuse one string instead of slicing a fresh
* one per occurrence; sharing also keeps their cached string hashes warm
* for the keyword/scope Map lookups downstream.
* @this {ParserInternals}
* @returns {string} the word
*/
readWord1() {
const input = this.input;
const start = this.pos;
const len = input.length;
// djb2-style hash folded into the scan loop (yuku scans each lexeme in
// one pass); computing it for the rare over-long words is cheaper than
// re-walking every word in a second pass
let hash = 0;
let pos = start;
while (pos < len) {
const ch = input.charCodeAt(pos);
if (ch < 128) {
if (IDENT_CHAR[ch] === 0) {
// backslash escape: restart cold so escape rules see the word
if (ch === 92) return base.readWord1.call(this);
break;
}
hash = (Math.imul(hash, 33) + ch) | 0;
pos++;
} else {
return base.readWord1.call(this);
}
}
this.containsEsc = false;
this.pos = pos;
const wordLen = pos - start;
// Single-char words skip the cache (V8 serves those slices from its
// single-character table without allocating); long words skip it too.
if (wordLen >= 2 && wordLen <= WORD_CACHE_MAX_LEN) {
const slot = hash & WORD_CACHE_MASK;
const cached = WORD_CACHE[slot];
if (
cached !== null &&
cached.length === wordLen &&
sameWord(cached, input, start, wordLen)
) {
return cached;
}
const word = input.slice(start, pos);
WORD_CACHE[slot] = word;
return word;
}
return input.slice(start, pos);
}
/**
* String fast path: one scan that both finds the closing quote and vets the
* span (the former `indexOf` + verify-loop pair read every string twice).
* Escapes, newlines in the span, old ecmaVersions (LS/PS terminate strings
* there) and location tracking restart acorn's implementation, which also
* produces its exact errors.
* @this {ParserInternals}
* @param {number} quote quote char code
* @returns {void}
*/
readString(quote) {
if (!this._noLocations || this._ecmaVersion < 10) {
return base.readString.call(this, quote);
}
const input = this.input;
const len = input.length;
const start = this.pos + 1;
let pos = start;
for (;;) {
if (pos >= len) this.raise(this.start, "Unterminated string constant");
const ch = input.charCodeAt(pos);
if (ch === quote) break;
// backslash, LF, CR
if (ch === 92 || ch === 10 || ch === 13) {
return base.readString.call(this, quote);
}
pos++;
}
this.pos = pos + 1;
this.finishToken(tokTypes.string, input.slice(start, pos));
}
/**
* Number fast path: plain integer literals (no leading zero, up to 15
* digits so the float is exact) are accumulated numerically — no slice,
* no parseFloat, no separator handling. Everything else (dots, exponents,
* bigints, separators, octal forms) restarts acorn's implementation.
* @this {ParserInternals}
* @param {boolean} startsWithDot whether the number started with a dot
* @returns {void}
*/
readNumber(startsWithDot) {
if (startsWithDot) return base.readNumber.call(this, startsWithDot);
const input = this.input;
const start = this.pos;
const len = input.length;
const first = input.charCodeAt(start);
let pos;
if (first === 48) {
const c1 = start + 1 < len ? input.charCodeAt(start + 1) : 0;
if (c1 === 46) {
// `0.<digits>`
pos = start + 1;
} else if (c1 > 127 || IDENT_CHAR[c1] === 1) {
// 0x/0o/0b, 0e…, 0n, 0_, legacy `0NN`, or `0`+identifier: acorn
return base.readNumber.call(this, startsWithDot);
} else {
// bare `0` before punctuation/operator/whitespace/EOF
this.pos = start + 1;
return this.finishToken(tokTypes.num, 0);
}
} else if (first > 48 && first <= 57) {
// integer digits, accumulated numerically for the integer-only case
let value = first - 48;
pos = start + 1;
while (pos < len) {
const ch = input.charCodeAt(pos);
if (ch >= 48 && ch <= 57) {
value = value * 10 + (ch - 48);
pos++;
} else {
break;
}
}
const after = pos < len ? input.charCodeAt(pos) : 0;
if (after !== 46) {
// no fraction: exponent, separator, bigint suffix or a trailing
// identifier char all need acorn's full handling and exact errors
if (
after === 101 ||
after === 69 ||
after === 95 ||
after === 110 ||
after > 127 ||
IDENT_CHAR[after] === 1
) {
return base.readNumber.call(this, startsWithDot);
}
// 15 digits always fit exactly into a double
if (pos - start > 15) {
return base.readNumber.call(this, startsWithDot);
}
this.pos = pos;
return this.finishToken(tokTypes.num, value);
}
// a fraction follows the integer part
} else {
return base.readNumber.call(this, startsWithDot);
}
// decimal fraction: `pos` is at the '.'
pos++;
while (pos < len) {
const ch = input.charCodeAt(pos);
if (ch >= 48 && ch <= 57) pos++;
else break;
}
const after = pos < len ? input.charCodeAt(pos) : 0;
// exponent, a second dot, separator, bigint suffix or trailing identifier
if (
after === 46 ||
after === 101 ||
after === 69 ||
after === 95 ||
after === 110 ||
after > 127 ||
IDENT_CHAR[after] === 1
) {
return base.readNumber.call(this, startsWithDot);
}
this.pos = pos;
this.finishToken(tokTypes.num, Number.parseFloat(input.slice(start, pos)));
}
/**
* Owned `strictDirective`: acorn's runs an anchored literal regex over
* `this.input.slice(start)` — a fresh sliced string per (sloppy-mode)
* function body. Sticky regexes at the offset scan the same grammar with no
* slice. Same directive-prologue semantics, including the ASI tail checks.
* @param {number} start offset of the function body's first statement
* @returns {boolean} true when a 'use strict' directive leads the prologue
* @this {ParserInternals & { input: string, options: { ecmaVersion: number } }}
*/
strictDirective(start) {
if (/** @type {number} */ (this.options.ecmaVersion) < 5) return false;
const input = this.input;
for (;;) {
// Skip whitespace and comments (acorn's `skipWhiteSpace`, sticky).
STRICT_SKIP_WS.lastIndex = start;
start += /** @type {RegExpExecArray} */ (STRICT_SKIP_WS.exec(input))[0]
.length;
STRICT_LITERAL.lastIndex = start;
const match = STRICT_LITERAL.exec(input);
if (!match) return false;
if ((match[1] || match[2]) === "use strict") {
STRICT_SKIP_WS.lastIndex = start + match[0].length;
const spaceAfter = /** @type {RegExpExecArray} */ (
STRICT_SKIP_WS.exec(input)
);
const end = spaceAfter.index + spaceAfter[0].length;
const next = input.charAt(end);
return (
next === ";" ||
next === "}" ||
(STRICT_LINE_BREAK.test(spaceAfter[0]) &&
!(
/[(`.[+\-/*%<>=,?^&]/.test(next) ||
(next === "!" && input.charAt(end + 1) === "=")
))
);
}
start += match[0].length;
// Skip semicolon, if any.
STRICT_SKIP_WS.lastIndex = start;
start += /** @type {RegExpExecArray} */ (STRICT_SKIP_WS.exec(input))[0]
.length;
if (input[start] === ";") start++;
}
}
/**
* Template fast path: when the chunk contains no backslash and no CR, the
* cooked value is one slice (LF/LS/PS cook to themselves). Escapes, CR
* normalization and location tracking restart acorn's implementation,
* which also produces its exact errors.
* @this {ParserInternals}
* @returns {void}
*/
readTmplToken() {
if (!this._noLocations) return base.readTmplToken.call(this);
const input = this.input;
const start = this.pos;
const len = input.length;
let pos = start;
while (pos < len) {
const ch = input.charCodeAt(pos);
if (ch === 96 || (ch === 36 && input.charCodeAt(pos + 1) === 123)) {
if (
pos === this.start &&
(this.type === tokTypes.template ||
this.type === tokTypes.invalidTemplate)
) {
if (ch === 36) {
this.pos = pos + 2;
return this.finishToken(tokTypes.dollarBraceL);
}
this.pos = pos + 1;
return this.finishToken(tokTypes.backQuote);
}
this.pos = pos;
return this.finishToken(tokTypes.template, input.slice(start, pos));
}
// backslash and CR need acorn's cooked-string building
if (ch === 92 || ch === 13) {
return base.readTmplToken.call(this);
}
pos++;
}
this.raise(this.start, "Unterminated template");
}
/**
* Fast path for the common run of plain ASCII whitespace; comments,
* unicode whitespace and location tracking delegate to acorn.
* @this {ParserInternals & { pos: number }}
* @returns {void}
*/
skipSpace() {
if (!this._noLocations) return base.skipSpace.call(this);
const input = this.input;
const len = input.length;
let pos = this.pos;
while (pos < len) {
const ch = input.charCodeAt(pos);
// 9-13 and 32 cover tab, LF, VT, FF, CR and space
if (ch === 32 || (ch > 8 && ch < 14)) {
pos++;
} else if (ch === 47 || ch > 127) {
// comments or unicode whitespace: let acorn handle the rest
this.pos = pos;
return base.skipSpace.call(this);
} else {
break;
}
}
this.pos = pos;
}
// ----- word classification (Map/Set lookups, replaces acorn's regexps) -----
/**
* Replaces acorn's `readWord`, whose `this.keywords.test(word)` runs an
* anchored alternation regexp for every identifier and keyword token; a
* Map lookup on the same keyword set is cheaper.
* @this {ParserInternals}
* @returns {void}
*/
readWord() {
const word = this.readWord1();
let type = tokTypes.name;
const len = word.length;
// every acorn keyword is 2-10 lowercase ASCII chars (`do`…`instanceof`),
// so most identifiers skip the Map probe entirely (yuku gates its keyword
// switch the same way)
if (len >= 2 && len <= 10) {
const first = word.charCodeAt(0);
if (first >= 97 && first <= 122) {
type = this._wordLookups.keywords.get(word) || tokTypes.name;
}
}
this.finishToken(type, word);
}
/**
* Mirror of acorn's `checkUnreserved` with its two per-identifier regexp
* tests (`keywords` and `reservedWords`/`reservedWordsStrict`) folded into a
* single `reservedKinds` lookup — one hash probe instead of two, and the
* common plain identifier misses it and returns. Branches and error
* messages match acorn exactly.
* @param {Identifier} ref identifier node
* @this {ParserInternals}
*/
checkUnreserved(ref) {
const { start, end, name } = ref;
// every reserved word and special name below is short lowercase ASCII, so
// most identifiers exit on this shape gate before any string compare
// (`yield`/`await`/`arguments` all pass it: lengths 5/5/9 ≤ reservedMaxLen,
// which keywords like `instanceof` keep at ≥ 10)
const lookups = this._wordLookups;
const nameLen = name.length;
if (nameLen < 2 || nameLen > lookups.reservedMaxLen) return;
const firstCC = name.charCodeAt(0);
if (firstCC < 97 || firstCC > 122) return;
// name-first ordering: acorn's `inGenerator`/`inAsync` are getters that
// walk the scope stack, so gate them behind the cheap string compare —
// a plain identifier never triggers them
if (name === "yield" && this.inGenerator) {
this.raiseRecoverable(
start,
"Cannot use 'yield' as identifier inside a generator"
);
} else if (name === "await" && this.inAsync) {
this.raiseRecoverable(
start,
"Cannot use 'await' as identifier inside an async function"
);
}
if (name === "arguments" && !(this.currentThisScope().flags & SCOPE_VAR)) {
this.raiseRecoverable(
start,
"Cannot use 'arguments' in class field initializer"
);
}
if ((name === "arguments" || name === "await") && this.inClassStaticBlock) {
this.raise(
start,
`Cannot use ${name} in class static initialization block`
);
}
const kind = lookups.reservedKinds.get(name);
if (kind === undefined) return;
if (kind === 1) {
this.raise(start, `Unexpected keyword '${name}'`);
}
if (
/** @type {number} */ (this.options.ecmaVersion) < 6 &&
this.input.slice(start, end).includes("\\")
) {
return;
}
if (kind === 2 || (kind === 3 && this.strict)) {
if (name === "await" && !this.inAsync) {
this.raiseRecoverable(
start,
"Cannot use keyword 'await' outside an async function"
);
}
this.raiseRecoverable(start, `The keyword '${name}' is reserved`);
}
}
/**
* Replaces acorn's `canInsertSemicolon`, whose line-break check slices the
* inter-token gap and runs a regexp on it for every ASI decision (hundreds
* of thousands per file). Scan the gap for a line terminator instead — no
* slice, no regexp.
* @returns {boolean} whether a semicolon may be inserted here
* @this {ParserInternals}
*/
canInsertSemicolon() {
if (this.type === tokTypes.eof || this.type === tokTypes.braceR) {
return true;
}
// the owned nextToken already classified the gap; 2 (comment/unicode in
// the gap, or a token from acorn's tokenizer) falls back to the scan
return this._gapHasNewline();
}
// ----- comment collection without eager text slicing -----
/**
* Replaces acorn's `skipLineComment` when comments are collected lazily:
* the same scan, but no text slice and no position objects. Acorn calls
* this for `//`, hashbangs and HTML-style comments (varying `startSkip`).
* @this {ParserInternals}
* @param {number} startSkip length of the comment opener
* @returns {void}
*/
skipLineComment(startSkip) {
const comments = this._lazyComments;
if (comments === undefined) {
return base.skipLineComment.call(this, startSkip);
}
const input = this.input;
const start = this.pos;
const len = input.length;
let pos = start + startSkip;
while (pos < len) {
const ch = input.charCodeAt(pos);
// LF, CR, LS, PS terminate the comment but are not part of it
if (ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029) break;
pos++;
}
this.pos = pos;
comments.push(new LazyComment(false, start + startSkip, start, pos, input));
}
/**
* Replaces acorn's `skipBlockComment` when comments are collected lazily.
* Locations are always off in lazy mode, so line breaks need no handling.
* @this {ParserInternals}
* @returns {void}
*/
skipBlockComment() {
const comments = this._lazyComments;
if (comments === undefined) {
return base.skipBlockComment.call(this);
}
const start = this.pos;
const end = this.input.indexOf("*/", (this.pos += 2));
if (end === -1) this.raise(this.pos - 2, "Unterminated comment");
this.pos = end + 2;
comments.push(
new LazyComment(true, start + 2, start, this.pos, this.input)
);
}
// ----- lazy range -----
/**
* @returns {Node} new node
* @this {ParserInternals}
*/
startNode() {
if (!this._lazy) return base.startNode.call(this);
return new LazyLocNode(this.start);
}
/**
* @param {number} pos start offset
* @param {Position=} loc start position when acorn tracks locations
* @returns {Node} new node
* @this {ParserInternals}
*/
startNodeAt(pos, loc) {
if (!this._lazy) return base.startNodeAt.call(this, pos, loc);
return new LazyLocNode(pos);
}
/**
* Lazy-mode `finishNode`: acorn's `locations`/`ranges` writes are dead when
* `range` is served lazily and `loc` not at all, so skip them and the `finishNodeAt`
* indirection. Runs once per node.
* @param {Node} node node to finish
* @param {string} type node type
* @returns {Node} the finished node
* @this {ParserInternals}
*/
finishNode(node, type) {
if (!this._lazy) return base.finishNode.call(this, node, type);
node.type = type;
node.end = this.lastTokEnd;
return node;
}
/**
* Mirror of acorn's `copyNode`, which bypasses `startNodeAt` via
* `new Node(...)` and would otherwise produce non-lazy nodes.
* @param {Node} node node to copy
* @returns {Node} copied node
* @this {ParserInternals}
*/
copyNode(node) {
const newNode = this.startNodeAt(node.start, this.startLoc);
const from = /** @type {Record<string, unknown>} */ (
/** @type {unknown} */ (node)
);
const to = /** @type {Record<string, unknown>} */ (
/** @type {unknown} */ (newNode)
);
for (const prop in from) to[prop] = from[prop];
return newNode;
}
// ----- owned node construction (single-shape nodes, replaces acorn's
// start-empty-then-mutate flow one node type at a time) -----
/**
* Owned `parseIdent` for the common name-token case: builds the finished
* `IdentifierNode` directly, skipping acorn's `parseIdentNode`/`startNode`/
* `finishNode` chain and its keyword branches. Keyword-as-identifier
* (`obj.class`) and non-lazy mode delegate to acorn.
* @param {boolean=} liberal whether reserved words are allowed
* @returns {Identifier} identifier node
* @this {ParserInternals}
*/
parseIdent(liberal) {
if (this.type !== tokTypes.name || !this._lazy) {
return base.parseIdent.call(this, liberal);
}
const node = /** @type {Identifier} */ (
/** @type {unknown} */ (
new IdentifierNode(
this.start,
this.end,
/** @type {string} */ (this.value)
)
)
);
this.next(Boolean(liberal));
if (!liberal) {
this.checkUnreserved(node);
if (node.name === "await" && !this.awaitIdentPos) {
this.awaitIdentPos = node.start;
}
}
return node;
}
/**
* Owned `parseSubscript`, an exact-semantics copy of acorn 8's with the
* node construction replaced: member and call nodes are built fully-formed
* after their property/arguments parse (the half-built node was never
* reachable during it), landing on `MemberExpressionNode`/
* `CallExpressionNode`'s single shapes. Pre-optional-chaining ecmaVersions
* and non-lazy mode delegate to acorn.
* @param {Expression} baseExpr subscript base
* @param {number} startPos expression start offset
* @param {Position | undefined} startLoc expression start position
* @param {boolean | undefined} noCalls whether calls are forbidden (`new` callee)
* @param {boolean} maybeAsyncArrow whether this may be an async arrow head
* @param {boolean} optionalChained whether the chain is already optional
* @param {boolean | string} forInit for-init context flag
* @returns {Expression} subscript element or `baseExpr` when done
* @this {ParserInternals}
*/
parseSubscript(
baseExpr,
startPos,
startLoc,
noCalls,
maybeAsyncArrow,
optionalChained,
forInit
) {
if (!this._subscriptFastPath) {
return base.parseSubscript.call(
this,
baseExpr,
startPos,
startLoc,
noCalls,
maybeAsyncArrow,
optionalChained,
forInit
);
}
const optional = this.eat(tokTypes.questionDot);
if (noCalls && optional) {
this.raise(
this.lastTokStart,
"Optional chaining cannot appear in the callee of new expressions"
);
}
const computed = this.eat(tokTypes.bracketL);
if (
computed ||
(optional &&
this.type !== tokTypes.parenL &&
this.type !== tokTypes.backQuote) ||
this.eat(tokTypes.dot)
) {
/** @type {Node} */
let property;
if (computed) {
property = this.parseExpression();
this.expect(tokTypes.bracketR);
} else if (
this.type === tokTypes.privateId &&
/** @type {string} */ (baseExpr.type) !== "Super"
) {
property = this.parsePrivateIdent();
} else {
property = this.parseIdent(this.options.allowReserved !== "never");
}
return /** @type {Expression} */ (
/** @type {unknown} */ (
new MemberExpressionNode(
startPos,
this.lastTokEnd,
baseExpr,
property,
computed,
optional
)
)
);
} else if (!noCalls && this.eat(tokTypes.parenL)) {
const refDestructuringErrors = this._acquireDestructuringErrors();
const oldYieldPos = this.yieldPos;
const oldAwaitPos = this.awaitPos;
const oldAwaitIdentPos = this.awaitIdentPos;
this.yieldPos = 0;
this.awaitPos = 0;
this.awaitIdentPos = 0;
const exprList = this.parseExprList(
tokTypes.parenR,
true,
false,
refDestructuringErrors
);
if (maybeAsyncArrow && !optional && this.shouldParseAsyncArrow()) {
this.checkPatternErrors(refDestructuringErrors, false);
this._releaseDestructuringErrors();
this.checkYieldAwaitInDefaultParams();
if (this.awaitIdentPos > 0) {
this.raise(
this.awaitIdentPos,
"Cannot use 'await' as identifier inside an async function"
);
}
this.yieldPos = oldYieldPos;
this.awaitPos = oldAwaitPos;
this.awaitIdentPos = oldAwaitIdentPos;
return this.parseSubscriptAsyncArrow(
startPos,
startLoc,
exprList,
forInit
);
}
this.checkExpressionErrors(refDestructuringErrors, true);
this._releaseDestructuringErrors();
this.yieldPos = oldYieldPos || this.yieldPos;
this.awaitPos = oldAwaitPos || this.awaitPos;
this.awaitIdentPos = oldAwaitIdentPos || this.awaitIdentPos;
return /** @type {Expression} */ (
/** @type {unknown} */ (
new CallExpressionNode(
startPos,
this.lastTokEnd,
baseExpr,
exprList,
optional
)
)
);
} else if (this.type === tokTypes.backQuote) {
if (optional || optionalChained) {
this.raise(
this.start,
"Optional chaining cannot appear in the tag of tagged template expressions"
);
}
const node = this.startNodeAt(startPos, startLoc);
/** @type {Node & { tag?: Expression, quasi?: Node }} */
(node).tag = baseExpr;
/** @type {Node & { tag?: Expression, quasi?: Node }} */
(node).quasi = this.parseTemplate({ isTagged: true });
return /** @type {Expression} */ (
/** @type {unknown} */ (
this.finishNode(node, "TaggedTemplateExpression")
)
);
}
return baseExpr;
}
/**
* Owned `parseStatement` for the hot statement heads: acorn starts a node
* before dispatching, but the owned statement parsers build their own
* single-shape nodes, so that started node was one discarded allocation per
* statement. Dispatch the common heads (`var`/`let`/`const`, `if`, `return`,
* blocks and plain expression statements) without it; everything rarer, the
* `name`-token ambiguities (`let`/`async`/`using`/`await` heads) and
* plugin-overridden parsers delegate to acorn.
* @param {string | null} context statement context
* @param {boolean=} topLevel whether parsing top-level statements
* @param {unknown=} exports export tracking object
* @returns {Node} statement
* @this {ParserInternals}
*/
parseStatement(context, topLevel, exports) {
if (!this._stmtFastPath) {
return base.parseStatement.call(this, context, topLevel, exports);
}
const startType = this.type;
switch (startType) {
case tokTypes._var:
case tokTypes._const: {
const kind = /** @type {string} */ (this.value);
if (context && kind !== "var") this.unexpected();
return this._parseVarStatementAt(this.start, kind);
}
case tokTypes._if:
return this._parseIfStatementAt(this.start);
case tokTypes._return:
return this._parseReturnStatementAt(this.start);
case tokTypes.braceL:
// the owned parseBlock never reads the started node
return this.parseBlock(true);
case tokTypes._break:
case tokTypes._continue:
case tokTypes._debugger:
case tokTypes._do:
case tokTypes._for:
case tokTypes._function:
case tokTypes._class:
case tokTypes._switch:
case tokTypes._throw:
case tokTypes._try:
case tokTypes._while:
case tokTypes._with:
case tokTypes.semi:
case tokTypes._export:
case tokTypes._import:
return base.parseStatement.call(this, context, topLevel, exports);
default: {
if (startType === tokTypes.name) {
const value = this.value;
if (value === "let") {
if (this.isLet(context)) {
// mirrors acorn's `context && kind !== "var"` rejection
if (context) this.unexpected();
return this._parseVarStatementAt(this.start, "let");
}
// `let` as a plain identifier: expression/label tail below
} else if (
value === "async" ||
value === "using" ||
value === "await"
) {
// async functions and using declarations keep acorn's
// lookahead-heavy classification
return base.parseStatement.call(this, context, topLevel, exports);
}
}
// unambiguous expression statement, with acorn's label tail
const start = this.start;
const maybeName = this.value;
const expr = this.parseExpression();
if (
startType === tokTypes.name &&
expr.type === "Identifier" &&
this.eat(tokTypes.colon)
) {
// labels are rare enough to pay for the started node
return this.parseLabeledStatement(
this.startNodeAt(start),
/** @type {string} */ (maybeName),
/** @type {Identifier} */ (expr),
context
);
}
return this._parseExpressionStatementAt(start, expr);
}
}
}
/**
* Owned `parseVarStatement`: the passed started node is filled by
* `parseVar` as acorn expects, then the finished statement lands on
* `VariableDeclarationNode`'s single shape. Non-lazy mode delegates.
* @param {Node} node started statement node from `parseStatement`
* @param {string} kind declaration kind
* @param {boolean=} allowMissingInitializer whether `const x;` is allowed
* @returns {Node} variable declaration
* @this {ParserInternals}
*/
parseVarStatement(node, kind, allowMissingInitializer) {
if (!this._lazy) {
return base.parseVarStatement.call(
this,
node,
kind,
allowMissingInitializer
);
}
return this._parseVarStatementAt(node.start, kind, allowMissingInitializer);
}
/**
* Statement-position `var`/`let`/`const` without a started node: the
* declaration lands directly on `VariableDeclarationNode`'s single shape.
* @param {number} start statement start offset
* @param {string} kind declaration kind
* @param {boolean=} allowMissingInitializer whether `const x;` is allowed
* @returns {Node} variable declaration
* @this {ParserInternals}
*/
_parseVarStatementAt(start, kind, allowMissingInitializer) {
this.next();
/** @type {Node[]} */
const declarations = [];
this._parseVarInto(declarations, false, kind, allowMissingInitializer);
this.semicolon();
return /** @type {Node} */ (
/** @type {unknown} */ (
new VariableDeclarationNode(start, this.lastTokEnd, declarations, kind)
)
);
}
/**
* Owned `parseVar`, an exact-semantics copy of acorn 8's that builds each
* declarator fully-formed on `VariableDeclaratorNode`'s single shape. The
* passed node keeps receiving `declarations`/`kind` because
* `parseForStatement` finishes it itself. Non-lazy mode delegates.
* @param {Node} node declaration node to fill
* @param {boolean} isFor whether parsing a `for` head
* @param {string} kind declaration kind
* @param {boolean=} allowMissingInitializer whether `const x;` is allowed
* @returns {Node} the filled node
* @this {ParserInternals}
*/
parseVar(node, isFor, kind, allowMissingInitializer) {
if (!this._lazy) {
return base.parseVar.call(
this,
node,
isFor,
kind,
allowMissingInitializer
);
}
/** @type {Node[]} */
const declarations = [];
const target =
/** @type {Node & { declarations: Node[], kind: string }} */ (node);
target.declarations = declarations;
target.kind = kind;
this._parseVarInto(declarations, isFor, kind, allowMissingInitializer);
return node;
}
/**
* The declarator loop of the owned `parseVar`, shared with the owned
* `parseStatement`'s node-free statement path.
* @param {Node[]} declarations output array for the declarators
* @param {boolean} isFor whether parsing a `for` head
* @param {string} kind declaration kind
* @param {boolean=} allowMissingInitializer whether `const x;` is allowed
* @returns {void}
* @this {ParserInternals}
*/
_parseVarInto(declarations, isFor, kind, allowMissingInitializer) {
const ecmaVersion = this._ecmaVersion;
const usingKind = kind === "using" || kind === "await using";
for (;;) {
const declStart = this.start;
const id = usingKind ? this.parseIdent() : this.parseBindingAtom();
this.checkLValPattern(
id,
kind === "var" ? BIND_VAR : BIND_LEXICAL,
false
);
/** @type {Expression | null} */
let init = null;
if (this.eat(tokTypes.eq)) {
init = this.parseMaybeAssign(isFor);
} else if (
!allowMissingInitializer &&
kind === "const" &&
!(
this.type === tokTypes._in ||
(ecmaVersion >= 6 && this.isContextual("of"))
)
) {
this.unexpected();
} else if (
!allowMissingInitializer &&
usingKind &&
ecmaVersion >= 17 &&
this.type !== tokTypes._in &&
!this.isContextual("of")
) {
this.raise(
this.lastTokEnd,
`Missing initializer in ${kind} declaration`
);
} else if (
!allowMissingInitializer &&
id.type !== "Identifier" &&
!(isFor && (this.type === tokTypes._in || this.isContextual("of")))
) {
this.raise(
this.lastTokEnd,
"Complex binding patterns require an initialization value"
);
}
declarations.push(
/** @type {Node} */ (
/** @type {unknown} */ (
new VariableDeclaratorNode(declStart, this.lastTokEnd, id, init)
)
)
);
if (!this.eat(tokTypes.comma)) break;
}
}
/**
* Owned `parseExpressionStatement`: the statement lands on
* `ExpressionStatementNode`'s single shape (the passed started node is
* discarded, matching acorn's observable output). Non-lazy mode delegates.
* @param {Node} node started statement node from `parseStatement`
* @param {Expression} expr the parsed expression
* @returns {Node} expression statement
* @this {ParserInternals}
*/
parseExpressionStatement(node, expr) {
if (!this._lazy) {
return base.parseExpressionStatement.call(this, node, expr);
}
return this._parseExpressionStatementAt(node.start, expr);
}
/**
* `parseExpressionStatement` without a started node.
* @param {number} start statement start offset
* @param {Expression} expr the parsed expression
* @returns {Node} expression statement
* @this {ParserInternals}
*/
_parseExpressionStatementAt(start, expr) {
this.semicolon();
return /** @type {Node} */ (
/** @type {unknown} */ (
new ExpressionStatementNode(start, this.lastTokEnd, expr)
)
);
}
/**
* Owned `parseBlock`, an exact-semantics copy of acorn 8's landing on
* `BlockStatementNode`'s single shape. Non-lazy mode delegates.
* @param {boolean=} createNewLexicalScope whether the block opens a scope
* @param {Node=} node started statement node, when called from `parseStatement`
* @param {boolean=} exitStrict whether to restore sloppy mode at the end
* @returns {Node} block statement
* @this {ParserInternals}
*/
parseBlock(createNewLexicalScope, node, exitStrict) {
if (!this._lazy) {
return base.parseBlock.call(
this,
createNewLexicalScope,
node,
exitStrict
);
}
if (createNewLexicalScope === undefined) createNewLexicalScope = true;
const start = this.start;
/** @type {Node[]} */
const body = [];
this.expect(tokTypes.braceL);
if (createNewLexicalScope) this.enterScope(0);
while (this.type !== tokTypes.braceR) {
body.push(this.parseStatement(null));
}
if (exitStrict) this.strict = false;
this.next();
if (createNewLexicalScope) this.exitScope();
return /** @type {Node} */ (
/** @type {unknown} */ (
new BlockStatementNode(start, this.lastTokEnd, body)
)
);
}
/**
* Owned `parseIfStatement` landing on `IfStatementNode`'s single shape.
* Non-lazy mode delegates to acorn.
* @param {Node} node started statement node from `parseStatement`
* @returns {Node} if statement
* @this {ParserInternals}
*/
parseIfStatement(node) {
if (!this._lazy) return base.parseIfStatement.call(this, node);
return this._parseIfStatementAt(node.start);
}
/**
* `parseIfStatement` without a started node.
* @param {number} start statement start offset
* @returns {Node} if statement
* @this {ParserInternals}
*/
_parseIfStatementAt(start) {
this.next();
const test = this.parseParenExpression();
// function declarations are allowed in branches outside strict mode
const consequent = this.parseStatement("if");
const alternate = this.eat(tokTypes._else)
? this.parseStatement("if")
: null;
return /** @type {Node} */ (
/** @type {unknown} */ (
new IfStatementNode(start, this.lastTokEnd, test, consequent, alternate)
)
);
}
/**
* Owned `parseReturnStatement` landing on `ReturnStatementNode`'s single
* shape. Non-lazy mode delegates to acorn.
* @param {Node} node started statement node from `parseStatement`
* @returns {Node} return statement
* @this {ParserInternals}
*/
parseReturnStatement(node) {
if (!this._lazy) {
if (!this.allowReturn) this._tryModuleFallback();
return base.parseReturnStatement.call(this, node);
}
return this._parseReturnStatementAt(node.start);
}
/**
* In `auto` source type a top-level `return` is script-only syntax. When the
* strict module parse reaches it with no module construct seen yet, downgrade
* to a sloppy script in place: the prefix parsed under stricter rules stays a
* valid sloppy prefix, so the second full parse is avoided.
* @returns {boolean} true when the parse was downgraded to script
* @this {ParserInternals}
*/
_tryModuleFallback() {
if (
!this._moduleFallback ||
this._moduleSyntaxSeen ||
!(this.currentVarScope().flags & SCOPE_TOP)
) {
return false;
}
this.options.allowReturnOutsideFunction = true;
// acorn stamps Program.sourceType from this at parseTopLevel's end
this.options.sourceType = "script";
this.strict = false;
this.inModule = false;
this._moduleFallback = false;
return true;
}
/**
* `parseReturnStatement` without a started node.
* @param {number} start statement start offset
* @returns {Node} return statement
* @this {ParserInternals}
*/
_parseReturnStatementAt(start) {
if (!this.allowReturn && !this._tryModuleFallback()) {
this.raise(this.start, "'return' outside of function");
}
this.next();
/** @type {Expression | null} */
let argument = null;
// `return` takes an optional argument, so eagerly look for a semicolon
if (!(this.eat(tokTypes.semi) || this.insertSemicolon())) {
argument = this.parseExpression();
this.semicolon();
}
return /** @type {Node} */ (
/** @type {unknown} */ (
new ReturnStatementNode(start, this.lastTokEnd, argument)
)
);
}
/**
* Owned `parseSpread` landing on `RestSpreadNode`'s single shape.
* Non-lazy mode delegates.
* @param {DestructuringErrorsShim | null=} refDestructuringErrors destructuring errors to fill
* @returns {Node} spread element
* @this {ParserInternals}
*/
parseSpread(refDestructuringErrors) {
if (!this._lazy) {
return base.parseSpread.call(this, refDestructuringErrors);
}
const start = this.start;
this.next();
const argument = this.parseMaybeAssign(false, refDestructuringErrors);
return /** @type {Node} */ (
/** @type {unknown} */ (
new RestSpreadNode(start, this.lastTokEnd, "SpreadElement", argument)
)
);
}
/**
* Owned `parseMaybeConditional` landing on
* `ConditionalExpressionNode`'s single shape. Non-lazy mode delegates.
* @param {boolean | string=} forInit for-init context flag
* @param {DestructuringErrorsShim | null=} refDestructuringErrors destructuring errors to fill
* @returns {Expression} expression node
* @this {ParserInternals}
*/
parseMaybeConditional(forInit, refDestructuringErrors) {
if (!this._lazy) {
return base.parseMaybeConditional.call(
this,
forInit,
refDestructuringErrors
);
}
const startPos = this.start;
const expr = this.parseExprOps(forInit, refDestructuringErrors);
if (this.checkExpressionErrors(refDestructuringErrors)) return expr;
if (
!(expr.type === "ArrowFunctionExpression" && expr.start === startPos) &&
this.eat(tokTypes.question)
) {
const consequent = this.parseMaybeAssign();
this.expect(tokTypes.colon);
const alternate = this.parseMaybeAssign(forInit);
return /** @type {Expression} */ (
/** @type {unknown} */ (
new ConditionalExpressionNode(
startPos,
this.lastTokEnd,
expr,
consequent,
alternate
)
)
);
}
return expr;
}
/**
* Owned `parseNew`, an exact-semantics copy of acorn 8's: `NewExpression`
* lands on `NewExpressionNode`'s single shape (zero-argument calls share
* one empty array like acorn's `empty`); the rare `new.target` path keeps
* the generic node. Non-lazy mode delegates.
* @returns {Expression} new expression or meta property
* @this {ParserInternals}
*/
parseNew() {
if (!this._lazy) return base.parseNew.call(this);
if (this.containsEsc) {
this.raiseRecoverable(this.start, "Escape sequence in keyword new");
}
const nodeStart = this.start;
this.next();
if (this._ecmaVersion >= 6 && this.type === tokTypes.dot) {
const node =
/** @type {Node & { meta?: Node, property?: Identifier }} */
(this.startNodeAt(nodeStart, undefined));
const meta =
/** @type {Node & { name?: string }} */
(this.startNodeAt(nodeStart, undefined));
meta.name = "new";
node.meta = this.finishNode(/** @type {Node} */ (meta), "Identifier");
this.next();
const containsEsc = this.containsEsc;
node.property = this.parseIdent(true);
if (node.property.name !== "target") {
this.raiseRecoverable(
node.property.start,
"The only valid meta property for new is 'new.target'"
);
}
if (containsEsc) {
this.raiseRecoverable(
nodeStart,
"'new.target' must not contain escaped characters"
);
}
if (!this.allowNewDotTarget) {
this.raiseRecoverable(
nodeStart,
"'new.target' can only be used in functions and class static block"
);
}
return /** @type {Expression} */ (
/** @type {unknown} */ (this.finishNode(node, "MetaProperty"))
);
}
const startPos = this.start;
const startLoc = this.startLoc;
const callee = this.parseSubscripts(
this.parseExprAtom(null, false, true),
startPos,
startLoc,
true,
false
);
if (/** @type {string} */ (callee.type) === "Super") {
this.raiseRecoverable(startPos, "Invalid use of 'super'");
}
/** @type {Expression[]} */
const args = this.eat(tokTypes.parenL)
? this.parseExprList(tokTypes.parenR, true, false)
: EMPTY_NEW_ARGS;
return /** @type {Expression} */ (
/** @type {unknown} */ (
new NewExpressionNode(nodeStart, this.lastTokEnd, callee, args)
)
);
}
/**
* Owned `parseTemplateElement` landing on `TemplateElementNode`'s single
* shape; matches acorn's CRLF normalization and invalid-escape handling.
* Non-lazy mode delegates.
* @param {{ isTagged: boolean }} opts whether the template is tagged
* @returns {Node} template element
* @this {ParserInternals}
*/
parseTemplateElement(opts) {
if (!this._lazy) return base.parseTemplateElement.call(this, opts);
const start = this.start;
/** @type {{ raw: string, cooked: string | null }} */
let value;
if (this.type === tokTypes.invalidTemplate) {
if (!opts.isTagged) {
this.raiseRecoverable(
this.start,
"Bad escape sequence in untagged template literal"
);
}
value = {
raw: /** @type {string} */ (this.value).replace(/\r\n?/g, "\n"),
cooked: null
};
} else {
const cooked = /** @type {string} */ (this.value);
// Every escape cooks strictly shorter and CRLF shortens, while a lone CR
// cooks to the LF the raw normalization would produce — so an
// equal-length cooked string IS the normalized raw. That covers every
// fast-path chunk (no backslash, no CR): raw and cooked share one string.
value = {
raw:
cooked.length === this.end - this.start
? cooked
: this.input.slice(this.start, this.end).replace(/\r\n?/g, "\n"),
cooked
};
}
this.next();
return /** @type {Node} */ (
/** @type {unknown} */ (
new TemplateElementNode(
start,
this.lastTokEnd,
value,
this.type === tokTypes.backQuote
)
)
);
}
/**
* Owned `parseTemplate` landing on `TemplateLiteralNode`'s single shape.
* Non-lazy mode delegates.
* @param {{ isTagged?: boolean }=} opts whether the template is tagged
* @returns {Node} template literal
* @this {ParserInternals}
*/
parseTemplate(opts) {
if (!this._lazy) {
return base.parseTemplate.call(
this,
/** @type {{ isTagged: boolean }} */ (opts)
);
}
const isTagged = (opts !== undefined && opts.isTagged) === true;
const start = this.start;
this.next();
/** @type {Expression[]} */
const expressions = [];
// one options object for all chunks (only `isTagged` is ever read)
const eltOpts = { isTagged };
let curElt = /** @type {Node & { tail?: boolean }} */ (
this.parseTemplateElement(eltOpts)
);
const quasis = [/** @type {Node} */ (curElt)];
while (!curElt.tail) {
if (this.type === tokTypes.eof) {
this.raise(this.pos, "Unterminated template literal");
}
this.expect(tokTypes.dollarBraceL);
expressions.push(this.parseExpression());
this.expect(tokTypes.braceR);
curElt = /** @type {Node & { tail?: boolean }} */ (
this.parseTemplateElement(eltOpts)
);
quasis.push(/** @type {Node} */ (curElt));
}
this.next();
return /** @type {Node} */ (
/** @type {unknown} */ (
new TemplateLiteralNode(start, this.lastTokEnd, expressions, quasis)
)
);
}
/**
* Owned `parseMaybeUnary`, an exact-semantics copy of acorn 8's: prefix
* unary/update and postfix update nodes are built fully-formed on
* `UnaryNode`'s shared single shape. Non-lazy mode delegates to acorn.
* @param {DestructuringErrorsShim | null} refDestructuringErrors destructuring errors to fill
* @param {boolean} sawUnary whether a unary operator was already consumed
* @param {boolean} incDec whether the caller was an update operator
* @param {boolean | string=} forInit for-init context flag
* @returns {Expression} expression node
* @this {ParserInternals}
*/
parseMaybeUnary(refDestructuringErrors, sawUnary, incDec, forInit) {
if (!this._lazy) {
return base.parseMaybeUnary.call(
this,
refDestructuringErrors,
sawUnary,
incDec,
forInit
);
}
const startPos = this.start;
const startLoc = this.startLoc;
/** @type {Expression} */
let expr;
if (this.isContextual("await") && this.canAwait) {
expr = this.parseAwait(forInit);
sawUnary = true;
} else if (/** @type {TokenTypeInternal} */ (this.type).prefix) {
const update = this.type === tokTypes.incDec;
const operator = /** @type {string} */ (this.value);
this.next();
const argument = this.parseMaybeUnary(null, true, update, forInit);
this.checkExpressionErrors(refDestructuringErrors, true);
if (update) {
this.checkLValSimple(argument);
} else if (
this.strict &&
operator === "delete" &&
isLocalVariableAccess(argument)
) {
this.raiseRecoverable(
startPos,
"Deleting local variable in strict mode"
);
} else if (operator === "delete" && isPrivateFieldAccess(argument)) {
this.raiseRecoverable(startPos, "Private fields can not be deleted");
} else {
sawUnary = true;
}
expr = /** @type {Expression} */ (
/** @type {unknown} */ (
new UnaryNode(
startPos,
this.lastTokEnd,
update ? "UpdateExpression" : "UnaryExpression",
operator,
true,
argument
)
)
);
} else if (!sawUnary && this.type === tokTypes.privateId) {
if (
(forInit || this.privateNameStack.length === 0) &&
this.options.checkPrivateFields
) {
this.unexpected();
}
expr = /** @type {Expression} */ (
/** @type {unknown} */ (this.parsePrivateIdent())
);
// a private name is only valid as `#x in obj`
if (this.type !== tokTypes._in) this.unexpected();
} else {
expr = this.parseExprSubscripts(refDestructuringErrors, forInit);
if (this.checkExpressionErrors(refDestructuringErrors)) return expr;
while (
/** @type {TokenTypeInternal} */ (this.type).postfix &&
!this.canInsertSemicolon()
) {
const operator = /** @type {string} */ (this.value);
this.checkLValSimple(expr);
this.next();
expr = /** @type {Expression} */ (
/** @type {unknown} */ (
new UnaryNode(
startPos,
this.lastTokEnd,
"UpdateExpression",
operator,
false,
expr
)
)
);
}
}
if (!incDec && this.eat(tokTypes.starstar)) {
if (sawUnary) {
this.unexpected(this.lastTokStart);
} else {
return this.buildBinary(
startPos,
startLoc,
expr,
this.parseMaybeUnary(null, false, false, forInit),
"**",
false
);
}
}
return expr;
}
/**
* Owned `parseMaybeAssign`, an exact-semantics copy of acorn 8's: the
* operator is captured before `next()` and the `AssignmentExpression` is
* built fully-formed on `AssignmentNode`'s single shape after the
* right-hand parse. Yield and non-lazy mode delegate to acorn.
* @param {boolean | string=} forInit for-init context flag
* @param {DestructuringErrorsShim | null=} refDestructuringErrors destructuring errors to fill
* @param {((this: unknown, left: Expression, startPos: number, startLoc?: Position) => Expression)=} afterLeftParse hook applied to the parsed left side
* @returns {Expression} expression node
* @this {ParserInternals}
*/
parseMaybeAssign(forInit, refDestructuringErrors, afterLeftParse) {
if (!this._lazy) {
return base.parseMaybeAssign.call(
this,
forInit,
refDestructuringErrors,
afterLeftParse
);
}
if (this.isContextual("yield")) {
if (this.inGenerator) return this.parseYield(forInit);
// the tokenizer assumed an expression follows `yield`, but this
// isn't that kind of yield
this.exprAllowed = false;
}
let ownDestructuringErrors = false;
let oldParenAssign = -1;
let oldTrailingComma = -1;
let oldDoubleProto = -1;
if (refDestructuringErrors) {
oldParenAssign = refDestructuringErrors.parenthesizedAssign;
oldTrailingComma = refDestructuringErrors.trailingComma;
oldDoubleProto = refDestructuringErrors.doubleProto;
refDestructuringErrors.parenthesizedAssign =
refDestructuringErrors.trailingComma = -1;
} else {
refDestructuringErrors = this._acquireDestructuringErrors();
ownDestructuringErrors = true;
}
const startPos = this.start;
const startLoc = this.startLoc;
if (this.type === tokTypes.parenL || this.type === tokTypes.name) {
this.potentialArrowAt = this.start;
this.potentialArrowInForAwait = forInit === "await";
}
let left = this.parseMaybeConditional(forInit, refDestructuringErrors);
if (afterLeftParse) {
left = afterLeftParse.call(this, left, startPos, startLoc);
}
if (/** @type {TokenTypeInternal} */ (this.type).isAssign) {
const operator = /** @type {string} */ (this.value);
if (this.type === tokTypes.eq) {
left = /** @type {Expression} */ (
this.toAssignable(left, false, refDestructuringErrors)
);
}
if (!ownDestructuringErrors) {
refDestructuringErrors.parenthesizedAssign =
refDestructuringErrors.trailingComma =
refDestructuringErrors.doubleProto =
-1;
}
if (refDestructuringErrors.shorthandAssign >= left.start) {
// shorthand default was used correctly
refDestructuringErrors.shorthandAssign = -1;
}
if (this.type === tokTypes.eq) this.checkLValPattern(left);
else this.checkLValSimple(left);
// the own record's last read was above, so the nested right-side
// parse below can already reuse its pool slot
if (ownDestructuringErrors) this._releaseDestructuringErrors();
this.next();
const right = this.parseMaybeAssign(forInit);
if (oldDoubleProto > -1) {
refDestructuringErrors.doubleProto = oldDoubleProto;
}
return /** @type {Expression} */ (
/** @type {unknown} */ (
new AssignmentNode(startPos, this.lastTokEnd, operator, left, right)
)
);
} else if (ownDestructuringErrors) {
this.checkExpressionErrors(refDestructuringErrors, true);
this._releaseDestructuringErrors();
}
if (oldParenAssign > -1) {
refDestructuringErrors.parenthesizedAssign = oldParenAssign;
}
if (oldTrailingComma > -1) {
refDestructuringErrors.trailingComma = oldTrailingComma;
}
return left;
}
/**
* Owned `parseExprOps`, an exact copy of acorn 8's, so the hot expression
* spine (`parseMaybeAssign` → here → `parseMaybeUnary`) stays monomorphic
* on owned code. Non-lazy mode delegates.
* @param {boolean | string=} forInit for-init context flag
* @param {DestructuringErrorsShim | null=} refDestructuringErrors destructuring errors to fill
* @returns {Expression} expression node
* @this {ParserInternals}
*/
parseExprOps(forInit, refDestructuringErrors) {
if (!this._lazy) {
return base.parseExprOps.call(this, forInit, refDestructuringErrors);
}
const startPos = this.start;
const startLoc = this.startLoc;
const expr = this.parseMaybeUnary(
refDestructuringErrors || null,
false,
false,
forInit
);
if (this.checkExpressionErrors(refDestructuringErrors)) return expr;
return expr.start === startPos && expr.type === "ArrowFunctionExpression"
? expr
: this.parseExprOp(expr, startPos, startLoc, -1, forInit);
}
/**
* Owned `parseExprOp`, an exact-semantics copy of acorn 8's with the
* same-precedence continuation turned from tail recursion into a loop —
* `a + b + c + d` runs one frame instead of one per operator. Non-lazy mode
* delegates.
* @param {Expression} left left operand
* @param {number} leftStartPos expression start offset
* @param {Position | undefined} leftStartLoc expression start position
* @param {number} minPrec minimal binding precedence to continue
* @param {boolean | string=} forInit for-init context flag (excludes `in`)
* @returns {Expression} expression node
* @this {ParserInternals}
*/
parseExprOp(left, leftStartPos, leftStartLoc, minPrec, forInit) {
if (!this._lazy) {
return base.parseExprOp.call(
this,
left,
leftStartPos,
leftStartLoc,
minPrec,
forInit
);
}
for (;;) {
const type = /** @type {TokenTypeInternal} */ (this.type);
let prec = type.binop;
// acorn's TokenType sets binop to null (never undefined) when absent
if (
prec === null ||
prec <= minPrec ||
(forInit && type === tokTypes._in)
) {
return left;
}
const logical =
type === tokTypes.logicalOR || type === tokTypes.logicalAND;
const coalesce = type === tokTypes.coalesce;
if (coalesce) {
// acorn parses `??`'s right at logical precedence so the mixing
// check below sees any unparenthesized `&&`/`||` as a sibling
prec = /** @type {number} */ (
/** @type {TokenTypeInternal} */ (tokTypes.logicalAND).binop
);
}
const op = /** @type {string} */ (this.value);
this.next();
const startPos = this.start;
const right = this.parseExprOp(
this.parseMaybeUnary(null, false, false, forInit),
startPos,
this.startLoc,
prec,
forInit
);
left = this.buildBinary(
leftStartPos,
leftStartLoc,
left,
right,
op,
logical || coalesce
);
if (
(logical && this.type === tokTypes.coalesce) ||
(coalesce &&
(this.type === tokTypes.logicalOR ||
this.type === tokTypes.logicalAND))
) {
this.raiseRecoverable(
this.start,
"Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses"
);
}
}
}
/**
* Owned `buildBinary` (acorn calls it only from `parseExprOp`): binary and
* logical nodes are built fully-formed on `BinaryNode`'s single shape.
* Non-lazy mode delegates to acorn.
* @param {number} startPos expression start offset
* @param {Position | undefined} startLoc expression start position
* @param {Expression} left left operand
* @param {Expression} right right operand
* @param {string} op operator text
* @param {boolean} logical whether this is a logical/coalesce expression
* @returns {Expression} binary or logical expression node
* @this {ParserInternals}
*/
buildBinary(startPos, startLoc, left, right, op, logical) {
if (!this._lazy) {
return base.buildBinary.call(
this,
startPos,
startLoc,
left,
right,
op,
logical
);
}
const rightNode = /** @type {Node} */ (/** @type {unknown} */ (right));
if (rightNode.type === "PrivateIdentifier") {
this.raise(
rightNode.start,
"Private identifier can only be left side of binary expression"
);
}
return /** @type {Expression} */ (
/** @type {unknown} */ (
new BinaryNode(
startPos,
this.lastTokEnd,
logical ? "LogicalExpression" : "BinaryExpression",
left,
op,
right
)
)
);
}
/**
* Owned `parseExprAtom` for the hot atoms: identifiers (with acorn's exact
* async-function/async-arrow detection), number/string literals, keyword
* literals (`true`/`false`/`null` — on the same `LiteralNode` shape as the
* rest) and `this`. Everything else, and non-lazy mode, delegates to acorn.
* @param {DestructuringErrorsShim | null=} refDestructuringErrors destructuring errors to fill
* @param {boolean | string=} forInit for-init context flag
* @param {boolean=} forNew whether parsed as a `new` callee
* @returns {Expression} expression atom
* @this {ParserInternals}
*/
parseExprAtom(refDestructuringErrors, forInit, forNew) {
if (!this._lazy) {
return base.parseExprAtom.call(
this,
refDestructuringErrors,
forInit,
forNew
);
}
// division in expression position: the tokenizer got confused, force a
// regexp re-read (mirrors the top of acorn's parseExprAtom)
if (this.type === tokTypes.slash) this.readRegexp();
const type = this.type;
if (type === tokTypes.name) {
const canBeArrow = this.potentialArrowAt === this.start;
const startPos = this.start;
const startLoc = this.startLoc;
const containsEsc = this.containsEsc;
let id = this.parseIdent(false);
// `async` compare first — almost every identifier fails it, skipping
// the version probe (all operands are pure)
if (
id.name === "async" &&
!containsEsc &&
this._ecmaVersion >= 8 &&
!this.canInsertSemicolon() &&
this.eat(tokTypes._function)
) {
this.overrideContext(tokContexts.f_expr);
return this.parseFunction(
this.startNodeAt(startPos, startLoc),
0,
false,
true,
forInit
);
}
if (canBeArrow && !this.canInsertSemicolon()) {
if (this.eat(tokTypes.arrow)) {
return this.parseArrowExpression(
this.startNodeAt(startPos, startLoc),
[id],
false,
forInit
);
}
if (
id.name === "async" &&
!containsEsc &&
this._ecmaVersion >= 8 &&
this.type === tokTypes.name &&
(!this.potentialArrowInForAwait ||
this.value !== "of" ||
this.containsEsc)
) {
id = this.parseIdent(false);
if (this.canInsertSemicolon() || !this.eat(tokTypes.arrow)) {
this.unexpected();
}
return this.parseArrowExpression(
this.startNodeAt(startPos, startLoc),
[id],
true,
forInit
);
}
}
return id;
}
if (type === tokTypes.num || type === tokTypes.string) {
return /** @type {Expression} */ (
/** @type {unknown} */ (this.parseLiteral(this.value))
);
}
if (
type === tokTypes._null ||
type === tokTypes._true ||
type === tokTypes._false
) {
const node = new LiteralNode(
this.start,
this.end,
type === tokTypes._null ? null : type === tokTypes._true,
/** @type {string} */ (type.keyword)
);
this.next();
return /** @type {Expression} */ (/** @type {unknown} */ (node));
}
if (type === tokTypes._this) {
const node = new ThisNode(this.start, this.end);
this.next();
return /** @type {Expression} */ (/** @type {unknown} */ (node));
}
if (type === tokTypes.bracketL) {
const start = this.start;
this.next();
const elements = this.parseExprList(
tokTypes.bracketR,
true,
true,
refDestructuringErrors
);
return /** @type {Expression} */ (
/** @type {unknown} */ (
new ArrayExpressionNode(start, this.lastTokEnd, elements)
)
);
}
return base.parseExprAtom.call(
this,
refDestructuringErrors,
forInit,
forNew
);
}
/**
* Owned `parseObj`, an exact-semantics copy of acorn 8's landing on
* `ObjectNode`'s single shape (the ES5 trailing-comma gate is dropped —
* the fast path requires ES11+). Non-lazy mode delegates.
* @param {boolean} isPattern whether parsing a binding pattern
* @param {DestructuringErrorsShim | null=} refDestructuringErrors destructuring errors to fill
* @returns {Node} object expression or pattern
* @this {ParserInternals}
*/
parseObj(isPattern, refDestructuringErrors) {
if (!this._subscriptFastPath) {
return base.parseObj.call(this, isPattern, refDestructuringErrors);
}
const start = this.start;
let first = true;
// acorn's ES6+ `checkPropClash` only ever reads/writes `.proto`, so the
// record is pooled by nesting depth (like `_deStack`); a subclass override
// might write arbitrary keys and gets a fresh `{}` instead. Depth resets
// implicitly since a raise aborts the whole parse.
const pooled = this._propHashFastPath;
/** @type {Record<string, unknown>} */
let propHash;
if (pooled) {
const stack = this._propHashStack;
const depth = this._propHashDepth++;
const cached = stack[depth];
if (cached !== undefined) {
cached.proto = false;
propHash = cached;
} else {
propHash = stack[depth] = { proto: false };
}
} else {
propHash = {};
}
/** @type {Node[]} */
const properties = [];
this.next();
while (!this.eat(tokTypes.braceR)) {
if (!first) {
this.expect(tokTypes.comma);
if (this.afterTrailingComma(tokTypes.braceR)) break;
} else {
first = false;
}
const prop = this.parseProperty(isPattern, refDestructuringErrors);
if (!isPattern) {
this.checkPropClash(prop, propHash, refDestructuringErrors);
}
properties.push(prop);
}
if (pooled) this._propHashDepth--;
return /** @type {Node} */ (
/** @type {unknown} */ (
new ObjectNode(
start,
this.lastTokEnd,
isPattern ? "ObjectPattern" : "ObjectExpression",
properties
)
)
);
}
/**
* Owned `parseProperty`, an exact-semantics copy of acorn 8's: spread/rest
* land fully-formed on `RestSpreadNode` and properties start pre-shaped on
* `PropertyNode`, which acorn's shared `parsePropertyName`/
* `parsePropertyValue` then fill in place (ES9+ semantics assumed via the
* ES11 fast-path gate). Non-lazy mode delegates.
* @param {boolean} isPattern whether parsing a binding pattern
* @param {DestructuringErrorsShim | null=} refDestructuringErrors destructuring errors to fill
* @returns {Node} property, spread element or rest element
* @this {ParserInternals}
*/
parseProperty(isPattern, refDestructuringErrors) {
if (!this._subscriptFastPath) {
return base.parseProperty.call(this, isPattern, refDestructuringErrors);
}
const nodeStart = this.start;
if (this.eat(tokTypes.ellipsis)) {
if (isPattern) {
const argument = /** @type {Node} */ (
/** @type {unknown} */ (this.parseIdent(false))
);
if (this.type === tokTypes.comma) {
this.raiseRecoverable(
this.start,
"Comma is not permitted after the rest element"
);
}
return /** @type {Node} */ (
/** @type {unknown} */ (
new RestSpreadNode(
nodeStart,
this.lastTokEnd,
"RestElement",
argument
)
)
);
}
const argument = /** @type {Node} */ (
/** @type {unknown} */ (
this.parseMaybeAssign(false, refDestructuringErrors)
)
);
// disallow trailing comma via `this.toAssignable()`
if (
this.type === tokTypes.comma &&
refDestructuringErrors &&
refDestructuringErrors.trailingComma < 0
) {
refDestructuringErrors.trailingComma = this.start;
}
return /** @type {Node} */ (
/** @type {unknown} */ (
new RestSpreadNode(
nodeStart,
this.lastTokEnd,
"SpreadElement",
argument
)
)
);
}
const prop = /** @type {Node} */ (
/** @type {unknown} */ (new PropertyNode(nodeStart))
);
let isGenerator = false;
/** @type {number | undefined} */
let startPos;
/** @type {Position | undefined} */
let startLoc;
if (isPattern || refDestructuringErrors) {
startPos = this.start;
startLoc = this.startLoc;
}
if (!isPattern) isGenerator = this.eat(tokTypes.star);
const containsEsc = this.containsEsc;
this.parsePropertyName(prop);
let isAsync = false;
if (!isPattern && !containsEsc && !isGenerator && this.isAsyncProp(prop)) {
isAsync = true;
isGenerator = this.eat(tokTypes.star);
this.parsePropertyName(prop);
}
this.parsePropertyValue(
prop,
isPattern,
isGenerator,
isAsync,
startPos,
startLoc,
refDestructuringErrors,
containsEsc
);
return this.finishNode(prop, "Property");
}
/**
* Owned `parseLiteral`: builds the finished `LiteralNode` directly; the
* `bigint` branch matches acorn's. Non-lazy mode delegates to acorn.
* @param {unknown} value literal value
* @returns {Node} literal node
* @this {ParserInternals}
*/
parseLiteral(value) {
if (!this._lazy) return base.parseLiteral.call(this, value);
const start = this.start;
const end = this.end;
const raw = this.input.slice(start, end);
const node = new LiteralNode(start, end, value, raw);
if (raw.charCodeAt(raw.length - 1) === 110) {
// acorn falls back to normalizing `raw` when `BigInt` is missing;
// every Node version webpack supports has it, so `value` is set
/** @type {LiteralNode & { bigint?: string }} */ (node).bigint =
/** @type {bigint} */ (value).toString();
}
this.next();
return /** @type {Node} */ (/** @type {unknown} */ (node));
}
/**
* Single-construction regexp literals: acorn validates the pattern and
* then builds the value with a second `new RegExp`. This override scans
* like acorn, keeps acorn's flag validation (for its exact messages) and
* lets one `new RegExp` be both the V8-backed validation and the value.
* @this {ParserInternals & { regexpState: unknown }}
* @returns {void}
*/
readRegexp() {
const input = this.input;
const start = this.pos;
const len = input.length;
let escaped = false;
let inClass = false;
let pos = start;
for (;;) {
if (pos >= len) this.raise(start, "Unterminated regular expression");
const ch = input.charCodeAt(pos);
// LF, CR, LS, PS
if (ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029) {
this.raise(start, "Unterminated regular expression");
}
if (escaped) {
escaped = false;
} else {
if (ch === 91) inClass = true;
else if (ch === 93 && inClass) inClass = false;
else if (ch === 47 && !inClass) break;
escaped = ch === 92;
}
pos++;
}
const pattern = input.slice(start, pos);
this.pos = pos + 1;
const flagsStart = this.pos;
const flags = this.readWord1();
if (this.containsEsc) this.unexpected(flagsStart);
// acorn's per-ecmaVersion flag validation, kept for its exact errors;
// the whitelist is precomputed in the constructor
const ecmaVersion = this._ecmaVersion;
const validFlags = this._validRegexpFlags;
let hasU = false;
let hasV = false;
for (let i = 0; i < flags.length; i++) {
const flag = flags.charAt(i);
if (!validFlags.includes(flag)) {
this.raise(start, "Invalid regular expression flag");
}
if (flags.includes(flag, i + 1)) {
this.raise(start, "Duplicate regular expression flag");
}
if (flag === "u") hasU = true;
if (flag === "v") hasV = true;
}
if (ecmaVersion >= 15 && hasU && hasV) {
this.raise(start, "Invalid regular expression flag");
}
let value = null;
try {
value = new RegExp(pattern, flags);
} catch (err) {
// V8's verdict on the pattern, like validateRegExpPattern below
this.raiseRecoverable(start, /** @type {Error} */ (err).message);
}
return this.finishToken(tokTypes.regexp, { pattern, flags, value });
}
// ----- regexp validation (V8-backed, replaces acorn's JS revalidation) -----
/**
* Acorn constructs the literal's `RegExp` value right after this hook, so
* V8 validates every pattern anyway; acorn's own JS copy of
* that validation costs several percent of parse time. Raise from V8's
* verdict instead — invalid patterns still fail the module build, only
* exotic engine-specific message texts may differ.
* @param {{ start: number, source: string, flags: string }} state acorn regexp validation state
* @this {ParserInternals}
*/
validateRegExpPattern(state) {
try {
// eslint-disable-next-line no-new
new RegExp(state.source, state.flags);
} catch (err) {
this.raiseRecoverable(state.start, /** @type {Error} */ (err).message);
}
}
// ----- scope tracking (Set-based, replaces acorn's array + indexOf) -----
/**
* @param {number} flags scope flags
* @this {ParserInternals}
*/
enterScope(flags) {
this.scopeStack.push(new Scope(flags));
}
/**
* Set-backed replacement for acorn's `declareName` on Set-backed scopes.
* @param {string} name declared name
* @param {number} bindingType acorn BIND_* binding type
* @param {number} pos source offset for redeclaration errors
* @this {ParserInternals}
*/
declareName(name, bindingType, pos) {
let redeclared = false;
if (bindingType === BIND_LEXICAL) {
const scope = this.currentScope();
redeclared =
(scope.lexical !== undefined && scope.lexical.has(name)) ||
(scope.functions !== undefined && scope.functions.has(name)) ||
(scope.var !== undefined && scope.var.has(name));
if (scope.lexical === undefined) {
scope.firstLexical = name;
scope.lexical = new Set();
}
scope.lexical.add(name);
if (this.inModule && scope.flags & SCOPE_TOP) {
delete this.undefinedExports[name];
}
} else if (bindingType === /* BIND_SIMPLE_CATCH */ 4) {
const scope = this.currentScope();
if (scope.lexical === undefined) {
scope.firstLexical = name;
scope.lexical = new Set();
}
scope.lexical.add(name);
} else if (bindingType === /* BIND_FUNCTION */ 3) {
const scope = this.currentScope();
redeclared = this.treatFunctionsAsVar
? scope.lexical !== undefined && scope.lexical.has(name)
: (scope.lexical !== undefined && scope.lexical.has(name)) ||
(scope.var !== undefined && scope.var.has(name));
(scope.functions || (scope.functions = new Set())).add(name);
} else {
for (let i = this.scopeStack.length - 1; i >= 0; --i) {
const scope = this.scopeStack[i];
if (
(scope.lexical !== undefined &&
scope.lexical.has(name) &&
!(
scope.flags & SCOPE_SIMPLE_CATCH && scope.firstLexical === name
)) ||
// lazy-Set check first: `functions` is undefined for almost all
// scopes, and the method call walks no state worth paying for then
(scope.functions !== undefined &&
!this.treatFunctionsAsVarInScope(scope) &&
scope.functions.has(name))
) {
redeclared = true;
break;
}
(scope.var || (scope.var = new Set())).add(name);
if (this.inModule && scope.flags & SCOPE_TOP) {
delete this.undefinedExports[name];
}
if (scope.flags & SCOPE_VAR) break;
}
}
if (redeclared) {
this.raiseRecoverable(
pos,
`Identifier '${name}' has already been declared`
);
}
}
/**
* Set-backed replacement for acorn's `checkLocalExport` on Set-backed scopes.
* @param {Identifier} id exported identifier
* @this {ParserInternals}
*/
checkLocalExport(id) {
const topScope = this.scopeStack[0];
if (
!(topScope.lexical !== undefined && topScope.lexical.has(id.name)) &&
!(topScope.var !== undefined && topScope.var.has(id.name))
) {
this.undefinedExports[id.name] = id;
}
}
// ----- import attributes (`with { ... }` / legacy `assert { ... }`) -----
/**
* @returns {ImportAttribute[]} import attributes
* @this {ParserInternals}
*/
parseWithClause() {
/** @type {ImportAttribute[] & { [LEGACY_ASSERT_ATTRIBUTES]?: boolean }} */
const nodes = [];
const isAssertLegacy = this.value === "assert";
if (isAssertLegacy) {
if (!this.eat(tokTypes.name)) {
return nodes;
}
} else if (!this.eat(tokTypes._with)) {
return nodes;
}
this.expect(tokTypes.braceL);
/** @type {Record<string, boolean>} */
const attributeKeys = {};
let first = true;
while (!this.eat(tokTypes.braceR)) {
if (!first) {
this.expect(tokTypes.comma);
if (this.afterTrailingComma(tokTypes.braceR)) {
break;
}
} else {
first = false;
}
const attr = this.parseImportAttribute();
const keyName =
/** @type {string} */
(attr.key.type === "Identifier" ? attr.key.name : attr.key.value);
if (Object.prototype.hasOwnProperty.call(attributeKeys, keyName)) {
this.raiseRecoverable(
attr.key.start,
`Duplicate attribute key '${keyName}'`
);
}
attributeKeys[keyName] = true;
nodes.push(attr);
}
if (isAssertLegacy) {
nodes[LEGACY_ASSERT_ATTRIBUTES] = true;
}
return nodes;
}
// ----- import phases (`import defer/source`, `import.defer/source()`) -----
/**
* @param {Node & { phase?: ImportPhase }} node import declaration node
* @returns {Node} finished node
* @this {ParserInternals}
*/
parseImport(node) {
this._moduleSyntaxSeen = true;
this._importPhase = null;
const result = base.parseImport.call(this, node);
if (this._importPhase) node.phase = this._importPhase;
return result;
}
/**
* Owned `parseExport` only to flag module syntax for the auto-fallback guard;
* parsing itself delegates to acorn.
* @param {Node} node started export node
* @param {unknown} exports export-name tracking object
* @returns {Node} export declaration
* @this {ParserInternals}
*/
parseExport(node, exports) {
this._moduleSyntaxSeen = true;
return base.parseExport.call(this, node, exports);
}
/**
* Owned `parseAwait` only to flag top-level await (module-only) for the
* auto-fallback guard; await inside a function is not module syntax.
* @param {boolean | string=} forInit for-loop init context flag
* @returns {Expression} await expression
* @this {ParserInternals}
*/
parseAwait(forInit) {
if (this.inModule && this.currentVarScope().flags & SCOPE_TOP) {
this._moduleSyntaxSeen = true;
}
return base.parseAwait.call(this, forInit);
}
/**
* @returns {AnyImportSpecifier[]} import specifiers
* @this {ParserInternals}
*/
parseImportSpecifiers() {
if (!this._importPhasesEnabled) {
return base.parseImportSpecifiers.call(this);
}
/** @type {ImportPhase | null} */
const phase = this.isContextual("defer")
? "defer"
: this.isContextual("source")
? "source"
: null;
if (!phase) return base.parseImportSpecifiers.call(this);
const phaseId = this.parseIdent();
if (this.isContextual("from") || this.type === tokTypes.comma) {
// `defer`/`source` was the default import name, not a phase modifier
const defaultSpecifier =
/** @type {ImportDefaultSpecifier} */
(
this.startNodeAt(
phaseId.start,
phaseId.loc ? phaseId.loc.start : undefined
)
);
defaultSpecifier.local = phaseId;
this.checkLValSimple(phaseId, BIND_LEXICAL);
/** @type {AnyImportSpecifier[]} */
const nodes = [
/** @type {ImportDefaultSpecifier} */
(this.finishNode(defaultSpecifier, "ImportDefaultSpecifier"))
];
if (this.eat(tokTypes.comma)) {
if (this.type !== tokTypes.star && this.type !== tokTypes.braceL) {
this.unexpected();
}
nodes.push(...base.parseImportSpecifiers.call(this));
}
return nodes;
}
this._importPhase = phase;
if (phase === "defer") {
if (this.type !== tokTypes.star) {
this.raiseRecoverable(
phaseId.start,
"'import defer' can only be used with namespace imports ('import defer * as identifierName from ...')."
);
}
} else if (this.type !== tokTypes.name) {
this.raiseRecoverable(
phaseId.start,
"'import source' can only be used with direct identifier specifier imports."
);
}
const specifiers = base.parseImportSpecifiers.call(this);
if (
phase === "source" &&
specifiers.some((s) => s.type !== "ImportDefaultSpecifier")
) {
this.raiseRecoverable(
phaseId.start,
"'import source' can only be used with direct identifier specifier imports ('import source identifierName from ...')."
);
}
return specifiers;
}
/**
* @param {boolean} forNew whether parsed as the operand of `new`
* @returns {Expression} expression node
* @this {ParserInternals}
*/
parseExprImport(forNew) {
const node = base.parseExprImport.call(this, forNew);
if (
this._importPhasesEnabled &&
node.type === "MetaProperty" &&
(node.property.name === "defer" || node.property.name === "source")
) {
if (this.type === tokTypes.parenL) {
if (forNew) {
// same guard acorn applies to `new import(...)`
this.raise(node.start, "import call cannot be the target of `new`");
}
const dynImport =
/** @type {ImportExpression & { phase?: ImportPhase }} */
(
this.parseDynamicImport(
this.startNodeAt(
node.start,
node.loc ? node.loc.start : undefined
)
)
);
dynImport.phase = node.property.name;
return dynImport;
}
this.raiseRecoverable(
node.start,
`'import.${node.property.name}' can only be used in a dynamic import.`
);
}
return node;
}
/**
* @param {Node & { property?: Identifier }} node started node with `meta` set to `import`
* @returns {Expression} MetaProperty node
* @this {ParserInternals}
*/
parseImportMeta(node) {
if (!this._importPhasesEnabled) {
// base only accepts `import.meta` here, which is module-only
this._moduleSyntaxSeen = true;
return base.parseImportMeta.call(this, node);
}
this.next();
const containsEsc = this.containsEsc;
const property = this.parseIdent(true);
node.property = property;
const { name } = property;
// only `import.meta` is module-only; `import.defer`/`.source` are dynamic
if (name === "meta") this._moduleSyntaxSeen = true;
if (name !== "meta" && name !== "defer" && name !== "source") {
this.raiseRecoverable(
property.start,
"The only valid meta property for import is 'import.meta'"
);
}
if (containsEsc) {
this.raiseRecoverable(
node.start,
`'import.${name}' must not contain escaped characters`
);
}
if (
name === "meta" &&
this.options.sourceType !== "module" &&
!this.options.allowImportExportEverywhere
) {
this.raiseRecoverable(
node.start,
"Cannot use 'import.meta' outside a module"
);
}
return /** @type {Expression} */ (this.finishNode(node, "MetaProperty"));
}
}
/** @typedef {import("../Dependency").SourcePosition} SourcePosition */
/**
* Whether a raw string literal contains a legacy octal (`\47`, `\0` followed by
* a digit) or non-octal decimal (`\8`, `\9`) escape — all SyntaxErrors in
* strict mode. Escaped backslashes (`\\`) and `\x` / `\u` escapes are skipped.
* @param {string} raw raw string literal text, including quotes
* @returns {boolean} true when a strict-forbidden escape is present
*/
const hasOctalEscape = (raw) => {
for (let i = 0; i < raw.length; i++) {
if (raw.charCodeAt(i) !== 92) continue;
const next = raw.charCodeAt(i + 1);
if (next === 92) {
i++;
continue;
}
// `\0` is a valid NUL escape unless a digit follows it.
if (next === 48) {
const after = raw.charCodeAt(i + 2);
if (after >= 48 && after <= 57) return true;
i++;
continue;
}
if (next >= 49 && next <= 57) return true;
}
return false;
};
// Location decoding for lazy-mode output: nodes carry only offsets (the parser
// skips acorn's location tracking), so line/column are derived on demand from
// these two helpers.
/**
* Offset of each line's first character. Char-code scan matching acorn's
* `lineBreak` semantics (CRLF is one break): a regex `exec` loop here
* allocates a match array per line.
* @param {string} source source code
* @returns {number[]} line start offsets
*/
const buildLineStarts = (source) => {
const len = source.length;
const lineStarts = [0];
for (let i = 0; i < len; i++) {
const ch = source.charCodeAt(i);
if (ch === 10) {
lineStarts.push(i + 1);
} else if (ch === 13) {
if (source.charCodeAt(i + 1) === 10) i++;
lineStarts.push(i + 1);
} else if (ch === 0x2028 || ch === 0x2029) {
lineStarts.push(i + 1);
}
}
return lineStarts;
};
/**
* Binary search for the line containing the offset.
* @param {number[]} lineStarts line start offsets
* @param {number} offset source offset
* @returns {SourcePosition} position (1-based line, 0-based column)
*/
const positionAt = (lineStarts, offset) => {
let lo = 0;
let hi = lineStarts.length - 1;
while (lo < hi) {
const mid = (lo + hi + 1) >>> 1;
if (lineStarts[mid] <= offset) lo = mid;
else hi = mid - 1;
}
return { line: lo + 1, column: offset - lineStarts[lo] };
};
module.exports.LEGACY_ASSERT_ATTRIBUTES = LEGACY_ASSERT_ATTRIBUTES;
module.exports.WebpackParser = WebpackParser;
module.exports.buildLineStarts = buildLineStarts;
module.exports.hasOctalEscape = hasOctalEscape;
module.exports.positionAt = positionAt;