UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

1,341 lines (1,278 loc) 136 kB
/* 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