import { AST_NODE_TYPES, ESLintUtils, TSESTree, } from '@typescript-eslint/utils'; import { isIdentifier } from '@typescript-eslint/utils/ast-utils'; // NOTE: The rule will be available in ESLint configs as "@nx/workspace-matching-state-variable" export const RULE_NAME = 'matching-state-variable'; export const rule = ESLintUtils.RuleCreator(() => __filename)({ name: RULE_NAME, meta: { type: 'problem', docs: { description: 'Ensure recoil value and setter are named after their atom name', recommended: 'recommended', }, fixable: 'code', schema: [], messages: { invalidVariableName: "Invalid usage of {{ hookName }}: the variable should be named '{{ expectedName }}' but found '{{ actualName }}'.", invalidSetterName: "Invalid usage of {{ hookName }}: Expected setter '{{ expectedName }}' but found '{{ actualName }}'.", }, }, defaultOptions: [], create: (context) => { return { VariableDeclarator: (node: TSESTree.VariableDeclarator) => { if ( node?.init?.type === AST_NODE_TYPES.CallExpression && isIdentifier(node.init.callee) && [ 'useRecoilState', 'useRecoilScopedState', 'useRecoilFamilyState', 'useRecoilScopedFamilyState', 'useRecoilValue', 'useRecoilScopedValue', ].includes(node.init.callee.name) ) { const stateNameBase = isIdentifier(node.init.arguments[0]) ? node.init.arguments[0].name : undefined; if (!stateNameBase) { return; } const expectedVariableNameBase = stateNameBase.replace( /(State|FamilyState|Selector|ScopedState|ScopedFamilyState|ScopedSelector)$/, '', ); if (isIdentifier(node.id)) { const actualVariableName = node.id.name; if (actualVariableName !== expectedVariableNameBase) { context.report({ node, messageId: 'invalidVariableName', data: { actualName: actualVariableName, expectedName: expectedVariableNameBase, hookName: stateNameBase, callee: node.init.callee.name, }, fix: (fixer) => { return fixer.replaceText(node.id, expectedVariableNameBase); }, }); } return; } if (node.id.type === AST_NODE_TYPES.ArrayPattern) { const actualVariableName = node.id.elements?.[0]?.type === AST_NODE_TYPES.Identifier ? node.id.elements[0].name : undefined; if ( actualVariableName && actualVariableName !== expectedVariableNameBase ) { context.report({ node, messageId: 'invalidVariableName', data: { actual: actualVariableName, expected: expectedVariableNameBase, callee: node.init.callee.name, }, fix: (fixer) => { if (node.id.type === AST_NODE_TYPES.ArrayPattern) { return fixer.replaceText( node.id.elements[0] as TSESTree.Node, expectedVariableNameBase, ); } return null; }, }); } if (isIdentifier(node.id.elements[1])) { const actualSetterName = node.id.elements[1].name; const expectedSetterName = `set${expectedVariableNameBase .charAt(0) .toUpperCase()}${expectedVariableNameBase.slice(1)}`; if (actualSetterName !== expectedSetterName) { context.report({ node, messageId: 'invalidSetterName', data: { hookName: stateNameBase, actualName: actualSetterName, expectedName: expectedSetterName, }, fix: (fixer) => { if (node.id.type === AST_NODE_TYPES.ArrayPattern) { return fixer.replaceText( node.id.elements[1]!, expectedSetterName, ); } return null; }, }); } } } } }, }; }, });