eslint-plugin-better-mutation
Version:
ESLint rules for controlling where and how mutation is used.
127 lines (117 loc) • 3.22 kB
JavaScript
'use strict';
const _ = require('lodash/fp');
const {
isObjectExpression,
isFunctionExpression,
isScopedVariable,
isExemptedReducer,
} = require('./utils/common');
const mutatingFunctions = [
'Object.assign',
'Object.defineProperties',
'Object.defineProperty',
'Object.setPrototypeOf',
'_.assign',
'_.assignIn',
'_.assignInWith',
'_.assignWith',
'_.defaults',
'_.defaultsDeep',
'_.fill',
'_.pull',
'_.pullAll',
'_.pullAllBy',
'_.pullAllWith',
'_.pullAt',
'_.merge',
'_.mergeWith',
'_.remove',
'_.reverse',
'_.set',
'_.setWith',
'_.unset',
'_.update',
'_.updateWith',
];
const isLodashFn = _.startsWith('_.');
function buildIsMutatingFunction(ignoredMethods, useLodashFunctionImports) {
const matchesSpecs = _.flow(
_.map(fn => useLodashFunctionImports && isLodashFn(fn) ? fn.slice(2) : fn),
_.reject(fn => _.includes(fn, ignoredMethods)),
_.map(fn => {
const [objectName, propertyName] = _.split('.', fn);
return propertyName
? ({
type: 'MemberExpression',
object: {
type: 'Identifier',
name: objectName,
},
property: {
type: 'Identifier',
name: propertyName,
},
})
: ({
type: 'Identifier',
name: objectName,
});
}),
)(mutatingFunctions);
return _.overSome(_.map(spec => _.matches(spec), matchesSpecs));
}
function isAllowedFirstArgument(arg, node, allowFunctionProps) {
return isObjectExpression(arg) || isFunctionExpression(arg) || isScopedVariable(arg, node.parent, allowFunctionProps);
}
const create = function (context) {
const ignoredMethods = _.getOr([], ['options', 0, 'ignoreMethods'], context);
const useLodashFunctionImports = _.getOr(false, ['options', 0, 'useLodashFunctionImports'], context);
const allowFunctionProps = _.getOr(false, ['options', 0, 'functionProps'], context);
const exemptedReducerCallees = _.getOr(['reduce'], ['options', 0, 'reducers'], context);
const isMutatingFunction = buildIsMutatingFunction(ignoredMethods, useLodashFunctionImports);
return {
CallExpression(node) {
if (isMutatingFunction(node.callee) && !isAllowedFirstArgument(node.arguments[0], node, allowFunctionProps) && !isExemptedReducer(exemptedReducerCallees, node.parent)) {
context.report({
node,
messageId: 'unsafeMutatingFunction',
});
}
},
};
};
module.exports = {
create,
meta: {
type: 'problem',
schema: [{
type: 'object',
properties: {
functionProps: {
type: 'boolean',
},
ignoredMethods: {
type: 'array',
items: {
type: 'string',
},
},
useLodashFunctionImports: {
type: 'boolean',
},
reducers: {
type: 'array',
items: {type: 'string'},
default: ['reduce'],
},
},
}],
docs: {
description: 'disallow unsafe mutating functions such as Object.assign',
recommended: 'error',
},
messages: {
unsafeMutatingFunction: 'Unsafe mutating functions are disallowed',
},
},
};