codtracker-js/node_modules/eslint/lib/rules/no-import-assign.js
2025-04-19 23:12:19 -04:00

224 lines
6.4 KiB
JavaScript

/**
* @fileoverview Rule to flag updates of imported bindings.
* @author Toru Nagashima <https://github.com/mysticatea>
*/
'use strict';
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const { findVariable } = require('@eslint-community/eslint-utils');
const astUtils = require('./utils/ast-utils');
const WellKnownMutationFunctions = {
Object: /^(?:assign|definePropert(?:y|ies)|freeze|setPrototypeOf)$/u,
Reflect: /^(?:(?:define|delete)Property|set(?:PrototypeOf)?)$/u,
};
/**
* Check if a given node is LHS of an assignment node.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is LHS.
*/
function isAssignmentLeft(node) {
const { parent } = node;
return (
(parent.type === 'AssignmentExpression' && parent.left === node) ||
// Destructuring assignments
parent.type === 'ArrayPattern' ||
(parent.type === 'Property' &&
parent.value === node &&
parent.parent.type === 'ObjectPattern') ||
parent.type === 'RestElement' ||
(parent.type === 'AssignmentPattern' && parent.left === node)
);
}
/**
* Check if a given node is the operand of mutation unary operator.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is the operand of mutation unary operator.
*/
function isOperandOfMutationUnaryOperator(node) {
const argumentNode =
node.parent.type === 'ChainExpression' ? node.parent : node;
const { parent } = argumentNode;
return (
(parent.type === 'UpdateExpression' && parent.argument === argumentNode) ||
(parent.type === 'UnaryExpression' &&
parent.operator === 'delete' &&
parent.argument === argumentNode)
);
}
/**
* Check if a given node is the iteration variable of `for-in`/`for-of` syntax.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is the iteration variable.
*/
function isIterationVariable(node) {
const { parent } = node;
return (
(parent.type === 'ForInStatement' && parent.left === node) ||
(parent.type === 'ForOfStatement' && parent.left === node)
);
}
/**
* Check if a given node is at the first argument of a well-known mutation function.
* - `Object.assign`
* - `Object.defineProperty`
* - `Object.defineProperties`
* - `Object.freeze`
* - `Object.setPrototypeOf`
* - `Reflect.defineProperty`
* - `Reflect.deleteProperty`
* - `Reflect.set`
* - `Reflect.setPrototypeOf`
* @param {ASTNode} node The node to check.
* @param {Scope} scope A `escope.Scope` object to find variable (whichever).
* @returns {boolean} `true` if the node is at the first argument of a well-known mutation function.
*/
function isArgumentOfWellKnownMutationFunction(node, scope) {
const { parent } = node;
if (parent.type !== 'CallExpression' || parent.arguments[0] !== node) {
return false;
}
const callee = astUtils.skipChainExpression(parent.callee);
if (
!astUtils.isSpecificMemberAccess(
callee,
'Object',
WellKnownMutationFunctions.Object
) &&
!astUtils.isSpecificMemberAccess(
callee,
'Reflect',
WellKnownMutationFunctions.Reflect
)
) {
return false;
}
const variable = findVariable(scope, callee.object);
return variable !== null && variable.scope.type === 'global';
}
/**
* Check if the identifier node is placed at to update members.
* @param {ASTNode} id The Identifier node to check.
* @param {Scope} scope A `escope.Scope` object to find variable (whichever).
* @returns {boolean} `true` if the member of `id` was updated.
*/
function isMemberWrite(id, scope) {
const { parent } = id;
return (
(parent.type === 'MemberExpression' &&
parent.object === id &&
(isAssignmentLeft(parent) ||
isOperandOfMutationUnaryOperator(parent) ||
isIterationVariable(parent))) ||
isArgumentOfWellKnownMutationFunction(id, scope)
);
}
/**
* Get the mutation node.
* @param {ASTNode} id The Identifier node to get.
* @returns {ASTNode} The mutation node.
*/
function getWriteNode(id) {
let node = id.parent;
while (
node &&
node.type !== 'AssignmentExpression' &&
node.type !== 'UpdateExpression' &&
node.type !== 'UnaryExpression' &&
node.type !== 'CallExpression' &&
node.type !== 'ForInStatement' &&
node.type !== 'ForOfStatement'
) {
node = node.parent;
}
return node || id;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow assigning to imported bindings',
recommended: true,
url: 'https://eslint.org/docs/latest/rules/no-import-assign',
},
schema: [],
messages: {
readonly: "'{{name}}' is read-only.",
readonlyMember: "The members of '{{name}}' are read-only.",
},
},
create(context) {
const sourceCode = context.sourceCode;
return {
ImportDeclaration(node) {
const scope = sourceCode.getScope(node);
for (const variable of sourceCode.getDeclaredVariables(node)) {
const shouldCheckMembers = variable.defs.some(
(d) => d.node.type === 'ImportNamespaceSpecifier'
);
let prevIdNode = null;
for (const reference of variable.references) {
const idNode = reference.identifier;
/*
* AssignmentPattern (e.g. `[a = 0] = b`) makes two write
* references for the same identifier. This should skip
* the one of the two in order to prevent redundant reports.
*/
if (idNode === prevIdNode) {
continue;
}
prevIdNode = idNode;
if (reference.isWrite()) {
context.report({
node: getWriteNode(idNode),
messageId: 'readonly',
data: { name: idNode.name },
});
} else if (shouldCheckMembers && isMemberWrite(idNode, scope)) {
context.report({
node: getWriteNode(idNode),
messageId: 'readonlyMember',
data: { name: idNode.name },
});
}
}
}
},
};
},
};