UNPKG

eslint-plugin-unicorn

Version:
493 lines (404 loc) 16 kB
import { findVariable, getStaticValue as getStaticValueFromEslintUtilities, hasSideEffect, } from '@eslint-community/eslint-utils'; import isGlobalIdentifier from './is-global-identifier.js'; import unwrapTypeScriptExpression from './unwrap-typescript-expression.js'; const unevaluatedExpressionTypes = new Set([ 'FunctionExpression', 'ArrowFunctionExpression', 'ClassExpression', ]); const staticPassThroughMethods = new Set([ 'freeze', 'preventExtensions', 'seal', ]); const staticGlobalProperties = new Map([ ['Math', new Set(['E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'PI', 'SQRT1_2', 'SQRT2'])], ['Number', new Set(['EPSILON', 'MAX_SAFE_INTEGER', 'MAX_VALUE', 'MIN_SAFE_INTEGER', 'MIN_VALUE', 'NaN', 'NEGATIVE_INFINITY', 'POSITIVE_INFINITY'])], ['String', new Set(['raw'])], ['Symbol', new Set(['asyncIterator', 'hasInstance', 'isConcatSpreadable', 'iterator', 'match', 'matchAll', 'replace', 'search', 'species', 'split', 'toPrimitive', 'toStringTag', 'unscopables'])], ]); // Like `getStaticValue`, this assumes that built-in globals are not monkey-patched. const isSafeStaticGlobalMember = (node, context) => Boolean( node.type === 'MemberExpression' && !node.optional && node.object.type === 'Identifier' && isGlobalIdentifier(node.object, context) && staticGlobalProperties.get(node.object.name)?.has( node.computed ? node.property.type === 'Literal' && typeof node.property.value === 'string' && node.property.value : node.property.type === 'Identifier' && node.property.name, ), ); export const isSafeStaticPassThroughCall = (node, context, visitedVariables = new Set()) => { node = unwrapTypeScriptExpression(node); if (node.type !== 'CallExpression') { return false; } const callee = unwrapTypeScriptExpression(node.callee); const object = unwrapTypeScriptExpression(callee.object); return !node.optional && node.arguments.length === 1 && node.arguments[0].type !== 'SpreadElement' && callee.type === 'MemberExpression' && !callee.computed && !callee.optional && object.type === 'Identifier' && object.name === 'Object' && isGlobalIdentifier(object, context) && callee.property.type === 'Identifier' && staticPassThroughMethods.has(callee.property.name) && getStaticValueIfNoSideEffectsInternal(node.arguments[0], context, visitedVariables) !== undefined; }; export const isBranchExpression = node => { node = unwrapTypeScriptExpression(node); while (node.type === 'UnaryExpression') { node = unwrapTypeScriptExpression(node.argument); } return node.type === 'ConditionalExpression' || node.type === 'LogicalExpression'; }; const getConstVariableDefinition = (node, context) => { if (node.type !== 'Identifier' || !context) { return; } const variable = findVariable(context.sourceCode.getScope(node), node); const definition = variable?.defs.length === 1 ? variable.defs[0] : undefined; if ( definition?.type !== 'Variable' || definition.parent?.kind !== 'const' || definition.node.id !== definition.name || !definition.node.init ) { return; } return {variable, initializer: definition.node.init}; }; const isRegExpValue = value => Object.prototype.toString.call(value) === '[object RegExp]'; const isRegExpConstructor = node => node?.type === 'NewExpression' && node.callee.type === 'Identifier' && node.callee.name === 'RegExp'; const isRegExpLiteral = node => node?.type === 'Literal' && node.regex; const getRegExpVariableDefinition = (node, context) => { if (node.type !== 'Identifier') { return; } const variable = findVariable(context.sourceCode.getScope(node), node); const definition = variable?.defs.length === 1 ? variable.defs[0] : undefined; if ( definition?.type !== 'Variable' || !definition.node.init || definition.node.id !== definition.name || variable.references.some(reference => reference.isWrite() && reference.identifier !== definition.name) ) { return; } return {initializer: definition.node.init}; }; const isSafeStaticRegExpConstructorArgument = (node, context) => { if (node.type === 'SpreadElement') { return false; } const staticValue = getStaticValueIfNoSideEffects(node, context); if (!staticValue) { return false; } return isRegExpValue(staticValue.value) || staticValue.value === null || ['bigint', 'boolean', 'number', 'string', 'undefined'].includes(typeof staticValue.value); }; export const getStaticRegExp = (node, context) => { const staticValue = getStaticValueIfNoSideEffects(node, context); if (staticValue && isRegExpValue(staticValue.value)) { return staticValue.value; } const definition = getRegExpVariableDefinition(node, context); const initializer = definition?.initializer; const constructor = isRegExpConstructor(node) ? node : (isRegExpConstructor(initializer) ? initializer : undefined); if ( !isRegExpLiteral(initializer) && (!constructor || constructor.arguments.some(argument => !isSafeStaticRegExpConstructorArgument(argument, context))) ) { return; } const result = getStaticValueFromEslintUtilities(node, context.sourceCode.getScope(node)); return result && isRegExpValue(result.value) ? result.value : undefined; }; const getChildNodes = node => Object.entries(node) .filter(([key]) => !['parent', 'loc', 'range'].includes(key) && !(node.type === 'Property' && key === 'key' && !node.computed) && !(node.type === 'MemberExpression' && key === 'property' && !node.computed), ) .flatMap(([, value]) => Array.isArray(value) ? value : [value]) .filter(value => value?.type); export const hasPotentiallyMutableBinding = (node, context, visitedVariables = new Set()) => { if (node.type === 'Identifier') { const variable = findVariable(context.sourceCode.getScope(node), node); const definition = variable?.defs.length === 1 ? variable.defs[0] : undefined; if (definition?.type !== 'Variable') { return false; } if (definition.parent?.kind !== 'const') { return true; } if (!definition.node.init || visitedVariables.has(variable)) { return false; } visitedVariables.add(variable); const result = hasPotentiallyMutableBinding(definition.node.init, context, visitedVariables); visitedVariables.delete(variable); return result; } if (unevaluatedExpressionTypes.has(node.type)) { return false; } return getChildNodes(node).some(child => hasPotentiallyMutableBinding(child, context, visitedVariables)); }; const hasSideEffectfulConstReference = (node, context, visitedVariables) => { if (node.type === 'Identifier') { return hasSideEffectfulConstInitializer(node, context, visitedVariables); } if (unevaluatedExpressionTypes.has(node.type)) { return false; } return getChildNodes(node).some(child => hasSideEffectfulConstReference(child, context, visitedVariables)); }; export const hasSideEffectfulConstInitializer = (node, context, visitedVariables = new Set()) => { const definition = getConstVariableDefinition(node, context); if (!definition) { return node.type !== 'Identifier' && hasSideEffectfulConstReference(node, context, visitedVariables); } if (visitedVariables.has(definition.variable)) { return true; } visitedVariables.add(definition.variable); const result = ( !isSafeStaticPassThroughCall(definition.initializer, context, visitedVariables) && hasSideEffect(definition.initializer, context.sourceCode, {considerGetters: true}) ) || hasSideEffectfulConstReference(definition.initializer, context, visitedVariables); visitedVariables.delete(definition.variable); return result; }; const isStaticPropertyValue = value => typeof value === 'string' || typeof value === 'number'; const getStaticPropertyName = (node, property, context) => { if (property.type === 'Identifier' && !node.computed) { return property.name; } if (property.type === 'Literal' && isStaticPropertyValue(property.value)) { return String(property.value); } if (!node.computed || !context || (property.type === 'Identifier' && !getConstVariableDefinition(property, context))) { return; } const staticValue = getStaticValueFromEslintUtilities(property, context.sourceCode.getScope(property)); if (staticValue && isStaticPropertyValue(staticValue.value)) { return String(staticValue.value); } }; const getStaticMemberName = (node, context) => getStaticPropertyName(node, node.property, context); const isSafeStaticObjectMember = (node, propertyName, context, visitedVariables) => { if ( propertyName === undefined || node.properties.some(property => property.type !== 'Property' || property.computed || property.key.name === '__proto__' || property.key.value === '__proto__' || hasPotentiallyMutableMemberAccess(property.value, context, visitedVariables), ) ) { return false; } let property; for (const candidate of node.properties) { if (getStaticPropertyName(candidate, candidate.key) === propertyName) { property = candidate; } } return Boolean( property && property.kind === 'init' && !property.method, ); }; const isSafeStaticArrayMember = (node, propertyName, context, visitedVariables) => { if (node.elements.some(element => element?.type === 'SpreadElement' || (element && hasPotentiallyMutableMemberAccess(element, context, visitedVariables)), )) { return false; } if (propertyName === 'length') { return true; } const index = Number(propertyName); if (String(index) !== propertyName || !Number.isSafeInteger(index) || index < 0 || index >= node.elements.length) { return false; } return Boolean(node.elements.at(index)); }; const isSafeStaticMemberObject = (node, propertyName, context, visitedVariables) => { const definition = getConstVariableDefinition(node, context); let primitiveValue; if (node.type === 'Literal') { primitiveValue = node.value; } else if (definition?.initializer.type === 'Literal') { primitiveValue = definition.initializer.value; } if (typeof primitiveValue === 'string') { if (propertyName === 'length') { return true; } const index = Number(propertyName); return String(index) === propertyName && Number.isSafeInteger(index) && index >= 0 && index < primitiveValue.length; } if (node.type === 'ArrayExpression') { return isSafeStaticArrayMember(node, propertyName, context, visitedVariables); } if (node.type === 'ObjectExpression') { return isSafeStaticObjectMember(node, propertyName, context, visitedVariables); } return false; }; const isSafeStaticMember = (node, context, visitedVariables) => { const propertyName = getStaticMemberName(node, context); if (node.computed && hasPotentiallyMutableMemberAccess(node.property, context, visitedVariables)) { return false; } return isSafeStaticMemberObject(node.object, propertyName, context, visitedVariables); }; export const hasPotentiallyMutableMemberAccess = (node, context, visitedVariables = new Set()) => { node = unwrapTypeScriptExpression(node); if (node.type === 'ChainExpression') { node = node.expression; } if (isSafeStaticPassThroughCall(node, context, visitedVariables)) { return false; } const definition = getConstVariableDefinition(node, context); if (definition) { if (visitedVariables.has(definition.variable)) { return true; } visitedVariables.add(definition.variable); const result = hasPotentiallyMutableMemberAccess(definition.initializer, context, visitedVariables); visitedVariables.delete(definition.variable); return result; } if (node.type === 'MemberExpression') { return !isSafeStaticGlobalMember(node, context) && !isSafeStaticMember(node, context, visitedVariables); } return getChildNodes(node).some(child => hasPotentiallyMutableMemberAccess(child, context, visitedVariables)); }; const hasUnsafeBindingInEvaluatedPath = (node, context, visitedVariables = new Set()) => { node = unwrapTypeScriptExpression(node); if (node.type === 'Identifier') { const variable = findVariable(context.sourceCode.getScope(node), node); const definition = variable?.defs.length === 1 ? variable.defs[0] : undefined; if (definition?.type !== 'Variable') { return false; } if (definition.parent?.kind !== 'const') { return true; } if (context.sourceCode.getRange(definition.node)[1] > context.sourceCode.getRange(node)[0]) { return true; } if (!definition.node.init || visitedVariables.has(variable)) { return false; } visitedVariables.add(variable); const result = hasUnsafeBindingInEvaluatedPath(definition.node.init, context, visitedVariables); visitedVariables.delete(variable); return result; } if (node.type === 'ConditionalExpression') { if (hasUnsafeBindingInEvaluatedPath(node.test, context, visitedVariables)) { return true; } const staticValue = getStaticValueIfNoSideEffectsInternal(node.test, context, visitedVariables); if (staticValue !== undefined) { return hasUnsafeBindingInEvaluatedPath( staticValue.value ? node.consequent : node.alternate, context, visitedVariables, ); } return hasUnsafeBindingInEvaluatedPath(node.consequent, context, visitedVariables) || hasUnsafeBindingInEvaluatedPath(node.alternate, context, visitedVariables); } if (node.type === 'LogicalExpression') { if (hasUnsafeBindingInEvaluatedPath(node.left, context, visitedVariables)) { return true; } const staticValue = getStaticValueIfNoSideEffectsInternal(node.left, context, visitedVariables); if (staticValue !== undefined) { let evaluatesRight; if (node.operator === '&&') { evaluatesRight = Boolean(staticValue.value); } else if (node.operator === '||') { evaluatesRight = !staticValue.value; } else { evaluatesRight = staticValue.value === null || staticValue.value === undefined; } return evaluatesRight && hasUnsafeBindingInEvaluatedPath(node.right, context, visitedVariables); } return hasUnsafeBindingInEvaluatedPath(node.right, context, visitedVariables); } if (node.type === 'SequenceExpression') { return node.expressions.some(expression => hasUnsafeBindingInEvaluatedPath(expression, context, visitedVariables)); } if (unevaluatedExpressionTypes.has(node.type)) { return false; } return getChildNodes(node).some(child => hasUnsafeBindingInEvaluatedPath(child, context, visitedVariables)); }; const getStaticValueIfNoSideEffectsInternal = (node, context, visitedVariables = new Set()) => { node = unwrapTypeScriptExpression(node); const {sourceCode} = context; // Static evaluation can execute allowlisted built-ins, so reject side-effectful expressions and constant initializers before they can perform expensive work. const hasSideEffects = hasSideEffect(node, sourceCode); if ( (hasSideEffects && !isSafeStaticPassThroughCall(node, context, visitedVariables)) || hasSideEffectfulConstInitializer(node, context, visitedVariables) ) { return; } // Most expressions have no static value at all, so evaluate first and only run the expensive mutability walk when there is a value to protect. const staticValue = getStaticValueFromEslintUtilities(node, sourceCode.getScope(node)); if (!staticValue) { return; } if (hasPotentiallyMutableMemberAccess(node, context, visitedVariables)) { return; } return staticValue; }; // `getStaticValue` is not flow-sensitive, so reject mutable bindings and constant references before their declarations complete on any path that can be evaluated. export const getStaticValueForControlFlow = (node, context) => { const staticValue = getStaticValueIfNoSideEffectsInternal(node, context); return staticValue !== undefined && !hasUnsafeBindingInEvaluatedPath(node, context) ? staticValue : undefined; }; /** Get the static value of a node only when evaluating it has no side effects or unsupported mutable member reads. @param {import('estree').Node} node @param {import('eslint').Rule.RuleContext} context @returns {object | undefined} */ export default function getStaticValueIfNoSideEffects(node, context) { return getStaticValueIfNoSideEffectsInternal(node, context); }