Skip to content

Commit

Permalink
feat(plugins/x): add autofix to 'no-forward-ref', closes #871
Browse files Browse the repository at this point in the history
  • Loading branch information
Rel1cx committed Dec 9, 2024
1 parent 02947db commit bec6a8a
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ ruleTester.run(RULE_NAME, rule, {
{
code: /* tsx */ `
import { forwardRef } from 'react'
forwardRef((props) => {
const Component = forwardRef((props) => {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
output: /* tsx */ `
import { forwardRef } from 'react'
const Component = ({ ref, ...props }) => {
return null;
};
`,
settings: {
"react-x": {
version: "19.0.0",
Expand All @@ -20,9 +26,13 @@ ruleTester.run(RULE_NAME, rule, {
{
code: /* tsx */ `
import { forwardRef } from 'react'
forwardRef((props) => null);
const Component = forwardRef((props) => null);
`,
errors: [{ messageId: "noForwardRef" }],
output: /* tsx */ `
import { forwardRef } from 'react'
const Component = ({ ref, ...props }) => null;
`,
settings: {
"react-x": {
version: "19.0.0",
Expand All @@ -32,11 +42,17 @@ ruleTester.run(RULE_NAME, rule, {
{
code: /* tsx */ `
import { forwardRef } from 'react'
forwardRef(function (props) {
const Component = forwardRef(function (props) {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
output: /* tsx */ `
import { forwardRef } from 'react'
const Component = function ({ ref, ...props }) {
return null;
};
`,
settings: {
"react-x": {
version: "19.0.0",
Expand All @@ -46,11 +62,17 @@ ruleTester.run(RULE_NAME, rule, {
{
code: /* tsx */ `
import { forwardRef } from 'react'
forwardRef(function Component(props) {
const Component = forwardRef(function Component(props) {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
output: /* tsx */ `
import { forwardRef } from 'react'
const Component = function Component({ ref, ...props }) {
return null;
};
`,
settings: {
"react-x": {
version: "19.0.0",
Expand All @@ -60,11 +82,17 @@ ruleTester.run(RULE_NAME, rule, {
{
code: /* tsx */ `
import * as React from 'react'
React.forwardRef((props) => {
const Component = React.forwardRef((props) => {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
output: /* tsx */ `
import * as React from 'react'
const Component = ({ ref, ...props }) => {
return null;
};
`,
settings: {
"react-x": {
version: "19.0.0",
Expand All @@ -74,9 +102,13 @@ ruleTester.run(RULE_NAME, rule, {
{
code: /* tsx */ `
import * as React from 'react'
React.forwardRef((props) => null);
const Component = React.forwardRef((props) => null);
`,
errors: [{ messageId: "noForwardRef" }],
output: /* tsx */ `
import * as React from 'react'
const Component = ({ ref, ...props }) => null;
`,
settings: {
"react-x": {
version: "19.0.0",
Expand All @@ -86,11 +118,17 @@ ruleTester.run(RULE_NAME, rule, {
{
code: /* tsx */ `
import * as React from 'react'
React.forwardRef(function (props) {
const Component = React.forwardRef(function (props) {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
output: /* tsx */ `
import * as React from 'react'
const Component = function ({ ref, ...props }) {
return null;
};
`,
settings: {
"react-x": {
version: "19.0.0",
Expand All @@ -100,17 +138,69 @@ ruleTester.run(RULE_NAME, rule, {
{
code: /* tsx */ `
import * as React from 'react'
React.forwardRef(function Component(props) {
const Component = React.forwardRef(function Component(props) {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
output: /* tsx */ `
import * as React from 'react'
const Component = function Component({ ref, ...props }) {
return null;
};
`,
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import * as React from 'react'
const Component = React.forwardRef(function Component(props, ref) {
return <div ref={ref} />;
});
`,
errors: [{ messageId: "noForwardRef" }],
output: /* tsx */ `
import * as React from 'react'
const Component = function Component({ ref, ...props }) {
return <div ref={ref} />;
};
`,
settings: {
"react-x": {
version: "19.0.0",
},
},
},
// {
// code: /* tsx */ `
// import * as React from 'react'
// interface ComponentProps {
// foo: string;
// }
// const Component = React.forwardRef<HTMLElement, ComponentProps>(function Component(props, ref) {
// return <div ref={ref} />;
// });
// `,
// errors: [{ messageId: "noForwardRef" }],
// output: /* tsx */ `
// import * as React from 'react'
// interface ComponentProps {
// foo: string;
// }
// const Component = function Component({ ref, ...props }: ComponentProps & { ref: React.RefObject<HTMLElement> }) {
// return <div ref={ref} />;
// };
// `,
// settings: {
// "react-x": {
// version: "19.0.0",
// },
// },
// },
],
valid: [
{
Expand Down
40 changes: 40 additions & 0 deletions packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as AST from "@eslint-react/ast";
import { isForwardRefCall } from "@eslint-react/core";
import { decodeSettings, normalizeSettings } from "@eslint-react/shared";
import type { RuleContext } from "@eslint-react/types";
import type { TSESTree } from "@typescript-eslint/types";
import { AST_NODE_TYPES } from "@typescript-eslint/types";
import type { RuleFix, RuleFixer } from "@typescript-eslint/utils/ts-eslint";
import { compare } from "compare-versions";
import type { CamelCase } from "string-ts";

Expand All @@ -15,6 +20,7 @@ export default createRule<[], MessageID>({
docs: {
description: "disallow the use of 'forwardRef'",
},
fixable: "code",
messages: {
noForwardRef: "In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead.",
},
Expand All @@ -31,9 +37,43 @@ export default createRule<[], MessageID>({
context.report({
messageId: "noForwardRef",
node,
fix: getFix(node, context),
});
},
};
},
defaultOptions: [],
});

// TODO: Add TypeScript type arguments support
function getFix(node: TSESTree.CallExpression, context: RuleContext): (fixer: RuleFixer) => RuleFix[] {
return (fixer) => {
const [componentNode] = node.arguments;
if (!componentNode || !AST.isFunction(componentNode)) return [];
return [
fixer.removeRange([node.range[0], componentNode.range[0]]),
fixer.removeRange([componentNode.range[1], node.range[1]]),
...getParamsFixes(componentNode, context, fixer),
];
};
}

function getParamsFixes(
node: AST.TSESTreeFunction,
context: RuleContext,
fixer: RuleFixer,
): RuleFix[] {
const [arg0, arg1] = node.params;
if (arg0?.type !== AST_NODE_TYPES.Identifier) return [];
if (!arg1) {
return [fixer.replaceText(arg0, `{ ref, ...${arg0.name} }`)];
}
if (arg1.type !== AST_NODE_TYPES.Identifier) return [];
return [
arg1.name === "ref"
? fixer.replaceText(arg0, `{ ref, ...${arg0.name} }`)
: fixer.replaceText(arg0, `{ ref: ${arg1.name}, ...${arg0.name} }`),
fixer.remove(arg1),
fixer.removeRange([arg0.range[1], arg1.range[0]]),
];
}

0 comments on commit bec6a8a

Please sign in to comment.