Skip to content

Commit 805218f

Browse files
committed
Integrate eslint react-fc-component-definition
1 parent 32dc428 commit 805218f

File tree

3 files changed

+114
-0
lines changed

3 files changed

+114
-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: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
return false
48+
}
49+
50+
function getTypeName(typeName) {
51+
if (typeName.type === 'Identifier') {
52+
return typeName.name
53+
}
54+
// Handle qualified names like `React.ReactElement` or `React.JSX.Element`
55+
// We only need the rightmost name since we use `.endsWith()` checks
56+
if (typeName.type === 'TSQualifiedName') {
57+
return typeName.right.name
58+
}
59+
return ''
60+
}
61+
62+
export default {
63+
meta: {
64+
type: 'suggestion',
65+
docs: {
66+
description:
67+
'Enforce React.FC<Props> for component definitions instead of explicit return types',
68+
category: 'Stylistic Issues',
69+
recommended: false
70+
},
71+
schema: []
72+
},
73+
create(context) {
74+
return {
75+
VariableDeclarator(node) {
76+
// Check if this is an arrow function
77+
if (node.init?.type !== 'ArrowFunctionExpression') return
78+
79+
// Check if the variable name is PascalCase (component convention)
80+
const variableName = node.id?.name
81+
if (!variableName || !isPascalCase(variableName)) return
82+
83+
// Check if the arrow function has an explicit return type
84+
const arrowFunction = node.init
85+
if (!arrowFunction.returnType) return
86+
87+
// Check if the return type is a JSX-related type
88+
if (!isJsxReturnType(arrowFunction.returnType)) return
89+
90+
// Check if the variable already has a React.FC type annotation
91+
const variableType = node.id.typeAnnotation
92+
if (variableType) {
93+
const typeName = getTypeName(
94+
variableType.typeAnnotation?.typeName || {}
95+
)
96+
if (
97+
typeName.endsWith('FC') ||
98+
typeName.endsWith('FunctionComponent')
99+
) {
100+
return // Already using React.FC
101+
}
102+
}
103+
104+
context.report({
105+
node: arrowFunction.returnType,
106+
message: `Component '${variableName}' should use React.FC<Props> instead of an explicit return type. Use: const ${variableName}: React.FC<Props> = props => { ... }`
107+
})
108+
}
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)