webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
7,729 lines • 245 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const { HookMap, SyncBailHook } = require("tapable");
const WebpackError = require("../WebpackError");
const Parser = require("../module/Parser");
const binarySearchBounds = require("../util/binarySearchBounds");
const {
CompilerHintNotationRegExp,
createMagicCommentContext,
getCommentsInRange,
parseCommentOptionsInRange
} = require("../util/magicComment");
const BasicEvaluatedExpression = require("./BasicEvaluatedExpression");
const StackedMap = require("./StackedMap");
const { nonASCIIwhitespace } = require("./syntax-parser");
const {
BLOCK_DECLARATIONS,
HOISTED_DECLARATIONS,
LEGACY_ASSERT_ATTRIBUTES,
MAX_MASKED_INDEX,
MODULE_DECLARATIONS,
WebpackParser,
buildLineStarts,
declaresIntoBlock,
hasOctalEscape,
isIdentifierChar,
overflowFrom,
positionAt
} = require("./syntax-parser");
/** @import { BuiltJavascriptModule } from "./JavascriptModule" */
/** @import { EcmaVersion, ParserOptions, ParserPlugin } from "./syntax-parser" */
/**
* @import {
* AssignmentExpression,
* BinaryExpression,
* BlockStatement,
* SequenceExpression,
* CallExpression,
* StaticBlock,
* ClassDeclaration,
* ForStatement,
* SwitchStatement,
* ClassExpression,
* SourceLocation,
* ConditionalExpression,
* Declaration,
* PrivateIdentifier,
* PropertyDefinition,
* Expression,
* ImportDeclaration,
* Identifier,
* VariableDeclaration,
* IfStatement,
* LabeledStatement,
* Literal,
* LogicalExpression,
* ChainExpression,
* MemberExpression,
* YieldExpression,
* MetaProperty,
* Property,
* AssignmentPattern,
* Pattern,
* UpdateExpression,
* ObjectExpression,
* UnaryExpression,
* ArrayExpression,
* ArrayPattern,
* AwaitExpression,
* ThisExpression,
* RestElement,
* ObjectPattern,
* SwitchCase,
* CatchClause,
* VariableDeclarator,
* ForInStatement,
* ForOfStatement,
* ReturnStatement,
* WithStatement,
* ThrowStatement,
* MethodDefinition,
* NewExpression,
* SpreadElement,
* FunctionExpression,
* WhileStatement,
* ArrowFunctionExpression,
* ExpressionStatement,
* ExportAllDeclaration,
* ExportNamedDeclaration,
* FunctionDeclaration,
* DoWhileStatement,
* TryStatement,
* Node,
* Program,
* Directive,
* Statement,
* ExportDefaultDeclaration,
* Super,
* TaggedTemplateExpression,
* TemplateLiteral,
* ModuleDeclaration,
* MaybeNamedFunctionDeclaration,
* MaybeNamedClassDeclaration
* } from "estree"
*/
/** @import { DependencyLocation } from "../graph/Dependency" */
/** @import { MagicCommentOptions } from "../util/magicComment" */
/** @typedef {import("estree").Comment & { start: number, end: number, loc?: SourceLocation | null }} Comment */
/**
* Defines the shared type used by this module.
* @template T
* @typedef {import("tapable").AsArray<T>} AsArray<T>
*/
/** @import { ParserState, PreparsedAst } from "../module/Parser" */
/** @import LocalModule from "../dependencies/amd/LocalModule" */
/**
* @import {
* ESMStarExportsList
* } from "../dependencies/esm/ESMExportImportedSpecifierDependency"
*/
/**
* Defines the known javascript parser state type used by this module.
* The `harmony*` properties forward to their `esm*` namesake.
* @typedef {object} KnownJavascriptParserState
* @property {Set<string>=} esmNamedExports
* @property {InstanceType<ESMStarExportsList>=} esmStarExports
* @property {number=} lastESMImportOrder
* @property {LocalModule[]=} localModules
* @property {Set<string>=} harmonyNamedExports deprecated, use `esmNamedExports`
* @property {InstanceType<ESMStarExportsList>=} harmonyStarExports deprecated, use `esmStarExports`
* @property {number=} lastHarmonyImportOrder deprecated, use `lastESMImportOrder`
*/
/** @typedef {ParserState<BuiltJavascriptModule> & KnownJavascriptParserState} JavascriptParserState */
/** @import Compilation from "../Compilation" */
/** @import Module from "../module/Module" */
/** @typedef {{ name: string | VariableInfo, rootInfo: string | VariableInfo, getMembers: () => Members, getMembersOptionals: () => MembersOptionals, getMemberRanges: () => MemberRanges }} GetInfoResult */
/** @typedef {{ consequent?: GuardFrame, alternate?: GuardFrame }} GuardCollection per-branch guard frames pushed onto the parser state guard stack */
/** @typedef {Statement | ModuleDeclaration | Expression | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration} StatementPathItem */
/** @typedef {(ident: string) => void} OnIdentString */
/** @typedef {(ident: string, identifier: Identifier) => void} OnIdent */
/** @typedef {StatementPathItem[]} StatementPath */
/** @typedef {(Statement | ModuleDeclaration)[]} HoistedDeclarations the declarations one scope hoists, in source order */
/** @typedef {(ModuleDeclaration | Statement | undefined)[]} ModuleDeclarations the program's import and re-export declarations in source order, each followed by the statement written before it */
/** @typedef {number | number[]} BlockDeclarations which statements of one list declare into its block scope: a mask of their indices, or the indices themselves once one is too large to mask */
/** @typedef {Set<DestructuringAssignmentProperty>} DestructuringAssignmentProperties */
// TODO remove cast when @types/estree has been updated to import phases
/** @typedef {import("estree").ImportExpression & { phase?: "defer" | "source" }} ImportExpression */
/** @type {string[]} */
const EMPTY_ARRAY = [];
/** @type {HoistedDeclarations} */
const EMPTY_HOISTED_DECLARATIONS = [];
// what the module pass reports; `export default` is the block pass's
const MODULE_STATEMENT_TYPES = new Set([
"ImportDeclaration",
"ExportNamedDeclaration",
"ExportAllDeclaration"
]);
// what the parser records per scope, and so what the hoisting pass reaches
const RECORDED_STATEMENT_TYPES = new Set([
"VariableDeclaration",
"FunctionDeclaration",
"ClassDeclaration"
]);
/** @type {ModuleDeclarations} */
const EMPTY_MODULE_DECLARATIONS = [];
// what the parser records per statement list, and so what the block pass
// reaches. An expression statement is recorded only when it assigns to an
// object pattern, so a tap on that type is not served from the list.
const BLOCK_DECLARATION_TYPES = new Set([
"VariableDeclaration",
"FunctionDeclaration",
"ClassDeclaration",
"ExportNamedDeclaration",
"ExportDefaultDeclaration"
]);
/** @type {MembersOptionals} */
const EMPTY_MEMBERS_OPTIONALS = [];
const ALLOWED_MEMBER_TYPES_CALL_EXPRESSION = 0b01;
const ALLOWED_MEMBER_TYPES_EXPRESSION = 0b10;
const ALLOWED_MEMBER_TYPES_ALL = 0b11;
// Shared `() => false` so the members-optionals fallback in walkCallExpression
// doesn't allocate a fresh closure per identifier-rooted call.
const RETURN_FALSE = () => false;
// Shared `() => []` for the member-less identifier/this evaluation results,
// replacing three fresh closures per evaluated free variable.
const RETURN_EMPTY_ARRAY = () => [];
// Shared no-result fallback for the identifier evaluators.
const RETURN_UNDEFINED = () => undefined;
// Syntax: https://developer.mozilla.org/en/SpiderMonkey/Parser_API
let parser = WebpackParser;
/** @typedef {Record<string, string> & { _isLegacyAssert?: boolean }} ImportAttributes */
// Conceptually pairs with ./syntax's LEGACY_ASSERT_ATTRIBUTES marker, but is
// pinned here by its public `JavascriptParser.getImportAttributes` typing.
/**
* Gets import attributes.
* @param {ImportDeclaration | ExportNamedDeclaration | ExportAllDeclaration | ImportExpression} node node with assertions
* @returns {ImportAttributes | undefined} import attributes
*/
const getImportAttributes = (node) => {
if (node.type === "ImportExpression") {
if (
node.options &&
node.options.type === "ObjectExpression" &&
node.options.properties[0] &&
node.options.properties[0].type === "Property" &&
node.options.properties[0].key.type === "Identifier" &&
(node.options.properties[0].key.name === "with" ||
node.options.properties[0].key.name === "assert") &&
node.options.properties[0].value.type === "ObjectExpression" &&
node.options.properties[0].value.properties.length > 0
) {
const properties =
/** @type {Property[]} */
(node.options.properties[0].value.properties);
const result = /** @type {ImportAttributes} */ ({});
for (const property of properties) {
const key =
/** @type {string} */
(
property.key.type === "Identifier"
? property.key.name
: /** @type {Literal} */ (property.key).value
);
result[key] =
/** @type {string} */
(/** @type {Literal} */ (property.value).value);
}
const key =
node.options.properties[0].key.type === "Identifier"
? node.options.properties[0].key.name
: /** @type {Literal} */ (node.options.properties[0].key).value;
if (key === "assert") {
result._isLegacyAssert = true;
}
return result;
}
return;
}
if (node.attributes === undefined || node.attributes.length === 0) {
return;
}
const result = /** @type {ImportAttributes} */ ({});
for (const attribute of node.attributes) {
const key =
/** @type {string} */
(
attribute.key.type === "Identifier"
? attribute.key.name
: attribute.key.value
);
result[key] = /** @type {string} */ (attribute.value.value);
}
if (
/** @type {typeof node.attributes & { [LEGACY_ASSERT_ATTRIBUTES]?: boolean }} */
(node.attributes)[LEGACY_ASSERT_ATTRIBUTES]
) {
result._isLegacyAssert = true;
}
return result;
};
/** @typedef {typeof VariableInfoFlags.Evaluated | typeof VariableInfoFlags.Free | typeof VariableInfoFlags.Normal | typeof VariableInfoFlags.Tagged} VariableInfoFlagsType */
const VariableInfoFlags = Object.freeze({
Evaluated: 0b000,
Free: 0b001,
Normal: 0b010,
Tagged: 0b100
});
class VariableInfo {
/**
* Creates an instance of VariableInfo.
* @param {ScopeInfo} declaredScope scope in which the variable is declared
* @param {string | undefined} name which name the variable use, defined name or free name or tagged name
* @param {VariableInfoFlagsType} flags how the variable is created
* @param {TagInfo | undefined} tagInfo info about tags
*/
constructor(declaredScope, name, flags, tagInfo) {
/** @type {ScopeInfo} */
this.declaredScope = declaredScope;
/** @type {string | undefined} */
this.name = name;
/** @type {VariableInfoFlagsType} */
this.flags = flags;
/** @type {TagInfo | undefined} */
this.tagInfo = tagInfo;
}
/**
* Checks whether this variable info is free.
* @returns {boolean} the variable is free or not
*/
isFree() {
return (this.flags & VariableInfoFlags.Free) > 0;
}
/**
* Checks whether this variable info is tagged.
* @returns {boolean} the variable is tagged by tagVariable or not
*/
isTagged() {
return (this.flags & VariableInfoFlags.Tagged) > 0;
}
}
/** @typedef {string | ScopeInfo | VariableInfo} ExportedVariableInfo */
/** @typedef {Literal | string | null | undefined} ImportSource */
/**
* Defines the internal parse options type used by this module.
* @typedef {Omit<ParseOptions, "sourceType" | "ecmaVersion"> & { sourceType: "module" | "script" | "auto" }} InternalParseOptions
*/
/**
* Defines the parse options type used by this module.
* @typedef {object} ParseOptions
* @property {"module" | "script"} sourceType
* @property {EcmaVersion} ecmaVersion
* @property {boolean=} locations
* @property {boolean=} comments
* @property {boolean=} ranges
* @property {boolean=} allowHashBang
* @property {boolean=} allowReturnOutsideFunction
* @property {boolean=} lazyNodes internal: serve `range` lazily and skip the parser's location/range tracking
* @property {Comment[]=} lazyComments internal: collect comments here without slicing their text eagerly
* @property {boolean=} importPhases enable parsing of the import phase proposals (import defer / import source)
* @property {boolean=} moduleFallback internal: for `auto`, let the parser downgrade module->script in place instead of re-parsing
*/
/**
* Defines the parse result type used by this module.
* @typedef {object} ParseResult
* @property {Program} ast
* @property {Comment[]} comments
*/
/**
* Defines the parse function type used by this module.
* @typedef {(code: string, options: ParseOptions) => ParseResult} ParseFunction
*/
/** @typedef {symbol} Tag */
/**
* @import {
* ESMSettings
* } from "../dependencies/esm/ESMImportDependencyParserPlugin"
*/
/** @import { ImportSettings } from "../dependencies/import/ImportParserPlugin" */
/** @import { GuardFrame } from "../dependencies/esm/ESMImportGuard" */
/**
* @import {
* CommonJsImportSettings
* } from "../dependencies/commonjs/CommonJsImportsParserPlugin"
*/
/** @import { CompatibilitySettings } from "./CompatibilityPlugin" */
/** @import { TopLevelSymbol } from "../optimize/InnerGraph" */
/** @typedef {ESMSettings | ImportSettings | CommonJsImportSettings | TopLevelSymbol | CompatibilitySettings} KnownTagData */
// The open branch is whatever a plugin tags a variable with.
/** @typedef {KnownTagData | EXPECTED_OBJECT} TagData */
/**
* Defines the tag info type used by this module.
* @typedef {object} TagInfo
* @property {Tag} tag
* @property {TagData=} data
* @property {TagInfo | undefined} next
*/
/** @typedef {string[]} CalleeMembers */
/** @typedef {string[]} Members */
/** @typedef {UpdateExpression | ForInStatement | ForOfStatement | AssignmentExpression} WriteStatement a construct that writes to the reference it carries */
/** @typedef {boolean[]} MembersOptionals */
/** @typedef {Range[]} MemberRanges */
const SCOPE_INFO_TERMINATED_RETURN = 1;
const SCOPE_INFO_TERMINATED_THROW = 2;
/**
* Defines the scope info type used by this module.
* @typedef {object} ScopeInfo
* @property {StackedMap<string, VariableInfo | ScopeInfo>} definitions
* @property {boolean | "arrow"} topLevelScope
* @property {boolean | string} inShorthand
* @property {boolean} inTaggedTemplateTag
* @property {boolean} inTry
* @property {boolean} isStrict
* @property {boolean} isAsmJs
* @property {undefined | 1 | 2} terminated
*/
/** @typedef {[number, number]} Range */
/**
* Defines the destructuring assignment property type used by this module.
* Carries only the range — consumers derive line/column via `getLocation`.
* @typedef {object} DestructuringAssignmentProperty
* @property {string} id
* @property {Range} range
* @property {Set<DestructuringAssignmentProperty> | undefined=} pattern
* @property {boolean | string} shorthand
*/
/**
* Helper function for joining two ranges into a single range. This is useful
* when working with AST nodes, as it allows you to combine the ranges of child nodes
* to create the range of the _parent node_.
* @param {Range} startRange start range to join
* @param {Range} endRange end range to join
* @returns {Range} joined range
* @example
* ```js
* const startRange = [0, 5];
* const endRange = [10, 15];
* const joinedRange = joinRanges(startRange, endRange);
* console.log(joinedRange); // [0, 15]
* ```
*/
const joinRanges = (startRange, endRange) => {
if (!endRange) return startRange;
if (!startRange) return endRange;
return [startRange[0], endRange[1]];
};
/**
* Helper function used to generate a string representation of a
* [member expression](https://github.com/estree/estree/blob/master/es5.md#memberexpression).
* @param {string} object object to name
* @param {Members} members members in source order
* @returns {string} member expression as a string
* @example
* ```js
* const members = ["property1", "property2", "property3"]; // Members parsed from the AST
* const name = objectAndMembersToName("myObject", members);
*
* console.log(name); // "myObject.property1.property2.property3"
* ```
*/
const objectAndMembersToName = (object, members) => {
let name = object;
for (let i = 0; i < members.length; i++) {
name = `${name}.${members[i]}`;
}
return name;
};
/**
* The range of the object below each of the first `count` members of a chain,
* in source order: what `extractMemberExpressionChain` reports as memberRanges.
* @param {Expression | Super} expression a member expression
* @param {number} count how many members the chain has
* @returns {MemberRanges} the ranges
*/
const memberRangesOf = (expression, count) => {
/** @type {MemberRanges} */
const ranges = [];
let expr = /** @type {MemberExpression} */ (expression);
for (let i = 0; i < count; i++) {
const object = expr.object;
ranges.push(/** @type {Range} */ (object.range));
expr = /** @type {MemberExpression} */ (object);
}
if (count > 1) ranges.reverse();
return ranges;
};
/**
* In simple logical cases, we can use valueAsExpression to assist us in evaluating the expression on
* either side of a [BinaryExpression](https://github.com/estree/estree/blob/master/es5.md#binaryexpression).
* This supports scenarios in webpack like conditionally `import()`'ing modules based on some simple evaluation:
*
* ```js
* if (1 === 3) {
* import("./moduleA"); // webpack will auto evaluate this and not import the modules
* }
* ```
*
* Additional scenarios include evaluation of strings inside of dynamic import statements:
*
* ```js
* const foo = "foo";
* const bar = "bar";
*
* import("./" + foo + bar); // webpack will auto evaluate this into import("./foobar")
* ```
* @param {boolean | number | bigint | string} value the value to convert to an expression
* @param {BinaryExpression | UnaryExpression} expr the expression being evaluated
* @param {boolean} sideEffects whether the expression has side effects
* @returns {BasicEvaluatedExpression | undefined} the evaluated expression
* @example
*
* ```js
* const binaryExpr = new BinaryExpression("+",
* { type: "Literal", value: 2 },
* { type: "Literal", value: 3 }
* );
*
* const leftValue = 2;
* const rightValue = 3;
*
* const leftExpr = valueAsExpression(leftValue, binaryExpr.left, false);
* const rightExpr = valueAsExpression(rightValue, binaryExpr.right, false);
* const result = new BasicEvaluatedExpression()
* .setNumber(leftExpr.number + rightExpr.number)
* .setRange(binaryExpr.range);
*
* console.log(result.number); // Output: 5
* ```
*/
const valueAsExpression = (value, expr, sideEffects) => {
switch (typeof value) {
case "boolean":
return new BasicEvaluatedExpression()
.setBoolean(value)
.setSideEffects(sideEffects)
.setRange(/** @type {Range} */ (expr.range));
case "number":
return new BasicEvaluatedExpression()
.setNumber(value)
.setSideEffects(sideEffects)
.setRange(/** @type {Range} */ (expr.range));
case "bigint":
return new BasicEvaluatedExpression()
.setBigInt(value)
.setSideEffects(sideEffects)
.setRange(/** @type {Range} */ (expr.range));
case "string":
return new BasicEvaluatedExpression()
.setString(value)
.setSideEffects(sideEffects)
.setRange(/** @type {Range} */ (expr.range));
}
};
// The constant operators, shared so evaluating `a - b` allocates no handler.
// JavaScript coerces any operand; each cast only lets TypeScript type the operator.
/** @typedef {ReturnType<BasicEvaluatedExpression["asCompileTimeValue"]>} CompileTimeValue */
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {number | bigint} the result
*/
const SUBTRACT = (l, r) =>
/** @type {number} */ (l) - /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {number | bigint} the result
*/
const MULTIPLY = (l, r) =>
/** @type {number} */ (l) * /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {number | bigint} the result
*/
const DIVIDE = (l, r) => /** @type {number} */ (l) / /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {number | bigint} the result
*/
const POWER = (l, r) => /** @type {number} */ (l) ** /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {number | bigint} the result
*/
const BITWISE_AND = (l, r) =>
/** @type {number} */ (l) & /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {number | bigint} the result
*/
const BITWISE_OR = (l, r) =>
/** @type {number} */ (l) | /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {number | bigint} the result
*/
const BITWISE_XOR = (l, r) =>
/** @type {number} */ (l) ^ /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {number} the result
*/
const UNSIGNED_SHIFT_RIGHT = (l, r) =>
/** @type {number} */ (l) >>> /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {number | bigint} the result
*/
const SHIFT_RIGHT = (l, r) =>
/** @type {number} */ (l) >> /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {number | bigint} the result
*/
const SHIFT_LEFT = (l, r) =>
/** @type {number} */ (l) << /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {boolean} the result
*/
const LESS_THAN = (l, r) =>
/** @type {number} */ (l) < /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {boolean} the result
*/
const GREATER_THAN = (l, r) =>
/** @type {number} */ (l) > /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {boolean} the result
*/
const LESS_THAN_OR_EQUAL = (l, r) =>
/** @type {number} */ (l) <= /** @type {number} */ (r);
/**
* @param {CompileTimeValue} l left operand
* @param {CompileTimeValue} r right operand
* @returns {boolean} the result
*/
const GREATER_THAN_OR_EQUAL = (l, r) =>
/** @type {number} */ (l) >= /** @type {number} */ (r);
/**
* @param {CompileTimeValue} v operand
* @returns {number | bigint} the result
*/
const BITWISE_NOT = (v) => ~(/** @type {number} */ (v));
/**
* @param {CompileTimeValue} v operand
* @returns {number} the result
*/
// eslint-disable-next-line no-implicit-coercion
const TO_NUMBER = (v) => +(/** @type {number} */ (v));
/**
* @param {CompileTimeValue} v operand
* @returns {number | bigint} the result
*/
const NEGATE = (v) => -(/** @type {number} */ (v));
/**
* Evaluates a binary expression if and only if it is a const operation (e.g. 1 + 2, "a" + "b", etc.).
* @template T
* @param {JavascriptParser} parser the parser evaluating the expression
* @param {BinaryExpression} expr the binary expression
* @param {(leftOperand: T, rightOperand: T) => boolean | number | bigint | string} operandHandler the handler for the operation (e.g. (a, b) => a + b)
* @returns {BasicEvaluatedExpression | undefined} the evaluated expression
*/
const handleConstOperation = (parser, expr, operandHandler) => {
const left = parser.evaluateExpression(expr.left);
if (!left.isCompileTimeValue()) return;
const right = parser.evaluateExpression(expr.right);
if (!right.isCompileTimeValue()) return;
const result = operandHandler(
/** @type {T} */ (left.asCompileTimeValue()),
/** @type {T} */ (right.asCompileTimeValue())
);
return valueAsExpression(
result,
expr,
left.couldHaveSideEffects() || right.couldHaveSideEffects()
);
};
/**
* Helper function to determine if two booleans are always different. This is used in `handleStrictEqualityComparison`
* to determine if an expressions boolean or nullish conversion is equal or not.
* @param {boolean} a first boolean to compare
* @param {boolean} b second boolean to compare
* @returns {boolean} true if the two booleans are always different, false otherwise
*/
const isAlwaysDifferent = (a, b) =>
(a === true && b === false) || (a === false && b === true);
/**
* Handle template string compare.
* @param {BasicEvaluatedExpression} left left
* @param {BasicEvaluatedExpression} right right
* @param {BinaryExpression} expr the comparison, for its range
* @param {boolean} eql true for "===" and false for "!=="
* @returns {BasicEvaluatedExpression | undefined} result
*/
const handleTemplateStringCompare = (left, right, expr, eql) => {
/**
* Returns value.
* @param {BasicEvaluatedExpression[]} parts parts
* @returns {string} value
*/
const getPrefix = (parts) => {
let value = "";
for (const p of parts) {
const v = p.asString();
if (v !== undefined) value += v;
else break;
}
return value;
};
/**
* Returns value.
* @param {BasicEvaluatedExpression[]} parts parts
* @returns {string} value
*/
const getSuffix = (parts) => {
let value = "";
for (let i = parts.length - 1; i >= 0; i--) {
const v = parts[i].asString();
if (v !== undefined) value = v + value;
else break;
}
return value;
};
const leftPrefix = getPrefix(
/** @type {BasicEvaluatedExpression[]} */ (left.parts)
);
const rightPrefix = getPrefix(
/** @type {BasicEvaluatedExpression[]} */ (right.parts)
);
const leftSuffix = getSuffix(
/** @type {BasicEvaluatedExpression[]} */ (left.parts)
);
const rightSuffix = getSuffix(
/** @type {BasicEvaluatedExpression[]} */ (right.parts)
);
const lenPrefix = Math.min(leftPrefix.length, rightPrefix.length);
const lenSuffix = Math.min(leftSuffix.length, rightSuffix.length);
const prefixMismatch =
lenPrefix > 0 &&
leftPrefix.slice(0, lenPrefix) !== rightPrefix.slice(0, lenPrefix);
const suffixMismatch =
lenSuffix > 0 &&
leftSuffix.slice(-lenSuffix) !== rightSuffix.slice(-lenSuffix);
if (prefixMismatch || suffixMismatch) {
return new BasicEvaluatedExpression()
.setRange(/** @type {Range} */ (expr.range))
.setBoolean(!eql)
.setSideEffects(
left.couldHaveSideEffects() || right.couldHaveSideEffects()
);
}
};
/**
* Helper function to handle BinaryExpressions using strict equality comparisons (e.g. "===" and "!==").
* @param {JavascriptParser} parser the parser evaluating the expression
* @param {BinaryExpression} expr the binary expression
* @param {boolean} eql true for "===" and false for "!=="
* @returns {BasicEvaluatedExpression | undefined} the evaluated expression
*/
const handleStrictEqualityComparison = (parser, expr, eql) => {
const left = parser.evaluateExpression(expr.left);
const right = parser.evaluateExpression(expr.right);
const leftConst = left.isCompileTimeValue();
const rightConst = right.isCompileTimeValue();
if (leftConst && rightConst) {
return new BasicEvaluatedExpression()
.setRange(/** @type {Range} */ (expr.range))
.setBoolean(
eql === (left.asCompileTimeValue() === right.asCompileTimeValue())
)
.setSideEffects(
left.couldHaveSideEffects() || right.couldHaveSideEffects()
);
}
if (left.isArray() && right.isArray()) {
return new BasicEvaluatedExpression()
.setRange(/** @type {Range} */ (expr.range))
.setBoolean(!eql)
.setSideEffects(
left.couldHaveSideEffects() || right.couldHaveSideEffects()
);
}
if (left.isTemplateString() && right.isTemplateString()) {
return handleTemplateStringCompare(left, right, expr, eql);
}
const leftPrimitive = left.isPrimitiveType();
const rightPrimitive = right.isPrimitiveType();
if (
// Primitive !== Object or
// compile-time object types are never equal to something at runtime
(leftPrimitive === false && (leftConst || rightPrimitive === true)) ||
(rightPrimitive === false && (rightConst || leftPrimitive === true)) ||
// Different nullish or boolish status also means not equal
isAlwaysDifferent(
/** @type {boolean} */ (left.asBool()),
/** @type {boolean} */ (right.asBool())
) ||
isAlwaysDifferent(
/** @type {boolean} */ (left.asNullish()),
/** @type {boolean} */ (right.asNullish())
)
) {
return new BasicEvaluatedExpression()
.setRange(/** @type {Range} */ (expr.range))
.setBoolean(!eql)
.setSideEffects(
left.couldHaveSideEffects() || right.couldHaveSideEffects()
);
}
};
/**
* Helper function to handle BinaryExpressions using abstract equality comparisons (e.g. "==" and "!=").
* @param {JavascriptParser} parser the parser evaluating the expression
* @param {BinaryExpression} expr the binary expression
* @param {boolean} eql true for "==" and false for "!="
* @returns {BasicEvaluatedExpression | undefined} the evaluated expression
*/
const handleAbstractEqualityComparison = (parser, expr, eql) => {
const left = parser.evaluateExpression(expr.left);
const right = parser.evaluateExpression(expr.right);
const leftConst = left.isCompileTimeValue();
const rightConst = right.isCompileTimeValue();
if (leftConst && rightConst) {
return new BasicEvaluatedExpression()
.setRange(/** @type {Range} */ (expr.range))
.setBoolean(
eql ===
// eslint-disable-next-line eqeqeq
(left.asCompileTimeValue() == right.asCompileTimeValue())
)
.setSideEffects(
left.couldHaveSideEffects() || right.couldHaveSideEffects()
);
}
if (left.isArray() && right.isArray()) {
return new BasicEvaluatedExpression()
.setRange(/** @type {Range} */ (expr.range))
.setBoolean(!eql)
.setSideEffects(
left.couldHaveSideEffects() || right.couldHaveSideEffects()
);
}
if (left.isTemplateString() && right.isTemplateString()) {
return handleTemplateStringCompare(left, right, expr, eql);
}
};
/**
* Evaluates a UnaryExpression if and only if it is a basic const operator (e.g. +a, -a, ~a).
* @template T
* @param {JavascriptParser} parser the parser evaluating the expression
* @param {UnaryExpression} expr the unary expression
* @param {(operand: T) => boolean | number | bigint | string} operandHandler handler for the operand
* @returns {BasicEvaluatedExpression | undefined} evaluated expression
*/
const handleUnaryConstOperation = (parser, expr, operandHandler) => {
const argument = parser.evaluateExpression(expr.argument);
if (!argument.isCompileTimeValue()) return;
const result = operandHandler(
/** @type {T} */ (argument.asCompileTimeValue())
);
return valueAsExpression(result, expr, argument.couldHaveSideEffects());
};
/**
* Grabs the name of a given expression and returns it as a string or undefined. Has particular
* handling for [Identifiers](https://github.com/estree/estree/blob/master/es5.md#identifier),
* [ThisExpressions](https://github.com/estree/estree/blob/master/es5.md#identifier), and
* [MetaProperties](https://github.com/estree/estree/blob/master/es2015.md#metaproperty) which is
* specifically for handling the `new.target` meta property.
* @param {Expression | SpreadElement | Super} expression expression
* @returns {string | "this" | undefined} name or variable info
*/
const getRootName = (expression) => {
switch (expression.type) {
case "Identifier":
return expression.name;
case "ThisExpression":
return "this";
case "MetaProperty":
return `${expression.meta.name}.${expression.property.name}`;
default:
return undefined;
}
};
/**
* @param {string} type AST node type
* @returns {boolean} true for FunctionExpression or ArrowFunctionExpression
*/
const isFunctionExpression = (type) =>
type === "FunctionExpression" || type === "ArrowFunctionExpression";
/**
* @param {FunctionExpression | ArrowFunctionExpression} fn function
* @returns {boolean} true when all params are plain identifiers
*/
const isSimpleFunction = (fn) => {
const params = fn.params;
for (let i = 0; i < params.length; i++) {
if (params[i].type !== "Identifier") return false;
}
return true;
};
/** @type {ParseOptions} */
const defaultParserOptions = {
sourceType: "module",
ecmaVersion: "latest",
ranges: false,
locations: false,
comments: false,
// https://github.com/tc39/proposal-hashbang
allowHashBang: true
};
// reused by _parse for every non-custom parse — all fields are reset per parse
/** @type {ParseOptions} */
const REUSED_PARSER_OPTIONS = { ...defaultParserOptions };
// Read-only snippet ASTs for `evaluate()`, released with the compilation;
// DefinePlugin evaluates the same strings once per usage per module.
/** @type {WeakMap<Compilation, Map<string, Program>>} */
const evaluateAstCaches = new WeakMap();
// The inner map is strong while the compilation lives, so cap it
const EVALUATE_AST_CACHE_LIMIT = 4096;
/**
* Add one declaration, starting the list at the first — most scopes declare
* nothing, and the shared empty list spares them an array.
* @param {HoistedDeclarations} hoisted the list so far
* @param {Statement | ModuleDeclaration} declaration what to add
* @returns {HoistedDeclarations} the list holding it
*/
const addHoistedDeclaration = (hoisted, declaration) => {
if (hoisted === EMPTY_HOISTED_DECLARATIONS) return [declaration];
hoisted.push(declaration);
return hoisted;
};
/**
* Collect the declarations that hoist to one scope, for an AST that did not
* come from webpack's parser and so carries no record of its own.
* @param {(Statement | ModuleDeclaration)[]} statements the scope's statements
* @param {HoistedDeclarations} hoisted the list so far
* @returns {HoistedDeclarations} the list holding what they declare
*/
const collectHoistedDeclarations = (statements, hoisted) => {
for (let index = 0, len = statements.length; index < len; index++) {
hoisted = collectHoistedDeclaration(statements[index], hoisted);
}
return hoisted;
};
/**
* Collect from one statement, descending exactly as far as a `var` reaches:
* through blocks and loop bodies, never into another function.
* @param {Statement | ModuleDeclaration} statement the statement
* @param {HoistedDeclarations} hoisted the list so far
* @returns {HoistedDeclarations} the list holding what it declares
*/
const collectHoistedDeclaration = (statement, hoisted) => {
switch (statement.type) {
case "VariableDeclaration":
if (statement.kind === "var") {
hoisted = addHoistedDeclaration(hoisted, statement);
}
break;
case "FunctionDeclaration":
case "ClassDeclaration":
hoisted = addHoistedDeclaration(hoisted, statement);
break;
case "BlockStatement":
hoisted = collectHoistedDeclarations(statement.body, hoisted);
break;
case "IfStatement":
hoisted = collectHoistedDeclaration(statement.consequent, hoisted);
if (statement.alternate) {
hoisted = collectHoistedDeclaration(statement.alternate, hoisted);
}
break;
case "ForStatement":
if (statement.init && statement.init.type === "VariableDeclaration") {
hoisted = collectHoistedDeclaration(statement.init, hoisted);
}
hoisted = collectHoistedDeclaration(statement.body, hoisted);
break;
case "ForInStatement":
case "ForOfStatement":
if (statement.left.type === "VariableDeclaration") {
hoisted = collectHoistedDeclaration(statement.left, hoisted);
}
hoisted = collectHoistedDeclaration(statement.body, hoisted);
break;
case "WhileStatement":
case "DoWhileStatement":
case "LabeledStatement":
case "WithStatement":
hoisted = collectHoistedDeclaration(statement.body, hoisted);
break;
case "TryStatement":
hoisted = collectHoistedDeclaration(statement.block, hoisted);
if (statement.handler) {
hoisted = collectHoistedDeclaration(statement.handler.body, hoisted);
}
if (statement.finalizer) {
hoisted = collectHoistedDeclaration(statement.finalizer, hoisted);
}
break;
case "SwitchStatement": {
const cases = statement.cases;
for (let index = 0, len = cases.length; index < len; index++) {
hoisted = collectHoistedDeclarations(cases[index].consequent, hoisted);
}
break;
}
}
return hoisted;
};
/**
* What the program imports and re-exports: the parser's own record, or a scan
* of the top-level statements for an AST some other parser built.
* @param {Program} ast the program
* @param {boolean} trusted whether the parser's own record may be read
* @returns {ModuleDeclarations} the declarations, in source order
*/
const moduleDeclarationsOf = (ast, trusted) => {
if (trusted) {
const recorded =
/** @type {Program & { [MODULE_DECLARATIONS]?: ModuleDeclarations }} */
(ast)[MODULE_DECLARATIONS];
if (recorded !== undefined) return recorded;
}
/** @type {ModuleDeclarations} */
let declarations = EMPTY_MODULE_DECLARATIONS;
const body = ast.body;
for (let index = 0, len = body.length; index < len; index++) {
const statement = body[index];
if (MODULE_STATEMENT_TYPES.has(statement.type)) {
if (declarations === EMPTY_MODULE_DECLARATIONS) declarations = [];
declarations.push(statement, index === 0 ? undefined : body[index - 1]);
}
}
return declarations;
};
/**
* What hoists to the scope the node opens: the parser's own record, or a walk
* of the body for an AST some other parser built.
* @param {Program | BlockStatement} node the node owning the scope
* @param {boolean} trusted whether the parser's own record may be read
* @returns {HoistedDeclarations} the declarations, in source order
*/
const hoistedDeclarationsOf = (node, trusted) => {
if (trusted) {
const recorded =
/** @type {(Program | BlockStatement) & { [HOISTED_DECLARATIONS]?: HoistedDeclarations }} */
(node)[HOISTED_DECLARATIONS];
if (recorded !== undefined) return recorded;
}
return collectHoistedDeclarations(node.body, EMPTY_HOISTED_DECLARATIONS);
};
/**
* Whether a statement is one of the list's own, or the declaration of an
* `export` that is, rather than nested in one of them. It is found by its start
* offset since the list is in source order.
* @param {(Statement | ModuleDeclaration)[]} statements the list
* @param {Statement | ModuleDeclaration | MaybeNamedFunctionDeclaration} statement the statement to find
* @returns {boolean} whether the list holds it directly
*/
const holdsStatement = (statements, statement) => {
const start = /** @type {Range} */ (statement.range)[0];
let low = 0;
let high = statements.length - 1;
while (low <= high) {
const middle = (low + high) >>> 1;
const candidate = statements[middle];
if (candidate === statement) return true;
if (/** @type {Range} */ (candidate.range)[0] < start) low = middle + 1;
else high = middle - 1;
}
// the last statement starting before it is the one that could hold it
const holder = high >= 0 ? statements[high] : undefined;
return (
holder !== undefined &&
(holder.type === "ExportNamedDeclaration" ||
holder.type === "ExportDefaultDeclaration") &&
holder.declaration === statement
);
};
/**
* Which statements of a list declare into its block scope, scanned for an AST
* some other parser built. The parser's own record is read instead when there
* is one.
* @param {(Statement | ModuleDeclaration)[]} statements the list
* @returns {BlockDeclarations} their indices, in source order
*/
const collectBlockDeclarations = (statements) => {
let mask = 0;
/** @type {number[] | undefined} */
let overflow;
for (let index = 0, len = statements.length; index < len; index++) {
if (!declaresIntoBlock(statements[index])) continue;
if (overflow !== undefined) overflow.push(index);
else if (index <= MAX_MASKED_INDEX) mask |= 1 << index;
else overflow = overflowFrom(mask, index);
}
return overflow === undefined ? mask : overflow;
};
/**
* @param {Comment} comment comment to compare
* @param {number} position source offset
* @returns {number} comparison of the comment end with the offset
*/
const compareCommentEnd = (comment, position) =>
/** @type {Range} */ (comment.range)[1] - position;
/**
* Whether the source between two offsets is whitespace only.
* @param {string} source the module source
* @param {number} start where to start
* @param {number} end where to stop
* @returns {boolean} whether nothing but whitespace lies between them
*/
const onlyWhitespaceBetween = (source, start, end) => {
for (let index = start; index < end; index++) {
const code = source.charCodeAt(index);
// the classification `skipSpace` uses: anything else is a token, and a
// token means the run of comments before the statement has ended
if (
code !== 32 &&
code !== 160 &&
!(code > 8 && code < 14) &&
!(code >= 5760 && nonASCIIwhitespace.test(source[index]))
) {
return false;
}
}
return true;
};
const CLASS_NAME = "JavascriptParser";
// Reserved words in strict mode, so unusable as binding names there (12.7.2).
const STRICT_MODE_RESERVED_WORDS = new Set([
"implements",
"interface",
"let",
"package",
"private",
"protected",
"public",
"static",
"yield"
]);
const STRICT_MODE_OUTPUT_REASON =
"The output is an ES module, which runs in strict mode.";
// `await` is reserved by the module goal rather than by strict mode itself.
const MODULE_GOAL_OUTPUT_REASON =
'The output is an ES module, where "await" is a reserved word.';
/**
* @extends {Parser<BuiltJavascriptModule>}
*/
class JavascriptParser extends Parser {
/**
* Creates an instance of JavascriptParser.
* @param {"module" | "script" | "auto"=} sourceType default source type
* @param {{ parse?: ParseFunction, typescript?: boolean, importPhases?: boolean, strictModeViolations?: "error" | "warn" | false }=} options parser options
*/
constructor(sourceType = "auto", options = {}) {
super();
this.hooks = Object.freeze({
/** @type {HookMap<SyncBailHook<[UnaryExpression], BasicEvaluatedExpression | null | undefined>>} */
evaluateTypeof: new HookMap(() => new SyncBailHook(["expression"])),
/** @type {HookMap<SyncBailHook<[Expression | SpreadElement | PrivateIdentifier | Super], BasicEvaluatedExpression | null | undefined>>} */
evaluate: new HookMap(() => new SyncBailHook(["expression"])),
/** @type {HookMap<SyncBailHook<[Identifier | ThisExpression | MemberExpression | MetaProperty], BasicEvaluatedExpression | null | undefined>>} */
evaluateIdentifier: new HookMap(() => new SyncBailHook(["expression"])),
/** @type {HookMap<SyncBailHook<[Identifier | ThisExpression | MemberExpression], BasicEvaluatedExpression | null | undefined>>} */
evaluateDefinedIdentifier: new HookMap(
() => new SyncBailHook(["expression"])
),
/**
* @type {HookMap<SyncBailHook<[NewExpression], BasicEvaluatedExpression | null | undefined>>}
* @since 5.73.0
*/
evaluateNewExpression: new HookMap(
() => new SyncBailHook(["expression"])
),
/**
* @type {HookMap<SyncBailHook<[CallExpression], BasicEvaluatedExpression | null | undefined>>}
* @since 5.73.0
*/
evaluateCallExpression: new HookMap(
() => new SyncBailHook(["expression"])
),
/** @type {HookMap<SyncBailHook<[CallExpression, BasicEvaluatedExpression], BasicEvaluatedExpression | null | undefined>>} */
evaluateCallExpressionMember: new HookMap(
() => new SyncBailHook(["expression", "param"])
),
/** @type {HookMap<SyncBailHook<[Expression | Declaration | PrivateIdentifier | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration, number], boolean | void>>} */
isPure: new HookMap(
() => new SyncBailHook(["expression", "commentsStartPosition"])
),
/**
* Every statement the hoisting pass reads, which a tap on this turns
* back into a walk of them all. Tap `preStatementByType` instead.
* @type {SyncBailHook<[Statement | ModuleDeclaration | MaybeNamedClassDeclaration | MaybeNamedFunctionDeclaration], boolean | void>}
* @deprecated TODO in the next major release: remove, with the statement walk it turns on
*/
preStatement: new SyncBailHook(["statement"]),
/**
* Each declaration the parser records: a variable, function or class
* one. Any other type turns the pass back into a walk of every
* statement, and goes away with that walk in the next major release.
* @type {HookMap<SyncBailHook<[Statement | ModuleDeclaration | MaybeNamedClassDeclaration | MaybeNamedFunctionDeclaration], boolean | void>>}
* @since 5.109.0
*/
preStatementByType: new HookMap(() => new SyncBailHook(["statement"])),
/**
* Every statement the block pass reads, which a tap on this turns back
* into a walk of them all. Tap `blockPreStatementByType` instead.
* @type {SyncBailHook<[Statement | ModuleDeclaration | MaybeNamedClassDeclaration | MaybeNamedFunctionDeclaration], boolean | void>}
* @deprecated TODO in the next major release: remove, with the statement walk it turns on
*/
blockPreStatement: new SyncBailHook(["declaration"]),
/**
* Each statement the parser records for the block pass: a variable, class
* or export declaration. Any other type turns the pass back into a walk
* of every statement, and goes away with that walk in the next major release.
* @type {HookMap<SyncBailHook<[Statement | ModuleDeclaration | MaybeNamedClassDeclaration | MaybeNamedFunctionDeclaration], boolean | void>>}
* @since 5.109.0
*/
blockPreStatementByType: new HookMap(
() => new SyncBailHook(["declaration"])
),
/** @type {SyncBailHook<[Statement | ModuleDeclaration | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration], boolean | void>} */
statement: new SyncBailHook(["statement"]),
/** @type {SyncBailHook<[IfStatement], boolean | void>} */
statementIf: new SyncBailHook(["statement"]),
/**
* @type {SyncBailHook<[Expression], GuardCollection | void>}
* @since 5.105.0
*/
collectGuards: new SyncBailHook(["expression"]),
/** @type {SyncBailHook<[Expression, ClassExpression | ClassDeclaration | MaybeNamedClassDeclaration], boolean | void>} */
classExtendsExpression: new SyncBailHook([
"expression",
"classDefinition"
]),
/** @type {SyncBailHook<[MethodDefinition | PropertyDefinition | StaticBlock, ClassExpression | ClassDeclaration | MaybeNamedClassDeclaration], boolean | void>} */
classBodyElement: new SyncBailHook(["element", "classDefinition"]),
/**
* @type {SyncBailHook<[Expression, MethodDefinition | PropertyDefinition, ClassExpression | ClassDeclaration | MaybeNamedClassDeclaration], boolean | void>}
* @since 5.36.0
*/
classBodyValue: new SyncBailHook([
"expression",
"element",
"classDefinition"
]),
/** @type {HookMap<SyncBailHook<[LabeledStatement], boolean | void>>} */
label: new HookMap(() => new SyncBailHook(["statement"])),
/** @type {SyncBailHook<[ImportDeclaration, ImportSource], boolean | void>} */
import: new SyncBailHook(["statement", "source"]),
/** @type {SyncBailHook<[ImportDeclaration, ImportSource, string | null, string], boolean | void>} */
importSpecifier: new SyncBailHook([
"statement",
"source",
"exportName",
"identifierName"
]),
/** @type {SyncBailHook<[ExportDefaultDeclaration | ExportNamedDeclaration], boolean | void>} */
export: new SyncBailHook(["statement"]),
/** @type {SyncBailHook<[ExportNamedDeclaration | ExportAllDeclaration, ImportSource], boolean | void>} */
exportImport: new SyncBailHook(["statement", "source"]),
/** @type {SyncBailHook<[ExportDefaultDeclaration | ExportNamedDeclaration | ExportAllDeclaration, Declaration], boolean | void>} */
exportDeclaration: new SyncBailHook(["statement", "declaration"]),
/** @type {SyncBailHook<[ExportDefaultDeclaration, MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration | Expression], boolean | void>} */
exportExpression: new SyncBailHook(["statement", "node"]),
/** @type {SyncBailHook<[ExportDefaultDeclaration | ExportNamedDeclaration | ExportAllDeclaration, string, string, number | undefined], boolean | void>} */
exportSpecifier: new SyncBailHook([
"statement",
"identifierName",
"exportName",
"index"
]),
/** @type {SyncBailHook<[ExportNamedDeclaration | ExportAllDeclaration, ImportSource, string | null, string | null, number | undefined], boolean | void>} */
exportImportSpecifier: new SyncBailHook([
"statement",
"source",
"identifierName",
"exportName",
"index"
]),
/** @type {SyncBailHook<[VariableDeclarator, VariableDeclaration], boolean | void>} */
preDeclarator: new SyncBailHook(["declarator", "statement"]),
/** @type {SyncBailHook<[VariableDeclarator, Statement], boolean | void>} */
declarator: new SyncBailHook(["declarator", "statement"]),
/** @type {HookMap<SyncBailHook<[Identifier], boolean | void>>} */
varDeclaration: new HookMap(() => new SyncBailHook(["declaration"])),
/** @type {HookMap<SyncBailHook<[Identifier], boolean | void>>} */
varDeclarationLet: new HookMap(() => new SyncBailHook(["declaration"])),
/** @type {HookMap<SyncBailHook<[Identifier], boolean | void>>} */
varDeclarationConst: new HookMap(() => new SyncBailHook(["declaration"])),
/**
* @type {HookMap<SyncBailHook<[Identifier], boolean | void>>}
* @since 5.100.0
*/
varDeclarationUsing: new HookMap(() => new SyncBailHook(["declaration"])),
/** @type {HookMap<SyncBailHook<[Identifier], boolean | void>>} */
varDeclarationVar: new HookMap(() => new SyncBailHook(["declaration"])),
/** @type {HookMap<SyncBailHook<[Identifier], boolean | void>>} */
pattern: new HookMap(() => new SyncBailHook(["pattern"])),
/**
* @type {SyncBailHook<[Expression], boolean | void>}
* @since 5.101.3
*/
collectDestructuringAssignmentProperties: new SyncBailHook([
"expression"
]),
/** @type {HookMap<SyncBailHook<[Expression], boolean | void>>} */
canRename: new HookMap(() => new SyncBailHook(["initExpression"])),
/** @type {HookMap<SyncBailHook<[Expression], boolean | void>>} */
rename: new HookMap(() => new SyncBailHook(["initExpression"])),
/** @type {HookMap<SyncBailHook<[AssignmentExpression], boolean | void>>} */
assign: new HookMap(() => new SyncBailHook(["expression"])),
/** @type {HookMap<SyncBailHook<[WriteStatement], boolean | void>>} */
write: new HookMap(() => new SyncBailHook(["node"])),
/** @type {HookMap<SyncBailHook<[WriteStatement, Members], boolean | void>>} */
writeMemberChain: new HookMap(
() => new SyncBailHook(["node", "members"])
),
/** @type {HookMap<SyncBailHook<[AssignmentExpression, Members], boolean | void>>} */
assignMemberChain: new HookMap(
() => new SyncBailHook(["expression", "members"])
),
/** @type {HookMap<SyncBailHook<[Expression], boolean | void>>} */
typeof: new HookMap(() => new SyncBailHook(["expression"])),
/** @type {SyncBailHook<[ImportExpression, CallExpression?], boolean | void>} */
importCall: new SyncBailHook(["expression", "importThen"]),
/** @type {SyncBailHook<[Expression | ForOfStatement | VariableDeclaration], boolean | void>} */
topLevelAwait: new SyncBailHook(["expression"]),
/** @type {HookMap<SyncBailHook<[CallExpression], boolean | void>>} */
call: new HookMap(() => new SyncBailHook(["expression"])),
/** Something like "a.b()" */
/** @type {HookMap<SyncBailHook<[CallExpression, Members, MembersOptionals, MemberRanges], boolean | void>>} */
callMemberChain: new HookMap(
() =>
new SyncBailHook([
"expression",
"members",
"membersOptionals",
"memberRanges"
])
),
/** Something like "a.b().c.d" */
/** @type {HookMap<SyncBailHook<[Expression, CalleeMembers, CallExpression, Members, MemberRanges], boolean | void>>} */
memberChainOfCallMemberChain: new HookMap(
() =>
new SyncBailHook([
"expression",
"calleeMembers",
"callExpression",
"members",
"memberRanges"
])
),
/** Something like "a.b().c.d()"" */
/** @type {HookMap<SyncBailHook<[CallExpression, CalleeMembers, CallExpression, Members, MemberRanges], boolean | void>>} */
callMemberChainOfCallMemberChain: new HookMap(
() =>
new SyncBailHook([
"expression",
"calleeMembers",
"innerCallExpression",
"members",
"memberRanges"
])
),
/** @type {SyncBailHook<[ChainExpression], boolean | void>} */
optionalChaining: new SyncBailHook(["optionalChaining"]),
/** @type {HookMap<SyncBailHook<[NewExpression], boolean | void>>} */
new: new HookMap(() => new SyncBailHook(["expression"])),
/**
* @type {SyncBailHook<[BinaryExpression], boolean | void>}
* @since 5.71.0
*/
binaryExpression: new SyncBailHook(["binaryExpression"]),
/** @type {HookMap<SyncBailHook<[Expression], boolean | void>>} */
expression: new HookMap(() => new SyncBailHook(["expression"])),
/** @type {HookMap<SyncBailHook<[MemberExpression, Members, MembersOptionals, MemberRanges], boolean | void>>} */
expressionMemberChain: new HookMap(
() =>
new SyncBailHook([
"expression",
"members",
"membersOptionals",
"memberRanges"
])
),
/** @type {HookMap<SyncBailHook<[MemberExpression, Members], boolean | void>>} */
unhandledExpressionMemberChain: new HookMap(
() => new SyncBailHook(["expression", "members"])
),
/** @type {SyncBailHook<[ConditionalExpression], boolean | void>} */
expressionConditionalOperator: new SyncBailHook(["expression"]),
/** @type {SyncBailHook<[LogicalExpression], boolean | void>} */
expressionLogicalOperator: new SyncBailHook(["expression"]),
/** @type {SyncBailHook<[Program, Comment[]], boolean | void>} */
program: new SyncBailHook(["ast", "comments"]),
/**
* @type {SyncBailHook<[ThrowStatement | ReturnStatement], boolean | void>}
* @since 5.99.0
*/
terminate: new SyncBailHook(["statement"]),
/** @type {SyncBailHook<[Program, Comment[]], boolean | void>} */
finish: new SyncBailHook(["ast", "comments"]),
/**
* @type {SyncBailHook<[Statement], boolean | void>}
* @since 5.99.9
*/
unusedStatement: new SyncBailHook(["statement"])
});
this.sourceType = sourceType;
this.options = options;
/** @type {ScopeInfo} */
this.scope = /** @type {ScopeInfo} */ (
/** @type {ScopeInfo | undefined} */ (undefined)
);
/** @type {JavascriptParserState} */
this.state = /** @type {JavascriptParserState} */ (
/** @type {JavascriptParserState | undefined} */ (undefined)
);
/** @type {Comment[] | undefined} */
this.comments = undefined;
// ASI overrides keyed by statement-end offset: true forces an ASI
// position, false forces a real semicolon. Absent offsets are derived
// from the source text. Allocated lazily by set/unsetAsiPosition.
/** @type {Map<number, boolean> | undefined} */
this.semicolons = undefined;
/** @type {StatementPath | undefined} */
this.statementPath = undefined;
/** @type {Statement | ModuleDeclaration | Expression | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration | undefined} */
this.prevStatement = undefined;
/** @type {boolean} */
this._hasParserRecord = false;
// the statements of the scope last hoisted into, which its block pass reads next
/** @type {(Statement | ModuleDeclaration)[] | undefined} */
this._scopeStatements = undefined;
// BACKWARD-COMPAT START
// WHY: the hoisting pass reaches declarations alone, so a tap that
// expects every statement would quietly stop seeing most of them. Such a
// tap turns the pass back into the walk it used to be, which no build
// webpack's own plugins drive ever asks for.
/** @type {boolean} */
this._needsFullPreWalk = false;
this.hooks.preStatementByType.intercept({
factory: (type, hook) => {
if (!RECORDED_STATEMENT_TYPES.has(type)) {
this._needsFullPreWalk = true;
}
return hook;
}
});
// The same for the block pass, over the types the parser records for it.
/** @type {boolean} */
this._needsFullBlockPreWalk = false;
this.hooks.blockPreStatementByType.intercept({
factory: (type, hook) => {
if (!BLOCK_DECLARATION_TYPES.has(type)) {
this._needsFullBlockPreWalk = true;
}
return hook;
}
});
// BACKWARD-COMPAT END
/** @type {WeakMap<Expression, DestructuringAssignmentProperties> | undefined} */
this.destructuringAssignmentProperties = undefined;
/** @type {TagData | undefined} */
this.currentTagData = undefined;
// True while parsing a loose module whose code is emitted as strict ESM.
this._strictInModuleOutput = false;
// Source text of the current parse, for offset → line/column mapping.
/** @type {string | undefined} */
this._source = undefined;
// Offset of each line's first character, built on first getLocation use.
/** @type {number[] | undefined} */
this._lineStarts = undefined;
// The last member expression resolved, kept while the node and the scope
// it resolved in stay the same: the walk and an evaluate of one node meet
/** @type {Expression | Super | undefined} */
this._memberExpression = undefined;
/** @type {ScopeInfo | undefined} */
this._memberScope = undefined;
/** @type {number} */
this._memberMask = 0;
/** @type {0 | 1 | 2} */
this._memberType = 0;
/** @type {string} */
this._memberName = "";
/** @type {string | VariableInfo | undefined} */
this._memberRootInfo = undefined;
/** @type {Members} */
this._members = EMPTY_ARRAY;
/** @type {MembersOptionals} */
this._membersOptionals = EMPTY_MEMBERS_OPTIONALS;
/** @type {CallExpression | undefined} */
this._memberCall = undefined;
/** @type {string} */
this._memberCalleeName = "";
/** @type {CalleeMembers} */
this._memberCalleeMembers = EMPTY_ARRAY;
/** @type {CallExpressionInfo | ExpressionExpressionInfo | undefined} */
this._memberInfo = undefined;
// True while only the parser's own taps sit on evaluate.for("Identifier");
// recomputed per parse, gates the defined-identifier fast paths.
this._evalIdentOwnTaps = false;
this.magicCommentContext = createMagicCommentContext();
// Reused for enterPatterns on every scope entry to avoid per-scope closures.
/** @type {OnIdentString} */
this._defineVariable = (ident) => this.defineVariable(ident);
// The expression an identifier evaluator is running for, read by the
// shared `_evaluateDefinedIdentifier` instead of a closure per evaluation
/** @type {Identifier | ThisExpression | MemberExpression | undefined} */
this._evaluatingExpression = undefined;
/** @type {(name?: string) => BasicEvaluatedExpression | null | undefined} */
this._evaluateDefinedIdentifier = (name) => {
const hook = this.hooks.evaluateDefinedIdentifier.get(
/** @type {string} */ (name)
);
if (hook !== undefined) {
return hook.call(
/** @type {Identifier | ThisExpression | MemberExpression} */
(this._evaluatingExpression)
);
}
};
this._initializeEvaluating();
// keyed taps are free for every other member chain, and a shadowed
// `arguments` binding never dispatches the free-variable hooks
this.hooks.expressionMemberChain
.for("arguments")
.tap(CLASS_NAME, (expression, members) => {
if (this._strictInModuleOutput) {
this._checkStrictModeArgumentsMember(members[0], expression);
}
});
this.hooks.callMemberChain
.for("arguments")
.tap(CLASS_NAME, (expression, members) => {
// longer chains re-enter the member walk, which reports them
if (this._strictInModuleOutput && members.length === 1) {
this._checkStrictModeArgumentsMember(members[0], expression);
}
});
}
_initializeEvaluating() {
this.hooks.evaluate.for("Literal").tap(CLASS_NAME, (_expr) => {
const expr = /** @type {Literal} */ (_expr);
switch (typeof expr.value) {
case "number":
return new BasicEvaluatedExpression()
.setNumber(expr.value)
.setRange(/** @type {Range} */ (expr.range));
case "bigint":
return new BasicEvaluatedExpression()
.setBigInt(expr.value)
.setRange(/** @type {Range} */ (expr.range));
case "string":
return new BasicEvaluatedExpression()
.setString(expr.value)
.setRange(/** @type {Range} */ (expr.range));
case "boolean":
return new BasicEvaluatedExpression()
.setBoolean(expr.value)
.setRange(/** @type {Range} */ (expr.range));
}
if (expr.value === null) {
return new BasicEvaluatedExpression()
.setNull()
.setRange(/** @type {Range} */ (expr.range));
}
if (expr.value instanceof RegExp) {
return new BasicEvaluatedExpression()
.setRegExp(expr.value)
.setRange(/** @type {Range} */ (expr.range));
}
});
this.hooks.evaluate.for("NewExpression").tap(CLASS_NAME, (_expr) => {
const expr = /** @type {NewExpression} */ (_expr);
const callee = expr.callee;
if (callee.type !== "Identifier") return;
if (callee.name !== "RegExp") {
return this._callHooksForName1(
this.hooks.evaluateNewExpression,
callee.name,
expr
);
} else if (
expr.arguments.length > 2 ||
this.getVariableInfo("RegExp") !== "RegExp"
) {
return;
}
/** @type {undefined | string} */
let regExp;
const arg1 = expr.arguments[0];
if (arg1) {
if (arg1.type === "SpreadElement") return;
const evaluatedRegExp = this.evaluateExpression(arg1);
if (!evaluatedRegExp) return;
regExp = evaluatedRegExp.asString();
if (!regExp) return;
} else {
return (
new BasicEvaluatedExpression()
// eslint-disable-next-line prefer-regex-literals
.setRegExp(new RegExp(""))
.setRange(/** @type {Range} */ (expr.range))
);
}
/** @type {undefined | string} */
let flags;
const arg2 = expr.arguments[1];
if (arg2) {
if (arg2.type === "SpreadElement") return;
const evaluatedFlags = this.evaluateExpression(arg2);
if (!evaluatedFlags) return;
if (!evaluatedFlags.isUndefined()) {
flags = evaluatedFlags.asString();
if (
flags === undefined ||
!BasicEvaluatedExpression.isValidRegExpFlags(flags)
) {
return;
}
}
}
return new BasicEvaluatedExpression()
.setRegExp(flags ? new RegExp(regExp, flags) : new RegExp(regExp))
.setRange(/** @type {Range} */ (expr.range));
});
this.hooks.evaluate.for("LogicalExpression").tap(CLASS_NAME, (_expr) => {
const expr = /** @type {LogicalExpression} */ (_expr);
const left = this.evaluateExpression(expr.left);
let returnRight = false;
/** @type {boolean | undefined} */
let allowedRight;
if (expr.operator === "&&") {
const leftAsBool = left.asBool();
if (leftAsBool === false) {
return left.setRange(/** @type {Range} */ (expr.range));
}
returnRight = leftAsBool === true;
allowedRight = false;
} else if (expr.operator === "||") {
const leftAsBool = left.asBool();
if (leftAsBool === true) {
return left.setRange(/** @type {Range} */ (expr.range));
}
returnRight = leftAsBool === false;
allowedRight = true;
} else if (expr.operator === "??") {
const leftAsNullish = left.asNullish();
if (leftAsNullish === false) {
return left.setRange(/** @type {Range} */ (expr.range));
}
if (leftAsNullish !== true) return;
returnRight = true;
} else {
return;
}
const right = this.evaluateExpression(expr.right);
if (returnRight) {
if (left.couldHaveSideEffects()) right.setSideEffects();
return right.setRange(/** @type {Range} */ (expr.range));
}
const asBool = right.asBool();
if (allowedRight === true && asBool === true) {
return new BasicEvaluatedExpression()
.setRange(/** @type {Range} */ (expr.range))
.setTruthy();
} else if (allowedRight === false && asBool === false) {
return new BasicEvaluatedExpression()
.setRange(/** @type {Range} */ (expr.range))
.setFalsy();
}
});
this.hooks.evaluate.for("BinaryExpression").tap(CLASS_NAME, (_expr) => {
const expr = /** @type {BinaryExpression} */ (_expr);
if (expr.operator === "+") {
const left = this.evaluateExpression(expr.left);
const right = this.evaluateExpression(expr.right);
const res = new BasicEvaluatedExpression();
if (left.isString()) {
if (right.isString()) {
res.setString(
/** @type {string} */ (left.string) +
/** @type {string} */ (right.string)
);
} else if (right.isNumber()) {
res.setString(/** @type {string} */ (left.string) + right.number);
} else if (
right.isWrapped() &&
right.prefix &&
right.prefix.isString()
) {
// "left" + ("prefix" + inner + "postfix")
// => ("leftPrefix" + inner + "postfix")
res.setWrapped(
new BasicEvaluatedExpression()
.setString(
/** @type {string} */ (left.string) +
/** @type {string} */ (right.prefix.string)
)
.setRange(
joinRanges(
/** @type {Range} */ (left.range),
/** @type {Range} */ (right.prefix.range)
)
),
right.postfix,
right.wrappedInnerExpressions
);
} else if (right.isWrapped()) {
// "left" + ([null] + inner + "postfix")
// => ("left" + inner + "postfix")
res.setWrapped(left, right.postfix, right.wrappedInnerExpressions);
} else {
// "left" + expr
// => ("left" + expr + "")
res.setWrapped(left, null, [right]);
}
} else if (left.isNumber()) {
if (right.isString()) {
res.setString(left.number + /** @type {string} */ (right.string));
} else if (right.isNumber()) {
res.setNumber(
/** @type {number} */ (left.number) +
/** @type {number} */ (right.number)
);
} else {
return;
}
} else if (left.isBigInt()) {
if (right.isBigInt()) {
res.setBigInt(
/** @type {bigint} */ (left.bigint) +
/** @type {bigint} */ (right.bigint)
);
}
} else if (left.isWrapped()) {
if (left.postfix && left.postfix.isString() && right.isString()) {
// ("prefix" + inner + "postfix") + "right"
// => ("prefix" + inner + "postfixRight")
res.setWrapped(
left.prefix,
new BasicEvaluatedExpression()
.setString(
/** @type {string} */ (left.postfix.string) +
/** @type {string} */ (right.string)
)
.setRange(
joinRanges(
/** @type {Range} */ (left.postfix.range),
/** @type {Range} */ (right.range)
)
),
left.wrappedInnerExpressions
);
} else if (
left.postfix &&
left.postfix.isString() &&
right.isNumber()
) {
// ("prefix" + inner + "postfix") + 123
// => ("prefix" + inner + "postfix123")
res.setWrapped(
left.prefix,
new BasicEvaluatedExpression()
.setString(
/** @type {string} */ (left.postfix.string) +
/** @type {number} */ (right.number)
)
.setRange(
joinRanges(
/** @type {Range} */ (left.postfix.range),
/** @type {Range} */ (right.range)
)
),
left.wrappedInnerExpressions
);
} else if (right.isString()) {
// ("prefix" + inner + [null]) + "right"
// => ("prefix" + inner + "right")
res.setWrapped(left.prefix, right, left.wrappedInnerExpressions);
} else if (right.isNumber()) {
// ("prefix" + inner + [null]) + 123
// => ("prefix" + inner + "123")
res.setWrapped(
left.prefix,
new BasicEvaluatedExpression()
.setString(String(right.number))
.setRange(/** @type {Range} */ (right.range)),
left.wrappedInnerExpressions
);
} else if (right.isWrapped()) {
// ("prefix1" + inner1 + "postfix1") + ("prefix2" + inner2 + "postfix2")
// ("prefix1" + inner1 + "postfix1" + "prefix2" + inner2 + "postfix2")
res.setWrapped(
left.prefix,
right.postfix,
left.wrappedInnerExpressions &&
right.wrappedInnerExpressions && [
...left.wrappedInnerExpressions,
...(left.postfix ? [left.postfix] : []),
...(right.prefix ? [right.prefix] : []),
...right.wrappedInnerExpressions
]
);
} else {
// ("prefix" + inner + postfix) + expr
// => ("prefix" + inner + postfix + expr + [null])
res.setWrapped(
left.prefix,
null,
left.wrappedInnerExpressions && [
...left.wrappedInnerExpressions,
...(left.postfix ? [left.postfix, right] : [right])
]
);
}
} else if (right.isString()) {
// left + "right"
// => ([null] + left + "right")
res.setWrapped(null, right, [left]);
} else if (right.isWrapped()) {
// left + (prefix + inner + "postfix")
// => ([null] + left + prefix + inner + "postfix")
res.setWrapped(
null,
right.postfix,
right.wrappedInnerExpressions && [
...(right.prefix ? [left, right.prefix] : [left]),
...right.wrappedInnerExpressions
]
);
} else {
return;
}
if (left.couldHaveSideEffects() || right.couldHaveSideEffects()) {
res.setSideEffects();
}
res.setRange(/** @type {Range} */ (expr.range));
return res;
} else if (expr.operator === "-") {
return handleConstOperation(this, expr, SUBTRACT);
} else if (expr.operator === "*") {
return handleConstOperation(this, expr, MULTIPLY);
} else if (expr.operator === "/") {
return handleConstOperation(this, expr, DIVIDE);
} else if (expr.operator === "**") {
return handleConstOperation(this, expr, POWER);
} else if (expr.operator === "===") {
return handleStrictEqualityComparison(this, expr, true);
} else if (expr.operator === "==") {
return handleAbstractEqualityComparison(this, expr, true);
} else if (expr.operator === "!==") {
return handleStrictEqualityComparison(this, expr, false);
} else if (expr.operator === "!=") {
return handleAbstractEqualityComparison(this, expr, false);
} else if (expr.operator === "&") {
return handleConstOperation(this, expr, BITWISE_AND);
} else if (expr.operator === "|") {
return handleConstOperation(this, expr, BITWISE_OR);
} else if (expr.operator === "^") {
return handleConstOperation(this, expr, BITWISE_XOR);
} else if (expr.operator === ">>>") {
return handleConstOperation(this, expr, UNSIGNED_SHIFT_RIGHT);
} else if (expr.operator === ">>") {
return handleConstOperation(this, expr, SHIFT_RIGHT);
} else if (expr.operator === "<<") {
return handleConstOperation(this, expr, SHIFT_LEFT);
} else if (expr.operator === "<") {
return handleConstOperation(this, expr, LESS_THAN);
} else if (expr.operator === ">") {
return handleConstOperation(this, expr, GREATER_THAN);
} else if (expr.operator === "<=") {
return handleConstOperation(this, expr, LESS_THAN_OR_EQUAL);
} else if (expr.operator === ">=") {
return handleConstOperation(this, expr, GREATER_THAN_OR_EQUAL);
} else if (expr.operator === "in") {
// `x in y` always evaluates to a boolean, so it is never nullish
const left = this.evaluateExpression(expr.left);
const right = this.evaluateExpression(expr.right);
return new BasicEvaluatedExpression()
.setNullish(false)
.setSideEffects(
left.couldHaveSideEffects() || right.couldHaveSideEffects()
)
.setRange(/** @type {Range} */ (expr.range));
}
});
this.hooks.evaluate.for("UnaryExpression").tap(CLASS_NAME, (_expr) => {
const expr = /** @type {UnaryExpression} */ (_expr);
if (expr.operator === "typeof") {
switch (expr.argument.type) {
case "Identifier": {
const res = this._callHooksForName1(
this.hooks.evaluateTypeof,
expr.argument.name,
expr
);
if (res !== undefined) return res;
break;
}
case "MetaProperty": {
const res = this._callHooksForName1(
this.hooks.evaluateTypeof,
/** @type {string} */
(getRootName(expr.argument)),
expr
);
if (res !== undefined) return res;
break;
}
case "MemberExpression": {
const res = this.callHooksForExpression(
this.hooks.evaluateTypeof,
expr.argument,
expr
);
if (res !== undefined) return res;
break;
}
case "ChainExpression": {
const res = this.callHooksForExpression(
this.hooks.evaluateTypeof,
expr.argument.expression,
expr
);
if (res !== undefined) return res;
break;
}
case "FunctionExpression": {
return new BasicEvaluatedExpression()
.setString("function")
.setRange(/** @type {Range} */ (expr.range));
}
}
const arg = this.evaluateExpression(expr.argument);
if (arg.isUnknown()) return;
if (arg.isString()) {
return new BasicEvaluatedExpression()
.setString("string")
.setRange(/** @type {Range} */ (expr.range));
}
if (arg.isWrapped()) {
return new BasicEvaluatedExpression()
.setString("string")
.setSideEffects()
.setRange(/** @type {Range} */ (expr.range));
}
if (arg.isUndefined()) {
return new BasicEvaluatedExpression()
.setString("undefined")
.setRange(/** @type {Range} */ (expr.range));
}
if (arg.isNumber()) {
return new BasicEvaluatedExpression()
.setString("number")
.setRange(/** @type {Range} */ (expr.range));
}
if (arg.isBigInt()) {
return new BasicEvaluatedExpression()
.setString("bigint")
.setRange(/** @type {Range} */ (expr.range));
}
if (arg.isBoolean()) {
return new BasicEvaluatedExpression()
.setString("boolean")
.setRange(/** @type {Range} */ (expr.range));
}
if (arg.isConstArray() || arg.isRegExp() || arg.isNull()) {
return new BasicEvaluatedExpression()
.setString("object")
.setRange(/** @type {Range} */ (expr.range));
}
if (arg.isArray()) {
return new BasicEvaluatedExpression()
.setString("object")
.setSideEffects(arg.couldHaveSideEffects())
.setRange(/** @type {Range} */ (expr.range));
}
} else if (expr.operator === "!") {
const argument = this.evaluateExpression(expr.argument);
const bool = argument.asBool();
if (typeof bool !== "boolean") return;
return new BasicEvaluatedExpression()
.setBoolean(!bool)
.setSideEffects(argument.couldHaveSideEffects())
.setRange(/** @type {Range} */ (expr.range));
} else if (expr.operator === "~") {
return handleUnaryConstOperation(this, expr, BITWISE_NOT);
} else if (expr.operator === "+") {
return handleUnaryConstOperation(this, expr, TO_NUMBER);
} else if (expr.operator === "-") {
return handleUnaryConstOperation(this, expr, NEGATE);
}
});
this.hooks.evaluateTypeof
.for("undefined")
.tap(CLASS_NAME, (expr) =>
new BasicEvaluatedExpression()
.setString("undefined")
.setRange(/** @type {Range} */ (expr.range))
);
this.hooks.evaluate.for("Identifier").tap(CLASS_NAME, (expr) => {
if (/** @type {Identifier} */ (expr).name === "undefined") {
return new BasicEvaluatedExpression()
.setUndefined()
.setRange(/** @type {Range} */ (expr.range));
}
});
/**
* Tap evaluate with variable info.
* @param {"Identifier" | "ThisExpression" | "MemberExpression"} exprType expression type name
* @param {(node: Expression | SpreadElement) => GetInfoResult | undefined} getInfo get info
* @returns {void}
*/
const tapEvaluateWithVariableInfo = (exprType, getInfo) => {
/** @type {Expression | undefined} */
let cachedExpression;
/** @type {GetInfoResult | undefined} */
let cachedInfo;
this.hooks.evaluate.for(exprType).tap(CLASS_NAME, (expr) => {
const expression =
/** @type {Identifier | ThisExpression | MemberExpression} */ (expr);
const info = getInfo(expression);
// Cache the result (even when undefined) so the stage-100 tap below
// reuses it instead of recomputing getInfo — getMemberExpressionInfo
// is expensive and previously ran twice for every rejected member.
cachedExpression = expression;
cachedInfo = info;
if (info !== undefined) {
// saved around the call: a hook may evaluate another identifier
const outer = this._evaluatingExpression;
this._evaluatingExpression = expression;
const result = this.callHooksForInfoWithFallback(
this.hooks.evaluateIdentifier,
info.name,
RETURN_UNDEFINED,
this._evaluateDefinedIdentifier,
expression
);
this._evaluatingExpression = outer;
return result;
}
});
this.hooks.evaluate
.for(exprType)
.tap({ name: CLASS_NAME, stage: 100 }, (expr) => {
const expression =
/** @type {Identifier | ThisExpression | MemberExpression} */
(expr);
const info =
cachedExpression === expression ? cachedInfo : getInfo(expression);
if (info !== undefined) {
return new BasicEvaluatedExpression()
.setIdentifier(
info.name,
info.rootInfo,
info.getMembers,
info.getMembersOptionals,
info.getMemberRanges
)
.setRange(/** @type {Range} */ (expression.range));
}
});
this.hooks.finish.tap(CLASS_NAME, () => {
// Cleanup for GC
cachedExpression = cachedInfo = undefined;
});
};
tapEvaluateWithVariableInfo("Identifier", (expr) => {
const info = this.getVariableInfo(/** @type {Identifier} */ (expr).name);
if (
typeof info === "string" ||
(JavascriptParser.isVariableInfo(info) &&
(info.isFree() ||
info.isTagged() ||
// Walk `tagInfo` in `callHooksForInfo` if tagInfo is present.
info.tagInfo !== undefined))
) {
return {
name: info,
rootInfo: info,
getMembers: RETURN_EMPTY_ARRAY,
getMembersOptionals: RETURN_EMPTY_ARRAY,
getMemberRanges: RETURN_EMPTY_ARRAY
};
}
});
tapEvaluateWithVariableInfo("ThisExpression", (_expr) => {
const info = this.getVariableInfo("this");
if (
typeof info === "string" ||
(JavascriptParser.isVariableInfo(info) &&
(info.isFree() || info.isTagged()))
) {
return {
name: info,
rootInfo: info,
getMembers: RETURN_EMPTY_ARRAY,
getMembersOptionals: RETURN_EMPTY_ARRAY,
getMemberRanges: RETURN_EMPTY_ARRAY
};
}
});
this.hooks.evaluate.for("MetaProperty").tap(CLASS_NAME, (expr) => {
const metaProperty = /** @type {MetaProperty} */ (expr);
return this._callHooksForName1(
this.hooks.evaluateIdentifier,
/** @type {string} */
(getRootName(metaProperty)),
metaProperty
);
});
tapEvaluateWithVariableInfo("MemberExpression", (expr) =>
this.getMemberExpressionInfo(
/** @type {MemberExpression} */ (expr),
ALLOWED_MEMBER_TYPES_EXPRESSION
)
);
this.hooks.evaluate.for("CallExpression").tap(CLASS_NAME, (expression) => {
const expr = /** @type {CallExpression} */ (expression);
if (
expr.callee.type === "MemberExpression" &&
expr.callee.property.type ===
(expr.callee.computed ? "Literal" : "Identifier")
) {
// type Super also possible here
const param = this.evaluateExpression(
/** @type {Expression} */ (expr.callee.object)
);
const property =
expr.callee.property.type === "Literal"
? `${expr.callee.property.value}`
: expr.callee.property.name;
const hook = this.hooks.evaluateCallExpressionMember.get(property);
if (hook !== undefined) {
return hook.call(expr, param);
}
} else if (expr.callee.type === "Identifier") {
return this._callHooksForName1(
this.hooks.evaluateCallExpression,
expr.callee.name,
expr
);
}
});
this.hooks.evaluateCallExpressionMember
.for("indexOf")
.tap(CLASS_NAME, (expr, param) => {
if (!param.isString()) return;
if (expr.arguments.length === 0) return;
const [arg1, arg2] = expr.arguments;
if (arg1.type === "SpreadElement") return;
const arg1Eval = this.evaluateExpression(arg1);
if (!arg1Eval.isString()) return;
const arg1Value = /** @type {string} */ (arg1Eval.string);
/** @type {number} */
let result;
if (arg2) {
if (arg2.type === "SpreadElement") return;
const arg2Eval = this.evaluateExpression(arg2);
if (!arg2Eval.isNumber()) return;
result = /** @type {string} */ (param.string).indexOf(
arg1Value,
arg2Eval.number
);
} else {
result = /** @type {string} */ (param.string).indexOf(arg1Value);
}
return new BasicEvaluatedExpression()
.setNumber(result)
.setSideEffects(param.couldHaveSideEffects())
.setRange(/** @type {Range} */ (expr.range));
});
this.hooks.evaluateCallExpressionMember
.for("replace")
.tap(CLASS_NAME, (expr, param) => {
if (!param.isString()) return;
if (expr.arguments.length !== 2) return;
if (expr.arguments[0].type === "SpreadElement") return;
if (expr.arguments[1].type === "SpreadElement") return;
const arg1 = this.evaluateExpression(expr.arguments[0]);
const arg2 = this.evaluateExpression(expr.arguments[1]);
if (!arg1.isString() && !arg1.isRegExp()) return;
const arg1Value = /** @type {string | RegExp} */ (
arg1.regExp || arg1.string
);
if (!arg2.isString()) return;
const arg2Value = /** @type {string} */ (arg2.string);
return new BasicEvaluatedExpression()
.setString(
/** @type {string} */ (param.string).replace(arg1Value, arg2Value)
)
.setSideEffects(param.couldHaveSideEffects())
.setRange(/** @type {Range} */ (expr.range));
});
for (const fn of ["substr", "substring", "slice"]) {
this.hooks.evaluateCallExpressionMember
.for(fn)
.tap(CLASS_NAME, (expr, param) => {
if (!param.isString()) return;
/** @type {BasicEvaluatedExpression} */
let arg1;
/** @type {string} */
let result;
const str = /** @type {string} */ (param.string);
switch (expr.arguments.length) {
case 1:
if (expr.arguments[0].type === "SpreadElement") return;
arg1 = this.evaluateExpression(expr.arguments[0]);
if (!arg1.isNumber()) return;
result = str[
/** @type {"substr" | "substring" | "slice"} */ (fn)
](/** @type {number} */ (arg1.number));
break;
case 2: {
if (expr.arguments[0].type === "SpreadElement") return;
if (expr.arguments[1].type === "SpreadElement") return;
arg1 = this.evaluateExpression(expr.arguments[0]);
const arg2 = this.evaluateExpression(expr.arguments[1]);
if (!arg1.isNumber()) return;
if (!arg2.isNumber()) return;
result = str[
/** @type {"substr" | "substring" | "slice"} */ (fn)
](
/** @type {number} */ (arg1.number),
/** @type {number} */ (arg2.number)
);
break;
}
default:
return;
}
return new BasicEvaluatedExpression()
.setString(result)
.setSideEffects(param.couldHaveSideEffects())
.setRange(/** @type {Range} */ (expr.range));
});
}
/**
* Gets simplified template result.
* @param {"cooked" | "raw"} kind kind of values to get
* @param {TemplateLiteral} templateLiteralExpr TemplateLiteral expr
* @returns {{ quasis: BasicEvaluatedExpression[], parts: BasicEvaluatedExpression[] }} Simplified template
*/
const getSimplifiedTemplateResult = (kind, templateLiteralExpr) => {
/** @type {BasicEvaluatedExpression[]} */
const quasis = [];
/** @type {BasicEvaluatedExpression[]} */
const parts = [];
for (let i = 0; i < templateLiteralExpr.quasis.length; i++) {
const quasiExpr = templateLiteralExpr.quasis[i];
const quasi = quasiExpr.value[kind];
if (i > 0) {
const prevExpr = parts[parts.length - 1];
const expr = this.evaluateExpression(
templateLiteralExpr.expressions[i - 1]
);
const exprAsString = expr.asString();
if (
typeof exprAsString === "string" &&
!expr.couldHaveSideEffects()
) {
// We can merge quasi + expr + quasi when expr
// is a const string
prevExpr.setString(prevExpr.string + exprAsString + quasi);
prevExpr.setRange([
/** @type {Range} */ (prevExpr.range)[0],
/** @type {Range} */ (quasiExpr.range)[1]
]);
// We unset the expression as it doesn't match to a single expression
prevExpr.setExpression(undefined);
continue;
}
parts.push(expr);
}
const part = new BasicEvaluatedExpression()
.setString(/** @type {string} */ (quasi))
.setRange(/** @type {Range} */ (quasiExpr.range))
.setExpression(quasiExpr);
quasis.push(part);
parts.push(part);
}
return {
quasis,
parts
};
};
this.hooks.evaluate.for("TemplateLiteral").tap(CLASS_NAME, (_node) => {
const node = /** @type {TemplateLiteral} */ (_node);
const { quasis, parts } = getSimplifiedTemplateResult("cooked", node);
if (parts.length === 1) {
return parts[0].setRange(/** @type {Range} */ (node.range));
}
return new BasicEvaluatedExpression()
.setTemplateString(quasis, parts, "cooked")
.setRange(/** @type {Range} */ (node.range));
});
this.hooks.evaluate
.for("TaggedTemplateExpression")
.tap(CLASS_NAME, (_node) => {
const node = /** @type {TaggedTemplateExpression} */ (_node);
const tag = this.evaluateExpression(node.tag);
if (tag.isIdentifier() && tag.identifier === "String.raw") {
const { quasis, parts } = getSimplifiedTemplateResult(
"raw",
node.quasi
);
return new BasicEvaluatedExpression()
.setTemplateString(quasis, parts, "raw")
.setRange(/** @type {Range} */ (node.range));
}
});
this.hooks.evaluateCallExpressionMember
.for("concat")
.tap(CLASS_NAME, (expr, param) => {
if (!param.isString() && !param.isWrapped()) return;
/** @type {undefined | BasicEvaluatedExpression} */
let stringSuffix;
let hasUnknownParams = false;
/** @type {BasicEvaluatedExpression[]} */
const innerExpressions = [];
for (let i = expr.arguments.length - 1; i >= 0; i--) {
const arg = expr.arguments[i];
if (arg.type === "SpreadElement") return;
const argExpr = this.evaluateExpression(arg);
if (
hasUnknownParams ||
(!argExpr.isString() && !argExpr.isNumber())
) {
hasUnknownParams = true;
innerExpressions.push(argExpr);
continue;
}
const value = argExpr.isString()
? /** @type {string} */ (argExpr.string)
: String(argExpr.number);
/** @type {string} */
const newString =
value +
(stringSuffix ? /** @type {string} */ (stringSuffix.string) : "");
const newRange = /** @type {Range} */ ([
/** @type {Range} */ (argExpr.range)[0],
/** @type {Range} */ ((stringSuffix || argExpr).range)[1]
]);
stringSuffix = new BasicEvaluatedExpression()
.setString(newString)
.setSideEffects(
(stringSuffix && stringSuffix.couldHaveSideEffects()) ||
argExpr.couldHaveSideEffects()
)
.setRange(newRange);
}
if (hasUnknownParams) {
const prefix = param.isString() ? param : param.prefix;
const inner =
param.isWrapped() && param.wrappedInnerExpressions
? [
...param.wrappedInnerExpressions,
...innerExpressions.reverse()
]
: innerExpressions.reverse();
return new BasicEvaluatedExpression()
.setWrapped(prefix, stringSuffix, inner)
.setRange(/** @type {Range} */ (expr.range));
} else if (param.isWrapped()) {
const postfix = stringSuffix || param.postfix;
const inner = param.wrappedInnerExpressions
? [...param.wrappedInnerExpressions, ...innerExpressions.reverse()]
: innerExpressions.reverse();
return new BasicEvaluatedExpression()
.setWrapped(param.prefix, postfix, inner)
.setRange(/** @type {Range} */ (expr.range));
}
const newString =
/** @type {string} */ (param.string) +
(stringSuffix ? stringSuffix.string : "");
return new BasicEvaluatedExpression()
.setString(newString)
.setSideEffects(
(stringSuffix && stringSuffix.couldHaveSideEffects()) ||
param.couldHaveSideEffects()
)
.setRange(/** @type {Range} */ (expr.range));
});
this.hooks.evaluateCallExpressionMember
.for("split")
.tap(CLASS_NAME, (expr, param) => {
if (!param.isString()) return;
if (expr.arguments.length !== 1) return;
if (expr.arguments[0].type === "SpreadElement") return;
/** @type {string[]} */
let result;
const arg = this.evaluateExpression(expr.arguments[0]);
if (arg.isString()) {
result =
/** @type {string} */
(param.string).split(/** @type {string} */ (arg.string));
} else if (arg.isRegExp()) {
result = /** @type {string} */ (param.string).split(
/** @type {RegExp} */ (arg.regExp)
);
} else {
return;
}
return new BasicEvaluatedExpression()
.setArray(result)
.setSideEffects(param.couldHaveSideEffects())
.setRange(/** @type {Range} */ (expr.range));
});
this.hooks.evaluate
.for("ConditionalExpression")
.tap(CLASS_NAME, (_expr) => {
const expr = /** @type {ConditionalExpression} */ (_expr);
const condition = this.evaluateExpression(expr.test);
const conditionValue = condition.asBool();
/** @type {BasicEvaluatedExpression} */
let res;
if (conditionValue === undefined) {
const consequent = this.evaluateExpression(expr.consequent);
const alternate = this.evaluateExpression(expr.alternate);
res = new BasicEvaluatedExpression();
if (consequent.isConditional()) {
res.setOptions(
/** @type {BasicEvaluatedExpression[]} */ (consequent.options)
);
} else {
res.setOptions([consequent]);
}
if (alternate.isConditional()) {
res.addOptions(
/** @type {BasicEvaluatedExpression[]} */ (alternate.options)
);
} else {
res.addOptions([alternate]);
}
} else {
res = this.evaluateExpression(
conditionValue ? expr.consequent : expr.alternate
);
if (condition.couldHaveSideEffects()) res.setSideEffects();
}
res.setRange(/** @type {Range} */ (expr.range));
return res;
});
this.hooks.evaluate.for("ArrayExpression").tap(CLASS_NAME, (_expr) => {
const expr = /** @type {ArrayExpression} */ (_expr);
const items = expr.elements.map(
(element) =>
element !== null &&
element.type !== "SpreadElement" &&
this.evaluateExpression(element)
);
if (!items.every(Boolean)) return;
return new BasicEvaluatedExpression()
.setItems(/** @type {BasicEvaluatedExpression[]} */ (items))
.setRange(/** @type {Range} */ (expr.range));
});
this.hooks.evaluate.for("ChainExpression").tap(CLASS_NAME, (_expr) => {
const expr = /** @type {ChainExpression} */ (_expr);
/** @type {Expression[]} */
const optionalExpressionsStack = [];
/** @type {Expression | Super} */
let next = expr.expression;
while (
next.type === "MemberExpression" ||
next.type === "CallExpression"
) {
if (next.type === "MemberExpression") {
if (next.optional) {
// SuperNode can not be optional
optionalExpressionsStack.push(
/** @type {Expression} */ (next.object)
);
}
next = next.object;
} else {
if (next.optional) {
// SuperNode can not be optional
optionalExpressionsStack.push(
/** @type {Expression} */ (next.callee)
);
}
next = next.callee;
}
}
while (optionalExpressionsStack.length > 0) {
const expression =
/** @type {Expression} */
(optionalExpressionsStack.pop());
const evaluated = this.evaluateExpression(expression);
if (evaluated.asNullish()) {
return evaluated.setRange(/** @type {Range} */ (_expr.range));
}
}
return this.evaluateExpression(expr.expression);
});
this.hooks.evaluate.for("SequenceExpression").tap(CLASS_NAME, (_expr) => {
const expr = /** @type {SequenceExpression} */ (_expr);
if (!expr.range) return;
let commentsStartPos = /** @type {Range} */ (expr.range)[0];
for (let i = 0; i < expr.expressions.length - 1; i++) {
const item = expr.expressions[i];
if (!item.range) return;
if (!this.isPure(item, commentsStartPos)) return;
commentsStartPos = /** @type {Range} */ (item.range)[1];
}
const last = expr.expressions[expr.expressions.length - 1];
const evaluated = this.evaluateExpression(last);
if (!evaluated.isCompileTimeValue()) return;
return evaluated.setRange(/** @type {Range} */ (expr.range));
});
}
/**
* Destructuring assignment properties for.
* @param {Expression} node node
* @returns {DestructuringAssignmentProperties | undefined} destructured identifiers
*/
destructuringAssignmentPropertiesFor(node) {
if (!this.destructuringAssignmentProperties) return;
return this.destructuringAssignmentProperties.get(node);
}
/**
* Gets rename identifier.
* @param {Expression | SpreadElement} expr expression
* @returns {string | VariableInfo | undefined} identifier
*/
getRenameIdentifier(expr) {
if (
expr.type === "Identifier" &&
this._evalIdentOwnTaps &&
expr.name !== "undefined" &&
this._isDefinedPlainVariable(expr.name)
) {
// the evaluation could only fall through — never an identifier result
return;
}
const result = this.evaluateExpression(expr);
if (result.isIdentifier()) {
return result.identifier;
}
}
/**
* Processes the provided classy.
* @param {ClassExpression | ClassDeclaration | MaybeNamedClassDeclaration} classy a class node
* @returns {void}
*/
walkClass(classy) {
if (
classy.superClass &&
!this.hooks.classExtendsExpression.call(classy.superClass, classy)
) {
this.walkExpression(classy.superClass);
}
if (classy.body && classy.body.type === "ClassBody") {
/** @type {Identifier[]} */
const scopeParams = [];
// Add class name in scope for recursive calls
if (classy.id) {
scopeParams.push(classy.id);
}
this.inClassScope(true, scopeParams, () => {
for (const classElement of classy.body.body) {
if (!this.hooks.classBodyElement.call(classElement, classy)) {
if (classElement.type === "StaticBlock") {
const wasTopLevel = this.scope.topLevelScope;
this.scope.topLevelScope = false;
this.walkBlockStatement(classElement);
this.scope.topLevelScope = wasTopLevel;
} else {
if (classElement.computed && classElement.key) {
this.walkExpression(classElement.key);
}
if (
classElement.value &&
!this.hooks.classBodyValue.call(
classElement.value,
classElement,
classy
)
) {
const wasTopLevel = this.scope.topLevelScope;
this.scope.topLevelScope = false;
this.walkExpression(classElement.value);
this.scope.topLevelScope = wasTopLevel;
}
}
}
}
});
}
}
/**
* Report what the program imports and re-exports, before anything reads a
* name. The list is what the parser recorded, in source order.
* @param {ModuleDeclarations} declarations the program's module declarations
*/
_modulePreWalkDeclarations(declarations) {
for (let index = 0, len = declarations.length; index < len; index += 2) {
const statement =
/** @type {ModuleDeclaration} */
(declarations[index]);
// what precedes it decides whether removing it joins two statements
this.prevStatement =
/** @type {Statement | undefined} */
(declarations[index + 1]);
/** @type {StatementPath} */
(this.statementPath).push(statement);
switch (statement.type) {
case "ImportDeclaration":
this.modulePreWalkImportDeclaration(statement);
break;
case "ExportAllDeclaration":
this.modulePreWalkExportAllDeclaration(statement);
break;
case "ExportNamedDeclaration":
this.modulePreWalkExportNamedDeclaration(statement);
break;
}
this.prevStatement =
/** @type {StatementPath} */
(this.statementPath).pop();
}
}
/**
* Declare what the scope the node opens hoists, before anything reads a
* name in it.
* @param {Program | BlockStatement} node the node owning the scope
*/
_preWalkScope(node) {
this._scopeStatements = node.body;
// BACKWARD-COMPAT START
if (this._needsFullPreWalk || this.hooks.preStatement.taps.length !== 0) {
if (node.type === "Program") this.preWalkStatements(node.body);
else this.preWalkStatement(node);
return;
}
// BACKWARD-COMPAT END
this._preWalkHoistedDeclarations(
hoistedDeclarationsOf(node, this._hasParserRecord)
);
}
/**
* Declare what hoists to the scope being entered, from what the parser
* recorded while reading it. `prevStatement` is not available here.
* @param {HoistedDeclarations} hoisted what the scope declares
*/
_preWalkHoistedDeclarations(hoisted) {
for (let index = 0, len = hoisted.length; index < len; index++) {
// WHY: the statements between two declarations are not read here, so
// the one before this declaration is unknown rather than the previous
// declaration. A tap needing it taps `preStatement`, which walks.
this.prevStatement = undefined;
this.preWalkStatement(hoisted[index]);
}
}
/**
* Whether a function declaration stays in its block rather than hoisting to
* the scope last pre-walked, which only sloppy-mode Annex B lets it do.
* @param {FunctionDeclaration | MaybeNamedFunctionDeclaration} statement function declaration
* @returns {boolean} whether its block, not the scope, binds it
*/
_isBlockScopedFunction(statement) {
const statements = this._scopeStatements;
// WHY: strictness is read here, not in the parser: `javascript/auto`
// reads a script as a strict module first.
return (
statements !== undefined &&
(this.scope.isStrict ||
statement.generator === true ||
statement.async === true) &&
!holdsStatement(statements, statement)
);
}
// BACKWARD-COMPAT START
/**
* Walk every statement of a scope to declare what hoists to it. Only a tap
* on the deprecated `preStatement` hook asks for this.
* @param {(Statement | ModuleDeclaration)[]} statements statements
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkStatements(statements) {
for (let index = 0, len = statements.length; index < len; index++) {
this.preWalkStatement(statements[index]);
}
}
/**
* Pre walk block statement.
* @param {BlockStatement} statement block statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkBlockStatement(statement) {
this.preWalkStatements(statement.body);
}
/**
* Pre walk if statement.
* @param {IfStatement} statement if statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkIfStatement(statement) {
this.preWalkStatement(statement.consequent);
if (statement.alternate) {
this.preWalkStatement(statement.alternate);
}
}
/**
* Pre walk labeled statement.
* @param {LabeledStatement} statement labeled statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkLabeledStatement(statement) {
this.preWalkStatement(statement.body);
}
/**
* Pre walk with statement.
* @param {WithStatement} statement with statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkWithStatement(statement) {
this.preWalkStatement(statement.body);
}
/**
* Pre walk switch statement.
* @param {SwitchStatement} statement switch statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkSwitchStatement(statement) {
this.preWalkSwitchCases(statement.cases);
}
/**
* Pre walk switch cases.
* @param {SwitchCase[]} switchCases switch cases
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkSwitchCases(switchCases) {
for (let index = 0, len = switchCases.length; index < len; index++) {
this.preWalkStatements(switchCases[index].consequent);
}
}
/**
* Pre walk try statement.
* @param {TryStatement} statement try statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkTryStatement(statement) {
this.preWalkStatement(statement.block);
if (statement.handler) this.preWalkCatchClause(statement.handler);
if (statement.finalizer) this.preWalkStatement(statement.finalizer);
}
/**
* Pre walk catch clause.
* @param {CatchClause} catchClause catch clause
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkCatchClause(catchClause) {
this.preWalkStatement(catchClause.body);
}
/**
* Pre walk while statement.
* @param {WhileStatement} statement while statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkWhileStatement(statement) {
this.preWalkStatement(statement.body);
}
/**
* Pre walk do while statement.
* @param {DoWhileStatement} statement do while statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkDoWhileStatement(statement) {
this.preWalkStatement(statement.body);
}
/**
* Pre walk for statement.
* @param {ForStatement} statement for statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkForStatement(statement) {
if (statement.init && statement.init.type === "VariableDeclaration") {
this.preWalkStatement(statement.init);
}
this.preWalkStatement(statement.body);
}
/**
* Pre walk for in statement.
* @param {ForInStatement} statement for in statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkForInStatement(statement) {
if (statement.left.type === "VariableDeclaration") {
this.preWalkStatement(statement.left);
}
this.preWalkStatement(statement.body);
}
/**
* Pre walk for of statement. `for await` is reported by the walk, not here.
* @param {ForOfStatement} statement for of statement
* @deprecated TODO in the next major release: remove, with the `preStatement` hook
*/
preWalkForOfStatement(statement) {
if (statement.left.type === "VariableDeclaration") {
this.preWalkStatement(statement.left);
}
this.preWalkStatement(statement.body);
}
/**
* Scan every top-level statement for the program's module declarations.
* The parser records them now, so nothing inside webpack scans for them.
* @deprecated TODO in the next major release: remove
* @param {(Statement | ModuleDeclaration)[]} statements statements
*/
modulePreWalkStatements(statements) {
/** @type {ModuleDeclarations} */
const declarations = [];
for (let index = 0, len = statements.length; index < len; index++) {
const statement = statements[index];
if (MODULE_STATEMENT_TYPES.has(statement.type)) {
declarations.push(
statement,
index === 0 ? undefined : statements[index - 1]
);
}
}
this._modulePreWalkDeclarations(declarations);
}
// BACKWARD-COMPAT END
// BACKWARD-COMPAT START
/**
* Walk every statement of a list to declare what it binds. Only a tap on the
* deprecated `blockPreStatement` hook asks for this.
* @param {(Statement | ModuleDeclaration)[]} statements statements
* @deprecated TODO in the next major release: remove, with the `blockPreStatement` hook
*/
blockPreWalkStatements(statements) {
for (let index = 0, len = statements.length; index < len; index++) {
const statement = statements[index];
this.blockPreWalkStatement(statement);
}
}
// BACKWARD-COMPAT END
/**
* Declare what the statement list binds into its own block scope, before any
* of it is walked. The list is what the parser recorded while reading it.
* @param {Program | BlockStatement | StaticBlock | SwitchCase} node the node owning the list
* @param {(Statement | ModuleDeclaration)[]} statements the list it holds
*/
_blockPreWalkScope(node, statements) {
// BACKWARD-COMPAT START
if (
this._needsFullBlockPreWalk ||
this.hooks.blockPreStatement.taps.length !== 0
) {
this.blockPreWalkStatements(statements);
return;
}
// BACKWARD-COMPAT END
/** @type {BlockDeclarations | undefined} */
let recorded;
if (this._hasParserRecord) {
recorded =
/** @type {typeof node & { [BLOCK_DECLARATIONS]?: BlockDeclarations }} */
(node)[BLOCK_DECLARATIONS];
// 72.8% of the lists a build reads declare nothing, and saying so is
// the whole pass for them
if (recorded === 0) return;
}
if (recorded === undefined) {
recorded = collectBlockDeclarations(statements);
if (recorded === 0) return;
}
this._blockPreWalkDeclarations(statements, recorded);
}
/**
* Declare what the recorded statements of the list bind, in source order.
* @param {(Statement | ModuleDeclaration)[]} statements the whole list
* @param {BlockDeclarations} declarations which of them declare
*/
_blockPreWalkDeclarations(statements, declarations) {
if (typeof declarations === "number") {
for (let bits = declarations; bits !== 0; bits &= bits - 1) {
this._blockPreWalkAt(statements, 31 - Math.clz32(bits & -bits));
}
return;
}
for (let index = 0, len = declarations.length; index < len; index++) {
this._blockPreWalkAt(statements, declarations[index]);
}
}
/**
* Declare what one statement of the list binds, with the statement written
* before it, so a tap reading `prevStatement` sees what the walk would show.
* @param {(Statement | ModuleDeclaration)[]} statements the whole list
* @param {number} index which statement declares
*/
_blockPreWalkAt(statements, index) {
// the first statement of the list has nothing before it, and leaves
// whatever the caller set standing, as walking the whole list did
if (index !== 0) this.prevStatement = statements[index - 1];
this.blockPreWalkStatement(statements[index]);
}
/**
* Walking iterates the statements and expressions and processes them
* @param {(Statement | ModuleDeclaration)[]} statements statements
*/
walkStatements(statements) {
let onlyFunctionDeclaration = false;
for (let index = 0, len = statements.length; index < len; index++) {
const statement = statements[index];
if (
onlyFunctionDeclaration &&
statement.type !== "FunctionDeclaration" &&
this.hooks.unusedStatement.call(/** @type {Statement} */ (statement))
) {
continue;
}
this.walkStatement(statement);
if (this.scope.terminated) {
onlyFunctionDeclaration = true;
}
}
}
/**
* Walking iterates the statements and expressions and processes them
* @param {Statement | ModuleDeclaration | MaybeNamedClassDeclaration | MaybeNamedFunctionDeclaration} statement statement
*/
preWalkStatement(statement) {
/** @type {StatementPath} */
(this.statementPath).push(statement);
const { preStatement, preStatementByType } = this.hooks;
// skip the broadcast entirely when nothing taps it; type-keyed taps
// only run for their statement type
let bail =
preStatement.taps.length !== 0 ? preStatement.call(statement) : undefined;
if (!bail) {
// WHY: a block-scoped function is the block pass's; the full pre-walk
// still shows it to `preStatement`, but not to the typed hooks, which
// would declare it in the scope (`InnerGraphPlugin` tags it top-level).
if (
statement.type === "FunctionDeclaration" &&
this._isBlockScopedFunction(statement)
) {
bail = true;
} else {
const typed = preStatementByType.get(statement.type);
if (typed !== undefined) bail = typed.call(statement);
}
}
if (bail) {
this.prevStatement =
/** @type {StatementPath} */
(this.statementPath).pop();
return;
}
// a class declaration reaches here for the typed hook alone: defining it
// is the block pass's. The rest are only walked into by the deprecated
// statement walk, which a tap expecting every statement turns back on.
switch (statement.type) {
case "VariableDeclaration":
this.preWalkVariableDeclaration(statement);
break;
case "FunctionDeclaration":
this.preWalkFunctionDeclaration(statement);
break;
case "IfStatement":
this.preWalkIfStatement(statement);
break;
case "BlockStatement":
this.preWalkBlockStatement(statement);
break;
case "ForStatement":
this.preWalkForStatement(statement);
break;
case "TryStatement":
this.preWalkTryStatement(statement);
break;
case "SwitchStatement":
this.preWalkSwitchStatement(statement);
break;
case "WhileStatement":
this.preWalkWhileStatement(statement);
break;
case "ForInStatement":
this.preWalkForInStatement(statement);
break;
case "ForOfStatement":
this.preWalkForOfStatement(statement);
break;
case "DoWhileStatement":
this.preWalkDoWhileStatement(statement);
break;
case "LabeledStatement":
this.preWalkLabeledStatement(statement);
break;
case "WithStatement":
this.preWalkWithStatement(statement);
break;
}
this.prevStatement =
/** @type {StatementPath} */
(this.statementPath).pop();
}
/**
* Block pre walk statement.
* @param {Statement | ModuleDeclaration | MaybeNamedClassDeclaration | MaybeNamedFunctionDeclaration} statement statement
*/
blockPreWalkStatement(statement) {
/** @type {StatementPath} */
(this.statementPath).push(statement);
const { blockPreStatement, blockPreStatementByType } = this.hooks;
let bail =
blockPreStatement.taps.length !== 0
? blockPreStatement.call(statement)
: undefined;
if (!bail) {
const typed = blockPreStatementByType.get(statement.type);
if (typed !== undefined) bail = typed.call(statement);
}
if (bail) {
this.prevStatement =
/** @type {StatementPath} */
(this.statementPath).pop();
return;
}
// cases ordered by measured statement frequency
switch (statement.type) {
case "VariableDeclaration":
this.blockPreWalkVariableDeclaration(statement);
break;
case "ExpressionStatement":
this.blockPreWalkExpressionStatement(statement);
break;
case "ClassDeclaration":
this.blockPreWalkClassDeclaration(statement);
break;
case "FunctionDeclaration":
this.blockPreWalkFunctionDeclaration(statement);
break;
case "ExportNamedDeclaration":
this.blockPreWalkExportNamedDeclaration(statement);
break;
case "ExportDefaultDeclaration":
this.blockPreWalkExportDefaultDeclaration(statement);
}
this.prevStatement =
/** @type {StatementPath} */
(this.statementPath).pop();
}
/**
* Processes the provided statement.
* @param {Statement | ModuleDeclaration | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration} statement statement
*/
walkStatement(statement) {
/** @type {StatementPath} */
(this.statementPath).push(statement);
const { statement: statementHook } = this.hooks;
if (
statementHook.taps.length !== 0 &&
statementHook.call(statement) !== undefined
) {
this.prevStatement =
/** @type {StatementPath} */
(this.statementPath).pop();
return;
}
// cases ordered by measured statement frequency; V8 compiles this to
// sequential compares, so hot types must come first
switch (statement.type) {
case "ExpressionStatement":
this.walkExpressionStatement(statement);
break;
case "VariableDeclaration":
this.walkVariableDeclaration(statement);
break;
case "ReturnStatement":
this.walkReturnStatement(statement);
break;
case "IfStatement":
this.walkIfStatement(statement);
break;
case "BlockStatement":
this.walkBlockStatement(statement);
break;
case "FunctionDeclaration":
this.walkFunctionDeclaration(statement);
break;
case "ForStatement":
this.walkForStatement(statement);
break;
case "TryStatement":
this.walkTryStatement(statement);
break;
case "SwitchStatement":
this.walkSwitchStatement(statement);
break;
case "ThrowStatement":
this.walkThrowStatement(statement);
break;
case "WhileStatement":
this.walkWhileStatement(statement);
break;
case "ClassDeclaration":
this.walkClassDeclaration(statement);
break;
case "ForInStatement":
this.walkForInStatement(statement);
break;
case "ForOfStatement":
this.walkForOfStatement(statement);
break;
case "DoWhileStatement":
this.walkDoWhileStatement(statement);
break;
case "ExportNamedDeclaration":
this.walkExportNamedDeclaration(statement);
break;
case "ExportDefaultDeclaration":
this.walkExportDefaultDeclaration(statement);
break;
case "LabeledStatement":
this.walkLabeledStatement(statement);
break;
case "WithStatement":
this.walkWithStatement(statement);
break;
}
this.prevStatement =
/** @type {StatementPath} */
(this.statementPath).pop();
}
/**
* Walks a statements that is nested within a parent statement
* and can potentially be a non-block statement.
* This enforces the nested statement to never be in ASI position.
* @param {Statement} statement the nested statement
*/
walkNestedStatement(statement) {
this.prevStatement = undefined;
this.walkStatement(statement);
}
// Real Statements
/**
* Walk block statement.
* @param {BlockStatement | StaticBlock} statement block statement
*/
walkBlockStatement(statement) {
const oldScope = this._enterBlockScope();
const body = statement.body;
const prev = this.prevStatement;
this._blockPreWalkScope(statement, body);
this.prevStatement = prev;
this.walkStatements(body);
this._exitBlockScope(oldScope, true);
}
/**
* Walk expression statement.
* @param {ExpressionStatement} statement expression statement
*/
walkExpressionStatement(statement) {
this.walkExpression(statement.expression);
}
/**
* Walks a conditional branch with its guard frame (if any) pushed onto the
* parser-state guard stack for the duration of the branch body.
* @param {GuardFrame | undefined | null} frame guard frame, or falsy when the branch is unguarded
* @param {() => void} walk branch walk
* @returns {void}
*/
walkGuardedBranch(frame, walk) {
if (!frame) {
walk();
return;
}
const stack = this.state.guardStack || (this.state.guardStack = []);
stack.push(frame);
try {
walk();
} finally {
stack.pop();
}
}
/**
* Processes the provided statement.
* @param {IfStatement} statement if statement
*/
walkIfStatement(statement) {
const result = this.hooks.statementIf.call(statement);
if (result === undefined) {
const guard = this.hooks.collectGuards.call(statement.test);
this.walkExpression(statement.test);
// allocate the branch closure only when a guard frame needs pushing
if (guard && guard.consequent) {
this.walkGuardedBranch(guard.consequent, () =>
this.walkNestedStatement(statement.consequent)
);
} else {
this.walkNestedStatement(statement.consequent);
}
const consequentTerminated = this.scope.terminated;
this.scope.terminated = undefined;
if (statement.alternate) {
const alternate = statement.alternate;
if (guard && guard.alternate) {
this.walkGuardedBranch(guard.alternate, () =>
this.walkNestedStatement(alternate)
);
} else {
this.walkNestedStatement(alternate);
}
}
const alternateTerminated = this.scope.terminated;
this.scope.terminated =
consequentTerminated && alternateTerminated
? alternateTerminated
: undefined;
} else if (result) {
this.walkNestedStatement(statement.consequent);
} else if (statement.alternate) {
this.walkNestedStatement(statement.alternate);
}
}
/**
* Walk labeled statement.
* @param {LabeledStatement} statement with statement
*/
walkLabeledStatement(statement) {
const hook = this.hooks.label.get(statement.label.name);
if (hook !== undefined) {
const result = hook.call(statement);
if (result === true) return;
}
const oldScope = this._enterBlockScope();
this.walkNestedStatement(statement.body);
this._exitBlockScope(oldScope, false);
}
/**
* Walk with statement.
* @param {WithStatement} statement with statement
*/
walkWithStatement(statement) {
if (this._strictInModuleOutput) {
this._reportStrictModeViolation(
"`with` statements are not allowed",
statement
);
}
const oldScope = this._enterBlockScope();
this.walkExpression(statement.object);
this.walkNestedStatement(statement.body);
this._exitBlockScope(oldScope, false);
}
/**
* Walk switch statement.
* @param {SwitchStatement} statement switch statement
*/
walkSwitchStatement(statement) {
this.walkExpression(statement.discriminant);
this.walkSwitchCases(statement.cases);
}
/**
* Walk terminating statement.
* @param {ReturnStatement | ThrowStatement} statement return or throw statement
*/
walkTerminatingStatement(statement) {
if (statement.argument) this.walkExpression(statement.argument);
// Skip top level scope because to handle `export` and `module.exports` after terminate
if (this.scope.topLevelScope === true) return;
if (this.hooks.terminate.call(statement)) {
this.scope.terminated =
statement.type === "ReturnStatement"
? SCOPE_INFO_TERMINATED_RETURN
: SCOPE_INFO_TERMINATED_THROW;
}
}
/**
* Walk return statement.
* @param {ReturnStatement} statement return statement
*/
walkReturnStatement(statement) {
this.walkTerminatingStatement(statement);
}
/**
* Walk throw statement.
* @param {ThrowStatement} statement return statement
*/
walkThrowStatement(statement) {
this.walkTerminatingStatement(statement);
}
/**
* Walk try statement.
* @param {TryStatement} statement try statement
*/
walkTryStatement(statement) {
if (this.scope.inTry) {
this.walkStatement(statement.block);
} else {
this.scope.inTry = true;
this.walkStatement(statement.block);
this.scope.inTry = false;
}
const tryTerminated = this.scope.terminated;
this.scope.terminated = undefined;
if (statement.handler) this.walkCatchClause(statement.handler);
const handlerTerminated = this.scope.terminated;
this.scope.terminated = undefined;
if (statement.finalizer) {
this.walkStatement(statement.finalizer);
}
const finalizerTerminated = this.scope.terminated;
this.scope.terminated = undefined;
if (finalizerTerminated) {
this.scope.terminated = finalizerTerminated;
} else if (
tryTerminated &&
(statement.handler ? handlerTerminated : true)
) {
this.scope.terminated = handlerTerminated || tryTerminated;
}
}
/**
* Walk while statement.
* @param {WhileStatement} statement while statement
*/
walkWhileStatement(statement) {
const oldScope = this._enterBlockScope();
this.walkExpression(statement.test);
this.walkNestedStatement(statement.body);
this._exitBlockScope(oldScope, false);
}
/**
* Walk do while statement.
* @param {DoWhileStatement} statement do while statement
*/
walkDoWhileStatement(statement) {
const oldScope = this._enterBlockScope();
this.walkNestedStatement(statement.body);
this.walkExpression(statement.test);
this._exitBlockScope(oldScope, false);
}
/**
* Walk for statement.
* @param {ForStatement} statement for statement
*/
walkForStatement(statement) {
const oldScope = this._enterBlockScope();
if (statement.init) {
if (statement.init.type === "VariableDeclaration") {
this.blockPreWalkVariableDeclaration(statement.init);
this.prevStatement = undefined;
this.walkStatement(statement.init);
} else {
this.walkExpression(statement.init);
}
}
if (statement.test) {
this.walkExpression(statement.test);
}
if (statement.update) {
this.walkExpression(statement.update);
}
const body = statement.body;
if (body.type === "BlockStatement") {
// no need to add additional scope
const prev = this.prevStatement;
this._blockPreWalkScope(body, body.body);
this.prevStatement = prev;
this.walkStatements(body.body);
} else {
this.walkNestedStatement(body);
}
this._exitBlockScope(oldScope, false);
}
/**
* Walk for in statement.
* @param {ForInStatement} statement for statement
*/
walkForInStatement(statement) {
const oldScope = this._enterBlockScope();
if (statement.left.type === "VariableDeclaration") {
this.blockPreWalkVariableDeclaration(statement.left);
this.walkVariableDeclaration(statement.left);
} else if (!this._walkWriteTarget(statement.left, statement)) {
this.walkPattern(statement.left);
}
this.walkExpression(statement.right);
const body = statement.body;
if (body.type === "BlockStatement") {
// no need to add additional scope
const prev = this.prevStatement;
this._blockPreWalkScope(body, body.body);
this.prevStatement = prev;
this.walkStatements(body.body);
} else {
this.walkNestedStatement(body);
}
this._exitBlockScope(oldScope, false);
}
/**
* Walk for of statement.
* @param {ForOfStatement} statement for statement
*/
walkForOfStatement(statement) {
if (statement.await && this.scope.topLevelScope === true) {
this.hooks.topLevelAwait.call(statement);
}
const oldScope = this._enterBlockScope();
if (statement.left.type === "VariableDeclaration") {
this.blockPreWalkVariableDeclaration(statement.left);
this.walkVariableDeclaration(statement.left);
} else if (!this._walkWriteTarget(statement.left, statement)) {
this.walkPattern(statement.left);
}
this.walkExpression(statement.right);
const body = statement.body;
if (body.type === "BlockStatement") {
// no need to add additional scope
const prev = this.prevStatement;
this._blockPreWalkScope(body, body.body);
this.prevStatement = prev;
this.walkStatements(body.body);
} else {
this.walkNestedStatement(body);
}
this._exitBlockScope(oldScope, false);
}
/**
* Pre walk function declaration.
* @param {FunctionDeclaration | MaybeNamedFunctionDeclaration} statement function declaration
*/
preWalkFunctionDeclaration(statement) {
if (statement.id) {
if (this._strictInModuleOutput) {
this._checkStrictModeBinding(statement.id.name, statement.id);
}
this.defineVariable(statement.id.name);
}
}
/**
* Bind a function declaration in the block holding it, when it does not
* hoist to the enclosing scope.
* @param {FunctionDeclaration | MaybeNamedFunctionDeclaration} statement function declaration
*/
blockPreWalkFunctionDeclaration(statement) {
if (statement.id && this._isBlockScopedFunction(statement)) {
this.defineVariable(statement.id.name);
}
}
/**
* Walk function declaration.
* @param {FunctionDeclaration | MaybeNamedFunctionDeclaration} statement function declaration
*/
walkFunctionDeclaration(statement) {
const wasTopLevel = this.scope.topLevelScope;
this.scope.topLevelScope = false;
if (this._strictInModuleOutput) {
this._checkStrictModeParams(statement.params);
}
const oldScope = this._enterFunctionScope(true, statement.params);
for (const param of statement.params) {
this.walkPattern(param);
}
this.detectMode(statement.body.body);
const prev = this.prevStatement;
this._preWalkScope(statement.body);
this.prevStatement = prev;
this.walkStatement(statement.body);
this.scope = oldScope;
this.scope.topLevelScope = wasTopLevel;
}
/**
* Block pre walk expression statement.
* @param {ExpressionStatement} statement expression statement
*/
blockPreWalkExpressionStatement(statement) {
const expression = statement.expression;
switch (expression.type) {
case "AssignmentExpression":
this.preWalkAssignmentExpression(expression);
}
}
/**
* Pre walk assignment expression.
* @param {AssignmentExpression} expression assignment expression
*/
preWalkAssignmentExpression(expression) {
this.enterDestructuringAssignment(expression.left, expression.right);
}
/**
* Enter destructuring assignment.
* @param {Pattern} pattern pattern
* @param {Expression} expression assignment expression
* @returns {Expression | undefined} destructuring expression
*/
enterDestructuringAssignment(pattern, expression) {
if (
pattern.type !== "ObjectPattern" ||
!this.destructuringAssignmentProperties
) {
return;
}
const expr =
expression.type === "AwaitExpression" ? expression.argument : expression;
const destructuring =
expr.type === "AssignmentExpression"
? this.enterDestructuringAssignment(expr.left, expr.right)
: this.hooks.collectDestructuringAssignmentProperties.call(expr)
? expr
: undefined;
if (destructuring) {
const keys = this._preWalkObjectPattern(pattern);
if (!keys) return;
// check multiple assignments
if (this.destructuringAssignmentProperties.has(destructuring)) {
const set =
/** @type {DestructuringAssignmentProperties} */
(this.destructuringAssignmentProperties.get(destructuring));
for (const id of keys) set.add(id);
} else {
this.destructuringAssignmentProperties.set(destructuring, keys);
}
}
return destructuring;
}
/**
* Module pre walk import declaration.
* @param {ImportDeclaration} statement statement
*/
modulePreWalkImportDeclaration(statement) {
const source = /** @type {ImportSource} */ (statement.source.value);
this.hooks.import.call(statement, source);
for (const specifier of statement.specifiers) {
const name = specifier.local.name;
switch (specifier.type) {
case "ImportDefaultSpecifier":
if (
!this.hooks.importSpecifier.call(statement, source, "default", name)
) {
this.defineVariable(name);
}
break;
case "ImportSpecifier":
if (
!this.hooks.importSpecifier.call(
statement,
source,
/** @type {Identifier} */
(specifier.imported).name ||
/** @type {string} */
(
/** @type {Literal} */
(specifier.imported).value
),
name
)
) {
this.defineVariable(name);
}
break;
case "ImportNamespaceSpecifier":
if (!this.hooks.importSpecifier.call(statement, source, null, name)) {
this.defineVariable(name);
}
break;
default:
this.defineVariable(name);
}
}
}
/**
* Processes the provided declaration.
* @param {Declaration} declaration declaration
* @param {OnIdent} onIdent on ident callback
*/
enterDeclaration(declaration, onIdent) {
switch (declaration.type) {
case "VariableDeclaration":
for (const declarator of declaration.declarations) {
switch (declarator.type) {
case "VariableDeclarator": {
this.enterPattern(declarator.id, onIdent);
break;
}
}
}
break;
case "FunctionDeclaration":
this.enterPattern(declaration.id, onIdent);
break;
case "ClassDeclaration":
this.enterPattern(declaration.id, onIdent);
break;
}
}
/**
* Module pre walk export named declaration.
* @param {ExportNamedDeclaration} statement statement
*/
modulePreWalkExportNamedDeclaration(statement) {
if (!statement.source) return;
const source = /** @type {ImportSource} */ (statement.source.value);
this.hooks.exportImport.call(statement, source);
if (statement.specifiers) {
for (
let specifierIndex = 0;
specifierIndex < statement.specifiers.length;
specifierIndex++
) {
const specifier = statement.specifiers[specifierIndex];
switch (specifier.type) {
case "ExportSpecifier": {
const localName =
/** @type {Identifier} */ (specifier.local).name ||
/** @type {string} */ (
/** @type {Literal} */ (specifier.local).value
);
const name =
/** @type {Identifier} */
(specifier.exported).name ||
/** @type {string} */
(/** @type {Literal} */ (specifier.exported).value);
this.hooks.exportImportSpecifier.call(
statement,
source,
localName,
name,
specifierIndex
);
break;
}
}
}
}
}
/**
* Block pre walk export named declaration.
* @param {ExportNamedDeclaration} statement statement
*/
blockPreWalkExportNamedDeclaration(statement) {
if (statement.source) return;
this.hooks.export.call(statement);
if (
statement.declaration &&
!this.hooks.exportDeclaration.call(statement, statement.declaration)
) {
const prev = this.prevStatement;
this.preWalkStatement(statement.declaration);
this.prevStatement = prev;
this.blockPreWalkStatement(statement.declaration);
let index = 0;
this.enterDeclaration(statement.declaration, (def) => {
this.hooks.exportSpecifier.call(statement, def, def, index++);
});
}
if (statement.specifiers) {
for (
let specifierIndex = 0;
specifierIndex < statement.specifiers.length;
specifierIndex++
) {
const specifier = statement.specifiers[specifierIndex];
switch (specifier.type) {
case "ExportSpecifier": {
const localName =
/** @type {Identifier} */ (specifier.local).name ||
/** @type {string} */ (
/** @type {Literal} */ (specifier.local).value
);
const name =
/** @type {Identifier} */
(specifier.exported).name ||
/** @type {string} */
(/** @type {Literal} */ (specifier.exported).value);
this.hooks.exportSpecifier.call(
statement,
localName,
name,
specifierIndex
);
break;
}
}
}
}
}
/**
* Walk export named declaration.
* @param {ExportNamedDeclaration} statement the statement
*/
walkExportNamedDeclaration(statement) {
if (statement.declaration) {
this.walkStatement(statement.declaration);
}
}
/**
* Block pre walk export default declaration.
* @param {ExportDefaultDeclaration} statement statement
*/
blockPreWalkExportDefaultDeclaration(statement) {
if (
statement.declaration.type === "FunctionDeclaration" ||
statement.declaration.type === "ClassDeclaration"
) {
const prev = this.prevStatement;
this.preWalkStatement(statement.declaration);
this.prevStatement = prev;
this.blockPreWalkStatement(statement.declaration);
}
if (
/** @type {MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration} */
(statement.declaration).id &&
statement.declaration.type !== "FunctionExpression" &&
statement.declaration.type !== "ClassExpression"
) {
const declaration =
/** @type {MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration} */
(statement.declaration);
this.hooks.exportSpecifier.call(
statement,
/** @type {Identifier} */
(declaration.id).name,
"default",
undefined
);
}
}
/**
* Walk export default declaration.
* @param {ExportDefaultDeclaration} statement statement
*/
walkExportDefaultDeclaration(statement) {
this.hooks.export.call(statement);
if (
/** @type {FunctionDeclaration | ClassDeclaration} */
(statement.declaration).id &&
statement.declaration.type !== "FunctionExpression" &&
statement.declaration.type !== "ClassExpression"
) {
const declaration =
/** @type {FunctionDeclaration | ClassDeclaration} */
(statement.declaration);
if (!this.hooks.exportDeclaration.call(statement, declaration)) {
this.walkStatement(declaration);
}
} else {
// Acorn parses `export default function() {}` as `FunctionDeclaration` and
// `export default class {}` as `ClassDeclaration`, both with `id = null`.
// These nodes must be treated as expressions.
if (
statement.declaration.type === "FunctionDeclaration" ||
statement.declaration.type === "ClassDeclaration"
) {
this.walkStatement(statement.declaration);
} else {
this.walkExpression(statement.declaration);
}
this.hooks.exportExpression.call(statement, statement.declaration);
}
}
/**
* Module pre walk export all declaration.
* @param {ExportAllDeclaration} statement statement
*/
modulePreWalkExportAllDeclaration(statement) {
const source = /** @type {ImportSource} */ (statement.source.value);
const name = statement.exported
? /** @type {Identifier} */
(statement.exported).name ||
/** @type {string} */
(/** @type {Literal} */ (statement.exported).value)
: null;
this.hooks.exportImport.call(statement, source);
this.hooks.exportImportSpecifier.call(statement, source, null, name, 0);
}
/**
* Pre walk variable declaration.
* @param {VariableDeclaration} statement variable declaration
*/
preWalkVariableDeclaration(statement) {
if (statement.kind !== "var") return;
this._preWalkVariableDeclaration(statement, this.hooks.varDeclarationVar);
}
/**
* Block pre walk variable declaration.
* @param {VariableDeclaration} statement variable declaration
*/
blockPreWalkVariableDeclaration(statement) {
if (statement.kind === "var") return;
if (statement.kind === "await using" && this.scope.topLevelScope === true) {
this.hooks.topLevelAwait.call(statement);
}
const hookMap =
statement.kind === "const"
? this.hooks.varDeclarationConst
: statement.kind === "using" || statement.kind === "await using"
? this.hooks.varDeclarationUsing
: this.hooks.varDeclarationLet;
this._preWalkVariableDeclaration(statement, hookMap);
}
/**
* Pre walk variable declaration.
* @param {VariableDeclaration} statement variable declaration
* @param {HookMap<SyncBailHook<[Identifier], boolean | void>>} hookMap map of hooks
*/
_preWalkVariableDeclaration(statement, hookMap) {
/** @type {OnIdent | undefined} */
let onIdent;
const checkStrictBindings = this._strictInModuleOutput;
const declarations = statement.declarations;
for (let i = 0, len = declarations.length; i < len; i++) {
const declarator = declarations[i];
if (declarator.type !== "VariableDeclarator") continue;
this.preWalkVariableDeclarator(declarator);
if (this.hooks.preDeclarator.call(declarator, statement)) continue;
const id = declarator.id;
if (id.type === "Identifier") {
if (checkStrictBindings) this._checkStrictModeBinding(id.name, id);
// fast path: plain `const x =` skips the enterPattern dispatch and
// the per-declaration onIdent closure (only patterns need it)
if (!this._callHooksForName1(this.hooks.pattern, id.name, id)) {
this._defineVariableForDeclaration(id.name, id, hookMap);
}
} else {
if (onIdent === undefined) {
onIdent = (name, ident) => {
if (checkStrictBindings) this._checkStrictModeBinding(name, ident);
this._defineVariableForDeclaration(name, ident, hookMap);
};
}
this.enterPattern(id, onIdent);
}
}
}
/**
* Defines a declared variable unless a declaration hook handles it.
* @param {string} name variable name
* @param {Identifier} ident identifier node
* @param {HookMap<SyncBailHook<[Identifier], boolean | void>>} hookMap kind-specific declaration hooks
* @returns {void}
*/
_defineVariableForDeclaration(name, ident, hookMap) {
let hook = hookMap.get(name);
if (hook === undefined || !hook.call(ident)) {
hook = this.hooks.varDeclaration.get(name);
if (hook === undefined || !hook.call(ident)) {
this.defineVariable(name);
}
}
}
/**
* Pre walk object pattern.
* @param {ObjectPattern} objectPattern object pattern
* @returns {DestructuringAssignmentProperties | undefined} set of names or undefined if not all keys are identifiers
*/
_preWalkObjectPattern(objectPattern) {
/** @type {DestructuringAssignmentProperties} */
const props = new Set();
const properties = objectPattern.properties;
for (let i = 0; i < properties.length; i++) {
const property = properties[i];
if (property.type !== "Property") return;
if (property.shorthand) {
if (property.value.type === "Identifier") {
this.scope.inShorthand = property.value.name;
} else if (
property.value.type === "AssignmentPattern" &&
property.value.left.type === "Identifier"
) {
this.scope.inShorthand = property.value.left.name;
}
}
const key = property.key;
if (key.type === "Identifier" && !property.computed) {
const pattern =
property.value.type === "ObjectPattern"
? this._preWalkObjectPattern(property.value)
: property.value.type === "ArrayPattern"
? this._preWalkArrayPattern(property.value)
: undefined;
props.add({
id: key.name,
range: /** @type {Range} */ (key.range),
pattern,
shorthand: this.scope.inShorthand
});
} else {
const id = this.evaluateExpression(key);
const str = id.asString();
if (str) {
const pattern =
property.value.type === "ObjectPattern"
? this._preWalkObjectPattern(property.value)
: property.value.type === "ArrayPattern"
? this._preWalkArrayPattern(property.value)
: undefined;
props.add({
id: str,
range: /** @type {Range} */ (key.range),
pattern,
shorthand: this.scope.inShorthand
});
} else {
// could not evaluate key
return;
}
}
this.scope.inShorthand = false;
}
return props;
}
/**
* Pre walk array pattern.
* @param {ArrayPattern} arrayPattern array pattern
* @returns {Set<DestructuringAssignmentProperty> | undefined} set of names or undefined if not all keys are identifiers
*/
_preWalkArrayPattern(arrayPattern) {
/** @type {Set<DestructuringAssignmentProperty>} */
const props = new Set();
const elements = arrayPattern.elements;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (!element) continue;
if (element.type === "RestElement") return;
const pattern =
element.type === "ObjectPattern"
? this._preWalkObjectPattern(element)
: element.type === "ArrayPattern"
? this._preWalkArrayPattern(element)
: undefined;
props.add({
id: `${i}`,
range: /** @type {Range} */ (element.range),
pattern,
shorthand: false
});
}
return props;
}
/**
* Pre walk variable declarator.
* @param {VariableDeclarator} declarator variable declarator
*/
preWalkVariableDeclarator(declarator) {
if (declarator.init) {
this.enterDestructuringAssignment(declarator.id, declarator.init);
}
}
/**
* Walk variable declaration.
* @param {VariableDeclaration} statement variable declaration
*/
walkVariableDeclaration(statement) {
const declarations = statement.declarations;
for (let i = 0, len = declarations.length; i < len; i++) {
const declarator = declarations[i];
switch (declarator.type) {
case "VariableDeclarator": {
const renameIdentifier =
declarator.init && this.getRenameIdentifier(declarator.init);
if (renameIdentifier && declarator.id.type === "Identifier") {
const hook = this.hooks.canRename.get(renameIdentifier);
if (
hook !== undefined &&
hook.call(/** @type {Expression} */ (declarator.init))
) {
// renaming with "var a = b;"
const hook = this.hooks.rename.get(renameIdentifier);
if (
hook === undefined ||
!hook.call(/** @type {Expression} */ (declarator.init))
) {
this.setVariable(declarator.id.name, renameIdentifier);
}
break;
}
}
if (!this.hooks.declarator.call(declarator, statement)) {
this.walkPattern(declarator.id);
if (declarator.init) this.walkExpression(declarator.init);
}
break;
}
}
}
}
/**
* Block pre walk class declaration.
* @param {ClassDeclaration | MaybeNamedClassDeclaration} statement class declaration
*/
blockPreWalkClassDeclaration(statement) {
if (statement.id) {
// only `await` reaches here: acorn rejects the other names itself,
// since a class body is always strict even in a loose script
if (this._strictInModuleOutput) {
this._checkStrictModeBinding(statement.id.name, statement.id);
}
this.defineVariable(statement.id.name);
}
}
/**
* Walk class declaration.
* @param {ClassDeclaration | MaybeNamedClassDeclaration} statement class declaration
*/
walkClassDeclaration(statement) {
this.walkClass(statement);
}
/**
* Processes the provided switch case.
* @param {SwitchCase[]} switchCases switch statement
*/
walkSwitchCases(switchCases) {
const oldScope = this._enterBlockScope();
const len = switchCases.length;
// pre-walk every case first: a `const` declared in a later case is already
// in scope for an earlier case that reads it (and fails at runtime)
for (let index = 0; index < len; index++) {
const switchCase = switchCases[index];
if (switchCase.consequent.length > 0) {
const prev = this.prevStatement;
this._blockPreWalkScope(switchCase, switchCase.consequent);
this.prevStatement = prev;
}
}
for (let index = 0; index < len; index++) {
const switchCase = switchCases[index];
if (switchCase.test) {
this.walkExpression(switchCase.test);
}
if (switchCase.consequent.length > 0) {
this.walkStatements(switchCase.consequent);
this.scope.terminated = undefined;
}
}
this._exitBlockScope(oldScope, false);
}
/**
* Processes the provided catch clause.
* @param {CatchClause} catchClause catch clause
*/
walkCatchClause(catchClause) {
const oldScope = this._enterBlockScope();
// Error binding is optional in catch clause since ECMAScript 2019
if (catchClause.param !== null) {
this.enterPattern(catchClause.param, (ident, identifier) => {
if (this._strictInModuleOutput) {
this._checkStrictModeBinding(ident, identifier);
}
this.defineVariable(ident);
});
this.walkPattern(catchClause.param);
}
const prev = this.prevStatement;
this.blockPreWalkStatement(catchClause.body);
this.prevStatement = prev;
this.walkStatement(catchClause.body);
this._exitBlockScope(oldScope, true);
}
/**
* Processes the provided pattern.
* @param {Pattern} pattern pattern
*/
walkPattern(pattern) {
switch (pattern.type) {
// plain identifier bindings are the common case and walk nothing
case "Identifier":
break;
case "ObjectPattern":
this.walkObjectPattern(pattern);
break;
case "ArrayPattern":
this.walkArrayPattern(pattern);
break;
case "AssignmentPattern":
this.walkAssignmentPattern(pattern);
break;
case "MemberExpression":
this.walkMemberExpression(pattern);
break;
case "RestElement":
this.walkRestElement(pattern);
break;
}
}
/**
* Walk assignment pattern.
* @param {AssignmentPattern} pattern assignment pattern
*/
walkAssignmentPattern(pattern) {
this.walkExpression(pattern.right);
this.walkPattern(pattern.left);
}
/**
* Walk object pattern.
* @param {ObjectPattern} pattern pattern
*/
walkObjectPattern(pattern) {
for (let i = 0, len = pattern.properties.length; i < len; i++) {
const prop = pattern.properties[i];
if (prop) {
if (prop.type === "RestElement") {
continue;
}
if (prop.computed) this.walkExpression(prop.key);
if (prop.value) this.walkPattern(prop.value);
}
}
}
/**
* Walk array pattern.
* @param {ArrayPattern} pattern array pattern
*/
walkArrayPattern(pattern) {
for (let i = 0, len = pattern.elements.length; i < len; i++) {
const element = pattern.elements[i];
if (element) this.walkPattern(element);
}
}
/**
* Processes the provided pattern.
* @param {RestElement} pattern rest element
*/
walkRestElement(pattern) {
this.walkPattern(pattern.argument);
}
/**
* Processes the provided expression.
* @param {(Expression | SpreadElement | null)[]} expressions expressions
*/
walkExpressions(expressions) {
for (let i = 0, len = expressions.length; i < len; i++) {
const expression = expressions[i];
if (expression) {
this.walkExpression(expression);
}
}
}
/**
* Processes the provided expression.
* @param {Expression | SpreadElement | PrivateIdentifier | Super} expression expression
*/
walkExpression(expression) {
// cases ordered by measured node frequency (identifiers are ~40% of all
// expressions) — V8 compiles this to sequential compares, so hot types
// must come first
switch (expression.type) {
case "Identifier":
this.walkIdentifier(expression);
break;
case "MemberExpression":
this.walkMemberExpression(expression);
break;
case "Literal":
if (this._strictInModuleOutput) {
this._checkStrictModeLiteral(expression);
}
break;
case "CallExpression":
this.walkCallExpression(expression);
break;
case "BinaryExpression":
this.walkBinaryExpression(expression);
break;
case "AssignmentExpression":
this.walkAssignmentExpression(expression);
break;
case "UnaryExpression":
this.walkUnaryExpression(expression);
break;
case "LogicalExpression":
this.walkLogicalExpression(expression);
break;
case "ArrowFunctionExpression":
this.walkArrowFunctionExpression(expression);
break;
case "ThisExpression":
this.walkThisExpression(expression);
break;
case "ConditionalExpression":
this.walkConditionalExpression(expression);
break;
case "ArrayExpression":
this.walkArrayExpression(expression);
break;
case "ObjectExpression":
this.walkObjectExpression(expression);
break;
case "FunctionExpression":
this.walkFunctionExpression(expression);
break;
case "NewExpression":
this.walkNewExpression(expression);
break;
case "TemplateLiteral":
this.walkTemplateLiteral(expression);
break;
case "SpreadElement":
this.walkSpreadElement(expression);
break;
case "SequenceExpression":
this.walkSequenceExpression(expression);
break;
case "UpdateExpression":
this.walkUpdateExpression(expression);
break;
case "AwaitExpression":
this.walkAwaitExpression(expression);
break;
case "ChainExpression":
this.walkChainExpression(expression);
break;
case "ClassExpression":
this.walkClassExpression(expression);
break;
case "ImportExpression":
this.walkImportExpression(expression);
break;
case "MetaProperty":
this.walkMetaProperty(expression);
break;
case "TaggedTemplateExpression":
this.walkTaggedTemplateExpression(expression);
break;
case "YieldExpression":
this.walkYieldExpression(expression);
break;
}
}
/**
* Reports octal number literals (`0777`) and octal string escapes (`"\47"`),
* both SyntaxErrors in strict-mode ESM output.
* @param {Literal} expression literal
*/
_checkStrictModeLiteral(expression) {
const raw = expression.raw;
if (raw === undefined) return;
if (typeof expression.value === "number") {
// Legacy octal (`0777`) or non-octal decimal (`08`) integer literal.
if (
raw.length > 1 &&
raw.charCodeAt(0) === 48 &&
raw.charCodeAt(1) >= 48 &&
raw.charCodeAt(1) <= 57
) {
this._reportStrictModeViolation(
"Octal literals are not allowed",
expression
);
}
} else if (typeof expression.value === "string" && hasOctalEscape(raw)) {
this._reportStrictModeViolation(
"Octal escape sequences are not allowed",
expression
);
}
}
/**
* Walk await expression.
* @param {AwaitExpression} expression await expression
*/
walkAwaitExpression(expression) {
if (this.scope.topLevelScope === true) {
this.hooks.topLevelAwait.call(expression);
}
this.walkExpression(expression.argument);
}
/**
* Walk array expression.
* @param {ArrayExpression} expression array expression
*/
walkArrayExpression(expression) {
if (expression.elements) {
this.walkExpressions(expression.elements);
}
}
/**
* Walk spread element.
* @param {SpreadElement} expression spread element
*/
walkSpreadElement(expression) {
if (expression.argument) {
this.walkExpression(expression.argument);
}
}
/**
* Walk object expression.
* @param {ObjectExpression} expression object expression
*/
walkObjectExpression(expression) {
for (
let propIndex = 0, len = expression.properties.length;
propIndex < len;
propIndex++
) {
const prop = expression.properties[propIndex];
this.walkProperty(prop);
}
}
/**
* Processes the provided prop.
* @param {Property | SpreadElement} prop property or spread element
*/
walkProperty(prop) {
if (prop.type === "SpreadElement") {
this.walkExpression(prop.argument);
return;
}
if (prop.computed) {
this.walkExpression(prop.key);
}
if (prop.shorthand && prop.value && prop.value.type === "Identifier") {
this.scope.inShorthand = prop.value.name;
this.walkIdentifier(prop.value);
this.scope.inShorthand = false;
} else {
this.walkExpression(
/** @type {Exclude<Property["value"], AssignmentPattern | ObjectPattern | ArrayPattern | RestElement>} */
(prop.value)
);
}
}
/**
* Walk function expression.
* @param {FunctionExpression} expression arrow function expression
*/
walkFunctionExpression(expression) {
const wasTopLevel = this.scope.topLevelScope;
this.scope.topLevelScope = false;
// Only copy params when the function name must be added (recursive calls);
// inFunctionScope reads params without mutating, like arrow functions.
const scopeParams = expression.id
? [...expression.params, expression.id]
: expression.params;
if (this._strictInModuleOutput) {
this._checkStrictModeParams(expression.params);
if (expression.id) {
this._checkStrictModeBinding(expression.id.name, expression.id);
}
}
const oldScope = this._enterFunctionScope(true, scopeParams);
for (const param of expression.params) {
this.walkPattern(param);
}
this.detectMode(expression.body.body);
const prev = this.prevStatement;
this._preWalkScope(expression.body);
this.prevStatement = prev;
this.walkStatement(expression.body);
this.scope = oldScope;
this.scope.topLevelScope = wasTopLevel;
}
/**
* Walk arrow function expression.
* @param {ArrowFunctionExpression} expression arrow function expression
*/
walkArrowFunctionExpression(expression) {
const wasTopLevel = this.scope.topLevelScope;
this.scope.topLevelScope = wasTopLevel ? "arrow" : false;
if (this._strictInModuleOutput) {
this._checkStrictModeParams(expression.params);
}
const oldScope = this._enterFunctionScope(false, expression.params);
for (const param of expression.params) {
this.walkPattern(param);
}
if (expression.body.type === "BlockStatement") {
this.detectMode(expression.body.body);
const prev = this.prevStatement;
this._preWalkScope(expression.body);
this.prevStatement = prev;
this.walkStatement(expression.body);
} else {
this.walkExpression(expression.body);
}
this.scope = oldScope;
this.scope.topLevelScope = wasTopLevel;
}
/**
* Walk sequence expression.
* @param {SequenceExpression} expression the sequence
*/
walkSequenceExpression(expression) {
if (!expression.expressions) return;
// We treat sequence expressions like statements when they are one statement level
// This has some benefits for optimizations that only work on statement level
const currentStatement =
/** @type {StatementPath} */
(this.statementPath)[
/** @type {StatementPath} */
(this.statementPath).length - 1
];
if (
currentStatement === expression ||
(currentStatement.type === "ExpressionStatement" &&
currentStatement.expression === expression)
) {
const old =
/** @type {StatementPathItem} */
(/** @type {StatementPath} */ (this.statementPath).pop());
const prev = this.prevStatement;
for (const expr of expression.expressions) {
/** @type {StatementPath} */
(this.statementPath).push(expr);
this.walkExpression(expr);
this.prevStatement =
/** @type {StatementPath} */
(this.statementPath).pop();
}
this.prevStatement = prev;
/** @type {StatementPath} */
(this.statementPath).push(old);
} else {
this.walkExpressions(expression.expressions);
}
}
/**
* Walk update expression.
* @param {UpdateExpression} expression the update expression
*/
walkUpdateExpression(expression) {
if (
this._strictInModuleOutput &&
expression.argument.type === "Identifier"
) {
this._checkStrictModeAssignTarget(expression.argument.name, expression);
}
if (this._walkWriteTarget(expression.argument, expression)) return;
this.walkExpression(expression.argument);
}
/**
* Offers a write that is not an assignment's left side — an update's target
* or a loop's binding — to the taps that care, `assign` carrying only an
* AssignmentExpression.
* @param {Expression | Pattern} target the written reference
* @param {WriteStatement} node the construct performing the write
* @returns {boolean} true when a tap rendered the write itself
*/
_walkWriteTarget(target, node) {
if (target.type === "Identifier") {
return Boolean(
this._callHooksForName1(this.hooks.write, target.name, node)
);
}
if (target.type.endsWith("Pattern")) {
this.enterPattern(/** @type {Pattern} */ (target), (name) => {
this._callHooksForName1(this.hooks.write, name, node);
});
this._walkPatternWriteMembers(target, node);
return false;
}
return Boolean(
target.type === "MemberExpression" &&
this._callWriteMemberChain(target, node)
);
}
/**
* Offers the member expressions a destructuring target writes to the
* `writeMemberChain` taps, which `enterPattern` passes over because they
* bind no name.
* @param {Pattern | Expression | null} pattern the destructuring target
* @param {WriteStatement} node the construct performing the write
* @returns {void}
*/
_walkPatternWriteMembers(pattern, node) {
if (!pattern) return;
switch (pattern.type) {
case "MemberExpression":
this._callWriteMemberChain(pattern, node);
break;
case "ObjectPattern":
for (const property of pattern.properties) {
this._walkPatternWriteMembers(
property.type === "RestElement"
? property.argument
: property.value,
node
);
}
break;
case "ArrayPattern":
for (const element of pattern.elements) {
this._walkPatternWriteMembers(element, node);
}
break;
case "AssignmentPattern":
this._walkPatternWriteMembers(pattern.left, node);
break;
case "RestElement":
this._walkPatternWriteMembers(pattern.argument, node);
break;
}
}
/**
* Offers a written member expression to the `writeMemberChain` taps.
* @param {MemberExpression} target the written member expression
* @param {WriteStatement} node the construct performing the write
* @returns {boolean} true when a tap rendered the write itself
*/
_callWriteMemberChain(target, node) {
return Boolean(
this._resolveMemberExpression(target, ALLOWED_MEMBER_TYPES_EXPRESSION) ===
1 &&
this._hasHooksForInfo(
this.hooks.writeMemberChain,
/** @type {string | VariableInfo} */ (this._memberRootInfo)
) &&
this.callHooksForInfo(
this.hooks.writeMemberChain,
/** @type {string | VariableInfo} */ (this._memberRootInfo),
node,
this._members
)
);
}
/**
* Reports `arguments.callee` / `arguments.caller` — both throw a TypeError
* once the module runs in strict-mode ESM output.
* @param {string} name accessed member name
* @param {Expression} node node to report the violation at
*/
_checkStrictModeArgumentsMember(name, node) {
if ((name === "callee" || name === "caller") && !this.scope.isStrict) {
this._reportStrictModeViolation(
`Accessing "arguments.${name}" is not allowed`,
node
);
}
}
/**
* Reports an assignment or update target that breaks in strict-mode ESM
* output: `eval` / `arguments` are a SyntaxError, the read-only globals
* (`undefined`, `NaN`, `Infinity`) a TypeError instead of a silent no-op.
* @param {string} name target identifier name
* @param {AssignmentExpression | UpdateExpression} node node to report the violation at
*/
_checkStrictModeAssignTarget(name, node) {
if (name === "eval" || name === "arguments") {
this._reportStrictModeViolation(
`Assigning to "${name}" is not allowed`,
node
);
} else if (
(name === "undefined" || name === "NaN" || name === "Infinity") &&
!this.scope.isStrict &&
!this.isVariableDefined(name)
) {
this._reportStrictModeViolation(
`Assigning to the read-only global "${name}" is not allowed`,
node
);
}
}
/**
* Reports a binding name a loose script allows but strict-mode ESM output
* rejects — the whole bundle fails to parse, so this is never merely latent.
* @param {string} name binding name
* @param {Statement | Expression | Identifier} node node to report the violation at
*/
_checkStrictModeBinding(name, node) {
if (name === "eval" || name === "arguments") {
this._reportStrictModeViolation(
`"${name}" is not allowed as a binding name`,
node
);
} else if (STRICT_MODE_RESERVED_WORDS.has(name)) {
this._reportStrictModeViolation(
`"${name}" is a reserved word in strict mode and is not allowed as a binding name`,
node
);
} else if (name === "await") {
this._reportStrictModeViolation(
'"await" is not allowed as a binding name',
node,
MODULE_GOAL_OUTPUT_REASON
);
}
}
/**
* Reports strict-mode-only errors in a function's parameter list: a duplicate
* simple parameter, or a parameter named `eval` / `arguments`.
* @param {(import("estree").Pattern)[]} params function parameters
*/
_checkStrictModeParams(params) {
/** @type {Set<string> | undefined} */
let seen;
for (const param of params) {
if (param.type !== "Identifier") continue;
const name = param.name;
if (name === "eval" || name === "arguments") {
this._reportStrictModeViolation(
`"${name}" is not allowed as a parameter name`,
param
);
} else if (STRICT_MODE_RESERVED_WORDS.has(name)) {
this._reportStrictModeViolation(
`"${name}" is a reserved word in strict mode and is not allowed as a parameter name`,
param
);
} else if (name === "await") {
this._reportStrictModeViolation(
'"await" is not allowed as a parameter name',
param,
MODULE_GOAL_OUTPUT_REASON
);
}
if (seen === undefined) seen = new Set();
if (seen.has(name)) {
this._reportStrictModeViolation(
`Duplicate parameter name "${name}" is not allowed`,
param
);
} else {
seen.add(name);
}
}
}
/**
* Walk unary expression.
* @param {UnaryExpression} expression the unary expression
*/
walkUnaryExpression(expression) {
if (expression.operator === "typeof") {
const result = this.callHooksForExpression(
this.hooks.typeof,
expression.argument,
expression
);
if (result === true) return;
if (expression.argument.type === "ChainExpression") {
const result = this.callHooksForExpression(
this.hooks.typeof,
expression.argument.expression,
expression
);
if (result === true) return;
}
} else if (
this._strictInModuleOutput &&
expression.operator === "delete" &&
expression.argument.type === "Identifier"
) {
this._reportStrictModeViolation(
`Deleting the unqualified identifier "${expression.argument.name}" is not allowed`,
expression
);
}
this.walkExpression(expression.argument);
}
/**
* Reports a construct that a loose script allows but that breaks once the
* module is emitted as strict-mode ESM output. Severity comes from the
* `strictModeViolations` parser option (`false` disables the checks upfront).
* The location is computed here, on the rare violation path only.
* @param {string} reason what is not allowed
* @param {Statement | Expression | Identifier} node node to report the violation at
* @param {string=} outputReason why the output rejects it, defaults to strict mode
*/
_reportStrictModeViolation(reason, node, outputReason) {
const diagnostic = new WebpackError(
`${reason}. ${outputReason === undefined ? STRICT_MODE_OUTPUT_REASON : outputReason}`
);
diagnostic.loc = this.getLocation(node);
if (this.options.strictModeViolations === "error") {
this.state.module.addError(diagnostic);
} else {
this.state.module.addWarning(diagnostic);
}
}
/**
* Walk left right expression.
* @param {LogicalExpression | BinaryExpression} expression the expression
*/
walkLeftRightExpression(expression) {
this.walkExpression(expression.left);
this.walkExpression(expression.right);
}
/**
* Walk binary expression.
* @param {BinaryExpression} expression the binary expression
*/
walkBinaryExpression(expression) {
if (this.hooks.binaryExpression.call(expression) === undefined) {
this.walkLeftRightExpression(expression);
}
}
/**
* Walk logical expression.
* @param {LogicalExpression} expression the logical expression
*/
walkLogicalExpression(expression) {
const result = this.hooks.expressionLogicalOperator.call(expression);
if (result === undefined) {
this.walkLeftRightExpression(expression);
} else if (result) {
this.walkExpression(expression.right);
}
}
/**
* Walk assignment expression.
* @param {AssignmentExpression} expression assignment expression
*/
walkAssignmentExpression(expression) {
if (expression.left.type === "Identifier") {
if (this._strictInModuleOutput) {
this._checkStrictModeAssignTarget(expression.left.name, expression);
}
const renameIdentifier = this.getRenameIdentifier(expression.right);
if (
renameIdentifier &&
this._callHooksForInfo1(
this.hooks.canRename,
renameIdentifier,
expression.right
)
) {
// renaming "a = b;"
if (
!this._callHooksForInfo1(
this.hooks.rename,
renameIdentifier,
expression.right
)
) {
this.setVariable(
expression.left.name,
typeof renameIdentifier === "string"
? this.getVariableInfo(renameIdentifier)
: renameIdentifier
);
}
return;
}
this.walkExpression(expression.right);
// inlined enterPattern → enterIdentifier for the Identifier left side:
// same hook sequence without the per-assignment closure
const leftName = expression.left.name;
if (
!this._callHooksForName1(
this.hooks.pattern,
leftName,
expression.left
) &&
!this._callHooksForName1(this.hooks.assign, leftName, expression)
) {
this.walkExpression(expression.left);
}
} else if (expression.left.type.endsWith("Pattern")) {
this.walkExpression(expression.right);
this.enterPattern(expression.left, (name, _decl) => {
if (!this._callHooksForName1(this.hooks.assign, name, expression)) {
this.defineVariable(name);
}
});
this._walkPatternWriteMembers(expression.left, expression);
this.walkPattern(expression.left);
} else if (expression.left.type === "MemberExpression") {
if (
this._resolveMemberExpression(
expression.left,
ALLOWED_MEMBER_TYPES_EXPRESSION
) === 1 &&
this._hasHooksForInfo(
this.hooks.assignMemberChain,
/** @type {string | VariableInfo} */ (this._memberRootInfo)
) &&
this.callHooksForInfo(
this.hooks.assignMemberChain,
/** @type {string | VariableInfo} */ (this._memberRootInfo),
expression,
this._members
)
) {
return;
}
this.walkExpression(expression.right);
this.walkExpression(expression.left);
} else {
this.walkExpression(expression.right);
this.walkExpression(
/** @type {Exclude<AssignmentExpression["left"], Identifier | RestElement | MemberExpression | ObjectPattern | ArrayPattern | AssignmentPattern>} */
(expression.left)
);
}
}
/**
* Walk conditional expression.
* @param {ConditionalExpression} expression conditional expression
*/
walkConditionalExpression(expression) {
const result = this.hooks.expressionConditionalOperator.call(expression);
if (result === undefined) {
const guard = this.hooks.collectGuards.call(expression.test);
this.walkExpression(expression.test);
// allocate the branch closure only when a guard frame needs pushing
if (guard && guard.consequent) {
this.walkGuardedBranch(guard.consequent, () =>
this.walkExpression(expression.consequent)
);
} else {
this.walkExpression(expression.consequent);
}
if (expression.alternate) {
const alternate = expression.alternate;
if (guard && guard.alternate) {
this.walkGuardedBranch(guard.alternate, () =>
this.walkExpression(alternate)
);
} else {
this.walkExpression(alternate);
}
}
} else if (result) {
this.walkExpression(expression.consequent);
} else if (expression.alternate) {
this.walkExpression(expression.alternate);
}
}
/**
* Walk new expression.
* @param {NewExpression} expression new expression
*/
walkNewExpression(expression) {
const result = this.callHooksForExpression(
this.hooks.new,
expression.callee,
expression
);
if (result === true) return;
this.walkExpression(expression.callee);
if (expression.arguments) {
this.walkExpressions(expression.arguments);
}
}
/**
* Walk yield expression.
* @param {YieldExpression} expression yield expression
*/
walkYieldExpression(expression) {
if (expression.argument) {
this.walkExpression(expression.argument);
}
}
/**
* Walk template literal.
* @param {TemplateLiteral} expression template literal
*/
walkTemplateLiteral(expression) {
if (expression.expressions) {
this.walkExpressions(expression.expressions);
}
}
/**
* Walk tagged template expression.
* @param {TaggedTemplateExpression} expression tagged template expression
*/
walkTaggedTemplateExpression(expression) {
if (expression.tag) {
this.scope.inTaggedTemplateTag = true;
this.walkExpression(expression.tag);
this.scope.inTaggedTemplateTag = false;
}
if (expression.quasi && expression.quasi.expressions) {
this.walkExpressions(expression.quasi.expressions);
}
}
/**
* Walk class expression.
* @param {ClassExpression} expression the class expression
*/
walkClassExpression(expression) {
// only `await` reaches here, as for class declarations
if (this._strictInModuleOutput && expression.id) {
this._checkStrictModeBinding(expression.id.name, expression.id);
}
this.walkClass(expression);
}
/**
* Walk chain expression.
* @param {ChainExpression} expression expression
*/
walkChainExpression(expression) {
const result = this.hooks.optionalChaining.call(expression);
if (result === undefined) {
if (expression.expression.type === "CallExpression") {
this.walkCallExpression(expression.expression);
} else {
this.walkMemberExpression(expression.expression);
}
}
}
/**
* Processes the provided function expression.
* @private
* @param {FunctionExpression | ArrowFunctionExpression} functionExpression function expression
* @param {(Expression | SpreadElement)[]} options options
* @param {Expression | SpreadElement | null} currentThis current this
*/
_walkIIFE(functionExpression, options, currentThis) {
/**
* Returns var info.
* @param {Expression | SpreadElement} argOrThis arg or this
* @returns {string | VariableInfo | undefined} var info
*/
const getVarInfo = (argOrThis) => {
const renameIdentifier = this.getRenameIdentifier(argOrThis);
if (
renameIdentifier &&
this._callHooksForInfo1(
this.hooks.canRename,
renameIdentifier,
/** @type {Expression} */
(argOrThis)
) &&
!this._callHooksForInfo1(
this.hooks.rename,
renameIdentifier,
/** @type {Expression} */
(argOrThis)
)
) {
return typeof renameIdentifier === "string"
? /** @type {string} */ (this.getVariableInfo(renameIdentifier))
: renameIdentifier;
}
this.walkExpression(argOrThis);
};
const { params, type } = functionExpression;
const arrow = type === "ArrowFunctionExpression";
// the walk skips walkFunctionExpression for a called function expression
if (this._strictInModuleOutput) {
this._checkStrictModeParams(params);
if (type === "FunctionExpression" && functionExpression.id) {
this._checkStrictModeBinding(
functionExpression.id.name,
functionExpression.id
);
}
}
const renameThis = currentThis ? getVarInfo(currentThis) : null;
const varInfoForArgs = options.map(getVarInfo);
const wasTopLevel = this.scope.topLevelScope;
this.scope.topLevelScope = wasTopLevel && arrow ? "arrow" : false;
const scopeParams =
/** @type {(Identifier | string)[]} */
(params.filter((identifier, idx) => !varInfoForArgs[idx]));
// Add function name in scope for recursive calls
if (
functionExpression.type === "FunctionExpression" &&
functionExpression.id
) {
scopeParams.push(functionExpression.id.name);
}
const oldScope = this._enterFunctionScope(true, scopeParams);
if (renameThis && !arrow) {
this.setVariable("this", renameThis);
}
for (let i = 0; i < varInfoForArgs.length; i++) {
const varInfo = varInfoForArgs[i];
if (!varInfo) continue;
if (!params[i] || params[i].type !== "Identifier") continue;
this.setVariable(/** @type {Identifier} */ (params[i]).name, varInfo);
}
if (functionExpression.body.type === "BlockStatement") {
this.detectMode(functionExpression.body.body);
const prev = this.prevStatement;
this._preWalkScope(functionExpression.body);
this.prevStatement = prev;
this.walkStatement(functionExpression.body);
} else {
this.walkExpression(functionExpression.body);
}
this.scope = oldScope;
this.scope.topLevelScope = wasTopLevel;
}
/**
* Walk import expression.
* @param {ImportExpression} expression import expression
*/
walkImportExpression(expression) {
const result = this.hooks.importCall.call(expression);
if (result === true) return;
this.walkExpression(expression.source);
}
/**
* Walk call expression.
* @param {CallExpression} expression expression
*/
walkCallExpression(expression) {
if (
expression.callee.type === "MemberExpression" &&
isFunctionExpression(expression.callee.object.type) &&
!expression.callee.computed &&
/** @type {boolean} */
(
/** @type {Identifier} */
(expression.callee.property).name === "call" ||
/** @type {Identifier} */
(expression.callee.property).name === "bind"
) &&
expression.arguments.length > 0 &&
isSimpleFunction(
/** @type {FunctionExpression | ArrowFunctionExpression} */
(expression.callee.object)
)
) {
// (function(…) { }.call/bind(?, …))
this._walkIIFE(
/** @type {FunctionExpression | ArrowFunctionExpression} */
(expression.callee.object),
expression.arguments.slice(1),
expression.arguments[0]
);
} else if (
isFunctionExpression(expression.callee.type) &&
isSimpleFunction(
/** @type {FunctionExpression | ArrowFunctionExpression} */
(expression.callee)
)
) {
// (function(…) { }(…))
this._walkIIFE(
/** @type {FunctionExpression | ArrowFunctionExpression} */
(expression.callee),
expression.arguments,
null
);
} else {
if (expression.callee.type === "MemberExpression") {
// callMemberChainOfCallMemberChain only applies to call-rooted
// chains (e.g. `a().b()`); for the common identifier/this-rooted
// callee the CALL lookup always rejects, so gate on the cheap root.
if (
this.getMemberExpressionRoot(expression.callee).type ===
"CallExpression"
) {
const exprInfo = this.getMemberExpressionInfo(
expression.callee,
ALLOWED_MEMBER_TYPES_CALL_EXPRESSION
);
if (exprInfo && exprInfo.type === "call") {
const result = this.callHooksForInfo(
this.hooks.callMemberChainOfCallMemberChain,
exprInfo.rootInfo,
expression,
exprInfo.getCalleeMembers(),
exprInfo.call,
exprInfo.getMembers(),
exprInfo.getMemberRanges()
);
if (result === true) return;
}
}
// import("./m").then(m => { ... })
if (
expression.callee.object.type === "ImportExpression" &&
expression.callee.property.type === "Identifier" &&
expression.callee.property.name === "then"
) {
const result = this.hooks.importCall.call(
expression.callee.object,
expression
);
if (result === true) return;
}
}
if (
expression.callee.type === "Identifier" &&
this._evalIdentOwnTaps &&
expression.callee.name !== "undefined" &&
this._isDefinedPlainVariable(expression.callee.name)
) {
// a defined, untagged callee can only evaluate to the fallthrough
// result, so no call hooks apply — walk callee and arguments
// directly, skipping the hook dispatch and its result object
this.walkExpression(expression.callee);
if (expression.arguments) this.walkExpressions(expression.arguments);
return;
}
const callee = this.evaluateExpression(expression.callee);
if (callee.isIdentifier()) {
const rootInfo =
/** @type {NonNullable<BasicEvaluatedExpression["rootInfo"]>} */
(callee.rootInfo);
if (this._hasHooksForInfo(this.hooks.callMemberChain, rootInfo)) {
const members =
/** @type {NonNullable<BasicEvaluatedExpression["getMembers"]>} */
(callee.getMembers)();
const result1 = this.callHooksForInfo(
this.hooks.callMemberChain,
rootInfo,
expression,
members,
callee.getMembersOptionals
? callee.getMembersOptionals()
: members.map(RETURN_FALSE),
callee.getMemberRanges ? callee.getMemberRanges() : []
);
if (result1 === true) return;
}
const result2 = this._callHooksForInfo1(
this.hooks.call,
/** @type {NonNullable<BasicEvaluatedExpression["identifier"]>} */
(callee.identifier),
expression
);
if (result2 === true) return;
}
if (expression.callee) {
if (expression.callee.type === "MemberExpression") {
// because of call context we need to walk the call context as expression
this.walkExpression(expression.callee.object);
if (expression.callee.computed === true) {
this.walkExpression(expression.callee.property);
}
} else {
this.walkExpression(expression.callee);
}
}
if (expression.arguments) this.walkExpressions(expression.arguments);
}
}
/**
* Walk member expression.
* @param {MemberExpression} expression member expression
*/
walkMemberExpression(expression) {
const type = this._resolveMemberExpression(
expression,
ALLOWED_MEMBER_TYPES_ALL
);
if (type === 1) {
// copied out first: a hook may resolve another chain and reuse the slots
const name = this._memberName;
const rootInfo = /** @type {string | VariableInfo} */ (
this._memberRootInfo
);
const members = this._members;
const membersOptionals = this._membersOptionals;
const result1 = this._callHooksForInfo1(
this.hooks.expression,
name,
expression
);
if (result1 === true) return;
if (this._hasHooksForInfo(this.hooks.expressionMemberChain, rootInfo)) {
const result2 = this.callHooksForInfo(
this.hooks.expressionMemberChain,
rootInfo,
expression,
members,
membersOptionals,
memberRangesOf(expression, members.length)
);
if (result2 === true) return;
}
this._walkMemberChain(
expression,
expression,
name,
rootInfo,
members,
members.length
);
return;
}
if (type === 2) {
const call = /** @type {CallExpression} */ (this._memberCall);
const result = this.callHooksForInfo(
this.hooks.memberChainOfCallMemberChain,
/** @type {string | VariableInfo} */ (this._memberRootInfo),
expression,
this._memberCalleeMembers,
call,
this._members,
memberRangesOf(expression, this._members.length)
);
if (result === true) return;
// Fast skip over the member chain as we already called memberChainOfCallMemberChain
// and call computed property are literals anyway
this.walkExpression(call);
return;
}
this.walkExpression(expression.object);
if (expression.computed === true) this.walkExpression(expression.property);
}
/**
* The walk `walkMemberExpressionWithExpressionName` does over a resolved
* chain, reading `members` up to `end` instead of popping a copy of it.
* @param {MemberExpression} outer the whole chain, for the unhandled hook
* @param {MemberExpression} expression the chain from here inwards
* @param {string} name name of `expression`
* @param {string | VariableInfo} rootInfo root info
* @param {Members} members members of the whole chain, in source order
* @param {number} end how many of `members` are inside `expression`
* @returns {void}
*/
_walkMemberChain(outer, expression, name, rootInfo, members, end) {
if (expression.object.type === "MemberExpression") {
const property = members[end - 1];
name = name.slice(0, -property.length - 1);
const result = this._callHooksForInfo1(
this.hooks.expression,
name,
expression.object
);
if (result === true) return;
this._walkMemberChain(
outer,
expression.object,
name,
rootInfo,
members,
end - 1
);
} else if (
!this._hasHooksForInfo(
this.hooks.unhandledExpressionMemberChain,
rootInfo
) ||
!this.callHooksForInfo(
this.hooks.unhandledExpressionMemberChain,
rootInfo,
outer,
members
)
) {
this.walkExpression(expression.object);
}
if (expression.computed === true) this.walkExpression(expression.property);
}
/**
* Walk member expression with expression name.
* @template R
* @param {MemberExpression} expression member expression
* @param {string} name name
* @param {string | VariableInfo} rootInfo root info
* @param {Members} members members
* @param {() => R | undefined} onUnhandled on unhandled callback
*/
walkMemberExpressionWithExpressionName(
expression,
name,
rootInfo,
members,
onUnhandled
) {
if (expression.object.type === "MemberExpression") {
// A nested MemberExpression keeps its info by calling walkMemberExpression
// directly. The property comes from `members` rather than the node, which
// may be a TemplateLiteral and carry neither `.name` nor `.value`.
const property = members[members.length - 1];
name = name.slice(0, -property.length - 1);
members.pop();
const result = this._callHooksForInfo1(
this.hooks.expression,
name,
expression.object
);
if (result === true) return;
this.walkMemberExpressionWithExpressionName(
expression.object,
name,
rootInfo,
members,
onUnhandled
);
} else if (!onUnhandled || !onUnhandled()) {
this.walkExpression(expression.object);
}
if (expression.computed === true) this.walkExpression(expression.property);
}
/**
* Walk this expression.
* @param {ThisExpression} expression this expression
*/
walkThisExpression(expression) {
this._callHooksForName1(this.hooks.expression, "this", expression);
}
/**
* Processes the provided expression.
* @param {Identifier} expression identifier
*/
walkIdentifier(expression) {
this._callHooksForName1(this.hooks.expression, expression.name, expression);
}
/**
* Walk meta property.
* @param {MetaProperty} metaProperty meta property
*/
walkMetaProperty(metaProperty) {
this.hooks.expression.for(getRootName(metaProperty)).call(metaProperty);
}
/**
* Call hooks for expression.
* @template T
* @template R
* @param {HookMap<SyncBailHook<T, R>>} hookMap hooks the should be called
* @param {Expression | Super} expr expression
* @param {AsArray<T>} args args for the hook
* @returns {R | undefined} result of hook
*/
callHooksForExpression(hookMap, expr, ...args) {
return this.callHooksForExpressionWithFallback(
hookMap,
expr,
undefined,
undefined,
...args
);
}
/**
* Call hooks for expression with fallback.
* @template T
* @template R
* @param {HookMap<SyncBailHook<T, R>>} hookMap hooks the should be called
* @param {Expression | Super} expr expression info
* @param {((name: string, rootInfo: string | ScopeInfo | VariableInfo, getMembers: () => Members) => R) | undefined} fallback callback when variable in not handled by hooks
* @param {((result?: string) => R | undefined) | undefined} defined callback when variable is defined
* @param {AsArray<T>} args args for the hook
* @returns {R | undefined} result of hook
*/
callHooksForExpressionWithFallback(
hookMap,
expr,
fallback,
defined,
...args
) {
const exprName = this.getMemberExpressionInfo(
expr,
ALLOWED_MEMBER_TYPES_EXPRESSION
);
if (exprName !== undefined) {
const members = exprName.getMembers();
return this.callHooksForInfoWithFallback(
hookMap,
members.length === 0 ? exprName.rootInfo : exprName.name,
fallback &&
((name) => fallback(name, exprName.rootInfo, exprName.getMembers)),
defined && (() => defined(exprName.name)),
...args
);
}
}
/**
* Call hooks for name.
* @template T
* @template R
* @param {HookMap<SyncBailHook<T, R>>} hookMap hooks the should be called
* @param {string} name key in map
* @param {AsArray<T>} args args for the hook
* @returns {R | undefined} result of hook
*/
callHooksForName(hookMap, name, ...args) {
return this._callHooksForInfo(
hookMap,
this.getVariableInfo(name),
undefined,
undefined,
args
);
}
/**
* Call hooks for info.
* @template T
* @template R
* @param {HookMap<SyncBailHook<T, R>>} hookMap hooks that should be called
* @param {ExportedVariableInfo} info variable info
* @param {AsArray<T>} args args for the hook
* @returns {R | undefined} result of hook
*/
callHooksForInfo(hookMap, info, ...args) {
return this._callHooksForInfo(hookMap, info, undefined, undefined, args);
}
/**
* Call hooks for info with fallback.
* @template T
* @template R
* @param {HookMap<SyncBailHook<T, R>>} hookMap hooks the should be called
* @param {ExportedVariableInfo} info variable info
* @param {((name: string) => R | undefined) | undefined} fallback callback when variable in not handled by hooks
* @param {((result?: string) => R | undefined) | undefined} defined callback when variable is defined
* @param {AsArray<T>} args args for the hook
* @returns {R | undefined} result of hook
*/
callHooksForInfoWithFallback(hookMap, info, fallback, defined, ...args) {
return this._callHooksForInfo(hookMap, info, fallback, defined, args);
}
/**
* Shared core for the callHooksFor* helpers. Takes `args` as an already
* collected array so the public wrappers each allocate it once instead of
* re-collecting it through several rest-parameter layers (hot per identifier).
* @template T
* @template R
* @param {HookMap<SyncBailHook<T, R>>} hookMap hooks that should be called
* @param {ExportedVariableInfo} info variable info
* @param {((name: string) => R | undefined) | undefined} fallback callback when variable is not handled by hooks
* @param {((result?: string) => R | undefined) | undefined} defined callback when variable is defined
* @param {AsArray<T>} args args for the hook
* @returns {R | undefined} result of hook
*/
_callHooksForInfo(hookMap, info, fallback, defined, args) {
/** @type {string} */
let name;
if (typeof info === "string") {
name = info;
} else {
if (!JavascriptParser.isVariableInfo(info)) {
if (defined !== undefined) {
return defined();
}
return;
}
let tagInfo = info.tagInfo;
while (tagInfo !== undefined) {
const hook = hookMap.get(tagInfo.tag);
if (hook !== undefined) {
this.currentTagData = tagInfo.data;
const result = hook.call(...args);
this.currentTagData = undefined;
if (result !== undefined) return result;
}
tagInfo = tagInfo.next;
}
if (!info.isFree() && !info.isTagged()) {
if (defined !== undefined) {
return defined();
}
return;
}
name = /** @type {string} */ (info.name);
}
const hook = hookMap.get(name);
if (hook !== undefined) {
const result = hook.call(...args);
if (result !== undefined) return result;
}
if (fallback !== undefined) {
return fallback(name);
}
}
/**
* Whether `_callHooksForInfo` would call any hook for this info, so a
* caller can skip building the arguments for a name nothing taps.
* @template T
* @template R
* @param {HookMap<SyncBailHook<T, R>>} hookMap hooks that would be called
* @param {ExportedVariableInfo} info variable info
* @returns {boolean} true when a hook is registered for the info
*/
_hasHooksForInfo(hookMap, info) {
if (typeof info === "string") return hookMap.get(info) !== undefined;
if (!JavascriptParser.isVariableInfo(info)) return false;
let tagInfo = info.tagInfo;
while (tagInfo !== undefined) {
if (hookMap.get(tagInfo.tag) !== undefined) return true;
tagInfo = tagInfo.next;
}
if (!info.isFree() && !info.isTagged()) return false;
return hookMap.get(/** @type {string} */ (info.name)) !== undefined;
}
/**
* Call hooks for name with fallback.
* @template T
* @template R
* @param {HookMap<SyncBailHook<T, R>>} hookMap hooks the should be called
* @param {string} name key in map
* @param {((value: string) => R | undefined) | undefined} fallback callback when variable in not handled by hooks
* @param {(() => R) | undefined} defined callback when variable is defined
* @param {AsArray<T>} args args for the hook
* @returns {R | undefined} result of hook
*/
callHooksForNameWithFallback(hookMap, name, fallback, defined, ...args) {
return this._callHooksForInfo(
hookMap,
this.getVariableInfo(name),
fallback,
defined,
args
);
}
/**
* Single-argument twin of `callHooksForName` (must stay in sync with
* `_callHooksForInfo`): every internal dispatch passes exactly one hook
* argument, so this skips the rest-array collection and spread call the
* generic path pays per identifier.
* @template T
* @template R
* @param {HookMap<SyncBailHook<T, R>>} hookMap hooks that should be called
* @param {string} name key in map
* @param {AsArray<T>[0]} arg the single hook argument
* @returns {R | undefined} result of hook
*/
_callHooksForName1(hookMap, name, arg) {
return this._callHooksForInfo1(hookMap, this.getVariableInfo(name), arg);
}
/**
* Single-argument twin of `callHooksForInfo` (must stay in sync with
* `_callHooksForInfo`), for the hot dispatch sites that pass one argument
* and no fallback/defined callbacks.
* @template T
* @template R
* @param {HookMap<SyncBailHook<T, R>>} hookMap hooks that should be called
* @param {ExportedVariableInfo} info variable info
* @param {AsArray<T>[0]} arg the single hook argument
* @returns {R | undefined} result of hook
*/
_callHooksForInfo1(hookMap, info, arg) {
/** @type {string} */
let name;
if (typeof info === "string") {
name = info;
} else {
if (!JavascriptParser.isVariableInfo(info)) {
return;
}
let tagInfo = info.tagInfo;
while (tagInfo !== undefined) {
const hook =
/** @type {SyncBailHook<[AsArray<T>[0]], R> | undefined} */
(hookMap.get(tagInfo.tag));
if (hook !== undefined) {
this.currentTagData = tagInfo.data;
const result = hook.call(arg);
this.currentTagData = undefined;
if (result !== undefined) return result;
}
tagInfo = tagInfo.next;
}
if (!info.isFree() && !info.isTagged()) {
return;
}
name = /** @type {string} */ (info.name);
}
const hook =
/** @type {SyncBailHook<[AsArray<T>[0]], R> | undefined} */
(hookMap.get(name));
if (hook !== undefined) {
const result = hook.call(arg);
if (result !== undefined) return result;
}
}
/**
* Processes the provided param.
* @deprecated
* @param {(string | Pattern | Property)[]} params scope params
* @param {() => void} fn inner function
* @returns {void}
*/
inScope(params, fn) {
const oldScope = this.scope;
this.scope = {
topLevelScope: oldScope.topLevelScope,
inTry: false,
inShorthand: false,
inTaggedTemplateTag: false,
isStrict: oldScope.isStrict,
isAsmJs: oldScope.isAsmJs,
terminated: undefined,
definitions: oldScope.definitions.createChild()
};
this.undefineVariable("this");
this.enterPatterns(params, this._defineVariable);
fn();
this.scope = oldScope;
}
/**
* Processes the provided has thi.
* @param {boolean} hasThis true, when this is defined
* @param {Identifier[]} params scope params
* @param {() => void} fn inner function
* @returns {void}
*/
inClassScope(hasThis, params, fn) {
const oldScope = this.scope;
this.scope = {
topLevelScope: oldScope.topLevelScope,
inTry: false,
inShorthand: false,
inTaggedTemplateTag: false,
isStrict: oldScope.isStrict,
isAsmJs: oldScope.isAsmJs,
terminated: undefined,
definitions: oldScope.definitions.createChild()
};
if (hasThis) {
this.undefineVariable("this");
}
this.enterPatterns(params, this._defineVariable);
fn();
this.scope = oldScope;
}
/**
* Processes the provided has thi.
* @param {boolean} hasThis true, when this is defined
* @param {(Pattern | string)[]} params scope params
* @param {() => void} fn inner function
* @returns {void}
*/
inFunctionScope(hasThis, params, fn) {
const oldScope = this._enterFunctionScope(hasThis, params);
fn();
this.scope = oldScope;
}
/**
* Processes the provided fn.
* @param {() => void} fn inner function
* @param {boolean} inExecutedPath executed state
* @returns {void}
*/
inBlockScope(fn, inExecutedPath = false) {
const oldScope = this._enterBlockScope();
fn();
this._exitBlockScope(oldScope, inExecutedPath);
}
/**
* Enters a fresh block scope: the closure-free core of `inBlockScope`, used
* directly by the internal walkers so each block does not allocate a
* closure and context. Pass the returned scope to `_exitBlockScope`.
* @returns {ScopeInfo} the previous scope
*/
_enterBlockScope() {
const oldScope = this.scope;
this.scope = {
topLevelScope: oldScope.topLevelScope,
inTry: oldScope.inTry,
inShorthand: false,
inTaggedTemplateTag: false,
isStrict: oldScope.isStrict,
isAsmJs: oldScope.isAsmJs,
terminated: oldScope.terminated,
definitions: oldScope.definitions.createChild()
};
return oldScope;
}
/**
* Leaves a block scope entered with `_enterBlockScope`.
* @param {ScopeInfo} oldScope scope returned by `_enterBlockScope`
* @param {boolean} inExecutedPath whether termination propagates to the outer scope
* @returns {void}
*/
_exitBlockScope(oldScope, inExecutedPath) {
const terminated = this.scope.terminated;
if (inExecutedPath && terminated) {
oldScope.terminated = terminated;
}
this.scope = oldScope;
}
/**
* Enters a fresh function scope: the closure-free core of `inFunctionScope`,
* used directly by the internal walkers. Restore `this.scope` to the
* returned scope when leaving.
* @param {boolean} hasThis true, when this is defined
* @param {(Pattern | string)[]} params scope params
* @returns {ScopeInfo} the previous scope
*/
_enterFunctionScope(hasThis, params) {
const oldScope = this.scope;
this.scope = {
topLevelScope: oldScope.topLevelScope,
inTry: false,
inShorthand: false,
inTaggedTemplateTag: false,
isStrict: oldScope.isStrict,
isAsmJs: oldScope.isAsmJs,
terminated: undefined,
definitions: oldScope.definitions.createChild()
};
if (hasThis) {
this.undefineVariable("this");
}
this.enterPatterns(params, this._defineVariable);
return oldScope;
}
/**
* The directive a statement spells, or `undefined` when it is not one. An
* AST webpack did not parse may leave the metadata off a directive, so the
* spelling is recovered from the literal itself there.
* @param {Directive | Statement | ModuleDeclaration} statement statement
* @returns {string | undefined} the spelling between the quotes
*/
getDirective(statement) {
if (statement.type !== "ExpressionStatement") return undefined;
const directive = /** @type {Directive} */ (statement).directive;
if (directive !== undefined || this._hasParserRecord) return directive;
const expression = statement.expression;
if (expression.type !== "Literal" || typeof expression.value !== "string") {
return undefined;
}
// a parenthesized string is an expression rather than a directive, and
// only the statement says so — the literal starts inside the parentheses
if (
statement.range !== undefined &&
expression.range !== undefined &&
statement.range[0] !== expression.range[0]
) {
return undefined;
}
// the raw text keeps an escape unexpanded, which is what separates a
// directive from a string that merely reads like one
return expression.raw === undefined
? expression.value
: expression.raw.slice(1, -1);
}
/**
* Processes the provided statement.
* @param {(Directive | Statement | ModuleDeclaration)[]} statements statements
*/
detectMode(statements) {
const statement = statements.length >= 1 ? statements[0] : undefined;
if (
statement === undefined ||
statement.type !== "ExpressionStatement" ||
statement.expression.type !== "Literal"
) {
return;
}
const value = /** @type {Literal} */ (statement.expression).value;
if (value === "use asm") {
this.scope.isAsmJs = true;
}
for (const directiveStatement of statements) {
const directive = this.getDirective(directiveStatement);
if (directive === undefined) break;
if (directive === "use strict") {
this.scope.isStrict = true;
break;
}
}
}
/**
* Processes the provided pattern.
* @param {(string | Pattern | Property)[]} patterns patterns
* @param {OnIdentString} onIdent on ident callback
*/
enterPatterns(patterns, onIdent) {
for (let i = 0, len = patterns.length; i < len; i++) {
const pattern = patterns[i];
if (typeof pattern !== "string") {
this.enterPattern(pattern, onIdent);
} else if (pattern) {
onIdent(pattern);
}
}
}
/**
* Processes the provided pattern.
* @param {Pattern | Property} pattern pattern
* @param {OnIdent} onIdent on ident callback
*/
enterPattern(pattern, onIdent) {
if (!pattern) return;
// plain identifiers dominate bindings, so match them first
switch (pattern.type) {
case "Identifier":
this.enterIdentifier(pattern, onIdent);
break;
case "ObjectPattern":
this.enterObjectPattern(pattern, onIdent);
break;
case "Property":
if (pattern.shorthand && pattern.value.type === "Identifier") {
this.scope.inShorthand = pattern.value.name;
this.enterIdentifier(pattern.value, onIdent);
this.scope.inShorthand = false;
} else {
this.enterPattern(/** @type {Pattern} */ (pattern.value), onIdent);
}
break;
case "ArrayPattern":
this.enterArrayPattern(pattern, onIdent);
break;
case "AssignmentPattern":
this.enterAssignmentPattern(pattern, onIdent);
break;
case "RestElement":
this.enterRestElement(pattern, onIdent);
break;
}
}
/**
* Processes the provided pattern.
* @param {Identifier} pattern identifier pattern
* @param {OnIdent} onIdent callback
*/
enterIdentifier(pattern, onIdent) {
if (!this._callHooksForName1(this.hooks.pattern, pattern.name, pattern)) {
onIdent(pattern.name, pattern);
}
}
/**
* Enter object pattern.
* @param {ObjectPattern} pattern object pattern
* @param {OnIdent} onIdent callback
*/
enterObjectPattern(pattern, onIdent) {
for (
let propIndex = 0, len = pattern.properties.length;
propIndex < len;
propIndex++
) {
const prop = pattern.properties[propIndex];
this.enterPattern(prop, onIdent);
}
}
/**
* Enter array pattern.
* @param {ArrayPattern} pattern object pattern
* @param {OnIdent} onIdent callback
*/
enterArrayPattern(pattern, onIdent) {
for (
let elementIndex = 0, len = pattern.elements.length;
elementIndex < len;
elementIndex++
) {
const element = pattern.elements[elementIndex];
if (element) {
this.enterPattern(element, onIdent);
}
}
}
/**
* Enter rest element.
* @param {RestElement} pattern object pattern
* @param {OnIdent} onIdent callback
*/
enterRestElement(pattern, onIdent) {
this.enterPattern(pattern.argument, onIdent);
}
/**
* Enter assignment pattern.
* @param {AssignmentPattern} pattern object pattern
* @param {OnIdent} onIdent callback
*/
enterAssignmentPattern(pattern, onIdent) {
this.enterPattern(pattern.left, onIdent);
}
/**
* Evaluate expression.
* @param {Expression | SpreadElement | PrivateIdentifier | Super} expression expression node
* @returns {BasicEvaluatedExpression} evaluation result
*/
evaluateExpression(expression) {
try {
const hook = this.hooks.evaluate.get(expression.type);
if (hook !== undefined) {
const result = hook.call(expression);
if (result !== undefined && result !== null) {
result.setExpression(expression);
return result;
}
}
} catch (err) {
// eslint-disable-next-line no-console
console.warn(err);
// ignore error
}
// no setRange: the range is served lazily from the expression on first
// read, so plain results don't materialize the node's range array at all
return new BasicEvaluatedExpression().setExpression(expression);
}
/**
* Returns parsed string.
* @param {Expression} expression expression
* @returns {string} parsed string
*/
parseString(expression) {
switch (expression.type) {
case "BinaryExpression":
if (expression.operator === "+") {
return (
this.parseString(/** @type {Expression} */ (expression.left)) +
this.parseString(expression.right)
);
}
break;
case "Literal":
return String(expression.value);
}
throw new Error(
`${expression.type} is not supported as parameter for require`
);
}
/** @typedef {{ range?: Range, value: string, code: boolean, conditional: false | CalculatedStringResult[] }} CalculatedStringResult */
/**
* Parses calculated string.
* @param {Expression} expression expression
* @returns {CalculatedStringResult} result
*/
parseCalculatedString(expression) {
switch (expression.type) {
case "BinaryExpression":
if (expression.operator === "+") {
const left = this.parseCalculatedString(
/** @type {Expression} */
(expression.left)
);
const right = this.parseCalculatedString(expression.right);
if (left.code) {
return {
range: left.range,
value: left.value,
code: true,
conditional: false
};
} else if (right.code) {
return {
range: [
/** @type {Range} */
(left.range)[0],
right.range
? right.range[1]
: /** @type {Range} */ (left.range)[1]
],
value: left.value + right.value,
code: true,
conditional: false
};
}
return {
range: [
/** @type {Range} */
(left.range)[0],
/** @type {Range} */
(right.range)[1]
],
value: left.value + right.value,
code: false,
conditional: false
};
}
break;
case "ConditionalExpression": {
const consequent = this.parseCalculatedString(expression.consequent);
const alternate = this.parseCalculatedString(expression.alternate);
/** @type {CalculatedStringResult[]} */
const items = [];
if (consequent.conditional) {
items.push(...consequent.conditional);
} else if (!consequent.code) {
items.push(consequent);
} else {
break;
}
if (alternate.conditional) {
items.push(...alternate.conditional);
} else if (!alternate.code) {
items.push(alternate);
} else {
break;
}
return {
range: undefined,
value: "",
code: true,
conditional: items
};
}
case "Literal":
return {
range: expression.range,
value: String(expression.value),
code: false,
conditional: false
};
}
return {
range: undefined,
value: "",
code: true,
conditional: false
};
}
/**
* Parses the provided source and updates the parser state.
* @param {string | Buffer | PreparsedAst} source the source to parse
* @param {ParserState} state the parser state
* @returns {ParserState} the parser state
*/
parse(source, state) {
if (source === null) {
throw new Error("source must not be null");
}
if (Buffer.isBuffer(source)) {
source = source.toString("utf8");
// Keep `state.source` as a string so downstream walkers can read
// the original text without re-decoding the Buffer on every use.
state.source = source;
}
let ast;
/** @type {Comment[]} */
let comments;
if (typeof source === "object") {
ast = /** @type {Program} */ (source);
comments = source.comments;
} else {
({ ast, comments } = JavascriptParser._parse(
source,
{
sourceType: this.sourceType,
// line/column locations are derived from node offsets via
// getLocation — parsers never need to track them
locations: false,
ranges: true,
comments: true,
importPhases: this.options.importPhases === true
},
this.options.parse
));
}
const oldScope = this.scope;
const oldState = this.state;
const oldComments = this.comments;
const oldSemicolons = this.semicolons;
const oldStatementPath = this.statementPath;
const oldPrevStatement = this.prevStatement;
const oldStrictInModuleOutput = this._strictInModuleOutput;
const oldSource = this._source;
const oldLineStarts = this._lineStarts;
const oldHasParserRecord = this._hasParserRecord;
// WHY: the parser's record of each scope is only the truth for a tree it
// just built. A loader's AST, and whatever `module.parser.javascript.parse`
// returns, may have been edited after it was read — or built by something
// else entirely — so both are collected from instead of trusted.
this._hasParserRecord =
typeof source !== "object" && typeof this.options.parse !== "function";
// a preparsed AST carries no text, but the build passes the module
// source in the state — offsets still map for it
this._source =
typeof source === "string"
? source
: typeof state.source === "string"
? state.source
: undefined;
this._lineStarts = undefined;
this.scope = {
topLevelScope: true,
inTry: false,
inShorthand: false,
inTaggedTemplateTag: false,
isStrict: false,
isAsmJs: false,
terminated: undefined,
definitions: new StackedMap()
};
this.state = /** @type {JavascriptParserState} */ (state);
this.comments = comments;
this.semicolons = undefined;
// evaluating a defined, untagged identifier can only fall through when
// every `evaluate.for("Identifier")` tap is the parser's own — plugins
// tapping or intercepting it disable the callee/rename fast paths
const identifierEvalHook = this.hooks.evaluate.get("Identifier");
this._evalIdentOwnTaps =
identifierEvalHook !== undefined &&
identifierEvalHook.interceptors.length === 0 &&
identifierEvalHook.taps.every((tap) => tap.name === CLASS_NAME);
this.statementPath = [];
this.prevStatement = undefined;
// The output is emitted as strict-mode ESM, but this module was parsed as
// a loose script — acorn skipped the strict early errors that will break
// the bundle at runtime. Enables the strict-mode diagnostics below.
this._strictInModuleOutput =
this.options.strictModeViolations !== false &&
state.compilation !== undefined &&
state.compilation.runtimeTemplate.isModule();
if (this.hooks.program.call(ast, comments) === undefined) {
this.destructuringAssignmentProperties = new WeakMap();
this.detectMode(ast.body);
// a strict program (ESM source, `"use strict"`) keeps its behavior in
// strict output — drop the flag so the walk pays no per-node checks
if (this._strictInModuleOutput && this.scope.isStrict) {
this._strictInModuleOutput = false;
}
this._modulePreWalkDeclarations(
moduleDeclarationsOf(ast, this._hasParserRecord)
);
this.prevStatement = undefined;
this._preWalkScope(ast);
this.prevStatement = undefined;
this._blockPreWalkScope(ast, ast.body);
this.prevStatement = undefined;
this.walkStatements(ast.body);
this.destructuringAssignmentProperties = undefined;
}
this.hooks.finish.call(ast, comments);
this._releaseMemberExpression();
this.scope = oldScope;
this.state = oldState;
this.comments = oldComments;
this.semicolons = oldSemicolons;
this.statementPath = oldStatementPath;
this.prevStatement = oldPrevStatement;
this._strictInModuleOutput = oldStrictInModuleOutput;
// release — these retain the whole source string and its line table
this._source = oldSource;
this._lineStarts = oldLineStarts;
this._hasParserRecord = oldHasParserRecord;
return state;
}
/**
* Returns evaluation result.
* @param {string} source source code
* @returns {BasicEvaluatedExpression} evaluation result
*/
evaluate(source) {
const importPhases = this.options.importPhases === true;
const compilation = this.state && this.state.compilation;
/** @type {Map<string, Program> | undefined} */
let cache;
// custom parse functions are not necessarily pure, do not cache for them
if (compilation !== undefined && typeof this.options.parse !== "function") {
cache = evaluateAstCaches.get(compilation);
if (cache === undefined) {
cache = new Map();
evaluateAstCaches.set(compilation, cache);
}
}
const cacheKey =
cache === undefined
? undefined
: `${this.sourceType}|${importPhases}|${source}`;
let ast =
cacheKey === undefined
? undefined
: /** @type {Map<string, Program>} */ (cache).get(cacheKey);
if (ast === undefined) {
({ ast } = JavascriptParser._parse(
`(${source})`,
{
sourceType: this.sourceType,
// ranges alone take the lazy path, which serves `range` from a
// getter and tracks no locations — as the module parse does
ranges: true,
importPhases
},
this.options.parse
));
if (cacheKey !== undefined) {
const map = /** @type {Map<string, Program>} */ (cache);
if (map.size >= EVALUATE_AST_CACHE_LIMIT) map.clear();
map.set(cacheKey, ast);
}
}
if (ast.body.length !== 1 || ast.body[0].type !== "ExpressionStatement") {
throw new Error("evaluate: Source is not a expression");
}
return this.evaluateExpression(ast.body[0].expression);
}
/**
* Checks whether this javascript parser is pure.
* @param {Expression | Declaration | PrivateIdentifier | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration | null | undefined} expr an expression
* @param {number} commentsStartPos source position from which annotation comments are checked
* @returns {boolean} true, when the expression is pure
*/
isPure(expr, commentsStartPos) {
if (!expr) return true;
const result = this.hooks.isPure
.for(expr.type)
.call(expr, commentsStartPos);
if (typeof result === "boolean") return result;
switch (expr.type) {
case "ClassDeclaration":
case "ClassExpression": {
if (expr.body.type !== "ClassBody") return false;
if (
expr.superClass &&
!this.isPure(expr.superClass, /** @type {Range} */ (expr.range)[0])
) {
return false;
}
const items = expr.body.body;
return items.every((item) => {
if (item.type === "StaticBlock") {
return false;
}
if (
item.computed &&
item.key &&
!this.isPure(
item.key,
/** @type {Range} */
(item.range)[0]
)
) {
return false;
}
if (
item.static &&
item.value &&
!this.isPure(
item.value,
item.key
? /** @type {Range} */ (item.key.range)[1]
: /** @type {Range} */ (item.range)[0]
)
) {
return false;
}
if (
expr.superClass &&
item.type === "MethodDefinition" &&
item.kind === "constructor"
) {
return false;
}
return true;
});
}
case "TemplateLiteral":
// Thread `commentsStartPos` through the interpolations, so a
// `/*#__PURE__*/` inside `${ … }`, or before the first one, is within
// the range scanned when the inner expression's purity is judged.
return expr.expressions.every((e) => {
const pureFlag = this.isPure(e, commentsStartPos);
commentsStartPos = /** @type {Range} */ (e.range)[1];
return pureFlag;
});
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
case "ThisExpression":
case "Literal":
case "Identifier":
case "PrivateIdentifier":
return true;
case "VariableDeclaration":
return expr.declarations.every((decl) =>
this.isPure(decl.init, /** @type {Range} */ (decl.range)[0])
);
case "ArrayExpression":
return expr.elements.every((element) => {
if (element === null) return true;
if (element.type === "SpreadElement") return false;
const pureFlag = this.isPure(element, commentsStartPos);
commentsStartPos = /** @type {Range} */ (element.range)[1];
return pureFlag;
});
case "ObjectExpression": {
return expr.properties.every((property) => {
if (property.type === "SpreadElement") return false;
if (
property.computed &&
!this.isPure(property.key, commentsStartPos)
) {
return false;
}
const pureFlag = this.isPure(
/** @type {Exclude<Property["value"], AssignmentPattern | ObjectPattern | ArrayPattern | RestElement>} */
(property.value),
/** @type {Range} */ (property.key.range)[1]
);
commentsStartPos = /** @type {Range} */ (property.range)[1];
return pureFlag;
});
}
case "ChainExpression":
return this.isPure(expr.expression, commentsStartPos);
case "UnaryExpression":
// WHY: these three produce their result without invoking user code on
// the operand. `typeof` returns a type tag and never throws, even for an
// undeclared identifier, and coerces nothing; `void` evaluates the
// operand and discards it, so it is pure exactly when the operand is; and
// `!` coerces through ToBoolean, which is defined to call no user code.
// `+`, `-`, `~` and `delete` fall through to the generic evaluator, which
// still recognizes literal cases such as `-1` and `+5`.
if (
expr.operator === "typeof" ||
expr.operator === "void" ||
expr.operator === "!"
) {
return this.isPure(expr.argument, commentsStartPos);
}
break;
case "MetaProperty":
return true;
case "BinaryExpression":
// WHY: strict (in)equality compares without coercion and never invokes user
// code on its operands, so the result is pure where both sides are. Every
// other binary operator may invoke `valueOf`, `toString`,
// `[Symbol.hasInstance]` or a Proxy trap, and falls through to the generic
// evaluator, which still recognizes the cases where both sides evaluate
// to known primitive literals.
if (expr.operator === "===" || expr.operator === "!==") {
return (
this.isPure(expr.left, commentsStartPos) &&
this.isPure(expr.right, /** @type {Range} */ (expr.left.range)[1])
);
}
break;
case "ConditionalExpression":
return (
this.isPure(expr.test, commentsStartPos) &&
this.isPure(
expr.consequent,
/** @type {Range} */ (expr.test.range)[1]
) &&
this.isPure(
expr.alternate,
/** @type {Range} */ (expr.consequent.range)[1]
)
);
case "LogicalExpression":
return (
this.isPure(expr.left, commentsStartPos) &&
this.isPure(expr.right, /** @type {Range} */ (expr.left.range)[1])
);
case "SequenceExpression":
return expr.expressions.every((expr) => {
const pureFlag = this.isPure(expr, commentsStartPos);
commentsStartPos = /** @type {Range} */ (expr.range)[1];
return pureFlag;
});
case "CallExpression": {
const pureFlag =
/** @type {Range} */ (expr.range)[0] - commentsStartPos > 12 &&
this.getComments([
commentsStartPos,
/** @type {Range} */ (expr.range)[0]
]).some(
(comment) =>
comment.type === "Block" &&
CompilerHintNotationRegExp.Pure.test(comment.value)
);
if (!pureFlag) return false;
commentsStartPos = /** @type {Range} */ (expr.callee.range)[1];
return expr.arguments.every((arg) => {
if (arg.type === "SpreadElement") return false;
const pureFlag = this.isPure(arg, commentsStartPos);
commentsStartPos = /** @type {Range} */ (arg.range)[1];
return pureFlag;
});
}
case "NewExpression": {
const pureFlag =
/** @type {Range} */ (expr.range)[0] - commentsStartPos > 12 &&
this.getComments([
commentsStartPos,
/** @type {Range} */ (expr.range)[0]
]).some(
(comment) =>
comment.type === "Block" &&
CompilerHintNotationRegExp.Pure.test(comment.value)
);
if (!pureFlag) return false;
commentsStartPos = /** @type {Range} */ (expr.callee.range)[1];
return expr.arguments.every((arg) => {
if (arg.type === "SpreadElement") return false;
const pureFlag = this.isPure(arg, commentsStartPos);
commentsStartPos = /** @type {Range} */ (arg.range)[1];
return pureFlag;
});
}
case "TaggedTemplateExpression": {
const pureFlag =
/** @type {Range} */ (expr.range)[0] - commentsStartPos > 12 &&
this.getComments([
commentsStartPos,
/** @type {Range} */ (expr.range)[0]
]).some(
(comment) =>
comment.type === "Block" &&
CompilerHintNotationRegExp.Pure.test(comment.value)
);
if (!pureFlag) return false;
commentsStartPos = /** @type {Range} */ (expr.tag.range)[1];
return expr.quasi.expressions.every((e) => {
const pureFlag = this.isPure(e, commentsStartPos);
commentsStartPos = /** @type {Range} */ (e.range)[1];
return pureFlag;
});
}
}
const evaluated = this.evaluateExpression(expr);
return !evaluated.couldHaveSideEffects();
}
/**
* Returns the location of a node or comment, computed from its offsets so
* ASTs from parsers without location support work too. Falls back to the
* node's own `loc` only when no source text is available (preparsed ASTs).
* Offsets can only be mapped while parsing — callbacks deferred past the
* `parse()` call must take the location upfront.
* @param {{ start?: number, end?: number, range?: Range, loc?: SourceLocation | null }} node node or comment
* @returns {DependencyLocation} location of the node
*/
getLocation(node) {
const source = this._source;
if (source !== undefined) {
const start =
typeof node.start === "number"
? node.start
: node.range
? node.range[0]
: undefined;
if (start !== undefined) {
const end =
typeof node.end === "number"
? node.end
: node.range
? node.range[1]
: start;
const lineStarts =
this._lineStarts || (this._lineStarts = buildLineStarts(source));
return {
start: positionAt(lineStarts, start),
end: positionAt(lineStarts, end)
};
}
}
const loc = node.loc;
if (!loc) {
return /** @type {DependencyLocation} */ (
/** @type {DependencyLocation | null | undefined} */ (loc)
);
}
// a plain copy: a foreign parser's location class must not reach the cache
return {
start: { line: loc.start.line, column: loc.start.column },
end: { line: loc.end.line, column: loc.end.column }
};
}
/**
* Returns comments in the range.
* @param {Range} range range
* @returns {Comment[]} comments in the range
*/
getComments(range) {
return getCommentsInRange(
/** @type {(Comment & { range: [number, number] })[]} */ (this.comments),
range
);
}
/**
* Where the comments written for `statement` begin: the run of comments
* before it that whitespace alone separates from it and from each other.
* @param {StatementPathItem} statement the statement they belong to
* @returns {number} the offset the run starts at, or the statement's own
*/
getAttachedCommentsStart(statement) {
let position = /** @type {Range} */ (statement.range)[0];
// what precedes `export function f() {}` is written for the export, and
// the export keyword is the token the run would otherwise stop at
const path = this.statementPath;
if (path !== undefined && path.length > 1) {
const outer = path[path.length - 2];
if (
(outer.type === "ExportNamedDeclaration" ||
outer.type === "ExportDefaultDeclaration") &&
outer.declaration === statement
) {
position = /** @type {Range} */ (outer.range)[0];
}
}
const comments = this.comments;
if (comments === undefined) return position;
const source = this._source;
for (
let index = binarySearchBounds.le(comments, position, compareCommentEnd);
index >= 0;
index--
) {
const range = /** @type {Range} */ (comments[index].range);
if (range[1] > position) continue;
// with no source to read, nothing can be ruled out, so the run is
// taken to reach as far as the comments themselves do
if (
source !== undefined &&
!onlyWhitespaceBetween(source, range[1], position)
) {
break;
}
position = range[0];
}
return position;
}
/**
* Reports whether `identifier` may occur as a word in the source text of
* `range`. A conservative pre-filter for expensive AST scans: `false` means
* it certainly does not occur, while a match inside a comment or a string
* yields `true`. Also `true` when no source text is available (preparsed
* AST), so callers must stay correct without the filter.
* @param {Range} range range to search
* @param {string} identifier identifier to look for
* @returns {boolean} false when the identifier certainly does not occur
*/
sourceMayContainIdentifier(range, identifier) {
const source = this._source;
if (source === undefined) return true;
const end = range[1];
const length = identifier.length;
for (
let index = source.indexOf(identifier, range[0]);
index !== -1 && index + length <= end;
index = source.indexOf(identifier, index + length)
) {
if (
(index === 0 || !isIdentifierChar(source.charCodeAt(index - 1))) &&
(index + length === source.length ||
!isIdentifierChar(source.charCodeAt(index + length)))
) {
return true;
}
}
return false;
}
/**
* First significant char code at or after `pos`, skipping whitespace,
* comments and grouping `)` (a parenthesized node's range excludes its
* wrapping parens, so a `,`/`;` separator can sit behind them), or -1 at
* end of source.
* @param {string} source source text
* @param {number} pos start offset
* @returns {number} char code of the next token, or -1
*/
_nextTokenCharCode(source, pos) {
const len = source.length;
for (let i = pos; i < len; i++) {
const c = source.charCodeAt(i);
// whitespace and line terminators acorn skips between tokens
if (
c === 32 ||
c === 9 ||
c === 10 ||
c === 13 ||
c === 11 ||
c === 12 ||
c === 41 /* ) */ ||
c === 0xa0 ||
c === 0xfeff ||
c === 0x2028 ||
c === 0x2029
) {
continue;
}
if (c === 47 /* / */) {
const n = source.charCodeAt(i + 1);
if (n === 47 /* / */) {
i += 2;
while (i < len) {
const cc = source.charCodeAt(i);
if (cc === 10 || cc === 13 || cc === 0x2028 || cc === 0x2029) break;
i++;
}
continue;
}
if (n === 42 /* * */) {
i += 2;
while (
i < len &&
!(source.charCodeAt(i) === 42 && source.charCodeAt(i + 1) === 47)
) {
i++;
}
i++;
continue;
}
}
return c;
}
return -1;
}
/**
* Whether a statement ending at `pos` relies on ASI (no real separator).
* Derived from the source text unless overridden by set/unsetAsiPosition.
* A statement terminated by a real `;` or continued by a `,` (a sequence
* element) is not an ASI position; anything else (newline, `}`, eof) is.
* @param {number} pos statement end offset
* @returns {boolean} true when ASI inserts a semicolon at this position
*/
_isAsiPosition(pos) {
if (this.semicolons !== undefined) {
const override = this.semicolons.get(pos);
if (override !== undefined) return override;
}
const source = this._source;
if (source === undefined) return true;
// a real semicolon is the statement's last char (exclusive end at pos)
if (source.charCodeAt(pos - 1) === 59 /* ; */) return false;
const next = this._nextTokenCharCode(source, pos);
return next !== 44 /* , */ && next !== 59; /* ; */
}
/**
* Checks whether this javascript parser is asi position.
* @param {number} pos source code position
* @returns {boolean} true when a semicolon has been inserted before this position, false if not
*/
isAsiPosition(pos) {
const currentStatement =
/** @type {StatementPath} */
(this.statementPath)[
/** @type {StatementPath} */
(this.statementPath).length - 1
];
if (currentStatement === undefined) throw new Error("Not in statement");
const range = /** @type {Range} */ (currentStatement.range);
return (
// Either asking directly for the end position of the current statement
(range[1] === pos && this._isAsiPosition(pos)) ||
// Or asking for the start position of the current statement,
// here we have to check multiple things
(range[0] === pos &&
// is there a previous statement which might be relevant?
this.prevStatement !== undefined &&
// is the end position of the previous statement an ASI position?
this._isAsiPosition(/** @type {Range} */ (this.prevStatement.range)[1]))
);
}
/**
* Updates asi position using the provided po.
* @param {number} pos source code position
* @returns {void}
*/
setAsiPosition(pos) {
(this.semicolons || (this.semicolons = new Map())).set(pos, true);
}
/**
* Unset asi position.
* @param {number} pos source code position
* @returns {void}
*/
unsetAsiPosition(pos) {
(this.semicolons || (this.semicolons = new Map())).set(pos, false);
}
/**
* Checks whether this javascript parser is statement level expression.
* @param {Expression} expr expression
* @returns {boolean} true, when the expression is a statement level expression
*/
isStatementLevelExpression(expr) {
const currentStatement =
/** @type {StatementPath} */
(this.statementPath)[
/** @type {StatementPath} */
(this.statementPath).length - 1
];
return (
expr === currentStatement ||
(currentStatement.type === "ExpressionStatement" &&
currentStatement.expression === expr)
);
}
/**
* Returns tag data.
* @param {string} name name
* @param {Tag} tag tag info
* @returns {TagData | undefined} tag data
*/
getTagData(name, tag) {
const info = this.scope.definitions.get(name);
if (JavascriptParser.isVariableInfo(info)) {
let tagInfo = info.tagInfo;
while (tagInfo !== undefined) {
if (tagInfo.tag === tag) return tagInfo.data;
tagInfo = tagInfo.next;
}
}
}
/**
* Processes the provided name.
* @param {string} name name
* @param {Tag} tag tag info
* @param {TagData=} data data
* @param {VariableInfoFlagsType=} flags flags
*/
tagVariable(name, tag, data, flags = VariableInfoFlags.Tagged) {
const oldInfo = this.scope.definitions.get(name);
/** @type {VariableInfo} */
let newInfo;
if (oldInfo === undefined) {
newInfo = new VariableInfo(this.scope, name, flags, {
tag,
data,
next: undefined
});
} else if (JavascriptParser.isVariableInfo(oldInfo)) {
newInfo = new VariableInfo(
oldInfo.declaredScope,
oldInfo.name,
/** @type {VariableInfoFlagsType} */ (oldInfo.flags | flags),
{
tag,
data,
next: oldInfo.tagInfo
}
);
} else {
newInfo = new VariableInfo(oldInfo, name, flags, {
tag,
data,
next: undefined
});
}
this.scope.definitions.set(name, newInfo);
this._memberExpression = undefined;
}
/**
* Processes the provided name.
* @param {string} name variable name
*/
defineVariable(name) {
const oldInfo = this.scope.definitions.get(name);
// Don't redefine variable in same scope to keep existing tags
if (
JavascriptParser.isVariableInfo(oldInfo) &&
oldInfo.declaredScope === this.scope
) {
return;
}
this.scope.definitions.set(name, this.scope);
this._memberExpression = undefined;
}
/**
* Processes the provided name.
* @param {string} name variable name
*/
undefineVariable(name) {
this.scope.definitions.delete(name);
this._memberExpression = undefined;
}
/**
* Checks whether this javascript parser is variable defined.
* @param {string} name variable name
* @returns {boolean} true, when variable is defined
*/
isVariableDefined(name) {
const info = this.scope.definitions.get(name);
if (info === undefined) return false;
if (JavascriptParser.isVariableInfo(info)) {
return !info.isFree();
}
return true;
}
/**
* Whether evaluating an identifier of this name can only fall through:
* the exact negative of the identifier evaluator's `getInfo` — defined in
* some scope and neither free nor tagged nor carrying tag info.
* @param {string} name variable name
* @returns {boolean} true when the identifier evaluates to nothing
*/
_isDefinedPlainVariable(name) {
const info = this.scope.definitions.get(name);
if (info === undefined) return false;
if (JavascriptParser.isVariableInfo(info)) {
return !(info.isFree() || info.isTagged() || info.tagInfo !== undefined);
}
return true;
}
/**
* Gets variable info.
* @param {string} name variable name
* @returns {ExportedVariableInfo} info for this variable
*/
getVariableInfo(name) {
const value = this.scope.definitions.get(name);
if (value === undefined) {
return name;
}
return value;
}
/**
* Updates variable using the provided name.
* @param {string} name variable name
* @param {ExportedVariableInfo} variableInfo new info for this variable
* @returns {void}
*/
setVariable(name, variableInfo) {
this._memberExpression = undefined;
if (typeof variableInfo === "string") {
if (variableInfo === name) {
this.scope.definitions.delete(name);
} else {
this.scope.definitions.set(
name,
new VariableInfo(
this.scope,
variableInfo,
VariableInfoFlags.Free,
undefined
)
);
}
} else {
this.scope.definitions.set(name, variableInfo);
}
}
/**
* Evaluated variable.
* @param {TagInfo} tagInfo tag info
* @returns {VariableInfo} variable info
*/
evaluatedVariable(tagInfo) {
return new VariableInfo(
this.scope,
undefined,
VariableInfoFlags.Evaluated,
tagInfo
);
}
/**
* Parses comment options.
* @param {Range} range range of the comment
* @returns {{ options: MagicCommentOptions | null, errors: (Error & { comment: Comment })[] | null }} result
*/
parseCommentOptions(range) {
return parseCommentOptionsInRange(
/** @type {(Comment & { range: [number, number], value: string })[]} */ (
this.comments
),
range,
this.magicCommentContext
);
}
/**
* Finds the root object of a member expression chain without allocating the
* member arrays. The traversal/break logic must stay in sync with
* `extractMemberExpressionChain`; it lets `getMemberExpressionInfo` reject
* unrecognized roots (~77% of calls) before paying for the arrays.
* @param {Expression | Super} expression a member expression
* @returns {Expression | Super} the root object of the chain
*/
getMemberExpressionRoot(expression) {
/** @type {Node} */
let expr = expression;
while (expr.type === "MemberExpression") {
if (expr.computed) {
const prop = expr.property;
if (
prop.type !== "Literal" &&
!(
prop.type === "TemplateLiteral" &&
prop.expressions.length === 0 &&
typeof prop.quasis[0].value.cooked === "string"
)
) {
break;
}
} else if (expr.property.type !== "Identifier") {
break;
}
expr = expr.object;
}
return /** @type {Expression | Super} */ (expr);
}
/**
* Extract member expression chain.
* @param {Expression | Super} expression a member expression
* @returns {{ members: Members, object: Expression | Super, membersOptionals: MembersOptionals, memberRanges: MemberRanges }} member names (reverse order) and remaining object
*/
extractMemberExpressionChain(expression) {
/** @type {Node} */
let expr = expression;
/** @type {Members} */
const members = [];
/** @type {MembersOptionals} */
const membersOptionals = [];
/** @type {MemberRanges} */
const memberRanges = [];
while (expr.type === "MemberExpression") {
if (expr.computed) {
const prop = expr.property;
if (prop.type === "Literal") {
members.push(`${prop.value}`); // the literal
} else if (
prop.type === "TemplateLiteral" &&
prop.expressions.length === 0 &&
typeof prop.quasis[0].value.cooked === "string"
) {
// `[`url`]` is statically a string just like `["url"]`
members.push(prop.quasis[0].value.cooked);
} else {
break;
}
memberRanges.push(/** @type {Range} */ (expr.object.range)); // the range of the expression fragment before the property
} else {
if (expr.property.type !== "Identifier") break;
members.push(expr.property.name); // the identifier
memberRanges.push(/** @type {Range} */ (expr.object.range)); // the range of the expression fragment before the identifier
}
membersOptionals.push(expr.optional);
expr = expr.object;
}
return {
members,
membersOptionals,
memberRanges,
object: expr
};
}
/**
* Gets free info from variable.
* @param {string} varName variable name
* @returns {{ name: string, info: VariableInfo | string } | undefined} name of the free variable and variable info for that
*/
getFreeInfoFromVariable(varName) {
const info = this.getVariableInfo(varName);
/** @type {string} */
let name;
if (JavascriptParser.isVariableInfo(info) && info.name) {
if (!info.isFree()) return;
name = info.name;
} else if (typeof info !== "string") {
return;
} else {
name = info;
}
return { info, name };
}
/**
* Gets name info from variable.
* @param {string} varName variable name
* @returns {{ name: string, info: VariableInfo | string } | undefined} name of the free variable and variable info for that
*/
getNameInfoFromVariable(varName) {
const info = this.getVariableInfo(varName);
/** @type {string} */
let name;
if (JavascriptParser.isVariableInfo(info) && info.name) {
if (!info.isFree() && !info.isTagged()) return;
name = info.name;
} else if (typeof info !== "string") {
return;
} else {
name = info;
}
return { info, name };
}
/** @typedef {{ type: "call", call: CallExpression, calleeName: string, rootInfo: string | VariableInfo, getCalleeMembers: () => CalleeMembers, name: string, getMembers: () => Members, getMembersOptionals: () => MembersOptionals, getMemberRanges: () => MemberRanges }} CallExpressionInfo */
/** @typedef {{ type: "expression", rootInfo: string | VariableInfo, name: string, getMembers: () => Members, getMembersOptionals: () => MembersOptionals, getMemberRanges: () => MemberRanges }} ExpressionExpressionInfo */
/**
* Resolves a member expression into the `_member*` slots, once per node and
* scope. Returns 0 when rejected, 1 for an `expression`, 2 for a `call`.
* @param {Expression | Super} expression a member expression
* @param {number} allowedTypes which types may be returned, presented in bit mask
* @returns {0 | 1 | 2} the kind resolved
*/
_resolveMemberExpression(expression, allowedTypes) {
if (
expression === this._memberExpression &&
this._memberScope === this.scope
) {
const type = this._memberType;
if (type !== 0) {
// the root kind is a fact of the node; the mask only says whether to report it
const bit =
type === 2
? ALLOWED_MEMBER_TYPES_CALL_EXPRESSION
: ALLOWED_MEMBER_TYPES_EXPRESSION;
return (allowedTypes & bit) === 0 ? 0 : type;
}
if ((allowedTypes & ~this._memberMask) === 0) return 0;
}
this._memberExpression = expression;
this._memberScope = this.scope;
this._memberMask = allowedTypes;
this._memberType = 0;
this._memberInfo = undefined;
// Resolve the root first (no allocation); most calls reject here and so
// never build the member arrays
const object = this.getMemberExpressionRoot(expression);
switch (object.type) {
case "CallExpression": {
if ((allowedTypes & ALLOWED_MEMBER_TYPES_CALL_EXPRESSION) === 0) {
return 0;
}
const calleeExpr = object.callee;
const callee =
calleeExpr.type === "MemberExpression"
? this.getMemberExpressionRoot(calleeExpr)
: calleeExpr;
const rootName = getRootName(callee);
if (!rootName) return this._rejectMemberExpression();
const resolvedRoot = this._resolveMemberRoot(rootName);
if (resolvedRoot === undefined) return this._rejectMemberExpression();
/** @type {CalleeMembers} */
let rootMembers = EMPTY_ARRAY;
if (calleeExpr.type === "MemberExpression") {
this._extractMembers(calleeExpr);
rootMembers = this._members;
}
this._extractMembers(expression);
const calleeName = objectAndMembersToName(resolvedRoot, rootMembers);
this._memberType = 2;
this._memberCall = object;
this._memberCalleeName = calleeName;
this._memberCalleeMembers = rootMembers;
this._memberName = objectAndMembersToName(
`${calleeName}()`,
this._members
);
return 2;
}
case "Identifier":
case "MetaProperty":
case "ThisExpression": {
if ((allowedTypes & ALLOWED_MEMBER_TYPES_EXPRESSION) === 0) return 0;
const rootName = getRootName(object);
if (!rootName) return this._rejectMemberExpression();
const resolvedRoot = this._resolveMemberRoot(rootName);
if (resolvedRoot === undefined) return this._rejectMemberExpression();
this._extractMembers(expression);
this._memberType = 1;
this._memberName = objectAndMembersToName(resolvedRoot, this._members);
return 1;
}
}
return this._rejectMemberExpression();
}
/**
* Records that the node resolves to nothing under any mask.
* @returns {0} rejected
*/
_rejectMemberExpression() {
this._memberMask = ALLOWED_MEMBER_TYPES_ALL;
return 0;
}
/**
* `getNameInfoFromVariable` for the resolver: stores the info in
* `_memberRootInfo` and returns the name, or undefined when rejected.
* @param {string} rootName name of the root variable
* @returns {string | undefined} the resolved root name
*/
_resolveMemberRoot(rootName) {
const info = this.getVariableInfo(rootName);
if (JavascriptParser.isVariableInfo(info) && info.name) {
if (!info.isFree() && !info.isTagged()) return;
this._memberRootInfo = info;
return info.name;
}
if (typeof info !== "string") return;
this._memberRootInfo = info;
return info;
}
/**
* Fills `_members` and `_membersOptionals` for the chain, in source order.
* @param {Expression | Super} expression a member expression
* @returns {void}
*/
_extractMembers(expression) {
/** @type {Node} */
let expr = expression;
/** @type {Members} */
const members = [];
/** @type {MembersOptionals} */
const membersOptionals = [];
while (expr.type === "MemberExpression") {
if (expr.computed) {
const prop = expr.property;
if (prop.type === "Literal") {
members.push(`${prop.value}`);
} else if (
prop.type === "TemplateLiteral" &&
prop.expressions.length === 0 &&
typeof prop.quasis[0].value.cooked === "string"
) {
members.push(prop.quasis[0].value.cooked);
} else {
break;
}
} else {
if (expr.property.type !== "Identifier") break;
members.push(expr.property.name);
}
membersOptionals.push(expr.optional);
expr = expr.object;
}
if (members.length > 1) {
members.reverse();
membersOptionals.reverse();
}
this._members = members;
this._membersOptionals = membersOptionals;
}
/**
* Drops the resolved member expression, so no AST outlives its parse.
* @returns {void}
*/
_releaseMemberExpression() {
this._memberExpression = undefined;
this._memberScope = undefined;
this._memberType = 0;
this._memberRootInfo = undefined;
this._members = EMPTY_ARRAY;
this._membersOptionals = EMPTY_MEMBERS_OPTIONALS;
this._memberCall = undefined;
this._memberCalleeMembers = EMPTY_ARRAY;
this._memberInfo = undefined;
}
/**
* Gets member expression info.
* @param {Expression | Super} expression a member expression
* @param {number} allowedTypes which types should be returned, presented in bit mask
* @returns {CallExpressionInfo | ExpressionExpressionInfo | undefined} expression info
*/
getMemberExpressionInfo(expression, allowedTypes) {
const type = this._resolveMemberExpression(expression, allowedTypes);
if (type === 0) return;
const cached = this._memberInfo;
if (cached !== undefined) return cached;
const members = this._members;
const membersOptionals = this._membersOptionals;
/** @type {MemberRanges | undefined} */
let memberRanges;
const getMembers = () => members;
const getMembersOptionals = () => membersOptionals;
const getMemberRanges = () =>
memberRanges ||
(memberRanges = memberRangesOf(expression, members.length));
const rootInfo = /** @type {string | VariableInfo} */ (
this._memberRootInfo
);
/** @type {CallExpressionInfo | ExpressionExpressionInfo} */
let info;
if (type === 2) {
const calleeMembers = this._memberCalleeMembers;
info = {
type: "call",
call: /** @type {CallExpression} */ (this._memberCall),
calleeName: this._memberCalleeName,
rootInfo,
getCalleeMembers: () => calleeMembers,
name: this._memberName,
getMembers,
getMembersOptionals,
getMemberRanges
};
} else {
info = {
type: "expression",
name: this._memberName,
rootInfo,
getMembers,
getMembersOptionals,
getMemberRanges
};
}
this._memberInfo = info;
return info;
}
/**
* Gets name for expression.
* @param {Expression} expression an expression
* @returns {{ name: string, rootInfo: ExportedVariableInfo, getMembers: () => Members } | undefined} name info
*/
getNameForExpression(expression) {
return this.getMemberExpressionInfo(
expression,
ALLOWED_MEMBER_TYPES_EXPRESSION
);
}
/**
* Whether a definition is a variable rather than a free name or a scope.
* @param {ExportedVariableInfo | undefined} value a scope definition
* @returns {value is VariableInfo} true when the value is a variable
*/
static isVariableInfo(value) {
return value instanceof VariableInfo;
}
/**
* Get module parse function.
* @param {Compilation} compilation compilation
* @param {Module} module module
* @returns {ParseFunction | undefined} parser
*/
static _getModuleParseFunction(compilation, module) {
// Get from module if available
if (module.is("normal") && module.parser instanceof JavascriptParser) {
return module.parser.options.parse;
}
// Fallback to the global javascript parse function
if (typeof compilation.options.module.parser.javascript !== "undefined") {
return compilation.options.module.parser.javascript.parse;
}
}
/**
* Returns parse result.
* @param {string} code source code
* @param {InternalParseOptions} options parsing options
* @param {ParseFunction=} customParse custom function to parse
* @returns {ParseResult} parse result
*/
static _parse(code, options, customParse) {
const type = options ? options.sourceType : "module";
/** @type {ParseOptions} */
let parserOptions;
if (typeof customParse === "function") {
// a user parse function may retain the options object — keep it fresh
parserOptions = {
...defaultParserOptions,
allowReturnOutsideFunction: type === "script",
...options,
sourceType: type === "auto" ? "module" : type
};
} else {
// reuse one options object across parses (parsing is synchronous) to
// skip re-materializing it per module; acorn copies it via getOptions.
// Every non-default key a caller may pass must be reset here.
parserOptions = REUSED_PARSER_OPTIONS;
parserOptions.importPhases = false;
Object.assign(parserOptions, defaultParserOptions);
parserOptions.allowReturnOutsideFunction = type === "script";
if (options) Object.assign(parserOptions, options);
parserOptions.sourceType = type === "auto" ? "module" : type;
// let WebpackParser downgrade module->script in place on script-only
// syntax so `auto` avoids a second full parse in the common case
parserOptions.moduleFallback = type === "auto";
}
// `locations` are acorn's to track, so the lazy path serves the callers
// that ask for ranges alone — which is every one inside webpack
const wantLazyNodes =
parserOptions.ranges === true && parserOptions.locations !== true;
/**
* Returns parse result.
* @param {string} code source code
* @param {ParseOptions} options parsing options
* @returns {ParseResult} parse result
*/
const internalParse = (code, options) => {
if (typeof customParse === "function") {
return customParse(code, options);
}
/** @type {Comment[]} */
const comments = [];
// Ranges alone (the main parse path and the concatenation re-parse) are
// served on demand; hoisted, since the auto-fallback re-enters here.
options.lazyNodes = wantLazyNodes;
if (wantLazyNodes) {
// the lazy plugin replaces acorn's native tracking — disabling it
// here lets WebpackParser skip its defensive options copy
options.ranges = false;
}
if (options.comments) {
if (wantLazyNodes) {
// the parser plugin collects the comments itself, deferring the
// text slice until some hook actually reads `value`
options.lazyComments = comments;
} else {
/** @type {ParserOptions} */
(options).onComment = comments;
}
}
const ast = /** @type {Program} */ (parser.parse(code, options));
return { ast, comments };
};
/** @type {Program | undefined} */
let ast;
/** @type {Comment[] | undefined} */
let comments;
let error;
let threw = false;
try {
({ ast, comments } = internalParse(code, parserOptions));
} catch (err) {
error = err;
threw = true;
}
if (threw && type === "auto") {
parserOptions.sourceType = "script";
parserOptions.allowReturnOutsideFunction = true;
// the retry is already a full script parse — no in-place downgrade
parserOptions.moduleFallback = false;
try {
({ ast, comments } = internalParse(code, parserOptions));
threw = false;
} catch (_err) {
// we use the error from first parse try
// so nothing to do here
}
}
// release per-parse state retained on the (possibly reused) options
// object — the comments array retains source text slices
parserOptions.lazyComments = undefined;
/** @type {ParserOptions} */
(parserOptions).onComment = undefined;
if (threw) {
throw error;
}
return /** @type {ParseResult} */ ({ ast, comments });
}
/**
* Returns parser.
* @param {ParserPlugin[]} plugins parser plugin
* @returns {typeof JavascriptParser} parser
*/
static extend(...plugins) {
parser = parser.extend(...plugins);
return JavascriptParser;
}
}
JavascriptParser.ALLOWED_MEMBER_TYPES_ALL = ALLOWED_MEMBER_TYPES_ALL;
JavascriptParser.ALLOWED_MEMBER_TYPES_CALL_EXPRESSION =
ALLOWED_MEMBER_TYPES_CALL_EXPRESSION;
JavascriptParser.ALLOWED_MEMBER_TYPES_EXPRESSION =
ALLOWED_MEMBER_TYPES_EXPRESSION;
JavascriptParser.VariableInfo = VariableInfo;
JavascriptParser.VariableInfoFlags = VariableInfoFlags;
JavascriptParser.getImportAttributes = getImportAttributes;
module.exports = JavascriptParser;