Skip to content

Commit 3005665

Browse files
author
Jon Tzeng
committed
Integrate eslint react-render-function-definition
Enforces that render helper functions (camelCase functions starting with "render") use React.ReactElement return type instead of ReactNode or JSX.Element. - Flags ReactNode (too broad - use ReactElement or explicit union) - Flags JSX.Element (use ReactElement for consistency) - Allows ReactElement, ReactElement | null, and other explicit unions
1 parent 14495b5 commit 3005665

File tree

4 files changed

+140
-3
lines changed

4 files changed

+140
-3
lines changed

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default [
4141

4242
// Add our own rules:
4343
'edge/react-fc-component-definition': 'warn',
44+
'edge/react-render-function-definition': 'warn',
4445
'edge/useAbortable-abort-check-param': 'error',
4546
'edge/useAbortable-abort-check-usage': 'error',
4647

scripts/eslint-plugin-edge/index.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import reactFcComponentDefinition from './react-fc-component-definition.mjs'
2+
import reactRenderFunctionDefinition from './react-render-function-definition.mjs'
23
import abortCheckParam from './useAbortable-abort-check-param.mjs'
34
import abortCheckUsage from './useAbortable-abort-check-usage.mjs'
45

56
export default {
67
meta: {
78
name: 'eslint-plugin-edge',
8-
version: '0.1.2',
9+
version: '0.1.3',
910
namespace: 'edge'
1011
},
1112
rules: {
1213
'react-fc-component-definition': reactFcComponentDefinition,
14+
'react-render-function-definition': reactRenderFunctionDefinition,
1315
'useAbortable-abort-check-param': abortCheckParam,
1416
'useAbortable-abort-check-usage': abortCheckUsage
1517
}

scripts/eslint-plugin-edge/react-fc-component-definition.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function isJsxType(node) {
4343
// Handle `React.ReactElement`, `React.JSX.Element`, etc.
4444
if (node.type === 'TSTypeReference') {
4545
const typeName = getTypeName(node.typeName)
46-
return JSX_RETURN_TYPES.some(t => typeName.endsWith(t))
46+
return JSX_RETURN_TYPES.includes(typeName)
4747
}
4848

4949
return false
@@ -54,7 +54,7 @@ function getTypeName(typeName) {
5454
return typeName.name
5555
}
5656
// Handle qualified names like `React.ReactElement` or `React.JSX.Element`
57-
// We only need the rightmost name since we use `.endsWith()` checks
57+
// We only need the rightmost name since we use exact matches
5858
if (typeName.type === 'TSQualifiedName') {
5959
return typeName.right.name
6060
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* ESLint rule to enforce React.ReactElement for render helper functions.
3+
*
4+
* Wrong:
5+
* const renderItem = (item: Item): React.ReactNode => { ... }
6+
* const renderHeader = (): JSX.Element => { ... }
7+
*
8+
* Correct:
9+
* const renderItem = (item: Item): React.ReactElement => { ... }
10+
* const renderBadge = (): React.ReactElement | null => { ... }
11+
*
12+
* This rule targets camelCase functions starting with "render" to distinguish
13+
* render helpers from components (which should use React.FC<Props>).
14+
*/
15+
16+
// Types that should be flagged and replaced with ReactElement
17+
const DISALLOWED_TYPES = [
18+
'ReactNode', // Too broad - use ReactElement or explicit union
19+
'Element' // JSX.Element - use ReactElement for consistency
20+
]
21+
22+
function isRenderFunction(name) {
23+
return /^render[A-Z]/.test(name)
24+
}
25+
26+
function getTypeName(typeName) {
27+
if (typeName.type === 'Identifier') {
28+
return typeName.name
29+
}
30+
// Handle qualified names like `React.ReactNode` or `JSX.Element`
31+
// We only need the rightmost name since we use exact matches
32+
if (typeName.type === 'TSQualifiedName') {
33+
return typeName.right.name
34+
}
35+
return ''
36+
}
37+
38+
function isDisallowedType(node) {
39+
if (!node) return null
40+
41+
// Handle `React.ReactNode`, `JSX.Element`, etc.
42+
if (node.type === 'TSTypeReference') {
43+
const typeName = getTypeName(node.typeName)
44+
if (DISALLOWED_TYPES.includes(typeName)) {
45+
return typeName
46+
}
47+
}
48+
49+
return null
50+
}
51+
52+
function checkReturnType(typeAnnotation) {
53+
if (!typeAnnotation) return null
54+
55+
const { typeAnnotation: innerType } = typeAnnotation
56+
57+
// Handle union types like `React.ReactElement | null`
58+
// Only flag if the union contains a disallowed type
59+
if (innerType.type === 'TSUnionType') {
60+
for (const t of innerType.types) {
61+
const disallowed = isDisallowedType(t)
62+
if (disallowed != null) {
63+
return disallowed
64+
}
65+
}
66+
return null
67+
}
68+
69+
return isDisallowedType(innerType)
70+
}
71+
72+
export default {
73+
meta: {
74+
type: 'suggestion',
75+
docs: {
76+
description:
77+
'Enforce React.ReactElement for render helper functions instead of ReactNode or JSX.Element',
78+
category: 'Stylistic Issues',
79+
recommended: false
80+
},
81+
schema: []
82+
},
83+
create(context) {
84+
return {
85+
VariableDeclarator(node) {
86+
// Check if this is an arrow function
87+
if (node.init?.type !== 'ArrowFunctionExpression') return
88+
89+
// Check if the variable name starts with "render" (camelCase render helper)
90+
const variableName = node.id?.name
91+
if (!variableName || !isRenderFunction(variableName)) return
92+
93+
// Check if the arrow function has an explicit return type
94+
const arrowFunction = node.init
95+
if (!arrowFunction.returnType) return
96+
97+
// Check if the return type is a disallowed type
98+
const disallowedType = checkReturnType(arrowFunction.returnType)
99+
if (disallowedType == null) return
100+
101+
const suggestion =
102+
disallowedType === 'ReactNode'
103+
? 'React.ReactElement (or React.ReactElement | null for nullable returns)'
104+
: 'React.ReactElement'
105+
106+
context.report({
107+
node: arrowFunction.returnType,
108+
message: `Render function '${variableName}' should return ${suggestion} instead of ${disallowedType}. Use: const ${variableName} = (...): React.ReactElement => { ... }`
109+
})
110+
},
111+
112+
// Also check regular function declarations
113+
FunctionDeclaration(node) {
114+
const functionName = node.id?.name
115+
if (!functionName || !isRenderFunction(functionName)) return
116+
117+
if (!node.returnType) return
118+
119+
const disallowedType = checkReturnType(node.returnType)
120+
if (disallowedType == null) return
121+
122+
const suggestion =
123+
disallowedType === 'ReactNode'
124+
? 'React.ReactElement (or React.ReactElement | null for nullable returns)'
125+
: 'React.ReactElement'
126+
127+
context.report({
128+
node: node.returnType,
129+
message: `Render function '${functionName}' should return ${suggestion} instead of ${disallowedType}. Use: function ${functionName}(...): React.ReactElement { ... }`
130+
})
131+
}
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)