UNPKG

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.

1,333 lines (1,250 loc) 154 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const { Parser: AcornParser, tokTypes } = require("acorn"); const { SyncBailHook, HookMap } = require("tapable"); const vm = require("vm"); const Parser = require("../Parser"); const StackedMap = require("../util/StackedMap"); const binarySearchBounds = require("../util/binarySearchBounds"); const { webpackCommentRegExp, createMagicCommentContext } = require("../util/magicComment"); const memoize = require("../util/memoize"); const BasicEvaluatedExpression = require("./BasicEvaluatedExpression"); /** @typedef {import("acorn").Options} AcornOptions */ /** @typedef {import("estree").AssignmentExpression} AssignmentExpression */ /** @typedef {import("estree").BinaryExpression} BinaryExpression */ /** @typedef {import("estree").BlockStatement} BlockStatement */ /** @typedef {import("estree").SequenceExpression} SequenceExpression */ /** @typedef {import("estree").CallExpression} CallExpression */ /** @typedef {import("estree").BaseCallExpression} BaseCallExpression */ /** @typedef {import("estree").StaticBlock} StaticBlock */ /** @typedef {import("estree").ClassDeclaration} ClassDeclaration */ /** @typedef {import("estree").ForStatement} ForStatement */ /** @typedef {import("estree").SwitchStatement} SwitchStatement */ /** @typedef {import("estree").ClassExpression} ClassExpression */ /** @typedef {import("estree").Comment} Comment */ /** @typedef {import("estree").ConditionalExpression} ConditionalExpression */ /** @typedef {import("estree").Declaration} Declaration */ /** @typedef {import("estree").PrivateIdentifier} PrivateIdentifier */ /** @typedef {import("estree").PropertyDefinition} PropertyDefinition */ /** @typedef {import("estree").Expression} Expression */ /** @typedef {import("estree").Identifier} Identifier */ /** @typedef {import("estree").VariableDeclaration} VariableDeclaration */ /** @typedef {import("estree").IfStatement} IfStatement */ /** @typedef {import("estree").LabeledStatement} LabeledStatement */ /** @typedef {import("estree").Literal} Literal */ /** @typedef {import("estree").LogicalExpression} LogicalExpression */ /** @typedef {import("estree").ChainExpression} ChainExpression */ /** @typedef {import("estree").MemberExpression} MemberExpression */ /** @typedef {import("estree").YieldExpression} YieldExpression */ /** @typedef {import("estree").MetaProperty} MetaProperty */ /** @typedef {import("estree").Property} Property */ /** @typedef {import("estree").AssignmentPattern} AssignmentPattern */ /** @typedef {import("estree").ChainElement} ChainElement */ /** @typedef {import("estree").Pattern} Pattern */ /** @typedef {import("estree").UpdateExpression} UpdateExpression */ /** @typedef {import("estree").ObjectExpression} ObjectExpression */ /** @typedef {import("estree").UnaryExpression} UnaryExpression */ /** @typedef {import("estree").ArrayExpression} ArrayExpression */ /** @typedef {import("estree").ArrayPattern} ArrayPattern */ /** @typedef {import("estree").AwaitExpression} AwaitExpression */ /** @typedef {import("estree").ThisExpression} ThisExpression */ /** @typedef {import("estree").RestElement} RestElement */ /** @typedef {import("estree").ObjectPattern} ObjectPattern */ /** @typedef {import("estree").SwitchCase} SwitchCase */ /** @typedef {import("estree").CatchClause} CatchClause */ /** @typedef {import("estree").VariableDeclarator} VariableDeclarator */ /** @typedef {import("estree").ForInStatement} ForInStatement */ /** @typedef {import("estree").ForOfStatement} ForOfStatement */ /** @typedef {import("estree").ReturnStatement} ReturnStatement */ /** @typedef {import("estree").WithStatement} WithStatement */ /** @typedef {import("estree").ThrowStatement} ThrowStatement */ /** @typedef {import("estree").MethodDefinition} MethodDefinition */ /** @typedef {import("estree").NewExpression} NewExpression */ /** @typedef {import("estree").SpreadElement} SpreadElement */ /** @typedef {import("estree").FunctionExpression} FunctionExpression */ /** @typedef {import("estree").WhileStatement} WhileStatement */ /** @typedef {import("estree").ArrowFunctionExpression} ArrowFunctionExpression */ /** @typedef {import("estree").ExpressionStatement} ExpressionStatement */ /** @typedef {import("estree").FunctionDeclaration} FunctionDeclaration */ /** @typedef {import("estree").DoWhileStatement} DoWhileStatement */ /** @typedef {import("estree").TryStatement} TryStatement */ /** @typedef {import("estree").Node} Node */ /** @typedef {import("estree").Program} Program */ /** @typedef {import("estree").Directive} Directive */ /** @typedef {import("estree").Statement} Statement */ /** @typedef {import("estree").ExportDefaultDeclaration} ExportDefaultDeclaration */ /** @typedef {import("estree").Super} Super */ /** @typedef {import("estree").TaggedTemplateExpression} TaggedTemplateExpression */ /** @typedef {import("estree").TemplateLiteral} TemplateLiteral */ /** @typedef {import("estree").AssignmentProperty} AssignmentProperty */ /** @typedef {import("estree").MaybeNamedFunctionDeclaration} MaybeNamedFunctionDeclaration */ /** @typedef {import("estree").MaybeNamedClassDeclaration} MaybeNamedClassDeclaration */ /** * @template T * @typedef {import("tapable").AsArray<T>} AsArray<T> */ /** @typedef {import("../Parser").ParserState} ParserState */ /** @typedef {import("../Parser").PreparsedAst} PreparsedAst */ /** @typedef {{ name: string | VariableInfo, rootInfo: string | VariableInfo, getMembers: () => string[], getMembersOptionals: () => boolean[], getMemberRanges: () => Range[] }} GetInfoResult */ /** @typedef {Statement | ModuleDeclaration | Expression | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration} StatementPathItem */ /** @typedef {(ident: string) => void} OnIdentString */ /** @typedef {(ident: string, identifier: Identifier) => void} OnIdent */ /** @typedef {StatementPathItem[]} StatementPath */ // TODO remove cast when @types/estree has been updated to import assertions /** @typedef {import("estree").BaseNode & { type: "ImportAttribute", key: Identifier | Literal, value: Literal }} ImportAttribute */ /** @typedef {import("estree").ImportDeclaration & { attributes?: Array<ImportAttribute> }} ImportDeclaration */ /** @typedef {import("estree").ExportNamedDeclaration & { attributes?: Array<ImportAttribute> }} ExportNamedDeclaration */ /** @typedef {import("estree").ExportAllDeclaration & { attributes?: Array<ImportAttribute> }} ExportAllDeclaration */ /** @typedef {import("estree").ImportExpression & { options?: Expression | null }} ImportExpression */ /** @typedef {ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration} ModuleDeclaration */ /** @type {string[]} */ const EMPTY_ARRAY = []; const ALLOWED_MEMBER_TYPES_CALL_EXPRESSION = 0b01; const ALLOWED_MEMBER_TYPES_EXPRESSION = 0b10; const ALLOWED_MEMBER_TYPES_ALL = 0b11; const LEGACY_ASSERT_ATTRIBUTES = Symbol("assert"); /** @type {(BaseParser: typeof AcornParser) => typeof AcornParser} */ const importAssertions = Parser => class extends Parser { /** * @this {TODO} * @returns {ImportAttribute[]} import attributes */ parseWithClause() { /** @type {ImportAttribute[]} */ const nodes = []; const isAssertLegacy = this.value === "assert"; if (isAssertLegacy) { if (!this.eat(tokTypes.name)) { return nodes; } } else if (!this.eat(tokTypes._with)) { return nodes; } this.expect(tokTypes.braceL); /** @type {Record<string, boolean>} */ const attributeKeys = {}; let first = true; while (!this.eat(tokTypes.braceR)) { if (!first) { this.expect(tokTypes.comma); if (this.afterTrailingComma(tokTypes.braceR)) { break; } } else { first = false; } const attr = /** @type {ImportAttribute} */ this.parseImportAttribute(); const keyName = attr.key.type === "Identifier" ? attr.key.name : attr.key.value; if (Object.prototype.hasOwnProperty.call(attributeKeys, keyName)) { this.raiseRecoverable( attr.key.start, `Duplicate attribute key '${keyName}'` ); } attributeKeys[keyName] = true; nodes.push(attr); } if (isAssertLegacy) { /** @type {EXPECTED_ANY} */ (nodes)[LEGACY_ASSERT_ATTRIBUTES] = true; } return nodes; } }; // Syntax: https://developer.mozilla.org/en/SpiderMonkey/Parser_API const parser = AcornParser.extend(importAssertions); /** @typedef {Record<string, string> & { _isLegacyAssert?: boolean }} ImportAttributes */ /** * @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 {EXPECTED_ANY} */ (node.attributes)[LEGACY_ASSERT_ATTRIBUTES]) { result._isLegacyAssert = true; } return result; }; class VariableInfo { /** * @param {ScopeInfo} declaredScope scope in which the variable is declared * @param {string | true | undefined} freeName which free name the variable aliases, or true when none * @param {TagInfo | undefined} tagInfo info about tags */ constructor(declaredScope, freeName, tagInfo) { this.declaredScope = declaredScope; this.freeName = freeName; this.tagInfo = tagInfo; } } /** @typedef {string | ScopeInfo | VariableInfo} ExportedVariableInfo */ /** @typedef {Literal | string | null | undefined} ImportSource */ /** @typedef {Omit<AcornOptions, "sourceType" | "ecmaVersion"> & { sourceType: "module" | "script" | "auto", ecmaVersion?: AcornOptions["ecmaVersion"] }} ParseOptions */ /** @typedef {symbol} Tag */ /** @typedef {Record<string, TODO>} TagData */ /** * @typedef {object} TagInfo * @property {Tag} tag * @property {TagData=} data * @property {TagInfo | undefined} next */ const SCOPE_INFO_TERMINATED_RETURN = 1; const SCOPE_INFO_TERMINATED_THROW = 2; /** * @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 */ /** * @typedef {object} DestructuringAssignmentProperty * @property {string} id * @property {Range | undefined=} range * @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 {string[]} membersReversed reversed list of members * @returns {string} member expression as a string * @example * ```js * const membersReversed = ["property1", "property2", "property3"]; // Members parsed from the AST * const name = objectAndMembersToName("myObject", membersReversed); * * console.log(name); // "myObject.property1.property2.property3" * ``` */ const objectAndMembersToName = (object, membersReversed) => { let name = object; for (let i = membersReversed.length - 1; i >= 0; i--) { name = `${name}.${membersReversed[i]}`; } return name; }; /** * 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; } }; /** @type {AcornOptions} */ const defaultParserOptions = { ranges: true, locations: true, ecmaVersion: "latest", sourceType: "module", // https://github.com/tc39/proposal-hashbang allowHashBang: true, onComment: undefined }; const EMPTY_COMMENT_OPTIONS = { options: null, errors: null }; const CLASS_NAME = "JavascriptParser"; class JavascriptParser extends Parser { /** * @param {"module" | "script" | "auto"} sourceType default source type */ constructor(sourceType = "auto") { 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>>} */ evaluateNewExpression: new HookMap( () => new SyncBailHook(["expression"]) ), /** @type {HookMap<SyncBailHook<[CallExpression], BasicEvaluatedExpression | null | undefined>>} */ 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"]) ), /** @type {SyncBailHook<[Statement | ModuleDeclaration | MaybeNamedClassDeclaration | MaybeNamedFunctionDeclaration], boolean | void>} */ preStatement: new SyncBailHook(["statement"]), /** @type {SyncBailHook<[Statement | ModuleDeclaration | MaybeNamedClassDeclaration | MaybeNamedFunctionDeclaration], boolean | void>} */ blockPreStatement: 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, 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>} */ 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, Statement], boolean | void>} */ preDeclarator: new SyncBailHook(["declarator", "statement"]), /** @type {SyncBailHook<[VariableDeclarator, Statement], boolean | void>} */ declarator: new SyncBailHook(["declarator", "statement"]), /** @type {HookMap<SyncBailHook<[Declaration], boolean | void>>} */ varDeclaration: new HookMap(() => new SyncBailHook(["declaration"])), /** @type {HookMap<SyncBailHook<[Declaration], boolean | void>>} */ varDeclarationLet: new HookMap(() => new SyncBailHook(["declaration"])), /** @type {HookMap<SyncBailHook<[Declaration], boolean | void>>} */ varDeclarationConst: new HookMap(() => new SyncBailHook(["declaration"])), /** @type {HookMap<SyncBailHook<[Declaration], boolean | void>>} */ varDeclarationVar: new HookMap(() => new SyncBailHook(["declaration"])), /** @type {HookMap<SyncBailHook<[Identifier], boolean | void>>} */ pattern: new HookMap(() => new SyncBailHook(["pattern"])), /** @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<[AssignmentExpression, string[]], boolean | void>>} */ assignMemberChain: new HookMap( () => new SyncBailHook(["expression", "members"]) ), /** @type {HookMap<SyncBailHook<[Expression], boolean | void>>} */ typeof: new HookMap(() => new SyncBailHook(["expression"])), /** @type {SyncBailHook<[ImportExpression], boolean | void>} */ importCall: new SyncBailHook(["expression"]), /** @type {SyncBailHook<[Expression | ForOfStatement], 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, string[], boolean[], Range[]], boolean | void>>} */ callMemberChain: new HookMap( () => new SyncBailHook([ "expression", "members", "membersOptionals", "memberRanges" ]) ), /** Something like "a.b().c.d" */ /** @type {HookMap<SyncBailHook<[Expression, string[], CallExpression, string[], Range[]], boolean | void>>} */ memberChainOfCallMemberChain: new HookMap( () => new SyncBailHook([ "expression", "calleeMembers", "callExpression", "members", "memberRanges" ]) ), /** Something like "a.b().c.d()"" */ /** @type {HookMap<SyncBailHook<[CallExpression, string[], CallExpression, string[], Range[]], 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>} */ binaryExpression: new SyncBailHook(["binaryExpression"]), /** @type {HookMap<SyncBailHook<[Expression], boolean | void>>} */ expression: new HookMap(() => new SyncBailHook(["expression"])), /** @type {HookMap<SyncBailHook<[MemberExpression, string[], boolean[], Range[]], boolean | void>>} */ expressionMemberChain: new HookMap( () => new SyncBailHook([ "expression", "members", "membersOptionals", "memberRanges" ]) ), /** @type {HookMap<SyncBailHook<[MemberExpression, string[]], 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>} */ terminate: new SyncBailHook(["statement"]), /** @type {SyncBailHook<[Program, Comment[]], boolean | void>} */ finish: new SyncBailHook(["ast", "comments"]) }); this.sourceType = sourceType; /** @type {ScopeInfo} */ this.scope = /** @type {TODO} */ (undefined); /** @type {ParserState} */ this.state = /** @type {TODO} */ (undefined); /** @type {Comment[] | undefined} */ this.comments = undefined; /** @type {Set<number> | undefined} */ this.semicolons = undefined; /** @type {StatementPath | undefined} */ this.statementPath = undefined; /** @type {Statement | ModuleDeclaration | Expression | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration | undefined} */ this.prevStatement = undefined; /** @type {WeakMap<Expression, Set<DestructuringAssignmentProperty>> | undefined} */ this.destructuringAssignmentProperties = undefined; /** @type {TagData | undefined} */ this.currentTagData = undefined; this.magicCommentContext = createMagicCommentContext(); this._initializeEvaluating(); } _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.callHooksForName( this.hooks.evaluateNewExpression, callee.name, expr ); } else if ( expr.arguments.length > 2 || this.getVariableInfo("RegExp") !== "RegExp" ) return; 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)) ); } 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(); } }); /** * 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)); } }; this.hooks.evaluate.for("BinaryExpression").tap(CLASS_NAME, _expr => { const expr = /** @type {BinaryExpression} */ (_expr); /** * Evaluates a binary expression if and only if it is a const operation (e.g. 1 + 2, "a" + "b", etc.). * @template T * @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 = operandHandler => { const left = this.evaluateExpression(expr.left); if (!left.isCompileTimeValue()) return; const right = this.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); /** * @param {BasicEvaluatedExpression} left left * @param {BasicEvaluatedExpression} right right * @param {BasicEvaluatedExpression} res res * @param {boolean} eql true for "===" and false for "!==" * @returns {BasicEvaluatedExpression | undefined} result */ const handleTemplateStringCompare = (left, right, res, eql) => { /** * @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; }; /** * @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 res .setBoolean(!eql) .setSideEffects( left.couldHaveSideEffects() || right.couldHaveSideEffects() ); } }; /** * Helper function to handle BinaryExpressions using strict equality comparisons (e.g. "===" and "!=="). * @param {boolean} eql true for "===" and false for "!==" * @returns {BasicEvaluatedExpression | undefined} the evaluated expression */ const handleStrictEqualityComparison = eql => { const left = this.evaluateExpression(expr.left); const right = this.evaluateExpression(expr.right); const res = new BasicEvaluatedExpression(); res.setRange(/** @type {Range} */ (expr.range)); const leftConst = left.isCompileTimeValue(); const rightConst = right.isCompileTimeValue(); if (leftConst && rightConst) { return res .setBoolean( eql === (left.asCompileTimeValue() === right.asCompileTimeValue()) ) .setSideEffects( left.couldHaveSideEffects() || right.couldHaveSideEffects() ); } if (left.isArray() && right.isArray()) { return res .setBoolean(!eql) .setSideEffects( left.couldHaveSideEffects() || right.couldHaveSideEffects() ); } if (left.isTemplateString() && right.isTemplateString()) { return handleTemplateStringCompare(left, right, res, 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 res .setBoolean(!eql) .setSideEffects( left.couldHaveSideEffects() || right.couldHaveSideEffects() ); } }; /** * Helper function to handle BinaryExpressions using abstract equality comparisons (e.g. "==" and "!="). * @param {boolean} eql true for "==" and false for "!=" * @returns {BasicEvaluatedExpression | undefined} the evaluated expression */ const handleAbstractEqualityComparison = eql => { const left = this.evaluateExpression(expr.left); const right = this.evaluateExpression(expr.right); const res = new BasicEvaluatedExpression(); res.setRange(/** @type {Range} */ (expr.range)); const leftConst = left.isCompileTimeValue(); const rightConst = right.isCompileTimeValue(); if (leftConst && rightConst) { return res .setBoolean( eql === // eslint-disable-next-line eqeqeq (left.asCompileTimeValue() == right.asCompileTimeValue()) ) .setSideEffects( left.couldHaveSideEffects() || right.couldHaveSideEffects() ); } if (left.isArray() && right.isArray()) { return res .setBoolean(!eql) .setSideEffects( left.couldHaveSideEffects() || right.couldHaveSideEffects() ); } if (left.isTemplateString() && right.isTemplateString()) { return handleTemplateStringCompare(left, right, res, eql); } }; 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 .concat(left.postfix ? [left.postfix] : []) .concat(right.prefix ? [right.prefix] : []) .concat(right.wrappedInnerExpressions) ); } else { // ("prefix" + inner + postfix) + expr // => ("prefix" + inner + postfix + expr + [null]) res.setWrapped( left.prefix, null, left.wrappedInnerExpressions && left.wrappedInnerExpressions.concat( 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]).concat( 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((l, r) => l - r); } else if (expr.operator === "*") { return handleConstOperation((l, r) => l * r); } else if (expr.operator === "/") { return handleConstOperation((l, r) => l / r); } else if (expr.operator === "**") { return handleConstOperation((l, r) => l ** r); } else if (expr.operator === "===") { return handleStrictEqualityComparison(true); } else if (expr.operator === "==") { return handleAbstractEqualityComparison(true); } else if (expr.operator === "!==") { return handleStrictEqualityComparison(false); } else if (expr.operator === "!=") { return handleAbstractEqualityComparison(false); } else if (expr.operator === "&") { return handleConstOperation((l, r) => l & r); } else if (expr.operator === "|") { return handleConstOperation((l, r) => l | r); } else if (expr.operator === "^") { return handleConstOperation((l, r) => l ^ r); } else if (expr.operator === ">>>") { return handleConstOperation((l, r) => l >>> r); } else if (expr.operator === ">>") { return handleConstOperation((l, r) => l >> r); } else if (expr.operator === "<<") { return handleConstOperation((l, r) => l << r); } else if (expr.operator === "<") { return handleConstOperation((l, r) => l < r); } else if (expr.operator === ">") { return handleConstOperation((l, r) => l > r); } else if (expr.operator === "<=") { return handleConstOperation((l, r) => l <= r); } else if (expr.operator === ">=") { return handleConstOperation((l, r) => l >= r); } }); this.hooks.evaluate.for("UnaryExpression").tap(CLASS_NAME, _expr => { const expr = /** @type {UnaryExpression} */ (_expr); /** * Evaluates a UnaryExpression if and only if it is a basic const operator (e.g. +a, -a, ~a). * @template T * @param {(operand: T) => boolean | number | bigint | string} operandHandler handler for the operand * @returns {BasicEvaluatedExpression | undefined} evaluated expression */ const handleConstOperation = operandHandler => { const argument = this.evaluateExpression(expr.argument); if (!argument.isCompileTimeValue()) return; const result = operandHandler( /** @type {T} */ (argument.asCompileTimeValue()) ); return valueAsExpression(result, expr, argument.couldHaveSideEffects()); }; if (expr.operator === "typeof") { switch (expr.argument.type) { case "Identifier": { const res = this.callHooksForName( this.hooks.evaluateTypeof, expr.argument.name, expr ); if (res !== undefined) return res; break; } case "MetaProperty": { const res = this.callHooksForName( 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 handleConstOperation(v => ~v); } else if (expr.operator === "+") { // eslint-disable-next-line no-implicit-coercion return handleC