123 lines
3.4 KiB
JavaScript
123 lines
3.4 KiB
JavaScript
|
/**
|
||
|
* @fileoverview Prevent usage of setState in lifecycle methods
|
||
|
* @author Yannick Croissant
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
const findLast = require('array.prototype.findlast');
|
||
|
|
||
|
const docsUrl = require('./docsUrl');
|
||
|
const report = require('./report');
|
||
|
const testReactVersion = require('./version').testReactVersion;
|
||
|
|
||
|
// ------------------------------------------------------------------------------
|
||
|
// Rule Definition
|
||
|
// ------------------------------------------------------------------------------
|
||
|
|
||
|
function mapTitle(methodName) {
|
||
|
const map = {
|
||
|
componentDidMount: 'did-mount',
|
||
|
componentDidUpdate: 'did-update',
|
||
|
componentWillUpdate: 'will-update',
|
||
|
};
|
||
|
const title = map[methodName];
|
||
|
if (!title) {
|
||
|
throw Error(`No docsUrl for '${methodName}'`);
|
||
|
}
|
||
|
return `no-${title}-set-state`;
|
||
|
}
|
||
|
|
||
|
const messages = {
|
||
|
noSetState: 'Do not use setState in {{name}}',
|
||
|
};
|
||
|
|
||
|
const methodNoopsAsOf = {
|
||
|
componentDidMount: '>= 16.3.0',
|
||
|
componentDidUpdate: '>= 16.3.0',
|
||
|
};
|
||
|
|
||
|
function shouldBeNoop(context, methodName) {
|
||
|
return methodName in methodNoopsAsOf
|
||
|
&& testReactVersion(context, methodNoopsAsOf[methodName])
|
||
|
&& !testReactVersion(context, '999.999.999'); // for when the version is not specified
|
||
|
}
|
||
|
|
||
|
module.exports = function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) {
|
||
|
return {
|
||
|
meta: {
|
||
|
docs: {
|
||
|
description: `Disallow usage of setState in ${methodName}`,
|
||
|
category: 'Best Practices',
|
||
|
recommended: false,
|
||
|
url: docsUrl(mapTitle(methodName)),
|
||
|
},
|
||
|
|
||
|
messages,
|
||
|
|
||
|
schema: [{
|
||
|
enum: ['disallow-in-func'],
|
||
|
}],
|
||
|
},
|
||
|
|
||
|
create(context) {
|
||
|
const mode = context.options[0] || 'allow-in-func';
|
||
|
|
||
|
function nameMatches(name) {
|
||
|
if (name === methodName) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (typeof shouldCheckUnsafeCb === 'function' && shouldCheckUnsafeCb(context)) {
|
||
|
return name === `UNSAFE_${methodName}`;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (shouldBeNoop(context, methodName)) {
|
||
|
return {};
|
||
|
}
|
||
|
|
||
|
// --------------------------------------------------------------------------
|
||
|
// Public
|
||
|
// --------------------------------------------------------------------------
|
||
|
|
||
|
return {
|
||
|
CallExpression(node) {
|
||
|
const callee = node.callee;
|
||
|
if (
|
||
|
callee.type !== 'MemberExpression'
|
||
|
|| callee.object.type !== 'ThisExpression'
|
||
|
|| callee.property.name !== 'setState'
|
||
|
) {
|
||
|
return;
|
||
|
}
|
||
|
const ancestors = context.getAncestors(callee);
|
||
|
let depth = 0;
|
||
|
findLast(ancestors, (ancestor) => {
|
||
|
// ancestors.some((ancestor) => {
|
||
|
if (/Function(Expression|Declaration)$/.test(ancestor.type)) {
|
||
|
depth += 1;
|
||
|
}
|
||
|
if (
|
||
|
(ancestor.type !== 'Property' && ancestor.type !== 'MethodDefinition' && ancestor.type !== 'ClassProperty' && ancestor.type !== 'PropertyDefinition')
|
||
|
|| !nameMatches(ancestor.key.name)
|
||
|
|| (mode !== 'disallow-in-func' && depth > 1)
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
report(context, messages.noSetState, 'noSetState', {
|
||
|
node: callee,
|
||
|
data: {
|
||
|
name: ancestor.key.name,
|
||
|
},
|
||
|
});
|
||
|
return true;
|
||
|
});
|
||
|
},
|
||
|
};
|
||
|
},
|
||
|
};
|
||
|
};
|