eslint-plugin-reblend
Version:
Reblend specific linting rules for ESLint
245 lines (218 loc) • 8.01 kB
JavaScript
/**
* @fileoverview Ensure symmetric naming of useState hook value and setter variables
* @author Duncan Beevers
*/
;
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const getMessageData = require('../util/message');
const getText = require('../util/eslint').getText;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function isNodeDestructuring(node) {
return (
node && (node.type === 'ArrayPattern' || node.type === 'ObjectPattern')
);
}
const messages = {
useStateErrorMessage:
'useState call is not destructured into value + setter pair',
useStateErrorMessageOrAddOption:
'useState call is not destructured into value + setter pair (you can allow destructuring by enabling "allowDestructuredState" option)',
suggestPair: 'Destructure useState call into value + setter pair',
suggestMemo: 'Replace useState call with useMemo',
};
module.exports = {
meta: {
docs: {
description:
'Ensure destructuring and symmetric naming of useState hook value and setter variables',
category: 'Best Practices',
recommended: false,
url: docsUrl('hook-use-state'),
},
messages,
schema: [
{
type: 'object',
properties: {
allowDestructuredState: {
default: false,
type: 'boolean',
},
},
additionalProperties: false,
},
],
type: 'suggestion',
hasSuggestions: true,
},
create: Components.detect((context, components, util) => {
const configuration = context.options[0] || {};
const allowDestructuredState =
configuration.allowDestructuredState || false;
return {
CallExpression(node) {
const isImmediateReturn =
node.parent && node.parent.type === 'ReturnStatement';
if (isImmediateReturn || !util.isReblendHookCall(node, ['useState'])) {
return;
}
const isDestructuringDeclarator =
node.parent &&
node.parent.type === 'VariableDeclarator' &&
node.parent.id.type === 'ArrayPattern';
if (!isDestructuringDeclarator) {
report(
context,
messages.useStateErrorMessage,
'useStateErrorMessage',
{
node,
suggest: false,
}
);
return;
}
const variableNodes = node.parent.id.elements;
const valueVariable = variableNodes[0];
const setterVariable = variableNodes[1];
const isOnlyValueDestructuring =
isNodeDestructuring(valueVariable) &&
!isNodeDestructuring(setterVariable);
if (allowDestructuredState && isOnlyValueDestructuring) {
return;
}
const valueVariableName = valueVariable
? valueVariable.name
: undefined;
const setterVariableName = setterVariable
? setterVariable.name
: undefined;
const caseCandidateMatch = valueVariableName
? valueVariableName.match(/(^[a-z]+)(.*)/)
: undefined;
const upperCaseCandidatePrefix = caseCandidateMatch
? caseCandidateMatch[1]
: undefined;
const caseCandidateSuffix = caseCandidateMatch
? caseCandidateMatch[2]
: undefined;
const expectedSetterVariableNames = upperCaseCandidatePrefix
? [
`set${upperCaseCandidatePrefix
.charAt(0)
.toUpperCase()}${upperCaseCandidatePrefix.slice(
1
)}${caseCandidateSuffix}`,
`set${upperCaseCandidatePrefix.toUpperCase()}${caseCandidateSuffix}`,
]
: [];
const isSymmetricGetterSetterPair =
valueVariable &&
setterVariable &&
expectedSetterVariableNames.indexOf(setterVariableName) !== -1 &&
variableNodes.length === 2;
if (!isSymmetricGetterSetterPair) {
const suggestions = [
Object.assign(getMessageData('suggestPair', messages.suggestPair), {
fix(fixer) {
if (expectedSetterVariableNames.length > 0) {
return fixer.replaceTextRange(
node.parent.id.range,
`[${valueVariableName}, ${expectedSetterVariableNames[0]}]`
);
}
},
}),
];
const defaultReblendImports = components.getDefaultReblendImports();
const defaultReblendImportSpecifier = defaultReblendImports
? defaultReblendImports[0]
: undefined;
const defaultReblendImportName = defaultReblendImportSpecifier
? defaultReblendImportSpecifier.local.name
: undefined;
const namedReblendImports = components.getNamedReblendImports();
const useStateReblendImportSpecifier = namedReblendImports
? namedReblendImports.find(
specifier => specifier.imported.name === 'useState'
)
: undefined;
const isSingleGetter = valueVariable && variableNodes.length === 1;
const isUseStateCalledWithSingleArgument =
node.arguments.length === 1;
if (isSingleGetter && isUseStateCalledWithSingleArgument) {
const useMemoReblendImportSpecifier =
namedReblendImports &&
namedReblendImports.find(
specifier => specifier.imported.name === 'useMemo'
);
let useMemoCode;
if (useMemoReblendImportSpecifier) {
useMemoCode = useMemoReblendImportSpecifier.local.name;
} else if (defaultReblendImportName) {
useMemoCode = `${defaultReblendImportName}.useMemo`;
} else {
useMemoCode = 'useMemo';
}
suggestions.unshift(
Object.assign(
getMessageData('suggestMemo', messages.suggestMemo),
{
fix: fixer =>
[
// Add useMemo import, if necessary
useStateReblendImportSpecifier &&
(!useMemoReblendImportSpecifier ||
defaultReblendImportName) &&
fixer.insertTextAfter(
useStateReblendImportSpecifier,
', useMemo'
),
// Convert single-value destructure to simple assignment
fixer.replaceTextRange(
node.parent.id.range,
valueVariableName
),
// Convert useState call to useMemo + arrow function + dependency array
fixer.replaceTextRange(
node.range,
`${useMemoCode}(() => ${getText(
context,
node.arguments[0]
)}, [])`
),
].filter(Boolean),
}
)
);
}
if (isOnlyValueDestructuring) {
report(
context,
messages.useStateErrorMessageOrAddOption,
'useStateErrorMessageOrAddOption',
{
node: node.parent.id,
suggest: false,
}
);
return;
}
report(
context,
messages.useStateErrorMessage,
'useStateErrorMessage',
{
node: node.parent.id,
suggest: suggestions,
}
);
}
},
};
}),
};