UNPKG

eslint-plugin-unicorn

Version:
411 lines (335 loc) • 11.1 kB
import {findVariable} from '@eslint-community/eslint-utils'; import { isCallOrNewExpression, isFunction, isMemberExpression, isMethodCall, } from './ast/index.js'; import builtinErrors from './shared/builtin-errors.js'; import {unwrapTypeScriptExpression as unwrapExpression} from './utils/index.js'; const MESSAGE_ID = 'no-error-property-assignment'; const messages = { [MESSAGE_ID]: 'Do not assign to `{{property}}` on a built-in error.', }; const errorProperties = new Set([ 'name', 'stack', 'cause', ]); const aggregateErrorProperties = new Set([ ...errorProperties, 'errors', ]); const getDisallowedProperties = constructorName => constructorName === 'AggregateError' ? aggregateErrorProperties : errorProperties; const getStaticPropertyName = memberExpression => { const {property} = memberExpression; if (!memberExpression.computed && property.type === 'Identifier') { return property.name; } if ( memberExpression.computed && property.type === 'Literal' && typeof property.value === 'string' ) { return property.value; } }; const getStaticPropertyNameFromProperty = property => { const {key} = property; if (!property.computed && key.type === 'Identifier') { return key.name; } if ( key.type === 'Literal' && typeof key.value === 'string' ) { return key.value; } }; const getVariable = (node, context) => findVariable(context.sourceCode.getScope(node), node); // Keep TypeScript support syntactic: unwrap assertions and `satisfies` so `new Error() as Error` is recognized, but do not use parser services. // A type named `Error` can describe subclasses or values returned from elsewhere, so type-aware matching would exceed the rule's known built-in-instance boundary and make results config-dependent. const getKnownErrorConstructorNameFromFrames = (variable, knownErrorVariableFrames) => { for (const {knownErrorVariables} of knownErrorVariableFrames.toReversed()) { if (knownErrorVariables.has(variable)) { return knownErrorVariables.get(variable); } } }; const getVariableFrameNode = variable => { const {scope} = variable; if (scope.block.type === 'Program') { return scope.block; } if ( scope.type === 'function' && scope.block.body?.type === 'BlockStatement' ) { return scope.block.body; } return scope.block; }; const setKnownErrorVariableFromDeclaration = (knownErrorVariableFrames, variable, constructorName) => { if (updateExistingKnownErrorVariable(knownErrorVariableFrames, variable, constructorName)) { return; } const frameNode = getVariableFrameNode(variable); const frame = knownErrorVariableFrames.findLast(frame => frame.node === frameNode); const currentFrame = knownErrorVariableFrames.at(-1); const targetFrame = frame ?? currentFrame; if ( targetFrame !== currentFrame && !canPropagateToFrame(knownErrorVariableFrames, targetFrame) ) { getLocalUpdateFrame(knownErrorVariableFrames).knownErrorVariables.set(variable, constructorName); return; } targetFrame.knownErrorVariables.set(variable, constructorName); }; const transparentBlockParentTypes = new Set([ 'BlockStatement', 'Program', 'StaticBlock', 'SwitchCase', ]); const isTransparentFrame = frame => frame.node.type === 'BlockStatement' && transparentBlockParentTypes.has(frame.node.parent.type) && !frame.isFunctionBoundary; // The root `Program` frame is never transparent, so a frame that is not on the stack cannot be reached const canPropagateToFrame = (knownErrorVariableFrames, targetFrame) => knownErrorVariableFrames .slice(knownErrorVariableFrames.indexOf(targetFrame) + 1) .every(frame => isTransparentFrame(frame)); // The root `Program` frame is never transparent, so this always finds a frame const getLocalUpdateFrame = knownErrorVariableFrames => knownErrorVariableFrames.findLast(frame => !isTransparentFrame(frame)); const updateExistingKnownErrorVariable = (knownErrorVariableFrames, variable, constructorName) => { let isCrossesFunctionBoundary = false; for (const frame of knownErrorVariableFrames.toReversed()) { if (frame.knownErrorVariables.has(variable)) { if (canPropagateToFrame(knownErrorVariableFrames, frame)) { frame.knownErrorVariables.set(variable, constructorName); return true; } // A reassignment inside a nested function may run before the outer value is read, and `constructorName` being `undefined` means the value stops being a built-in error either way, so the outer knowledge no longer holds. if (!isCrossesFunctionBoundary || constructorName === undefined) { frame.knownErrorVariables.set(variable, undefined); } getLocalUpdateFrame(knownErrorVariableFrames).knownErrorVariables.set(variable, constructorName); return true; } if (frame.isFunctionBoundary) { isCrossesFunctionBoundary = true; } } return false; }; const setKnownErrorVariableFromAssignment = (knownErrorVariableFrames, variable, constructorName) => { if (updateExistingKnownErrorVariable(knownErrorVariableFrames, variable, constructorName)) { return; } const frameNode = getVariableFrameNode(variable); const frame = knownErrorVariableFrames.findLast(frame => frame.node === frameNode); if ( frame && canPropagateToFrame(knownErrorVariableFrames, frame) ) { frame.knownErrorVariables.set(variable, constructorName); return; } getLocalUpdateFrame(knownErrorVariableFrames).knownErrorVariables.set(variable, constructorName); }; const frameNodeTypes = new Set([ 'Program', 'BlockStatement', 'StaticBlock', 'SwitchCase', ]); const isFrameNode = node => frameNodeTypes.has(node.type); const isDirectChildOfCurrentFrame = node => { const {parent} = node; if (isFrameNode(parent)) { return true; } return ( parent.type === 'VariableDeclaration' && ( isFrameNode(parent.parent) || (parent.parent.type === 'ForStatement' && parent.parent.init === parent) ) ); }; // Only a plain statement at the frame level always runs before the code that follows it const isUnconditionalAssignment = assignmentExpression => { const {parent} = assignmentExpression; return (parent.type === 'ForStatement' && parent.init === assignmentExpression) || (parent.type === 'ExpressionStatement' && isDirectChildOfCurrentFrame(parent)); }; const getErrorConstructorName = (node, context) => { node = unwrapExpression(node); if (!( isCallOrNewExpression(node, { names: builtinErrors, optional: false, }) && context.sourceCode.isGlobalReference(node.callee) )) { return; } return node.callee.name; }; const getKnownErrorConstructorName = (node, context, knownErrorVariables) => { node = unwrapExpression(node); const constructorName = getErrorConstructorName(node, context); if (constructorName) { return constructorName; } if (node.type !== 'Identifier') { return; } return getKnownErrorConstructorNameFromFrames( getVariable(node, context), knownErrorVariables, ); }; const getPropertyProblem = (node, property) => ({ node, messageId: MESSAGE_ID, data: {property}, }); function * getObjectAssignProblems(callExpression, context, knownErrorVariables) { if (!( isMethodCall(callExpression, { object: 'Object', method: 'assign', minimumArguments: 2, optionalMember: false, optionalCall: false, }) && context.sourceCode.isGlobalReference(callExpression.callee.object) )) { return; } const [target, ...sources] = callExpression.arguments; const constructorName = getKnownErrorConstructorName(target, context, knownErrorVariables); if (!constructorName) { return; } const disallowedProperties = getDisallowedProperties(constructorName); for (const source of sources) { if (source.type !== 'ObjectExpression') { continue; } for (const property of source.properties) { if (property.type !== 'Property') { continue; } const propertyName = getStaticPropertyNameFromProperty(property); if (disallowedProperties.has(propertyName)) { yield getPropertyProblem(property.key, propertyName); } } } } const getDirectAssignmentProblem = (assignmentExpression, context, knownErrorVariables) => { // `error.name++` writes the property as well const target = assignmentExpression.type === 'UpdateExpression' ? assignmentExpression.argument : assignmentExpression.left; if (!isMemberExpression(target, {optional: false})) { return; } const memberExpression = target; const constructorName = getKnownErrorConstructorName(memberExpression.object, context, knownErrorVariables); if (!constructorName) { return; } const propertyName = getStaticPropertyName(memberExpression); if (getDisallowedProperties(constructorName).has(propertyName)) { return getPropertyProblem(memberExpression.property, propertyName); } }; const updateKnownErrorVariable = (assignmentExpression, context, knownErrorVariables) => { const {left} = assignmentExpression; if (left.type !== 'Identifier') { return; } const variable = getVariable(left, context); if (!variable) { return; } // Any other write, like a conditional one or one inside an expression, makes the value unknown const constructorName = assignmentExpression.operator === '=' && isUnconditionalAssignment(assignmentExpression) ? getErrorConstructorName(assignmentExpression.right, context) : undefined; setKnownErrorVariableFromAssignment(knownErrorVariables, variable, constructorName); }; /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const knownErrorVariables = [{ node: context.sourceCode.ast, knownErrorVariables: new Map(), isFunctionBoundary: false, }]; context.on(['BlockStatement', 'StaticBlock', 'SwitchCase'], node => { knownErrorVariables.push({ node, knownErrorVariables: new Map(), isFunctionBoundary: node.type === 'BlockStatement' && isFunction(node.parent), }); }); context.onExit(['BlockStatement', 'StaticBlock', 'SwitchCase'], () => { knownErrorVariables.pop(); }); context.on('VariableDeclarator', node => { if ( node.id.type !== 'Identifier' || !isDirectChildOfCurrentFrame(node) ) { return; } const constructorName = getErrorConstructorName(node.init, context); if ( !constructorName && !node.init ) { return; } const [variable] = context.sourceCode.getDeclaredVariables(node); setKnownErrorVariableFromDeclaration(knownErrorVariables, variable, constructorName ?? undefined); }); context.on(['AssignmentExpression', 'UpdateExpression'], node => { const problem = getDirectAssignmentProblem(node, context, knownErrorVariables); if (node.type === 'AssignmentExpression') { updateKnownErrorVariable(node, context, knownErrorVariables); } return problem; }); context.on('CallExpression', node => getObjectAssignProblems(node, context, knownErrorVariables)); }; /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'problem', docs: { description: 'Disallow assigning to built-in error properties.', recommended: 'unopinionated', }, schema: [], messages, languages: [ 'js/js', ], }, }; export default config;