UNPKG

eslint-plugin-react-snob

Version:
189 lines (188 loc) 8.69 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.propsInterfaceNaming = void 0; const utils_1 = require("@typescript-eslint/utils"); const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/yourusername/eslint-plugin-react-snob/blob/main/docs/rules/${name}.md`); function isComponentFunction(node) { if (node.type === 'FunctionDeclaration' && node.id) { return /^[A-Z]/.test(node.id.name); } if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier' && /^[A-Z]/.test(node.id.name)) { return true; } return false; } function extractComponentName(node) { if (node.type === 'FunctionDeclaration' && node.id) { return node.id.name; } if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier') { return node.id.name; } return null; } function findPropsInterface(node, sourceCode) { let propsInterface = null; if (node.type === 'FunctionDeclaration') { const firstParam = node.params[0]; if (firstParam && firstParam.type === 'ObjectPattern' && firstParam.typeAnnotation) { const typeAnnotation = firstParam.typeAnnotation.typeAnnotation; if (typeAnnotation.type === 'TSTypeReference' && typeAnnotation.typeName.type === 'Identifier') { propsInterface = { name: typeAnnotation.typeName.name, node: typeAnnotation }; } } } else if (node.type === 'VariableDeclarator' && node.init) { if (node.init.type === 'ArrowFunctionExpression') { const firstParam = node.init.params[0]; if (firstParam && firstParam.type === 'ObjectPattern' && firstParam.typeAnnotation) { const typeAnnotation = firstParam.typeAnnotation.typeAnnotation; if (typeAnnotation.type === 'TSTypeReference' && typeAnnotation.typeName.type === 'Identifier') { propsInterface = { name: typeAnnotation.typeName.name, node: typeAnnotation }; } } } else if (node.init.type === 'CallExpression') { // Handle forwardRef - check if it's a direct call to forwardRef if (node.init.callee.type === 'Identifier' && node.init.callee.name === 'forwardRef') { // Check type parameters first (forwardRef<RefType, PropsType>) if (node.init.typeArguments && node.init.typeArguments.params.length >= 2) { const propsTypeParam = node.init.typeArguments.params[1]; if (propsTypeParam.type === 'TSTypeReference' && propsTypeParam.typeName.type === 'Identifier') { propsInterface = { name: propsTypeParam.typeName.name, node: propsTypeParam }; } } else { // Fallback to checking function parameter annotations const forwardRefCallback = node.init.arguments[0]; if (forwardRefCallback && forwardRefCallback.type === 'ArrowFunctionExpression') { const firstParam = forwardRefCallback.params[0]; if (firstParam && firstParam.type === 'ObjectPattern' && firstParam.typeAnnotation) { const typeAnnotation = firstParam.typeAnnotation.typeAnnotation; if (typeAnnotation.type === 'TSTypeReference' && typeAnnotation.typeName.type === 'Identifier') { propsInterface = { name: typeAnnotation.typeName.name, node: typeAnnotation }; } } } } } // Handle React.forwardRef else if (node.init.callee.type === 'MemberExpression' && node.init.callee.object.type === 'Identifier' && node.init.callee.object.name === 'React' && node.init.callee.property.type === 'Identifier' && node.init.callee.property.name === 'forwardRef') { // Check type parameters first (React.forwardRef<RefType, PropsType>) if (node.init.typeArguments && node.init.typeArguments.params.length >= 2) { const propsTypeParam = node.init.typeArguments.params[1]; if (propsTypeParam.type === 'TSTypeReference' && propsTypeParam.typeName.type === 'Identifier') { propsInterface = { name: propsTypeParam.typeName.name, node: propsTypeParam }; } } else { // Fallback to checking function parameter annotations const forwardRefCallback = node.init.arguments[0]; if (forwardRefCallback && forwardRefCallback.type === 'ArrowFunctionExpression') { const firstParam = forwardRefCallback.params[0]; if (firstParam && firstParam.type === 'ObjectPattern' && firstParam.typeAnnotation) { const typeAnnotation = firstParam.typeAnnotation.typeAnnotation; if (typeAnnotation.type === 'TSTypeReference' && typeAnnotation.typeName.type === 'Identifier') { propsInterface = { name: typeAnnotation.typeName.name, node: typeAnnotation }; } } } } } } } return propsInterface; } exports.propsInterfaceNaming = createRule({ create(context) { return { FunctionDeclaration(node) { if (!isComponentFunction(node)) return; const componentName = extractComponentName(node); if (!componentName) return; const propsInterface = findPropsInterface(node, context.getSourceCode()); if (!propsInterface) return; const expectedName = `${componentName}Props`; if (propsInterface.name !== expectedName) { context.report({ messageId: 'incorrectPropsInterfaceName', node: propsInterface.node, data: { actual: propsInterface.name, expected: expectedName, component: componentName } }); } }, VariableDeclarator(node) { if (!isComponentFunction(node)) return; const componentName = extractComponentName(node); if (!componentName) return; const propsInterface = findPropsInterface(node, context.getSourceCode()); if (!propsInterface) return; const expectedName = `${componentName}Props`; if (propsInterface.name !== expectedName) { context.report({ messageId: 'incorrectPropsInterfaceName', node: propsInterface.node, data: { actual: propsInterface.name, expected: expectedName, component: componentName } }); } } }; }, defaultOptions: [], meta: { docs: { description: 'Enforce that React component props interfaces follow the naming convention ComponentNameProps', }, fixable: undefined, messages: { incorrectPropsInterfaceName: 'Props interface for component "{{component}}" should be named "{{expected}}", but found "{{actual}}".', }, schema: [], type: 'suggestion', }, name: 'props-interface-naming', });