Skip to content

Commit 9f36ade

Browse files
authored
Add support for components wrapped with forwardRef (#5)
* add test cases for forwardRef * add support for components wrapped in forwardRef
1 parent 0607850 commit 9f36ade

File tree

3 files changed

+195
-3
lines changed

3 files changed

+195
-3
lines changed

.vscode/launch.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Debug Jest Tests",
6+
"type": "node",
7+
"request": "launch",
8+
"runtimeArgs": [
9+
"--inspect-brk",
10+
"${workspaceRoot}/node_modules/.bin/jest",
11+
"--runInBand"
12+
],
13+
"console": "integratedTerminal",
14+
"internalConsoleOptions": "neverOpen"
15+
}
16+
]
17+
}

index.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,29 @@ function getReturnStatement(node) {
4848
);
4949
}
5050

51+
if (node.type === 'ArrowFunctionExpression') return node.body;
5152
return node.type === 'VariableDeclaration'
5253
? node.declarations?.[0]?.init?.body?.body?.find(
5354
(statement) => statement.type === 'ReturnStatement',
54-
) ?? node.declarations?.[0]?.init?.body
55+
) ??
56+
node.declarations?.[0]?.init?.arguments?.[0]?.body ??
57+
node.declarations?.[0]?.init?.body
5558
: node.body?.body?.find(
5659
(statement) => statement.type === 'ReturnStatement',
5760
);
5861
}
5962

63+
function isForwardRef(node) {
64+
if (!Boolean(node)) {
65+
return;
66+
}
67+
68+
return node.type === 'VariableDeclaration'
69+
? node.declarations?.[0]?.init?.callee?.property?.name === 'forwardRef'
70+
: node.callee?.name === 'forwardRef' ||
71+
node.callee?.property?.name === 'forwardRef';
72+
}
73+
6074
function isTreeDone(node, excludeComponentNames) {
6175
return (
6276
node.type === 'JSXElement' &&
@@ -107,12 +121,21 @@ const rules = {
107121
return {
108122
Program(node) {
109123
const componentNodes = node.body
110-
.map((child) => child?.declaration ?? child)
124+
.map((child) => {
125+
const declaration = child?.declaration ?? child;
126+
if (isForwardRef(declaration)) {
127+
// do something
128+
return declaration?.arguments?.[0] ?? declaration;
129+
}
130+
return declaration;
131+
})
111132
.filter(
112133
(child) =>
113134
child.type === 'VariableDeclaration' ||
114135
child.type === 'FunctionDeclaration' ||
115-
child.type === 'ClassDeclaration',
136+
child.type === 'ClassDeclaration' ||
137+
child.type === 'FunctionExpression' ||
138+
child.type === 'ArrowFunctionExpression',
116139
)
117140
.filter((child) => {
118141
let flag = false;

test.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,118 @@ const classComponentNested = `class Car extends React.Component {
129129
}
130130
}`;
131131

132+
const reactForwardRef = /* tsx */ `
133+
export const InternalLink = React.forwardRef<HTMLAnchorElement, InternalLinkProps>(
134+
({ variant, ...props }, ref) => (
135+
<Link data-component="InternalLink"
136+
ref={ref}
137+
className={classNames(css.link, { [css.inverse]: variant === 'inverse' })}
138+
{...props}
139+
>
140+
{props.children}
141+
</Link>
142+
),
143+
);`;
144+
145+
const reactForwardRefError = /* tsx */ `
146+
export const InternalLink = React.forwardRef<HTMLAnchorElement, InternalLinkProps>(
147+
({ variant, ...props }, ref) => (
148+
<Link
149+
ref={ref}
150+
className={classNames(css.link, { [css.inverse]: variant === 'inverse' })}
151+
{...props}
152+
>
153+
{props.children}
154+
</Link>
155+
),
156+
);`;
157+
158+
const forwardRef = /* tsx */ `
159+
export const InternalLink = forwardRef<HTMLAnchorElement, InternalLinkProps>(
160+
({ variant, ...props }, ref) => (
161+
<Link data-component="InternalLink"
162+
ref={ref}
163+
className={classNames(css.link, { [css.inverse]: variant === 'inverse' })}
164+
{...props}
165+
>
166+
{props.children}
167+
</Link>
168+
),
169+
);`;
170+
171+
const forwardRefError = /* tsx */ `
172+
export const InternalLink = forwardRef<HTMLAnchorElement, InternalLinkProps>(
173+
({ variant, ...props }, ref) => (
174+
<Link
175+
ref={ref}
176+
className={classNames(css.link, { [css.inverse]: variant === 'inverse' })}
177+
{...props}
178+
>
179+
{props.children}
180+
</Link>
181+
),
182+
);`;
183+
184+
const defaultReactForwardRef = /* tsx */ `
185+
export default React.forwardRef<HTMLAnchorElement, InternalLinkProps>(
186+
function InternalLink({ variant, ...props }, ref) {
187+
return (
188+
<Link data-component="InternalLink"
189+
ref={ref}
190+
className={classNames(css.link, { [css.inverse]: variant === 'inverse' })}
191+
{...props}
192+
>
193+
{props.children}
194+
</Link>
195+
);
196+
}
197+
);`;
198+
199+
const defaultReactForwardRefError = /* tsx */ `
200+
export default React.forwardRef<HTMLAnchorElement, InternalLinkProps>(
201+
function InternalLink({ variant, ...props }, ref) {
202+
return (
203+
<Link
204+
ref={ref}
205+
className={classNames(css.link, { [css.inverse]: variant === 'inverse' })}
206+
{...props}
207+
>
208+
{props.children}
209+
</Link>
210+
);
211+
}
212+
);`;
213+
214+
const defaultForwardRef = /* tsx */ `
215+
export default forwardRef<HTMLAnchorElement, InternalLinkProps>(
216+
function InternalLink({ variant, ...props }, ref) {
217+
return (
218+
<Link data-component="InternalLink"
219+
ref={ref}
220+
className={classNames(css.link, { [css.inverse]: variant === 'inverse' })}
221+
{...props}
222+
>
223+
{props.children}
224+
</Link>
225+
);
226+
}
227+
);`;
228+
229+
const defaultForwardRefError = /* tsx */ `
230+
export default forwardRef<HTMLAnchorElement, InternalLinkProps>(
231+
function InternalLink({ variant, ...props }, ref) {
232+
return (
233+
<Link
234+
ref={ref}
235+
className={classNames(css.link, { [css.inverse]: variant === 'inverse' })}
236+
{...props}
237+
>
238+
{props.children}
239+
</Link>
240+
);
241+
}
242+
);`;
243+
132244
const tests = {
133245
'data-component': {
134246
// Require the actual rule definition
@@ -174,6 +286,18 @@ const tests = {
174286
// Multiple return paths should not trigger the eslint warning
175287
code: fragmentsWontUpdate,
176288
},
289+
{
290+
code: reactForwardRef,
291+
},
292+
{
293+
code: forwardRef,
294+
},
295+
{
296+
code: defaultReactForwardRef,
297+
},
298+
{
299+
code: defaultForwardRef,
300+
},
177301
],
178302
invalid: [
179303
{
@@ -220,6 +344,34 @@ const tests = {
220344
'Component2 is missing the data-component attribute for the top-level element.',
221345
],
222346
},
347+
{
348+
code: reactForwardRefError,
349+
output: reactForwardRef,
350+
errors: [
351+
'InternalLink is missing the data-component attribute for the top-level element.',
352+
],
353+
},
354+
{
355+
code: forwardRefError,
356+
output: forwardRef,
357+
errors: [
358+
'InternalLink is missing the data-component attribute for the top-level element.',
359+
],
360+
},
361+
{
362+
code: defaultReactForwardRefError,
363+
output: defaultReactForwardRef,
364+
errors: [
365+
'InternalLink is missing the data-component attribute for the top-level element.',
366+
],
367+
},
368+
{
369+
code: defaultForwardRefError,
370+
output: defaultForwardRef,
371+
errors: [
372+
'InternalLink is missing the data-component attribute for the top-level element.',
373+
],
374+
},
223375
],
224376
},
225377
},

0 commit comments

Comments
 (0)