eslint-plugin-react-snob
Version:
An ESLint plugin for React best practices
189 lines (188 loc) • 8.69 kB
JavaScript
"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',
});