Skip to content

Commit 0047404

Browse files
authored
Add codemod for required initial value in useRef (#217)
* Add codemod for required initial value in `useRef` * Rebase * Format
1 parent e4c8df6 commit 0047404

7 files changed

Lines changed: 162 additions & 6 deletions

File tree

.changeset/fifty-cheetahs-dance.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"types-react-codemod": minor
3+
---
4+
5+
Add codemod for required initial value in `useRef`
6+
7+
Added as `experimental-useRef-required-initial`.
8+
Can be used on 18.x types but only intended for once https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64920 lands.

README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ Positionals:
3838
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
3939
"deprecated-sfc", "deprecated-stateless-component",
4040
"deprecated-void-function-component", "experimental-refobject-defaults",
41-
"implicit-children", "preset-18", "preset-19", "scoped-jsx",
42-
"useCallback-implicit-any"]
41+
"experimental-useRef-required-initial", "implicit-children", "preset-18",
42+
"preset-19", "scoped-jsx", "useCallback-implicit-any"]
4343
paths [string] [required]
4444

4545
Options:
@@ -267,7 +267,7 @@ In earlier versions of `@types/react` this codemod would change the typings.
267267

268268
### `experimental-refobject-defaults`
269269

270-
WARNING: This is an experimental codemod to intended for codebases using published types.
270+
WARNING: This is an experimental codemod to intended for codebases using unpublished types.
271271
Only use if you're using https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64896.
272272

273273
`RefObject` no longer makes `current` nullable by default
@@ -307,6 +307,31 @@ If the import style doesn't match your preferences, you should set up auto-fixab
307307
+const element: React.JSX.Element = <div />;
308308
```
309309

310+
### `experimental-useRef-required-initial`
311+
312+
WARNING: This is an experimental codemod to intended for codebases using unpublished types.
313+
Only use if you're using https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64920.
314+
315+
`useRef` now always requires an initial value.
316+
Implicit `undefined` is forbidden
317+
318+
```diff
319+
import * as React from "react";
320+
-React.useRef()
321+
+React.useRef(undefined)
322+
```
323+
324+
#### `experimental-useRef-required-initial` false-negative pattern A
325+
326+
Importing `useRef` via aliased named import will result in the transform being skipped.
327+
328+
```tsx
329+
import { useRef as useReactRef } from "react";
330+
331+
// not transformed
332+
useReactRef<number>();
333+
```
334+
310335
## Supported platforms
311336

312337
The following list contains officially supported runtimes.

bin/__tests__/types-react-codemod.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ describe("types-react-codemod", () => {
2525
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
2626
"deprecated-sfc", "deprecated-stateless-component",
2727
"deprecated-void-function-component", "experimental-refobject-defaults",
28-
"implicit-children", "preset-18", "preset-19", "scoped-jsx",
29-
"useCallback-implicit-any"]
28+
"experimental-useRef-required-initial", "implicit-children", "preset-18",
29+
"preset-19", "scoped-jsx", "useCallback-implicit-any"]
3030
paths [string] [required]
3131
3232
Options:

transforms/__tests__/preset-19.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ describe("preset-19", () => {
77
let deprecatedReactChildTransform;
88
let deprecatedReactTextTransform;
99
let deprecatedVoidFunctionComponentTransform;
10+
let refobjectDefaultsTransform;
11+
let useRefRequiredInitialTransform;
1012

1113
function applyTransform(source, options = {}) {
1214
return JscodeshiftTestUtils.applyTransform(preset19Transform, options, {
@@ -33,6 +35,12 @@ describe("preset-19", () => {
3335
deprecatedVoidFunctionComponentTransform = mockTransform(
3436
"../deprecated-void-function-component",
3537
);
38+
refobjectDefaultsTransform = mockTransform(
39+
"../experimental-refobject-defaults",
40+
);
41+
useRefRequiredInitialTransform = mockTransform(
42+
"../experimental-useRef-required-initial",
43+
);
3644

3745
preset19Transform = require("../preset-19");
3846
});
@@ -53,11 +61,15 @@ describe("preset-19", () => {
5361
"deprecated-react-child",
5462
"deprecated-react-text",
5563
"deprecated-void-function-component",
64+
"experimental-refobject-defaults",
65+
"experimental-useRef-required-initial",
5666
].join(","),
5767
});
5868

5969
expect(deprecatedReactChildTransform).toHaveBeenCalled();
6070
expect(deprecatedReactTextTransform).toHaveBeenCalled();
6171
expect(deprecatedVoidFunctionComponentTransform).toHaveBeenCalled();
72+
expect(refobjectDefaultsTransform).toHaveBeenCalled();
73+
expect(useRefRequiredInitialTransform).toHaveBeenCalled();
6274
});
6375
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const { describe, expect, test } = require("@jest/globals");
2+
const dedent = require("dedent");
3+
const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils");
4+
const useRefRequiredInitial = require("../experimental-useRef-required-initial");
5+
6+
function applyTransform(source, options = {}) {
7+
return JscodeshiftTestUtils.applyTransform(useRefRequiredInitial, options, {
8+
path: "test.tsx",
9+
source: dedent(source),
10+
});
11+
}
12+
13+
describe("transform useRef-required-initial", () => {
14+
test("not modified", () => {
15+
expect(
16+
applyTransform(`
17+
import * as React from 'react';
18+
interface Props {
19+
children?: ReactNode;
20+
}
21+
`),
22+
).toMatchInlineSnapshot(`
23+
"import * as React from 'react';
24+
interface Props {
25+
children?: ReactNode;
26+
}"
27+
`);
28+
});
29+
30+
test("named import", () => {
31+
expect(
32+
applyTransform(`
33+
import { useRef } from 'react';
34+
const myRef = useRef<number>();
35+
`),
36+
).toMatchInlineSnapshot(`
37+
"import { useRef } from 'react';
38+
const myRef = useRef<number>(undefined);"
39+
`);
40+
});
41+
42+
test("false-negative named renamed import", () => {
43+
expect(
44+
applyTransform(`
45+
import { useRef as useReactRef } from 'react';
46+
const myRef = useReactRef<number>();
47+
`),
48+
).toMatchInlineSnapshot(`
49+
"import { useRef as useReactRef } from 'react';
50+
const myRef = useReactRef<number>();"
51+
`);
52+
});
53+
54+
test("namespace import", () => {
55+
expect(
56+
applyTransform(`
57+
import * as React from 'react';
58+
const myRef = React.useRef<number>();
59+
`),
60+
).toMatchInlineSnapshot(`
61+
"import * as React from 'react';
62+
const myRef = React.useRef<number>(undefined);"
63+
`);
64+
});
65+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const parseSync = require("./utils/parseSync");
2+
const t = require("@babel/types");
3+
const traverse = require("@babel/traverse").default;
4+
5+
/**
6+
* @type {import('jscodeshift').Transform}
7+
*
8+
* Summary for Klarna's klapp@?
9+
* TODO
10+
*/
11+
const useRefRequiredInitialTransform = (file) => {
12+
const ast = parseSync(file);
13+
14+
let changedSome = false;
15+
16+
// ast.get("program").value is sufficient for unit tests but not actually running it on files
17+
// TODO: How to test?
18+
const traverseRoot = ast.paths()[0].value;
19+
traverse(traverseRoot, {
20+
CallExpression({ node: callExpression }) {
21+
const isUseRefCall =
22+
(callExpression.callee.type === "Identifier" &&
23+
callExpression.callee.name === "useRef") ||
24+
(callExpression.callee.type === "MemberExpression" &&
25+
callExpression.callee.property.type === "Identifier" &&
26+
callExpression.callee.property.name === "useRef");
27+
28+
if (isUseRefCall && callExpression.arguments.length === 0) {
29+
changedSome = true;
30+
callExpression.arguments = [t.identifier("undefined")];
31+
}
32+
},
33+
});
34+
35+
// Otherwise some files will be marked as "modified" because formatting changed
36+
if (changedSome) {
37+
return ast.toSource();
38+
}
39+
return file.source;
40+
};
41+
42+
module.exports = useRefRequiredInitialTransform;

transforms/preset-19.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const deprecatedReactTextTransform = require("./deprecated-react-text");
33
const deprecatedVoidFunctionComponentTransform = require("./deprecated-void-function-component");
44
const refobjectDefaultsTransform = require("./experimental-refobject-defaults");
55
const scopedJsxTransform = require("./scoped-jsx");
6+
const useRefRequiredInitialTransform = require("./experimental-useRef-required-initial");
67

78
/**
89
* @type {import('jscodeshift').Transform}
@@ -24,12 +25,15 @@ const transform = (file, api, options) => {
2425
if (transformNames.has("deprecated-void-function-component")) {
2526
transforms.push(deprecatedVoidFunctionComponentTransform);
2627
}
27-
if (transformNames.has("plain-refs")) {
28+
if (transformNames.has("experimental-refobject-defaults")) {
2829
transforms.push(refobjectDefaultsTransform);
2930
}
3031
if (transformNames.has("scoped-jsx")) {
3132
transforms.push(scopedJsxTransform);
3233
}
34+
if (transformNames.has("experimental-useRef-required-initial")) {
35+
transforms.push(useRefRequiredInitialTransform);
36+
}
3337

3438
let wasAlwaysSkipped = true;
3539
const newSource = transforms.reduce((currentFileSource, transform) => {

0 commit comments

Comments
 (0)