diff --git a/src/get-static-value.mjs b/src/get-static-value.mjs index 12cbc9f..fb6300f 100644 --- a/src/get-static-value.mjs +++ b/src/get-static-value.mjs @@ -3,6 +3,7 @@ import { findVariable } from "./find-variable.mjs" /** @typedef {import("./types.mjs").StaticValue} StaticValue */ /** @typedef {import("eslint").Scope.Scope} Scope */ +/** @typedef {import("eslint").Scope.Variable} Variable */ /** @typedef {import("estree").Node} Node */ /** @typedef {import("@typescript-eslint/types").TSESTree.Node} TSESTreeNode */ /** @typedef {import("@typescript-eslint/types").TSESTree.AST_NODE_TYPES} TSESTreeNodeTypes */ @@ -266,9 +267,40 @@ function getElementValues(nodeList, initialScope) { return valueList } +/** + * Checks if a variable is a built-in global. + * @param {Variable|null} variable The variable to check. + * @returns {variable is Variable & {defs:[]}} + */ +function isBuiltinGlobal(variable) { + return ( + variable != null && + variable.defs.length === 0 && + builtinNames.has(variable.name) && + variable.name in globalObject + ) +} + +/** + * Checks if a variable can be considered as a constant. + * @param {Variable} variable + * @returns {variable is Variable & {defs: [import("eslint").Scope.Definition & { type: "Variable" }]}} True if the variable can be considered as a constant. + */ +function canBeConsideredConst(variable) { + if (variable.defs.length !== 1) { + return false + } + const def = variable.defs[0] + return Boolean( + def.parent && + def.type === "Variable" && + (def.parent.kind === "const" || isEffectivelyConst(variable)), + ) +} + /** * Returns whether the given variable is never written to after initialization. - * @param {import("eslint").Scope.Variable} variable + * @param {Variable} variable * @returns {boolean} */ function isEffectivelyConst(variable) { @@ -283,6 +315,68 @@ function isEffectivelyConst(variable) { return false } +/** + * Checks if a variable has mutation in its property. + * @param {Variable} variable The variable to check. + * @param {Scope|null} initialScope The scope to start finding variable. Optional. If the node is a computed property node and this scope was given, this checks the computed property name by the `getStringIfConstant` function with the scope, and returns the value of it. + * @returns {boolean} True if the variable has mutation in its property. + */ +function hasMutationInProperty(variable, initialScope) { + for (const ref of variable.references) { + let node = /** @type {TSESTreeNode} */ (ref.identifier) + while (node && node.parent && node.parent.type === "MemberExpression") { + node = node.parent + } + if (!node || !node.parent) { + continue + } + if ( + (node.parent.type === "AssignmentExpression" && + node.parent.left === node) || + (node.parent.type === "UpdateExpression" && + node.parent.argument === node) + ) { + // This is a mutation. + return true + } + if ( + node.parent.type === "CallExpression" && + node.parent.callee === node && + node.type === "MemberExpression" + ) { + const methodName = getStaticPropertyNameValue(node, initialScope) + if (isNameOfMutationArrayMethod(methodName)) { + // This is a mutation. + return true + } + } + } + return false + + /** + * Checks if a method name is one of the mutation array methods. + * @param {StaticValue|null} methodName The method name to check. + * @returns {boolean} True if the method name is a mutation array method. + */ + function isNameOfMutationArrayMethod(methodName) { + if (methodName == null || methodName.value == null) { + return false + } + const name = methodName.value + return ( + name === "copyWithin" || + name === "fill" || + name === "pop" || + name === "push" || + name === "reverse" || + name === "shift" || + name === "sort" || + name === "splice" || + name === "unshift" + ) + } +} + /** * @template {TSESTreeNodeTypes} T * @callback VisitorCallback @@ -512,28 +606,35 @@ const operations = Object.freeze({ if (initialScope != null) { const variable = findVariable(initialScope, node) - // Built-in globals. - if ( - variable != null && - variable.defs.length === 0 && - builtinNames.has(variable.name) && - variable.name in globalObject - ) { - return { value: globalObject[variable.name] } - } + if (variable != null) { + // Built-in globals. + if (isBuiltinGlobal(variable)) { + return { value: globalObject[variable.name] } + } - // Constants. - if (variable != null && variable.defs.length === 1) { - const def = variable.defs[0] - if ( - def.parent && - def.type === "Variable" && - (def.parent.kind === "const" || - isEffectivelyConst(variable)) && - // TODO(mysticatea): don't support destructuring here. - def.node.id.type === "Identifier" - ) { - return getStaticValueR(def.node.init, initialScope) + // Constants. + if (canBeConsideredConst(variable)) { + const def = variable.defs[0] + if ( + // TODO(mysticatea): don't support destructuring here. + def.node.id.type === "Identifier" + ) { + const init = getStaticValueR( + def.node.init, + initialScope, + ) + if ( + init && + typeof init.value === "object" && + init.value !== null + ) { + if (hasMutationInProperty(variable, initialScope)) { + // This variable has mutation in its property. + return null + } + } + return init + } } } } diff --git a/test/get-static-value.mjs b/test/get-static-value.mjs index aca4d3d..28d8f3b 100644 --- a/test/get-static-value.mjs +++ b/test/get-static-value.mjs @@ -398,6 +398,55 @@ const aMap = Object.freeze({ }, ] : []), + // Mutations + { + code: "const a = {foo: 'a'}; a.bar = 'b'; a", + expected: null, + }, + { + code: "const a = ['a']; a[0] = 'b'; a.join()", + expected: null, + }, + { + code: "const a = ['a']; a.copyWithin(0, 1); a.join()", + expected: null, + }, + { + code: "const a = ['a']; a.fill('b'); a.join()", + expected: null, + }, + { + code: "const a = ['a']; a.pop(); a.join()", + expected: null, + }, + { + code: "const a = ['a']; a.push('b'); a.join()", + expected: null, + }, + { + code: "const a = ['a', 'b']; a.reverse(); a.join()", + expected: null, + }, + { + code: "const a = ['a']; a.shift(); a.join()", + expected: null, + }, + { + code: "const a = ['b', 'a', 'c']; a.sort(); a.join()", + expected: null, + }, + { + code: "const a = ['a', 'c']; a.splice(1, 0, 'b'); a.join()", + expected: null, + }, + { + code: "const a = ['a']; a.unshift('b'); a.join()", + expected: null, + }, + { + code: "const a = {foo: ['a']}; a.foo.shift(); a", + expected: null, + }, // TypeScript support { code: `const a = 42; a as number;`,