Skip to content

Commit f703bb2

Browse files
authored
feat(plugins/x): add autofix to 'no-forward-ref', closes #871 (#874)
1 parent 02947db commit f703bb2

File tree

3 files changed

+201
-8
lines changed

3 files changed

+201
-8
lines changed

packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.spec.ts

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ ruleTester.run(RULE_NAME, rule, {
66
{
77
code: /* tsx */ `
88
import { forwardRef } from 'react'
9-
forwardRef((props) => {
9+
const Component = forwardRef((props) => {
1010
return null;
1111
});
1212
`,
1313
errors: [{ messageId: "noForwardRef" }],
14+
output: /* tsx */ `
15+
import { forwardRef } from 'react'
16+
const Component = ({ ref, ...props }) => {
17+
return null;
18+
};
19+
`,
1420
settings: {
1521
"react-x": {
1622
version: "19.0.0",
@@ -20,9 +26,13 @@ ruleTester.run(RULE_NAME, rule, {
2026
{
2127
code: /* tsx */ `
2228
import { forwardRef } from 'react'
23-
forwardRef((props) => null);
29+
const Component = forwardRef((props) => null);
2430
`,
2531
errors: [{ messageId: "noForwardRef" }],
32+
output: /* tsx */ `
33+
import { forwardRef } from 'react'
34+
const Component = ({ ref, ...props }) => null;
35+
`,
2636
settings: {
2737
"react-x": {
2838
version: "19.0.0",
@@ -32,11 +42,17 @@ ruleTester.run(RULE_NAME, rule, {
3242
{
3343
code: /* tsx */ `
3444
import { forwardRef } from 'react'
35-
forwardRef(function (props) {
45+
const Component = forwardRef(function (props) {
3646
return null;
3747
});
3848
`,
3949
errors: [{ messageId: "noForwardRef" }],
50+
output: /* tsx */ `
51+
import { forwardRef } from 'react'
52+
const Component = function ({ ref, ...props }) {
53+
return null;
54+
};
55+
`,
4056
settings: {
4157
"react-x": {
4258
version: "19.0.0",
@@ -46,11 +62,17 @@ ruleTester.run(RULE_NAME, rule, {
4662
{
4763
code: /* tsx */ `
4864
import { forwardRef } from 'react'
49-
forwardRef(function Component(props) {
65+
const Component = forwardRef(function Component(props) {
5066
return null;
5167
});
5268
`,
5369
errors: [{ messageId: "noForwardRef" }],
70+
output: /* tsx */ `
71+
import { forwardRef } from 'react'
72+
const Component = function Component({ ref, ...props }) {
73+
return null;
74+
};
75+
`,
5476
settings: {
5577
"react-x": {
5678
version: "19.0.0",
@@ -60,11 +82,17 @@ ruleTester.run(RULE_NAME, rule, {
6082
{
6183
code: /* tsx */ `
6284
import * as React from 'react'
63-
React.forwardRef((props) => {
85+
const Component = React.forwardRef((props) => {
6486
return null;
6587
});
6688
`,
6789
errors: [{ messageId: "noForwardRef" }],
90+
output: /* tsx */ `
91+
import * as React from 'react'
92+
const Component = ({ ref, ...props }) => {
93+
return null;
94+
};
95+
`,
6896
settings: {
6997
"react-x": {
7098
version: "19.0.0",
@@ -74,9 +102,13 @@ ruleTester.run(RULE_NAME, rule, {
74102
{
75103
code: /* tsx */ `
76104
import * as React from 'react'
77-
React.forwardRef((props) => null);
105+
const Component = React.forwardRef((props) => null);
78106
`,
79107
errors: [{ messageId: "noForwardRef" }],
108+
output: /* tsx */ `
109+
import * as React from 'react'
110+
const Component = ({ ref, ...props }) => null;
111+
`,
80112
settings: {
81113
"react-x": {
82114
version: "19.0.0",
@@ -86,11 +118,17 @@ ruleTester.run(RULE_NAME, rule, {
86118
{
87119
code: /* tsx */ `
88120
import * as React from 'react'
89-
React.forwardRef(function (props) {
121+
const Component = React.forwardRef(function (props) {
90122
return null;
91123
});
92124
`,
93125
errors: [{ messageId: "noForwardRef" }],
126+
output: /* tsx */ `
127+
import * as React from 'react'
128+
const Component = function ({ ref, ...props }) {
129+
return null;
130+
};
131+
`,
94132
settings: {
95133
"react-x": {
96134
version: "19.0.0",
@@ -100,11 +138,83 @@ ruleTester.run(RULE_NAME, rule, {
100138
{
101139
code: /* tsx */ `
102140
import * as React from 'react'
103-
React.forwardRef(function Component(props) {
141+
const Component = React.forwardRef(function Component(props) {
104142
return null;
105143
});
106144
`,
107145
errors: [{ messageId: "noForwardRef" }],
146+
output: /* tsx */ `
147+
import * as React from 'react'
148+
const Component = function Component({ ref, ...props }) {
149+
return null;
150+
};
151+
`,
152+
settings: {
153+
"react-x": {
154+
version: "19.0.0",
155+
},
156+
},
157+
},
158+
{
159+
code: /* tsx */ `
160+
import * as React from 'react'
161+
const Component = React.forwardRef(function Component(props, ref) {
162+
return <div ref={ref} />;
163+
});
164+
`,
165+
errors: [{ messageId: "noForwardRef" }],
166+
output: /* tsx */ `
167+
import * as React from 'react'
168+
const Component = function Component({ ref, ...props }) {
169+
return <div ref={ref} />;
170+
};
171+
`,
172+
settings: {
173+
"react-x": {
174+
version: "19.0.0",
175+
},
176+
},
177+
},
178+
{
179+
code: /* tsx */ `
180+
import * as React from 'react'
181+
interface ComponentProps {
182+
foo: string;
183+
}
184+
const Component = React.forwardRef<HTMLElement, ComponentProps>(function Component(props, ref) {
185+
return <div ref={ref} />;
186+
});
187+
`,
188+
errors: [{ messageId: "noForwardRef" }],
189+
output: /* tsx */ `
190+
import * as React from 'react'
191+
interface ComponentProps {
192+
foo: string;
193+
}
194+
const Component = function Component({ ref, ...props }: ComponentProps & { ref: React.RefObject<HTMLElement> }) {
195+
return <div ref={ref} />;
196+
};
197+
`,
198+
settings: {
199+
"react-x": {
200+
version: "19.0.0",
201+
},
202+
},
203+
},
204+
{
205+
code: /* tsx */ `
206+
import * as React from 'react'
207+
const Component = React.forwardRef<HTMLElement, { foo: string }>(function Component(props, ref) {
208+
return <div ref={ref} />;
209+
});
210+
`,
211+
errors: [{ messageId: "noForwardRef" }],
212+
output: /* tsx */ `
213+
import * as React from 'react'
214+
const Component = function Component({ ref, ...props }: { foo: string } & { ref: React.RefObject<HTMLElement> }) {
215+
return <div ref={ref} />;
216+
};
217+
`,
108218
settings: {
109219
"react-x": {
110220
version: "19.0.0",

packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import * as AST from "@eslint-react/ast";
12
import { isForwardRefCall } from "@eslint-react/core";
23
import { decodeSettings, normalizeSettings } from "@eslint-react/shared";
4+
import type { RuleContext } from "@eslint-react/types";
5+
import type { TSESTree } from "@typescript-eslint/types";
6+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
7+
import type { RuleFix, RuleFixer } from "@typescript-eslint/utils/ts-eslint";
38
import { compare } from "compare-versions";
49
import type { CamelCase } from "string-ts";
510

@@ -15,6 +20,7 @@ export default createRule<[], MessageID>({
1520
docs: {
1621
description: "disallow the use of 'forwardRef'",
1722
},
23+
fixable: "code",
1824
messages: {
1925
noForwardRef: "In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead.",
2026
},
@@ -31,9 +37,84 @@ export default createRule<[], MessageID>({
3137
context.report({
3238
messageId: "noForwardRef",
3339
node,
40+
fix: getFix(node, context),
3441
});
3542
},
3643
};
3744
},
3845
defaultOptions: [],
3946
});
47+
48+
function getFix(node: TSESTree.CallExpression, context: RuleContext): (fixer: RuleFixer) => RuleFix[] {
49+
return (fixer) => {
50+
const [componentNode] = node.arguments;
51+
if (!componentNode || !AST.isFunction(componentNode)) return [];
52+
return [
53+
fixer.removeRange([node.range[0], componentNode.range[0]]),
54+
fixer.removeRange([componentNode.range[1], node.range[1]]),
55+
...getParamsFixes(componentNode, context, node.typeArguments?.params ?? [], fixer),
56+
];
57+
};
58+
}
59+
60+
function getParamsFixes(
61+
node: AST.TSESTreeFunction,
62+
context: RuleContext,
63+
typeArguments: TSESTree.TypeNode[],
64+
fixer: RuleFixer,
65+
): RuleFix[] {
66+
const [arg0, arg1] = node.params;
67+
const [typeArg0, typeArg1] = typeArguments;
68+
if (arg0?.type !== AST_NODE_TYPES.Identifier) return [];
69+
if (!arg1) {
70+
return [fixer.replaceText(
71+
arg0,
72+
[
73+
"{",
74+
"ref,",
75+
`...${arg0.name}`,
76+
"}",
77+
].join(" "),
78+
)] as const;
79+
}
80+
if (arg1.type !== AST_NODE_TYPES.Identifier) return [];
81+
if (!typeArg0 || !typeArg1) {
82+
return [
83+
fixer.replaceText(
84+
arg0,
85+
[
86+
"{",
87+
arg1.name === "ref"
88+
? `ref,`
89+
: `ref: ${arg1.name},`,
90+
`...${arg0.name}`,
91+
"}",
92+
].join(" "),
93+
),
94+
fixer.remove(arg1),
95+
fixer.removeRange([arg0.range[1], arg1.range[0]]),
96+
] as const;
97+
}
98+
const getText = (node: TSESTree.Node) => context.sourceCode.getText(node);
99+
return [
100+
fixer.replaceText(
101+
arg0,
102+
[
103+
"{",
104+
arg1.name === "ref"
105+
? `ref,`
106+
: `ref: ${arg1.name},`,
107+
`...${arg0.name}`,
108+
"}:",
109+
getText(typeArg1),
110+
"&",
111+
"{",
112+
`ref:`,
113+
`React.RefObject<${getText(typeArg0)}>`,
114+
"}",
115+
].join(" "),
116+
),
117+
fixer.remove(arg1),
118+
fixer.removeRange([arg0.range[1], arg1.range[0]]),
119+
] as const;
120+
}

website/pages/docs/rules/no-forward-ref.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# no-forward-ref
22

3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
35
## Rule category
46

57
Restriction.

0 commit comments

Comments
 (0)