Skip to content

Commit 7535bfc

Browse files
authored
feat: Add no-implicit-ref-callback-return transform (#369)
1 parent e3bcbe5 commit 7535bfc

File tree

8 files changed

+154
-4
lines changed

8 files changed

+154
-4
lines changed

.changeset/thick-snakes-float.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"types-react-codemod": minor
3+
---
4+
5+
Add `no-implicit-ref-callback-return` transform
6+
7+
Ensures you don't accidentally return anything from ref callbacks since the return value was always ignored.
8+
With ref cleanups, this is no longer the case and flagged in types to avoid mistakes.
9+
10+
```diff
11+
-<div ref={current => (instance = current)} />
12+
+<div ref={current => {instance = current}} />
13+
```
14+
15+
The transform is opt-in in the `preset-19` in case you already used ref cleanups in Canary releases.

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ Positionals:
3838
"deprecated-react-fragment", "deprecated-react-node-array",
3939
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
4040
"deprecated-sfc", "deprecated-stateless-component",
41-
"deprecated-void-function-component", "implicit-children", "preset-18",
42-
"preset-19", "refobject-defaults", "scoped-jsx", "useCallback-implicit-any",
41+
"deprecated-void-function-component", "implicit-children",
42+
"no-implicit-ref-callback-return", "preset-18", "preset-19",
43+
"refobject-defaults", "scoped-jsx", "useCallback-implicit-any",
4344
"useRef-required-initial"]
4445
paths [string] [required]
4546

@@ -353,6 +354,23 @@ In earlier versions of `@types/react` this codemod would change the typings.
353354
+const Component: React.FunctionComponent = () => {}
354355
```
355356

357+
### `no-implicit-ref-callback-return`
358+
359+
Off by default in `preset-19`. Can be enabled when running `preset-19`.
360+
361+
WARNING: Manually review changes in case you already used ref cleanups in Canary builds.
362+
363+
Ensures you don't accidentally return anything from ref callbacks since the return value was always ignored.
364+
With ref cleanups, this is no longer the case and flagged in types to avoid mistakes.
365+
366+
```diff
367+
-<div ref={current => (instance = current)} />
368+
+<div ref={current => {instance = current}} />
369+
```
370+
371+
This only works for the `ref` prop.
372+
The codemod will not apply to other props that take refs (e.g. `innerRef`).
373+
356374
### `refobject-defaults`
357375

358376
WARNING: This is an experimental codemod to intended for codebases using unpublished types.

bin/__tests__/types-react-codemod.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ describe("types-react-codemod", () => {
2626
"deprecated-react-fragment", "deprecated-react-node-array",
2727
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
2828
"deprecated-sfc", "deprecated-stateless-component",
29-
"deprecated-void-function-component", "implicit-children", "preset-18",
30-
"preset-19", "refobject-defaults", "scoped-jsx", "useCallback-implicit-any",
29+
"deprecated-void-function-component", "implicit-children",
30+
"no-implicit-ref-callback-return", "preset-18", "preset-19",
31+
"refobject-defaults", "scoped-jsx", "useCallback-implicit-any",
3132
"useRef-required-initial"]
3233
paths [string] [required]
3334

bin/types-react-codemod.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ async function main() {
105105
{ checked: true, value: "deprecated-react-fragment" },
106106
{ checked: true, value: "deprecated-react-text" },
107107
{ checked: true, value: "deprecated-void-function-component" },
108+
{ checked: false, value: "no-implicit-ref-callback-return" },
108109
{ checked: true, value: "refobject-defaults" },
109110
{ checked: true, value: "scoped-jsx" },
110111
{ checked: true, value: "useRef-required-initial" },
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const { expect, test } = require("@jest/globals");
2+
const dedent = require("dedent");
3+
const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils");
4+
const noImplicitRefCallbackReturnTransform = require("../no-implicit-ref-callback-return");
5+
6+
function applyTransform(source, options = {}) {
7+
return JscodeshiftTestUtils.applyTransform(
8+
noImplicitRefCallbackReturnTransform,
9+
options,
10+
{
11+
path: "test.tsx",
12+
source: dedent(source),
13+
},
14+
);
15+
}
16+
17+
test("not modified", () => {
18+
expect(
19+
applyTransform(`
20+
<div ref={current => {instance = current}} />
21+
`),
22+
).toMatchInlineSnapshot(`"<div ref={current => {instance = current}} />"`);
23+
});
24+
25+
test("replaces implicit return of assignment expression with block", () => {
26+
expect(
27+
applyTransform(`
28+
<div ref={current => (instance = current)} />
29+
`),
30+
).toMatchInlineSnapshot(`
31+
"<div ref={current => {
32+
(instance = current);
33+
}} />"
34+
`);
35+
});
36+
37+
test("replaces implicit return of identifier with block", () => {
38+
expect(
39+
applyTransform(`
40+
<div ref={current => current} />
41+
`),
42+
).toMatchInlineSnapshot(`
43+
"<div ref={current => {
44+
current;
45+
}} />"
46+
`);
47+
});
48+
49+
test("function expression", () => {
50+
expect(
51+
applyTransform(`
52+
<div ref={function (current) { instance = current }} />
53+
`),
54+
).toMatchInlineSnapshot(
55+
`"<div ref={function (current) { instance = current }} />"`,
56+
);
57+
});
58+
59+
test("only applies to `ref` prop", () => {
60+
expect(
61+
applyTransform(`
62+
<div innerRef={current => (instance = current)} />
63+
`),
64+
).toMatchInlineSnapshot(
65+
`"<div innerRef={current => (instance = current)} />"`,
66+
);
67+
});

transforms/__tests__/preset-19.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ describe("preset-19", () => {
1111
let deprecatedReactFragmentTransform;
1212
let deprecatedReactTextTransform;
1313
let deprecatedVoidFunctionComponentTransform;
14+
let noImplicitRefCallbackReturnTransform;
1415
let refobjectDefaultsTransform;
1516
let scopedJSXTransform;
1617
let useRefRequiredInitialTransform;
@@ -48,6 +49,9 @@ describe("preset-19", () => {
4849
deprecatedVoidFunctionComponentTransform = mockTransform(
4950
"../deprecated-void-function-component",
5051
);
52+
noImplicitRefCallbackReturnTransform = mockTransform(
53+
"../no-implicit-ref-callback-return",
54+
);
5155
refobjectDefaultsTransform = mockTransform("../refobject-defaults");
5256
scopedJSXTransform = mockTransform("../scoped-jsx");
5357
useRefRequiredInitialTransform = mockTransform(
@@ -77,6 +81,7 @@ describe("preset-19", () => {
7781
"deprecated-react-node-array",
7882
"deprecated-react-text",
7983
"deprecated-void-function-component",
84+
"no-implicit-ref-callback-return",
8085
"refobject-defaults",
8186
"scoped-jsx",
8287
"useRef-required-initial",
@@ -90,6 +95,7 @@ describe("preset-19", () => {
9095
expect(deprecatedReactFragmentTransform).toHaveBeenCalled();
9196
expect(deprecatedReactTextTransform).toHaveBeenCalled();
9297
expect(deprecatedVoidFunctionComponentTransform).toHaveBeenCalled();
98+
expect(noImplicitRefCallbackReturnTransform).toHaveBeenCalled();
9399
expect(refobjectDefaultsTransform).toHaveBeenCalled();
94100
expect(scopedJSXTransform).toHaveBeenCalled();
95101
expect(useRefRequiredInitialTransform).toHaveBeenCalled();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const parseSync = require("./utils/parseSync");
2+
3+
/**
4+
* @type {import('jscodeshift').Transform}
5+
*/
6+
const noImplicitRefCallbackReturnTransform = (file, api) => {
7+
const j = api.jscodeshift;
8+
const ast = parseSync(file);
9+
10+
let changedSome = false;
11+
12+
ast
13+
.find(j.JSXAttribute, (jsxAttribute) => {
14+
return jsxAttribute.name.name === "ref";
15+
})
16+
.forEach((jsxAttributePath) => {
17+
const jsxAttribute = jsxAttributePath.node;
18+
if (
19+
jsxAttribute.value?.type === "JSXExpressionContainer" &&
20+
jsxAttribute.value.expression.type === "ArrowFunctionExpression" &&
21+
jsxAttribute.value.expression.body.type !== "BlockStatement"
22+
) {
23+
changedSome = true;
24+
25+
jsxAttribute.value.expression.body = j.blockStatement([
26+
j.expressionStatement(jsxAttribute.value.expression.body),
27+
]);
28+
}
29+
});
30+
31+
// Otherwise some files will be marked as "modified" because formatting changed
32+
if (changedSome) {
33+
return ast.toSource();
34+
}
35+
return file.source;
36+
};
37+
38+
module.exports = noImplicitRefCallbackReturnTransform;

transforms/preset-19.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const deprecatedReactNodeArrayTransform = require("./deprecated-react-node-array
55
const deprecatedReactFragmentTransform = require("./deprecated-react-fragment");
66
const deprecatedReactTextTransform = require("./deprecated-react-text");
77
const deprecatedVoidFunctionComponentTransform = require("./deprecated-void-function-component");
8+
const noImplicitRefCallbackReturnTransform = require("./no-implicit-ref-callback-return");
89
const refobjectDefaultsTransform = require("./refobject-defaults");
910
const scopedJsxTransform = require("./scoped-jsx");
1011
const useRefRequiredInitialTransform = require("./useRef-required-initial");
@@ -41,6 +42,9 @@ const transform = (file, api, options) => {
4142
if (transformNames.has("deprecated-void-function-component")) {
4243
transforms.push(deprecatedVoidFunctionComponentTransform);
4344
}
45+
if (transformNames.has("no-implicit-ref-callback-return")) {
46+
transforms.push(noImplicitRefCallbackReturnTransform);
47+
}
4448
if (transformNames.has("refobject-defaults")) {
4549
transforms.push(refobjectDefaultsTransform);
4650
}

0 commit comments

Comments
 (0)