eslint-plugin-better-mutation
Version:
ESLint rules for controlling where and how mutation is used.
300 lines (243 loc) • 9.45 kB
JavaScript
const _ = require('lodash/fp');
const isReference = _.flow(
_.property('type'),
_.includes(_, ['MemberExpression', 'Identifier']),
);
const isExportDeclaration = _.flow(
_.property('type'),
_.includes(_, ['ExportDefaultDeclaration', 'ExportNamedDeclaration']),
);
const isLiteral = _.flow(_.property('type'), _.includes(_, ['ObjectExpression', 'ArrayExpression', 'Literal']));
const isObjectExpression = _.flow(
_.property('type'),
_.includes(_, ['ObjectExpression', 'ArrayExpression']),
);
const isNewExpression = _.flow(
_.property('type'),
_.includes(_, ['NewExpression']),
);
const isCallExpression = _.flow(
_.property('type'),
_.includes(_, ['CallExpression']),
);
const isFunctionExpression = _.flow(
_.property('type'),
_.includes(_, ['FunctionExpression', 'ArrowFunctionExpression']),
);
const isConditionalExpression = _.flow(
_.property('type'),
_.includes(_, ['ConditionalExpression']),
);
const isClassOrFunctionDeclaration = _.flow(
_.property('type'),
_.includes(_, ['ClassDeclaration', 'FunctionDeclaration']),
);
const isEndOfBlock = _.flow(
_.property('type'),
_.includes(_, ['Program', 'FunctionDeclaration', 'ClassDeclaration', 'FunctionExpression', 'ArrowFunctionExpression']),
);
const buildInitializer = callee => {
if (callee.name) {
return callee.name;
}
if (callee.object?.name) {
return `${callee.object.name}.${callee.property.name}`;
}
return null;
};
const isExemptedInitializer = (rhsExpression, exemptedInitializers) => {
if (!isCallExpression(rhsExpression)) {
return false;
}
const initializer = buildInitializer(rhsExpression.callee);
if (exemptedInitializers?.includes(initializer)) {
return true;
}
return isExemptedInitializer(rhsExpression.callee.object, exemptedInitializers);
};
function getBlockAncestor(node) {
if (isEndOfBlock(node)) {
return node;
}
return getBlockAncestor(node.parent);
}
function isFunctionDeclaration(identifier) {
return _.overEvery([isClassOrFunctionDeclaration, _.matches({id: {name: identifier}})]);
}
function isExportedFunctionDeclaration(identifier) {
return function (node) {
return isExportDeclaration(node) && isFunctionDeclaration(identifier)(_.get('declaration')(node));
};
}
function isForStatementVariable(identifier, node, exemptedInitializers) {
if (node.type === 'ForStatement') {
return isVariableDeclaration(identifier, false, exemptedInitializers)(node.init);
}
return false;
}
function getReference(node) {
switch (node.type) {
case 'MemberExpression': {
return node.object;
}
case 'Identifier': {
return node;
}
default: {
return undefined;
}
}
}
function isValidInit(rhsExpression, node, allowFunctionProps, exemptedInitializers) {
return !_.isNil(rhsExpression) && (
isLiteral(rhsExpression)
|| (isNewExpression(rhsExpression) && rhsExpression.callee.name !== 'Object') // In JS, the Object constructor doesnt allocate memory so it is buggy and should be avoided
|| isExemptedInitializer(rhsExpression, exemptedInitializers)
// TODO Replace/Add the following handling for exemptedInitializers by permitting 'let a = c(); a = 1;' by ensuring that function c() { return {} };
// isCallExpression(rhsExpression) /* && called Function always returns a ValidInit */ ||
|| (isReference(rhsExpression) && isScopedVariable(getReference(rhsExpression), node.parent, allowFunctionProps, exemptedInitializers))
|| (isConditionalExpression(rhsExpression) && isValidInit(rhsExpression.alternate, node, allowFunctionProps, exemptedInitializers) && isValidInit(rhsExpression.consequent, node, allowFunctionProps, exemptedInitializers))
);
}
function getLeftMostObject(arg) {
const object = _.get('object')(arg);
if (!object) {
return arg;
}
return getLeftMostObject(object);
}
function getIdentifierDeclaration(identifier, node) {
const declarations = _.get('declarations', node) || [];
return declarations.find(n => {
if (n.type !== 'VariableDeclarator') {
return false;
}
const id = _.get('id', n);
if (_.get('type', id) === 'ObjectPattern') {
const destructuredProperties = _.get('properties', id) || [];
return _.find(prop => prop.value?.name === identifier || (prop.type === 'RestElement' && prop.argument?.name === identifier), destructuredProperties);
}
if (_.get('type', id) === 'ArrayPattern') {
const destructuredElements = _.get('elements', id) || [];
return _.find(element => element?.name === identifier || (element?.type === 'RestElement' && element.argument?.name === identifier), destructuredElements);
}
return _.get('name', id) === identifier;
});
}
function isVariableDeclaration(identifier, allowFunctionProps, exemptedInitializers) {
return function (node) {
const finalNode = node || {}; // Todo not sure about this defaulting. seems to fix weird bug
if (finalNode.type !== 'VariableDeclaration') {
return false;
}
const declaration = getIdentifierDeclaration(identifier, finalNode);
// Debug('%j', {
// f: 'isVariableDeclaration',
// nodeType: finalNode.type,
// declarationType: declaration?.type,
// isValidInit: isValidInit(_.get('init', declaration), finalNode, exemptedInitializers),
// });
return (
!_.isNil(declaration)
&& isValidInit(_.get('init', declaration), finalNode, allowFunctionProps, exemptedInitializers)
);
};
}
function isLetDeclaration(identifier) {
return function (node) { // Todo not sure about this defaulting. seems to fix weird bug
const finalNode = node || {};
if (finalNode.type !== 'VariableDeclaration' || finalNode.kind !== 'let') {
return false;
}
const declaration = getIdentifierDeclaration(identifier, finalNode);
return !_.isNil(declaration);
};
}
function isScopedVariableIdentifier(identifier, node, allowFunctionProps, exemptedInitializers) {
if (_.isNil(node)) {
return false;
}
return _.some(isVariableDeclaration(identifier, allowFunctionProps, exemptedInitializers), node.body)
|| isForStatementVariable(identifier, node)
|| (!isEndOfBlock(node) && isScopedVariableIdentifier(identifier, node.parent, allowFunctionProps, exemptedInitializers));
}
function isScopedLetIdentifier(identifier, node) {
if (_.isNil(node)) {
return false;
}
return _.some(isLetDeclaration(identifier))(node.body)
|| (!isEndOfBlock(node) && isScopedLetIdentifier(identifier, node.parent));
}
function isScopedLetVariableAssignment(node) {
if (_.get('operator', node) !== '=') {
return false;
}
const identifier = _.get('name')(getLeftMostObject(node.left));
return isScopedLetIdentifier(identifier, node.parent);
}
function isScopedVariable(arg, node, allowFunctionProps, exemptedInitializers) {
const identifier = _.get('name')(getLeftMostObject(arg));
if (!identifier) {
return false;
}
return (allowFunctionProps && isScopedFunctionIdentifier(identifier, node)) || isScopedVariableIdentifier(identifier, node, allowFunctionProps, exemptedInitializers);
}
function isScopedFunctionIdentifier(identifier, node) {
if (_.isNil(node)) {
return false;
}
return _.some(
n => isFunctionDeclaration(identifier)(n) || isExportedFunctionDeclaration(identifier)(n),
)(node.body)
|| (!isEndOfBlock(node) && isScopedFunctionIdentifier(identifier, node.parent));
}
function isScopedFunction(arg, node) {
const identifier = _.get('name')(getLeftMostObject(arg));
return isScopedFunctionIdentifier(identifier, node);
}
function isExemptedReducer(exemptedReducerCallees, node) {
const endOfBlockNode = getBlockAncestor(node);
const callee = _.get('parent.callee', endOfBlockNode);
return callee && _.includes(_.getOr(_.get('name', callee), 'property.name', callee), exemptedReducerCallees);
}
/**
* Determines whether the given node is the update node of a `ForStatement`.
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is `ForStatement` update.
* @author Ian Christian Myers
* @author Brody McKee (github.com/mrmckeb)
*/
function isForStatementUpdate(node) {
const {parent} = node;
return parent.type === 'ForStatement' && parent.update === node;
}
/**
* Determines whether the given node is considered to be a for loop "afterthought" by the logic of this rule.
* In particular, it returns `true` if the given node is either:
* - The update node of a `ForStatement`: for (;; i++) {}
* - An operand of a sequence expression that is the update node: for (;; foo(), i++) {}
* - An operand of a sequence expression that is child of another sequence expression, etc.,
* up to the sequence expression that is the update node: for (;; foo(), (bar(), (baz(), i++))) {}
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is a for loop afterthought.
* @author Ian Christian Myers
* @author Brody McKee (github.com/mrmckeb)
*/
function isForLoopAfterthought(node) {
const {parent} = node;
if (parent.type === 'SequenceExpression') {
return isForLoopAfterthought(parent);
}
return isForStatementUpdate(node);
}
module.exports = {
isExemptedReducer,
isExemptedInitializer,
isFunctionExpression,
isObjectExpression,
isScopedLetVariableAssignment,
isScopedFunction,
isScopedVariable,
isValidInit,
isForLoopAfterthought,
};