UNPKG

vue-simple-range-slider

Version:

Change Your numeric value or numeric range value with dragging handles

1,422 lines (1,342 loc) 115 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").ArrayExpression} ArrayExpressionNode */ /** @typedef {import("estree").BinaryExpression} BinaryExpressionNode */ /** @typedef {import("estree").BlockStatement} BlockStatementNode */ /** @typedef {import("estree").SequenceExpression} SequenceExpressionNode */ /** @typedef {import("estree").CallExpression} CallExpressionNode */ /** @typedef {import("estree").ClassDeclaration} ClassDeclarationNode */ /** @typedef {import("estree").ClassExpression} ClassExpressionNode */ /** @typedef {import("estree").Comment} CommentNode */ /** @typedef {import("estree").ConditionalExpression} ConditionalExpressionNode */ /** @typedef {import("estree").Declaration} DeclarationNode */ /** @typedef {import("estree").PrivateIdentifier} PrivateIdentifierNode */ /** @typedef {import("estree").PropertyDefinition} PropertyDefinitionNode */ /** @typedef {import("estree").Expression} ExpressionNode */ /** @typedef {import("estree").Identifier} IdentifierNode */ /** @typedef {import("estree").IfStatement} IfStatementNode */ /** @typedef {import("estree").LabeledStatement} LabeledStatementNode */ /** @typedef {import("estree").Literal} LiteralNode */ /** @typedef {import("estree").LogicalExpression} LogicalExpressionNode */ /** @typedef {import("estree").ChainExpression} ChainExpressionNode */ /** @typedef {import("estree").MemberExpression} MemberExpressionNode */ /** @typedef {import("estree").MetaProperty} MetaPropertyNode */ /** @typedef {import("estree").MethodDefinition} MethodDefinitionNode */ /** @typedef {import("estree").ModuleDeclaration} ModuleDeclarationNode */ /** @typedef {import("estree").NewExpression} NewExpressionNode */ /** @typedef {import("estree").Node} AnyNode */ /** @typedef {import("estree").Program} ProgramNode */ /** @typedef {import("estree").Statement} StatementNode */ /** @typedef {import("estree").ImportDeclaration} ImportDeclarationNode */ /** @typedef {import("estree").ExportNamedDeclaration} ExportNamedDeclarationNode */ /** @typedef {import("estree").ExportDefaultDeclaration} ExportDefaultDeclarationNode */ /** @typedef {import("estree").ExportAllDeclaration} ExportAllDeclarationNode */ /** @typedef {import("estree").Super} SuperNode */ /** @typedef {import("estree").TaggedTemplateExpression} TaggedTemplateExpressionNode */ /** @typedef {import("estree").TemplateLiteral} TemplateLiteralNode */ /** @typedef {import("estree").ThisExpression} ThisExpressionNode */ /** @typedef {import("estree").UnaryExpression} UnaryExpressionNode */ /** @typedef {import("estree").VariableDeclarator} VariableDeclaratorNode */ /** @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[] }} 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} 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 {LiteralNode | 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} inShorthand * @property {boolean} isStrict * @property {boolean} isAsmJs * @property {boolean} inTry */ const joinRanges = (startRange, endRange) => { if (!endRange) return startRange; if (!startRange) return endRange; return [startRange[0], endRange[1]]; }; const objectAndMembersToName = (object, membersReversed) => { let name = object; for (let i = membersReversed.length - 1; i >= 0; i--) { name = name + "." + membersReversed[i]; } return name; }; 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<[UnaryExpressionNode], BasicEvaluatedExpression | undefined | null>>} */ evaluateTypeof: new HookMap(() => new SyncBailHook(["expression"])), /** @type {HookMap<SyncBailHook<[ExpressionNode], BasicEvaluatedExpression | undefined | null>>} */ evaluate: new HookMap(() => new SyncBailHook(["expression"])), /** @type {HookMap<SyncBailHook<[IdentifierNode | ThisExpressionNode | MemberExpressionNode | MetaPropertyNode], BasicEvaluatedExpression | undefined | null>>} */ evaluateIdentifier: new HookMap(() => new SyncBailHook(["expression"])), /** @type {HookMap<SyncBailHook<[IdentifierNode | ThisExpressionNode | MemberExpressionNode], BasicEvaluatedExpression | undefined | null>>} */ evaluateDefinedIdentifier: new HookMap( () => new SyncBailHook(["expression"]) ), /** @type {HookMap<SyncBailHook<[NewExpressionNode], BasicEvaluatedExpression | undefined | null>>} */ evaluateNewExpression: new HookMap( () => new SyncBailHook(["expression"]) ), /** @type {HookMap<SyncBailHook<[CallExpressionNode], BasicEvaluatedExpression | undefined | null>>} */ evaluateCallExpression: new HookMap( () => new SyncBailHook(["expression"]) ), /** @type {HookMap<SyncBailHook<[CallExpressionNode, BasicEvaluatedExpression | undefined], BasicEvaluatedExpression | undefined | null>>} */ evaluateCallExpressionMember: new HookMap( () => new SyncBailHook(["expression", "param"]) ), /** @type {HookMap<SyncBailHook<[ExpressionNode | DeclarationNode | PrivateIdentifierNode, number], boolean | void>>} */ isPure: new HookMap( () => new SyncBailHook(["expression", "commentsStartPosition"]) ), /** @type {SyncBailHook<[StatementNode | ModuleDeclarationNode], boolean | void>} */ preStatement: new SyncBailHook(["statement"]), /** @type {SyncBailHook<[StatementNode | ModuleDeclarationNode], boolean | void>} */ blockPreStatement: new SyncBailHook(["declaration"]), /** @type {SyncBailHook<[StatementNode | ModuleDeclarationNode], boolean | void>} */ statement: new SyncBailHook(["statement"]), /** @type {SyncBailHook<[IfStatementNode], boolean | void>} */ statementIf: new SyncBailHook(["statement"]), /** @type {SyncBailHook<[ExpressionNode, ClassExpressionNode | ClassDeclarationNode], boolean | void>} */ classExtendsExpression: new SyncBailHook([ "expression", "classDefinition" ]), /** @type {SyncBailHook<[MethodDefinitionNode | PropertyDefinitionNode, ClassExpressionNode | ClassDeclarationNode], boolean | void>} */ classBodyElement: new SyncBailHook(["element", "classDefinition"]), /** @type {SyncBailHook<[ExpressionNode, MethodDefinitionNode | PropertyDefinitionNode, ClassExpressionNode | ClassDeclarationNode], boolean | void>} */ classBodyValue: new SyncBailHook([ "expression", "element", "classDefinition" ]), /** @type {HookMap<SyncBailHook<[LabeledStatementNode], boolean | void>>} */ label: new HookMap(() => new SyncBailHook(["statement"])), /** @type {SyncBailHook<[ImportDeclarationNode, ImportSource], boolean | void>} */ import: new SyncBailHook(["statement", "source"]), /** @type {SyncBailHook<[ImportDeclarationNode, ImportSource, string, string], boolean | void>} */ importSpecifier: new SyncBailHook([ "statement", "source", "exportName", "identifierName" ]), /** @type {SyncBailHook<[ExportNamedDeclarationNode | ExportAllDeclarationNode], boolean | void>} */ export: new SyncBailHook(["statement"]), /** @type {SyncBailHook<[ExportNamedDeclarationNode | ExportAllDeclarationNode, ImportSource], boolean | void>} */ exportImport: new SyncBailHook(["statement", "source"]), /** @type {SyncBailHook<[ExportNamedDeclarationNode | ExportAllDeclarationNode, DeclarationNode], boolean | void>} */ exportDeclaration: new SyncBailHook(["statement", "declaration"]), /** @type {SyncBailHook<[ExportDefaultDeclarationNode, DeclarationNode], boolean | void>} */ exportExpression: new SyncBailHook(["statement", "declaration"]), /** @type {SyncBailHook<[ExportNamedDeclarationNode | ExportAllDeclarationNode, string, string, number | undefined], boolean | void>} */ exportSpecifier: new SyncBailHook([ "statement", "identifierName", "exportName", "index" ]), /** @type {SyncBailHook<[ExportNamedDeclarationNode | ExportAllDeclarationNode, ImportSource, string, string, number | undefined], boolean | void>} */ exportImportSpecifier: new SyncBailHook([ "statement", "source", "identifierName", "exportName", "index" ]), /** @type {SyncBailHook<[VariableDeclaratorNode, StatementNode], boolean | void>} */ preDeclarator: new SyncBailHook(["declarator", "statement"]), /** @type {SyncBailHook<[VariableDeclaratorNode, StatementNode], boolean | void>} */ declarator: new SyncBailHook(["declarator", "statement"]), /** @type {HookMap<SyncBailHook<[DeclarationNode], boolean | void>>} */ varDeclaration: new HookMap(() => new SyncBailHook(["declaration"])), /** @type {HookMap<SyncBailHook<[DeclarationNode], boolean | void>>} */ varDeclarationLet: new HookMap(() => new SyncBailHook(["declaration"])), /** @type {HookMap<SyncBailHook<[DeclarationNode], boolean | void>>} */ varDeclarationConst: new HookMap(() => new SyncBailHook(["declaration"])), /** @type {HookMap<SyncBailHook<[DeclarationNode], boolean | void>>} */ varDeclarationVar: new HookMap(() => new SyncBailHook(["declaration"])), /** @type {HookMap<SyncBailHook<[IdentifierNode], boolean | void>>} */ pattern: new HookMap(() => new SyncBailHook(["pattern"])), /** @type {HookMap<SyncBailHook<[ExpressionNode], boolean | void>>} */ canRename: new HookMap(() => new SyncBailHook(["initExpression"])), /** @type {HookMap<SyncBailHook<[ExpressionNode], boolean | void>>} */ rename: new HookMap(() => new SyncBailHook(["initExpression"])), /** @type {HookMap<SyncBailHook<[import("estree").AssignmentExpression], boolean | void>>} */ assign: new HookMap(() => new SyncBailHook(["expression"])), /** @type {HookMap<SyncBailHook<[import("estree").AssignmentExpression, string[]], boolean | void>>} */ assignMemberChain: new HookMap( () => new SyncBailHook(["expression", "members"]) ), /** @type {HookMap<SyncBailHook<[ExpressionNode], boolean | void>>} */ typeof: new HookMap(() => new SyncBailHook(["expression"])), /** @type {SyncBailHook<[ExpressionNode], boolean | void>} */ importCall: new SyncBailHook(["expression"]), /** @type {SyncBailHook<[ExpressionNode], boolean | void>} */ topLevelAwait: new SyncBailHook(["expression"]), /** @type {HookMap<SyncBailHook<[ExpressionNode], boolean | void>>} */ call: new HookMap(() => new SyncBailHook(["expression"])), /** Something like "a.b()" */ /** @type {HookMap<SyncBailHook<[CallExpressionNode, string[], boolean[]], boolean | void>>} */ callMemberChain: new HookMap( () => new SyncBailHook(["expression", "members", "membersOptionals"]) ), /** Something like "a.b().c.d" */ /** @type {HookMap<SyncBailHook<[ExpressionNode, string[], CallExpressionNode, string[]], boolean | void>>} */ memberChainOfCallMemberChain: new HookMap( () => new SyncBailHook([ "expression", "calleeMembers", "callExpression", "members" ]) ), /** Something like "a.b().c.d()"" */ /** @type {HookMap<SyncBailHook<[ExpressionNode, string[], CallExpressionNode, string[]], boolean | void>>} */ callMemberChainOfCallMemberChain: new HookMap( () => new SyncBailHook([ "expression", "calleeMembers", "innerCallExpression", "members" ]) ), /** @type {SyncBailHook<[ChainExpressionNode], boolean | void>} */ optionalChaining: new SyncBailHook(["optionalChaining"]), /** @type {HookMap<SyncBailHook<[NewExpressionNode], boolean | void>>} */ new: new HookMap(() => new SyncBailHook(["expression"])), /** @type {SyncBailHook<[BinaryExpressionNode], boolean | void>} */ binaryExpression: new SyncBailHook(["binaryExpression"]), /** @type {HookMap<SyncBailHook<[ExpressionNode], boolean | void>>} */ expression: new HookMap(() => new SyncBailHook(["expression"])), /** @type {HookMap<SyncBailHook<[ExpressionNode, string[], boolean[]], boolean | void>>} */ expressionMemberChain: new HookMap( () => new SyncBailHook(["expression", "members", "membersOptionals"]) ), /** @type {HookMap<SyncBailHook<[ExpressionNode, string[]], boolean | void>>} */ unhandledExpressionMemberChain: new HookMap( () => new SyncBailHook(["expression", "members"]) ), /** @type {SyncBailHook<[ExpressionNode], boolean | void>} */ expressionConditionalOperator: new SyncBailHook(["expression"]), /** @type {SyncBailHook<[ExpressionNode], boolean | void>} */ expressionLogicalOperator: new SyncBailHook(["expression"]), /** @type {SyncBailHook<[ProgramNode, CommentNode[]], boolean | void>} */ program: new SyncBailHook(["ast", "comments"]), /** @type {SyncBailHook<[ProgramNode, CommentNode[]], 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 {(StatementNode|ExpressionNode)[]} */ this.statementPath = undefined; this.prevStatement = undefined; this.currentTagData = undefined; this._initializeEvaluating(); } _initializeEvaluating() { this.hooks.evaluate.for("Literal").tap("JavascriptParser", _expr => { const expr = /** @type {LiteralNode} */ (_expr); switch (typeof expr.value) { case "number": return new BasicEvaluatedExpression() .setNumber(expr.value) .setRange(expr.range); case "bigint": return new BasicEvaluatedExpression() .setBigInt(expr.value) .setRange(expr.range); case "string": return new BasicEvaluatedExpression() .setString(expr.value) .setRange(expr.range); case "boolean": return new BasicEvaluatedExpression() .setBoolean(expr.value) .setRange(expr.range); } if (expr.value === null) { return new BasicEvaluatedExpression().setNull().setRange(expr.range); } if (expr.value instanceof RegExp) { return new BasicEvaluatedExpression() .setRegExp(expr.value) .setRange(expr.range); } }); this.hooks.evaluate.for("NewExpression").tap("JavascriptParser", _expr => { const expr = /** @type {NewExpressionNode} */ (_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(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(expr.range); }); this.hooks.evaluate .for("LogicalExpression") .tap("JavascriptParser", _expr => { const expr = /** @type {LogicalExpressionNode} */ (_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(); } }); const valueAsExpression = (value, expr, sideEffects) => { switch (typeof value) { case "boolean": return new BasicEvaluatedExpression() .setBoolean(value) .setSideEffects(sideEffects) .setRange(expr.range); case "number": return new BasicEvaluatedExpression() .setNumber(value) .setSideEffects(sideEffects) .setRange(expr.range); case "bigint": return new BasicEvaluatedExpression() .setBigInt(value) .setSideEffects(sideEffects) .setRange(expr.range); case "string": return new BasicEvaluatedExpression() .setString(value) .setSideEffects(sideEffects) .setRange(expr.range); } }; this.hooks.evaluate .for("BinaryExpression") .tap("JavascriptParser", _expr => { const expr = /** @type {BinaryExpressionNode} */ (_expr); const handleConstOperation = fn => { const left = this.evaluateExpression(expr.left); if (!left.isCompileTimeValue()) return; const right = this.evaluateExpression(expr.right); if (!right.isCompileTimeValue()) return; const result = fn( left.asCompileTimeValue(), right.asCompileTimeValue() ); return valueAsExpression( result, expr, left.couldHaveSideEffects() || right.couldHaveSideEffects() ); }; 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); if ( leftPrefix.slice(0, lenPrefix) !== rightPrefix.slice(0, lenPrefix) || leftSuffix.slice(-lenSuffix) !== rightSuffix.slice(-lenSuffix) ) { return res .setBoolean(!eql) .setSideEffects( left.couldHaveSideEffects() || right.couldHaveSideEffects() ); } }; const handleStrictEqualityComparison = 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 === (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(left.asBool(), right.asBool()) || isAlwaysDifferent(left.asNullish(), right.asNullish()) ) { return res .setBoolean(!eql) .setSideEffects( left.couldHaveSideEffects() || right.couldHaveSideEffects() ); } }; 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 {UnaryExpressionNode} */ (_expr); const handleConstOperation = fn => { const argument = this.evaluateExpression(expr.argument); if (!argument.isCompileTimeValue()) return; const result = fn(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 {IdentifierNode} */ (expr).name === "undefined") { return new BasicEvaluatedExpression() .setUndefined() .setRange(expr.range); } }); /** * @param {string} exprType expression type name * @param {function(ExpressionNode): GetInfoResult | undefined} getInfo get info * @returns {void} */ const tapEvaluateWithVariableInfo = (exprType, getInfo) => { /** @type {ExpressionNode | undefined} */ let cachedExpression = undefined; /** @type {GetInfoResult | undefined} */ let cachedInfo = undefined; this.hooks.evaluate.for(exprType).tap("JavascriptParser", expr => { const expression = /** @type {MemberExpressionNode} */ (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 ) .setRange(expr.range); } }); this.hooks.finish.tap("JavascriptParser", () => { // Cleanup for GC cachedExpression = cachedInfo = undefined; }); }; tapEvaluateWithVariableInfo("Identifier", expr => { const info = this.getVariableInfo( /** @type {IdentifierNode} */ (expr).name ); if ( typeof info === "string" || (info instanceof VariableInfo && typeof info.freeName === "string") ) { return { name: info, rootInfo: info, getMembers: () => [], getMembersOptionals: () => [] }; } }); 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: () => [] }; } }); this.hooks.evaluate.for("MetaProperty").tap("JavascriptParser", expr => { const metaProperty = /** @type {MetaPropertyNode} */ (expr); return this.callHooksForName( this.hooks.evaluateIdentifier, getRootName(expr), metaProperty ); }); tapEvaluateWithVariableInfo("MemberExpression", expr => this.getMemberExpressionInfo( /** @type {MemberExpressionNode} */ (expr), ALLOWED_MEMBER_TYPES_EXPRESSION ) ); this.hooks.evaluate.for("CallExpression").tap("JavascriptParser", _expr => { const expr = /** @type {CallExpressionNode} */ (_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 {ExpressionNode} */ (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 {TemplateLiteralNode} templateLiteralExpr TemplateLiteral expr * @returns {{quasis: BasicEvaluatedExpression[], parts: BasicEvaluatedExpression[]}} Simplified template */ const getSimplifiedTemplateResult = (kind, templateLiteralExpr) => { /** @type {BasicEvaluatedExpression[]} */ const quasis = []; /** @type {BasicEvaluatedExpression[]} */ const parts = []; for (let i = 0; i < templateLiteralExpr.quasis.length; i++) { const quasiExpr = templateLiteralExpr.quasis[i]; const quasi = quasiExpr.value[kind]; if (i > 0) { const prevExpr = parts[parts.length - 1]; const expr = this.evaluateExpression( templateLiteralExpr.expressions[i - 1] ); const exprAsString = expr.asString(); if ( typeof exprAsString === "string" && !expr.couldHaveSideEffects() ) { // We can merge quasi + expr + quasi when expr // is a const string prevExpr.setString(prevExpr.string + exprAsString + quasi); prevExpr.setRange([prevExpr.range[0], quasiExpr.range[1]]); // We unset the expression as it doesn't match to a single expression prevExpr.setExpression(undefined); continue; } parts.push(expr); } const part = new BasicEvaluatedExpression() .setString(quasi) .setRange(quasiExpr.range) .setExpression(quasiExpr); quasis.push(part); parts.push(part); } return { quasis, parts }; }; this.hooks.evaluate .for("TemplateLiteral") .tap("JavascriptParser", _node => { const node = /** @type {TemplateLiteralNode} */ (_node); const { quasis, parts } = getSimplifiedTemplateResult("cooked", node); if (parts.length === 1) { return parts[0].setRange(node.range); } return new BasicEvaluatedExpression() .setTemplateString(quasis, parts, "cooked") .setRange(node.range); }); this.hooks.evaluate .for("TaggedTemplateExpression") .tap("JavascriptParser", _node => { const node = /** @type {TaggedTemplateExpressionNode} */ (_node); const tag = this.evaluateExpression(node.tag); if (tag.isIdentifier() && tag.identifier === "String.raw") { const { quasis, parts } = getSimplifiedTemplateResult( "raw", node.quasi ); return new BasicEvaluatedExpression() .setTemplateString(quasis, parts, "raw") .setRange(node.range); } }); this.hooks.evaluateCallExpressionMember .for("concat") .tap("JavascriptParser", (expr, param) => { if (!param.isString() && !param.isWrapped()) return; let stringSuffix = null; let hasUnknownParams = false; const innerExpressions = []; for (let i = expr.arguments.length - 1; i >= 0; i--) { const arg = expr.arguments[i]; if (arg.type === "SpreadElement") return; const argExpr = this.evaluateExpression(arg); if ( hasUnknownParams || (!argExpr.isString() && !argExpr.isNumber()) ) { hasUnknownParams = true; innerExpressions.push(argExpr); continue; } const value = argExpr.isString() ? argExpr.string : "" + argExpr.number; const newString = value + (stringSuffix ? stringSuffix.string : ""); const newRange = [ argExpr.range[0], (stringSuffix || argExpr).range[1] ]; stringSuffix = new BasicEvaluatedExpression() .setString(newString) .setSideEffects( (stringSuffix && stringSuffix.couldHaveSideEffects()) || argExpr.couldHaveSideEffects() ) .setRange(newRange); } if (hasUnknownParams) { const prefix = param.isString() ? param : param.prefix; const inner = param.isWrapped() && param.wrappedInnerExpressions ? param.wrappedInnerExpressions.concat(innerExpressions.reverse()) : innerExpressions.reverse(); return new BasicEvaluatedExpression() .setWrapped(prefix, stringSuffix, inner) .setRange(expr.range); } else if (param.isWrapped()) { const postfix = stringSuffix || param.postfix; const inner = param.wrappedInnerExpressions ? param.wrappedInnerExpressions.concat(innerExpressions.reverse()) : innerExpressions.reverse(); return new BasicEvaluatedExpression() .setWrapped(param.prefix, postfix, inner) .setRange(expr.range); } else { const newString = param.string + (stringSuffix ? stringSuffix.string : ""); return new BasicEvaluatedExpression() .setString(newString) .setSideEffects( (stringSuffix && stringSuffix.couldHaveSideEffects()) || param.couldHaveSideEffects() ) .setRange(expr.range); } }); this.hooks.evaluateCallExpressionMember .for("split") .tap("JavascriptParser", (expr, param) => { if (!param.isString()) return; if (expr.arguments.length !== 1) return; if (expr.arguments[0].type === "SpreadElement") return; let result; const arg = this.evaluateExpression(expr.arguments[0]); if (arg.isString()) { result = param.string.split(arg.string); } else if (arg.isRegExp()) { result = param.string.split(arg.regExp); } else { return; } return new BasicEvaluatedExpression() .setArray(result) .setSideEffects(param.couldHaveSideEffects()) .setRange(expr.range); }); this.hooks.evaluate .for("ConditionalExpression") .tap("JavascriptParser", _expr => { const expr = /** @type {ConditionalExpressionNode} */ (_expr); const condition = this.evaluateExpression(expr.test); const conditionValue = condition.asBool(); let res; if (conditionValue === undefined) { const consequent = this.evaluateExpression(expr.consequent); const alternate = this.evaluateExpression(expr.alternate); res = new BasicEvaluatedExpression(); if (consequent.isConditional()) { res.setOptions(consequent.options); } else { res.setOptions([consequent]); } if (alternate.isConditional()) { res.addOptions(alternate.options); } else { res.addOptions([alternate]); } } else { res = this.evaluateExpression( conditionValue ? expr.consequent : expr.alternate ); if (condition.couldHaveSideEffects()) res.setSideEffects(); } res.setRange(expr.range); return res; }); this.hooks.evaluate .for("ArrayExpression") .tap("JavascriptParser", _expr => { const expr = /** @type {ArrayExpressionNode} */ (_expr); const items = expr.elements.map(element => { return ( element !== null && element.type !== "SpreadElement" && this.evaluateExpression(element) ); }); if (!items.every(Boolean)) return; return new BasicEvaluatedExpression() .setItems(items) .setRange(expr.range); }); this.hooks.evaluate .for("ChainExpression") .tap("JavascriptParser", _expr => { const expr = /** @type {ChainExpressionNode} */ (_expr); /** @type {ExpressionNode[]} */ const optionalExpressionsStack = []; /** @type {ExpressionNode|SuperNode} */ let next = expr.expression; while ( next.type === "MemberExpression" || next.type === "CallExpression" ) { if (next.type === "MemberExpression") { if (next.optional) { // SuperNode can not be optional optionalExpressionsStack.push( /** @type {ExpressionNode} */ (next.object) ); } next = next.object; } else { if (next.optional) { // SuperNode can not be optional optionalExpressionsStack.push( /** @type {ExpressionNode} */ (next.callee) ); } next = next.callee; } } while (optionalExpressionsStack.length > 0) { const expression = optionalExpressionsStack.pop(); const evaluated = this.evaluateExpression(expression); if (evaluated.asNullish()) { return evaluated.setRange(_expr.range); } } return this.evaluateExpression(expr.expression); }); } getRenameIdentifier(expr) { const result = this.evaluateExpression(expr); if (result.isIdentifier()) { return result.identifier; } } /** * @param {Cla