Skip to content

Commit 84d05d5

Browse files
authored
feat: add codemod feature to react-x/no-string-refs, closes #1044 (#1045)
1 parent f32610a commit 84d05d5

File tree

4 files changed

+161
-38
lines changed

4 files changed

+161
-38
lines changed

apps/website/content/docs/rules/overview.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Linter rules can have false positives, false negatives, and some rules are depen
6161
| [`no-set-state-in-component-did-mount`](./no-set-state-in-component-did-mount) | 1️⃣ | | Disallow calling `this.setState` in `componentDidMount` outside of functions, such as callbacks | |
6262
| [`no-set-state-in-component-did-update`](./no-set-state-in-component-did-update) | 1️⃣ | | Disallow calling `this.setState` in `componentDidUpdate` outside of functions, such as callbacks | |
6363
| [`no-set-state-in-component-will-update`](./no-set-state-in-component-will-update) | 1️⃣ | | Disallow calling `this.setState` in `componentWillUpdate` outside of functions, such as callbacks | |
64-
| [`no-string-refs`](./no-string-refs) | 2️⃣ | | Disallow deprecated string `refs` | |
64+
| [`no-string-refs`](./no-string-refs) | 2️⃣ | `🔄` | Replaces string refs with callback refs | >=16.3.0 |
6565
| [`no-unsafe-component-will-mount`](./no-unsafe-component-will-mount) | 1️⃣ | | Warns the usage of `UNSAFE_componentWillMount` in class components | |
6666
| [`no-unsafe-component-will-receive-props`](./no-unsafe-component-will-receive-props) | 1️⃣ | | Warns the usage of `UNSAFE_componentWillReceiveProps` in class components | |
6767
| [`no-unsafe-component-will-update`](./no-unsafe-component-will-update) | 1️⃣ | | Warns the usage of `UNSAFE_componentWillUpdate` in class components | |

packages/plugins/eslint-plugin-react-x/src/rules/no-string-refs.md

+22-11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ react-x/no-string-refs
1414
@eslint-react/no-string-refs
1515
```
1616

17+
**Features**
18+
19+
`🔄`
20+
1721
**Presets**
1822

1923
- `x`
@@ -23,32 +27,39 @@ react-x/no-string-refs
2327

2428
## Description
2529

26-
Disallow deprecated string `refs`.
30+
Replaces string refs with callback refs.
2731

2832
String refs are deprecated in React. Use callback refs instead.
2933

3034
## Examples
3135

32-
### Failing
36+
### Before
3337

3438
```tsx
3539
import React from "react";
3640

37-
function MyComponent() {
38-
return <div ref="ref" />;
39-
// ^^^^^
40-
// - [Deprecated] Use callback refs instead.
41+
class Input extends React.Component {
42+
focus = () => {
43+
this.refs.input.focus();
44+
};
45+
render() {
46+
return <input ref="input" />;
47+
}
4148
}
4249
```
4350

44-
### Passing
51+
### After
4552

4653
```tsx
47-
import React, { useRef } from "react";
54+
import React from "react";
4855

49-
function MyComponent() {
50-
const ref = useRef<HTMLDivElement>(null);
51-
return <div ref={ref} />;
56+
class Input extends React.Component {
57+
focus = () => {
58+
this.refs.input.focus();
59+
};
60+
render() {
61+
return <input ref={ref => this.refs.input = ref} />;
62+
}
5263
}
5364
```
5465

packages/plugins/eslint-plugin-react-x/src/rules/no-string-refs.spec.ts

+86
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,92 @@ ruleTester.run(RULE_NAME, rule, {
2626
}
2727
`,
2828
errors: [{ messageId: "noStringRefs" }],
29+
output: tsx`
30+
class Input extends React.Component {
31+
focus = () => {
32+
this.refs.input.focus();
33+
}
34+
35+
render() {
36+
return <input ref={(ref) => { this.refs["input"] = ref; }} />;
37+
}
38+
}
39+
`,
40+
},
41+
{
42+
code: tsx`
43+
class Input extends React.Component {
44+
focus = () => {
45+
this.refs.input.focus();
46+
}
47+
48+
render() {
49+
return <input ref={"input"} />;
50+
}
51+
}
52+
`,
53+
errors: [{ messageId: "noStringRefs" }],
54+
output: tsx`
55+
class Input extends React.Component {
56+
focus = () => {
57+
this.refs.input.focus();
58+
}
59+
60+
render() {
61+
return <input ref={(ref) => { this.refs["input"] = ref; }} />;
62+
}
63+
}
64+
`,
65+
},
66+
{
67+
code: tsx`
68+
class Input extends React.Component {
69+
focus = () => {
70+
this.refs.input.focus();
71+
}
72+
73+
render() {
74+
return <input ref={\`input\`} />;
75+
}
76+
}
77+
`,
78+
errors: [{ messageId: "noStringRefs" }],
79+
output: tsx`
80+
class Input extends React.Component {
81+
focus = () => {
82+
this.refs.input.focus();
83+
}
84+
85+
render() {
86+
return <input ref={(ref) => { this.refs[\`input\`] = ref; }} />;
87+
}
88+
}
89+
`,
90+
},
91+
{
92+
code: tsx`
93+
class Input extends React.Component {
94+
focus = () => {
95+
this.refs.input.focus();
96+
}
97+
98+
render() {
99+
return <input ref={\`inp\${"ut"}\`} />;
100+
}
101+
}
102+
`,
103+
errors: [{ messageId: "noStringRefs" }],
104+
output: tsx`
105+
class Input extends React.Component {
106+
focus = () => {
107+
this.refs.input.focus();
108+
}
109+
110+
render() {
111+
return <input ref={(ref) => { this.refs[\`inp\${"ut"}\`] = ref; }} />;
112+
}
113+
}
114+
`,
29115
},
30116
],
31117
valid: [

packages/plugins/eslint-plugin-react-x/src/rules/no-string-refs.ts

+52-26
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,27 @@ import type { RuleContext, RuleFeature } from "@eslint-react/kit";
22
import type { TSESTree } from "@typescript-eslint/types";
33
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
44
import type { CamelCase } from "string-ts";
5+
import * as ER from "@eslint-react/core";
56
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
67

78
import { createRule } from "../utils";
89

910
export const RULE_NAME = "no-string-refs";
1011

11-
export const RULE_FEATURES = [] as const satisfies RuleFeature[];
12+
export const RULE_FEATURES = [
13+
"MOD",
14+
] as const satisfies RuleFeature[];
1215

1316
export type MessageID = CamelCase<typeof RULE_NAME>;
1417

15-
function containsStringLiteral({ value }: TSESTree.JSXAttribute) {
16-
return value?.type === T.Literal && typeof value.value === "string";
17-
}
18-
19-
function containsStringExpressionContainer({ value }: TSESTree.JSXAttribute) {
20-
if (value?.type !== T.JSXExpressionContainer) {
21-
return false;
22-
}
23-
if (value.expression.type === T.Literal) {
24-
return typeof value.expression.value === "string";
25-
}
26-
27-
return value.expression.type === T.TemplateLiteral;
28-
}
29-
3018
export default createRule<[], MessageID>({
3119
meta: {
3220
type: "problem",
3321
docs: {
34-
description: "Disallow deprecated string `refs`.",
22+
description: "Replaces string refs with callback refs.",
3523
[Symbol.for("rule_features")]: RULE_FEATURES,
3624
},
25+
fixable: "code",
3726
messages: {
3827
noStringRefs: "[Deprecated] Use callback refs instead.",
3928
},
@@ -45,17 +34,54 @@ export default createRule<[], MessageID>({
4534
});
4635

4736
export function create(context: RuleContext<MessageID, []>): RuleListener {
37+
const state = {
38+
isWithinClassComponent: false,
39+
};
40+
41+
function onClassBodyEnter(node: TSESTree.ClassBody) {
42+
if (ER.isClassComponent(node.parent)) {
43+
state.isWithinClassComponent = true;
44+
}
45+
}
46+
47+
function onClassBodyExit() {
48+
state.isWithinClassComponent = false;
49+
}
50+
4851
return {
52+
ClassBody: onClassBodyEnter,
53+
"ClassBody:exit": onClassBodyExit,
4954
JSXAttribute(node) {
50-
if (node.name.name !== "ref") {
51-
return;
52-
}
53-
if (containsStringLiteral(node) || containsStringExpressionContainer(node)) {
54-
context.report({
55-
messageId: "noStringRefs",
56-
node,
57-
});
58-
}
55+
if (node.name.name !== "ref") return;
56+
const refNameText = getAttributeValueText(context, node.value);
57+
if (refNameText == null) return;
58+
context.report({
59+
messageId: "noStringRefs",
60+
node,
61+
fix(fixer) {
62+
if (node.value == null) return null;
63+
if (!state.isWithinClassComponent) return null;
64+
return fixer.replaceText(node.value, `{(ref) => { this.refs[${refNameText}] = ref; }}`);
65+
},
66+
});
5967
},
6068
};
6169
}
70+
71+
function getAttributeValueText(context: RuleContext, node: TSESTree.JSXAttribute["value"]) {
72+
if (node == null) return null;
73+
switch (true) {
74+
case node.type === T.Literal
75+
&& typeof node.value === "string":
76+
return context.sourceCode.getText(node);
77+
case node.type === T.JSXExpressionContainer
78+
&& node.expression.type === T.Literal
79+
&& typeof node.expression.value === "string":
80+
return context.sourceCode.getText(node.expression);
81+
case node.type === T.JSXExpressionContainer
82+
&& node.expression.type === T.TemplateLiteral:
83+
return context.sourceCode.getText(node.expression);
84+
default:
85+
return null;
86+
}
87+
}

0 commit comments

Comments
 (0)