@quick-game/cli
Version:
Command line interface for rapid qg development
1,341 lines (1,278 loc) • 136 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const { Parser: AcornParser } = require("acorn");
const { importAssertions } = require("acorn-import-assertions");
const { SyncBailHook, HookMap } = require("tapable");
const vm = require("vm");
const Parser = require("../Parser");
const StackedMap = require("../util/StackedMap");
const binarySearchBounds = require("../util/binarySearchBounds");
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").ImportExpression} ImportExpression */
/** @typedef {import("estree").ClassDeclaration} ClassDeclaration */
/** @typedef {import("estree").ForStatement} ForStatement */
/** @typedef {import("estree").SwitchStatement} SwitchStatement */
/** @typedef {import("estree").ExportNamedDeclaration} ExportNamedDeclaration */
/** @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").ModuleDeclaration} ModuleDeclaration */
/** @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} AnyNode */
/** @typedef {import("estree").Program} Program */
/** @typedef {import("estree").Directive} Directive */
/** @typedef {import("estree").Statement} Statement */
/** @typedef {import("estree").ImportDeclaration} ImportDeclaration */
/** @typedef {import("estree").ExportDefaultDeclaration} ExportDefaultDeclaration */
/** @typedef {import("estree").ExportAllDeclaration} ExportAllDeclaration */
/** @typedef {import("estree").Super} Super */
/** @typedef {import("estree").TaggedTemplateExpression} TaggedTemplateExpression */
/** @typedef {import("estree").TemplateLiteral} TemplateLiteral */
/** @typedef {Record<string, any>} Assertions */
/** @template T @typedef {import("tapable").AsArray<T>} AsArray<T> */
/** @typedef {import("../Parser").ParserState} ParserState */
/** @typedef {import("../Parser").PreparsedAst} PreparsedAst */
/** @typedef {{declaredScope: ScopeInfo, freeName: string | true, tagInfo: TagInfo | undefined}} VariableInfoInterface */
/** @typedef {{ name: string | VariableInfo, rootInfo: string | VariableInfo, getMembers: () => string[], getMembersOptionals: () => boolean[], getMemberRanges: () => Range[] }} GetInfoResult */
const EMPTY_ARRAY = [];
const ALLOWED_MEMBER_TYPES_CALL_EXPRESSION = 0b01;
const ALLOWED_MEMBER_TYPES_EXPRESSION = 0b10;
const ALLOWED_MEMBER_TYPES_ALL = 0b11;
// Syntax: https://developer.mozilla.org/en/SpiderMonkey/Parser_API
const parser = AcornParser.extend(importAssertions);
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 {Object} TagInfo
* @property {any} tag
* @property {any} data
* @property {TagInfo | undefined} next
*/
/**
* @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
*/
/** @typedef {[number, number]} Range */
/**
* 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 {[number, number]} startRange start range to join
* @param {[number, number]} endRange end range to join
* @returns {[number, number]} 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 | 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: null
};
// regexp to match at least one "magic comment"
const webpackCommentRegExp = new RegExp(/(^|\W)webpack[A-Z]{1,}[A-Za-z]{1,}:/);
const EMPTY_COMMENT_OPTIONS = {
options: null,
errors: null
};
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 | undefined | null>>} */
evaluateTypeof: new HookMap(() => new SyncBailHook(["expression"])),
/** @type {HookMap<SyncBailHook<[Expression], BasicEvaluatedExpression | undefined | null>>} */
evaluate: new HookMap(() => new SyncBailHook(["expression"])),
/** @type {HookMap<SyncBailHook<[Identifier | ThisExpression | MemberExpression | MetaProperty], BasicEvaluatedExpression | undefined | null>>} */
evaluateIdentifier: new HookMap(() => new SyncBailHook(["expression"])),
/** @type {HookMap<SyncBailHook<[Identifier | ThisExpression | MemberExpression], BasicEvaluatedExpression | undefined | null>>} */
evaluateDefinedIdentifier: new HookMap(
() => new SyncBailHook(["expression"])
),
/** @type {HookMap<SyncBailHook<[NewExpression], BasicEvaluatedExpression | undefined | null>>} */
evaluateNewExpression: new HookMap(
() => new SyncBailHook(["expression"])
),
/** @type {HookMap<SyncBailHook<[CallExpression], BasicEvaluatedExpression | undefined | null>>} */
evaluateCallExpression: new HookMap(
() => new SyncBailHook(["expression"])
),
/** @type {HookMap<SyncBailHook<[CallExpression, BasicEvaluatedExpression | undefined], BasicEvaluatedExpression | undefined | null>>} */
evaluateCallExpressionMember: new HookMap(
() => new SyncBailHook(["expression", "param"])
),
/** @type {HookMap<SyncBailHook<[Expression | Declaration | PrivateIdentifier, number], boolean | void>>} */
isPure: new HookMap(
() => new SyncBailHook(["expression", "commentsStartPosition"])
),
/** @type {SyncBailHook<[Statement | ModuleDeclaration], boolean | void>} */
preStatement: new SyncBailHook(["statement"]),
/** @type {SyncBailHook<[Statement | ModuleDeclaration], boolean | void>} */
blockPreStatement: new SyncBailHook(["declaration"]),
/** @type {SyncBailHook<[Statement | ModuleDeclaration], boolean | void>} */
statement: new SyncBailHook(["statement"]),
/** @type {SyncBailHook<[IfStatement], boolean | void>} */
statementIf: new SyncBailHook(["statement"]),
/** @type {SyncBailHook<[Expression, ClassExpression | ClassDeclaration], boolean | void>} */
classExtendsExpression: new SyncBailHook([
"expression",
"classDefinition"
]),
/** @type {SyncBailHook<[MethodDefinition | PropertyDefinition | StaticBlock, ClassExpression | ClassDeclaration], boolean | void>} */
classBodyElement: new SyncBailHook(["element", "classDefinition"]),
/** @type {SyncBailHook<[Expression, MethodDefinition | PropertyDefinition, ClassExpression | ClassDeclaration], 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, string], boolean | void>} */
importSpecifier: new SyncBailHook([
"statement",
"source",
"exportName",
"identifierName"
]),
/** @type {SyncBailHook<[ExportNamedDeclaration | ExportAllDeclaration], boolean | void>} */
export: new SyncBailHook(["statement"]),
/** @type {SyncBailHook<[ExportNamedDeclaration | ExportAllDeclaration, ImportSource], boolean | void>} */
exportImport: new SyncBailHook(["statement", "source"]),
/** @type {SyncBailHook<[ExportNamedDeclaration | ExportAllDeclaration, Declaration], boolean | void>} */
exportDeclaration: new SyncBailHook(["statement", "declaration"]),
/** @type {SyncBailHook<[ExportDefaultDeclaration, Declaration], boolean | void>} */
exportExpression: new SyncBailHook(["statement", "declaration"]),
/** @type {SyncBailHook<[ExportNamedDeclaration | ExportAllDeclaration, string, string, number | undefined], boolean | void>} */
exportSpecifier: new SyncBailHook([
"statement",
"identifierName",
"exportName",
"index"
]),
/** @type {SyncBailHook<[ExportNamedDeclaration | ExportAllDeclaration, ImportSource, string, string, 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], 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[]], boolean | void>>} */
memberChainOfCallMemberChain: new HookMap(
() =>
new SyncBailHook([
"expression",
"calleeMembers",
"callExpression",
"members"
])
),
/** Something like "a.b().c.d()"" */
/** @type {HookMap<SyncBailHook<[CallExpression, string[], CallExpression, string[]], boolean | void>>} */
callMemberChainOfCallMemberChain: new HookMap(
() =>
new SyncBailHook([
"expression",
"calleeMembers",
"innerCallExpression",
"members"
])
),
/** @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<[Program, Comment[]], boolean | void>} */
finish: new SyncBailHook(["ast", "comments"])
});
this.sourceType = sourceType;
/** @type {ScopeInfo} */
this.scope = undefined;
/** @type {ParserState} */
this.state = undefined;
this.comments = undefined;
this.semicolons = undefined;
/** @type {(Statement | ModuleDeclaration | Expression)[]} */
this.statementPath = undefined;
/** @type {Statement | ModuleDeclaration | Expression | undefined} */
this.prevStatement = undefined;
/** @type {WeakMap<Expression, Set<string>>} */
this.destructuringAssignmentProperties = undefined;
this.currentTagData = undefined;
this._initializeEvaluating();
}
_initializeEvaluating() {
this.hooks.evaluate.for("Literal").tap("JavascriptParser", _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("JavascriptParser", _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, flags;
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()
.setRegExp(new RegExp(""))
.setRange(/** @type {Range} */ (expr.range));
}
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("JavascriptParser", _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(expr.range);
returnRight = leftAsBool === true;
allowedRight = false;
} else if (expr.operator === "||") {
const leftAsBool = left.asBool();
if (leftAsBool === true) return left.setRange(expr.range);
returnRight = leftAsBool === false;
allowedRight = true;
} else if (expr.operator === "??") {
const leftAsNullish = left.asNullish();
if (leftAsNullish === false) return left.setRange(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(expr.range);
}
const asBool = right.asBool();
if (allowedRight === true && asBool === true) {
return new BasicEvaluatedExpression()
.setRange(expr.range)
.setTruthy();
} else if (allowedRight === false && asBool === false) {
return new BasicEvaluatedExpression().setRange(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("JavascriptParser", _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(
left.asCompileTimeValue(),
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);
const handleTemplateStringCompare = (left, right, res, eql) => {
const getPrefix = parts => {
let value = "";
for (const p of parts) {
const v = p.asString();
if (v !== undefined) value += v;
else break;
}
return 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(left.parts);
const rightPrefix = getPrefix(right.parts);
const leftSuffix = getSuffix(left.parts);
const rightSuffix = getSuffix(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(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(left.string + right.string);
} else if (right.isNumber()) {
res.setString(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(left.string + right.prefix.string)
.setRange(joinRanges(left.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 + right.string);
} else if (right.isNumber()) {
res.setNumber(left.number + right.number);
} else {
return;
}
} else if (left.isBigInt()) {
if (right.isBigInt()) {
res.setBigInt(left.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(left.postfix.string + right.string)
.setRange(joinRanges(left.postfix.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(left.postfix.string + right.number)
.setRange(joinRanges(left.postfix.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(right.number + "")
.setRange(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(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("JavascriptParser", _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(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,
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(expr.range);
}
}
const arg = this.evaluateExpression(expr.argument);
if (arg.isUnknown()) return;
if (arg.isString()) {
return new BasicEvaluatedExpression()
.setString("string")
.setRange(expr.range);
}
if (arg.isWrapped()) {
return new BasicEvaluatedExpression()
.setString("string")
.setSideEffects()
.setRange(expr.range);
}
if (arg.isUndefined()) {
return new BasicEvaluatedExpression()
.setString("undefined")
.setRange(expr.range);
}
if (arg.isNumber()) {
return new BasicEvaluatedExpression()
.setString("number")
.setRange(expr.range);
}
if (arg.isBigInt()) {
return new BasicEvaluatedExpression()
.setString("bigint")
.setRange(expr.range);
}
if (arg.isBoolean()) {
return new BasicEvaluatedExpression()
.setString("boolean")
.setRange(expr.range);
}
if (arg.isConstArray() || arg.isRegExp() || arg.isNull()) {
return new BasicEvaluatedExpression()
.setString("object")
.setRange(expr.range);
}
if (arg.isArray()) {
return new BasicEvaluatedExpression()
.setString("object")
.setSideEffects(arg.couldHaveSideEffects())
.setRange(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(expr.range);
} else if (expr.operator === "~") {
return handleConstOperation(v => ~v);
} else if (expr.operator === "+") {
return handleConstOperation(v => +v);
} else if (expr.operator === "-") {
return handleConstOperation(v => -v);
}
});
this.hooks.evaluateTypeof.for("undefined").tap("JavascriptParser", expr => {
return new BasicEvaluatedExpression()
.setString("undefined")
.setRange(expr.range);
});
this.hooks.evaluate.for("Identifier").tap("JavascriptParser", expr => {
if (/** @type {Identifier} */ (expr).name === "undefined") {
return new BasicEvaluatedExpression()
.setUndefined()
.setRange(expr.range);
}
});
/**
* @param {string} exprType expression type name
* @param {function(Expression): GetInfoResult | undefined} getInfo get info
* @returns {void}
*/
const tapEvaluateWithVariableInfo = (exprType, getInfo) => {
/** @type {Expression | undefined} */
let cachedExpression = undefined;
/** @type {GetInfoResult | undefined} */
let cachedInfo = undefined;
this.hooks.evaluate.for(exprType).tap("JavascriptParser", expr => {
const expression = /** @type {MemberExpression} */ (expr);
const info = getInfo(expr);
if (info !== undefined) {
return this.callHooksForInfoWithFallback(
this.hooks.evaluateIdentifier,
info.name,
name => {
cachedExpression = expression;
cachedInfo = info;
},
name => {
const hook = this.hooks.evaluateDefinedIdentifier.get(name);
if (hook !== undefined) {
return hook.call(expression);
}
},
expression
);
}
});
this.hooks.evaluate
.for(exprType)
.tap({ name: "JavascriptParser", stage: 100 }, expr => {
const info = cachedExpression === expr ? cachedInfo : getInfo(expr);
if (info !== undefined) {
return new BasicEvaluatedExpression()
.setIdentifier(
info.name,
info.rootInfo,
info.getMembers,
info.getMembersOptionals,
info.getMemberRanges
)
.setRange(expr.range);
}
});
this.hooks.finish.tap("JavascriptParser", () => {
// Cleanup for GC
cachedExpression = cachedInfo = undefined;
});
};
tapEvaluateWithVariableInfo("Identifier", expr => {
const info = this.getVariableInfo(/** @type {Identifier} */ (expr).name);
if (
typeof info === "string" ||
(info instanceof VariableInfo && typeof info.freeName === "string")
) {
return {
name: info,
rootInfo: info,
getMembers: () => [],
getMembersOptionals: () => [],
getMemberRanges: () => []
};
}
});
tapEvaluateWithVariableInfo("ThisExpression", expr => {
const info = this.getVariableInfo("this");
if (
typeof info === "string" ||
(info instanceof VariableInfo && typeof info.freeName === "string")
) {
return {
name: info,
rootInfo: info,
getMembers: () => [],
getMembersOptionals: () => [],
getMemberRanges: () => []
};
}
});
this.hooks.evaluate.for("MetaProperty").tap("JavascriptParser", expr => {
const metaProperty = /** @type {MetaProperty} */ (expr);
return this.callHooksForName(
this.hooks.evaluateIdentifier,
getRootName(expr),
metaProperty
);
});
tapEvaluateWithVariableInfo("MemberExpression", expr =>
this.getMemberExpressionInfo(
/** @type {MemberExpression} */ (expr),
ALLOWED_MEMBER_TYPES_EXPRESSION
)
);
this.hooks.evaluate.for("CallExpression").tap("JavascriptParser", _expr => {
const expr = /** @type {CallExpression} */ (_expr);
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.callHooksForName(
this.hooks.evaluateCallExpression,
expr.callee.name,
expr
);
}
});
this.hooks.evaluateCallExpressionMember
.for("indexOf")
.tap("JavascriptParser", (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 = arg1Eval.string;
let result;
if (arg2) {
if (arg2.type === "SpreadElement") return;
const arg2Eval = this.evaluateExpression(arg2);
if (!arg2Eval.isNumber()) return;
result = param.string.indexOf(arg1Value, arg2Eval.number);
} else {
result = param.string.indexOf(arg1Value);
}
return new BasicEvaluatedExpression()
.setNumber(result)
.setSideEffects(param.couldHaveSideEffects())
.setRange(expr.range);
});
this.hooks.evaluateCallExpressionMember
.for("replace")
.tap("JavascriptParser", (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;
let arg1 = this.evaluateExpression(expr.arguments[0]);
let arg2 = this.evaluateExpression(expr.arguments[1]);
if (!arg1.isString() && !arg1.isRegExp()) return;
const arg1Value = arg1.regExp || arg1.string;
if (!arg2.isString()) return;
const arg2Value = arg2.string;
return new BasicEvaluatedExpression()
.setString(param.string.replace(arg1Value, arg2Value))
.setSideEffects(param.couldHaveSideEffects())
.setRange(expr.range);
});
["substr", "substring", "slice"].forEach(fn => {
this.hooks.evaluateCallExpressionMember
.for(fn)
.tap("JavascriptParser", (expr, param) => {
if (!param.isString()) return;
let arg1;
let result,
str = 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[fn](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[fn](arg1.number, arg2.number);
break;
}
default:
return;
}
return new BasicEvaluatedExpression()
.setString(result)
.setSideEffects(param.couldHaveSideEffects())
.setRange(expr.range);
});
});
/**
* @param {"cooked" | "raw"} kind kind of values to get
* @param {TemplateLiteral} templateLiteralExpr TemplateLiteral expr
* @returns {{quasis: BasicEvaluatedExpression[], parts: BasicEvaluatedExpression[]}} Simplified template