Skip to content

Commit beae433

Browse files
committed
Integrate eslint react-fc-component-definition
1 parent 46f8f21 commit beae433

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default [
4040
'react-native/sort-styles': 'off',
4141

4242
// Add our own rules:
43+
'edge/react-fc-component-definition': 'warn',
4344
'edge/useAbortable-abort-check-param': 'error',
4445
'edge/useAbortable-abort-check-usage': 'error',
4546

scripts/eslint-plugin-edge/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import reactFcComponentDefinition from './react-fc-component-definition.mjs'
12
import abortCheckParam from './useAbortable-abort-check-param.mjs'
23
import abortCheckUsage from './useAbortable-abort-check-usage.mjs'
34

@@ -8,6 +9,7 @@ export default {
89
namespace: 'edge'
910
},
1011
rules: {
12+
'react-fc-component-definition': reactFcComponentDefinition,
1113
'useAbortable-abort-check-param': abortCheckParam,
1214
'useAbortable-abort-check-usage': abortCheckUsage
1315
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* ESLint rule to enforce React.FC<Props> for component definitions.
3+
*
4+
* Wrong:
5+
* export const Component = (props: Props): React.ReactElement | null => { ... }
6+
*
7+
* Correct:
8+
* export const Component: React.FC<Props> = props => { ... }
9+
*
10+
* This rule only targets PascalCase-named arrow functions to distinguish
11+
* components from render helper functions (which should use explicit return types).
12+
*/
13+
14+
const JSX_RETURN_TYPES = [
15+
'ReactElement',
16+
'ReactNode',
17+
'Element', // JSX.Element
18+
'JSXElement'
19+
]
20+
21+
function isPascalCase(name) {
22+
return /^[A-Z][a-zA-Z0-9]*$/.test(name)
23+
}
24+
25+
function isJsxReturnType(typeAnnotation) {
26+
if (!typeAnnotation) return false
27+
28+
const { typeAnnotation: innerType } = typeAnnotation
29+
30+
// Handle union types like `React.ReactElement | null`
31+
if (innerType.type === 'TSUnionType') {
32+
return innerType.types.some(t => isJsxType(t))
33+
}
34+
35+
return isJsxType(innerType)
36+
}
37+
38+
function isJsxType(node) {
39+
if (!node) return false
40+
41+
// Handle `React.ReactElement`, `React.JSX.Element`, etc.
42+
if (node.type === 'TSTypeReference') {
43+
const typeName = getTypeName(node.typeName)
44+
return JSX_RETURN_TYPES.some(t => typeName.endsWith(t))
45+
}
46+
47+
// Handle `null` in union types - not a JSX type itself
48+
if (node.type === 'TSNullKeyword') {
49+
return false
50+
}
51+
52+
return false
53+
}
54+
55+
function getTypeName(typeName) {
56+
if (typeName.type === 'Identifier') {
57+
return typeName.name
58+
}
59+
// Handle qualified names like `React.ReactElement` or `React.JSX.Element`
60+
if (typeName.type === 'TSQualifiedName') {
61+
return getTypeName(typeName.left) + '.' + typeName.right.name
62+
}
63+
return ''
64+
}
65+
66+
export default {
67+
meta: {
68+
type: 'suggestion',
69+
docs: {
70+
description:
71+
'Enforce React.FC<Props> for component definitions instead of explicit return types',
72+
category: 'Stylistic Issues',
73+
recommended: false
74+
},
75+
schema: []
76+
},
77+
create(context) {
78+
return {
79+
VariableDeclarator(node) {
80+
// Check if this is an arrow function
81+
if (node.init?.type !== 'ArrowFunctionExpression') return
82+
83+
// Check if the variable name is PascalCase (component convention)
84+
const variableName = node.id?.name
85+
if (!variableName || !isPascalCase(variableName)) return
86+
87+
// Check if the arrow function has an explicit return type
88+
const arrowFunction = node.init
89+
if (!arrowFunction.returnType) return
90+
91+
// Check if the return type is a JSX-related type
92+
if (!isJsxReturnType(arrowFunction.returnType)) return
93+
94+
// Check if the variable already has a React.FC type annotation
95+
const variableType = node.id.typeAnnotation
96+
if (variableType) {
97+
const typeName = getTypeName(
98+
variableType.typeAnnotation?.typeName || {}
99+
)
100+
if (
101+
typeName.endsWith('FC') ||
102+
typeName.endsWith('FunctionComponent')
103+
) {
104+
return // Already using React.FC
105+
}
106+
}
107+
108+
context.report({
109+
node: arrowFunction.returnType,
110+
message: `Component '${variableName}' should use React.FC<Props> instead of an explicit return type. Use: const ${variableName}: React.FC<Props> = props => { ... }`
111+
})
112+
}
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)