Skip to content

Commit

Permalink
Add codemod for required initial value in useRef (#217)
Browse files Browse the repository at this point in the history
* Add codemod for required initial value in `useRef`

* Rebase

* Format
  • Loading branch information
eps1lon committed Dec 5, 2023
1 parent e4c8df6 commit 0047404
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 6 deletions.
8 changes: 8 additions & 0 deletions .changeset/fifty-cheetahs-dance.md
@@ -0,0 +1,8 @@
---
"types-react-codemod": minor
---

Add codemod for required initial value in `useRef`

Added as `experimental-useRef-required-initial`.
Can be used on 18.x types but only intended for once https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64920 lands.
31 changes: 28 additions & 3 deletions README.md
Expand Up @@ -38,8 +38,8 @@ Positionals:
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
"deprecated-sfc", "deprecated-stateless-component",
"deprecated-void-function-component", "experimental-refobject-defaults",
"implicit-children", "preset-18", "preset-19", "scoped-jsx",
"useCallback-implicit-any"]
"experimental-useRef-required-initial", "implicit-children", "preset-18",
"preset-19", "scoped-jsx", "useCallback-implicit-any"]
paths [string] [required]

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

### `experimental-refobject-defaults`

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

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

### `experimental-useRef-required-initial`

WARNING: This is an experimental codemod to intended for codebases using unpublished types.
Only use if you're using https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64920.

`useRef` now always requires an initial value.
Implicit `undefined` is forbidden

```diff
import * as React from "react";
-React.useRef()
+React.useRef(undefined)
```

#### `experimental-useRef-required-initial` false-negative pattern A

Importing `useRef` via aliased named import will result in the transform being skipped.

```tsx
import { useRef as useReactRef } from "react";

// not transformed
useReactRef<number>();
```

## Supported platforms

The following list contains officially supported runtimes.
Expand Down
4 changes: 2 additions & 2 deletions bin/__tests__/types-react-codemod.js
Expand Up @@ -25,8 +25,8 @@ describe("types-react-codemod", () => {
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
"deprecated-sfc", "deprecated-stateless-component",
"deprecated-void-function-component", "experimental-refobject-defaults",
"implicit-children", "preset-18", "preset-19", "scoped-jsx",
"useCallback-implicit-any"]
"experimental-useRef-required-initial", "implicit-children", "preset-18",
"preset-19", "scoped-jsx", "useCallback-implicit-any"]
paths [string] [required]
Options:
Expand Down
12 changes: 12 additions & 0 deletions transforms/__tests__/preset-19.js
Expand Up @@ -7,6 +7,8 @@ describe("preset-19", () => {
let deprecatedReactChildTransform;
let deprecatedReactTextTransform;
let deprecatedVoidFunctionComponentTransform;
let refobjectDefaultsTransform;
let useRefRequiredInitialTransform;

function applyTransform(source, options = {}) {
return JscodeshiftTestUtils.applyTransform(preset19Transform, options, {
Expand All @@ -33,6 +35,12 @@ describe("preset-19", () => {
deprecatedVoidFunctionComponentTransform = mockTransform(
"../deprecated-void-function-component",
);
refobjectDefaultsTransform = mockTransform(
"../experimental-refobject-defaults",
);
useRefRequiredInitialTransform = mockTransform(
"../experimental-useRef-required-initial",
);

preset19Transform = require("../preset-19");
});
Expand All @@ -53,11 +61,15 @@ describe("preset-19", () => {
"deprecated-react-child",
"deprecated-react-text",
"deprecated-void-function-component",
"experimental-refobject-defaults",
"experimental-useRef-required-initial",
].join(","),
});

expect(deprecatedReactChildTransform).toHaveBeenCalled();
expect(deprecatedReactTextTransform).toHaveBeenCalled();
expect(deprecatedVoidFunctionComponentTransform).toHaveBeenCalled();
expect(refobjectDefaultsTransform).toHaveBeenCalled();
expect(useRefRequiredInitialTransform).toHaveBeenCalled();
});
});
65 changes: 65 additions & 0 deletions transforms/__tests__/useRef-required-initial.js
@@ -0,0 +1,65 @@
const { describe, expect, test } = require("@jest/globals");
const dedent = require("dedent");
const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils");
const useRefRequiredInitial = require("../experimental-useRef-required-initial");

function applyTransform(source, options = {}) {
return JscodeshiftTestUtils.applyTransform(useRefRequiredInitial, options, {
path: "test.tsx",
source: dedent(source),
});
}

describe("transform useRef-required-initial", () => {
test("not modified", () => {
expect(
applyTransform(`
import * as React from 'react';
interface Props {
children?: ReactNode;
}
`),
).toMatchInlineSnapshot(`
"import * as React from 'react';
interface Props {
children?: ReactNode;
}"
`);
});

test("named import", () => {
expect(
applyTransform(`
import { useRef } from 'react';
const myRef = useRef<number>();
`),
).toMatchInlineSnapshot(`
"import { useRef } from 'react';
const myRef = useRef<number>(undefined);"
`);
});

test("false-negative named renamed import", () => {
expect(
applyTransform(`
import { useRef as useReactRef } from 'react';
const myRef = useReactRef<number>();
`),
).toMatchInlineSnapshot(`
"import { useRef as useReactRef } from 'react';
const myRef = useReactRef<number>();"
`);
});

test("namespace import", () => {
expect(
applyTransform(`
import * as React from 'react';
const myRef = React.useRef<number>();
`),
).toMatchInlineSnapshot(`
"import * as React from 'react';
const myRef = React.useRef<number>(undefined);"
`);
});
});
42 changes: 42 additions & 0 deletions transforms/experimental-useRef-required-initial.js
@@ -0,0 +1,42 @@
const parseSync = require("./utils/parseSync");
const t = require("@babel/types");
const traverse = require("@babel/traverse").default;

/**
* @type {import('jscodeshift').Transform}
*
* Summary for Klarna's klapp@?
* TODO
*/
const useRefRequiredInitialTransform = (file) => {
const ast = parseSync(file);

let changedSome = false;

// ast.get("program").value is sufficient for unit tests but not actually running it on files
// TODO: How to test?
const traverseRoot = ast.paths()[0].value;
traverse(traverseRoot, {
CallExpression({ node: callExpression }) {
const isUseRefCall =
(callExpression.callee.type === "Identifier" &&
callExpression.callee.name === "useRef") ||
(callExpression.callee.type === "MemberExpression" &&
callExpression.callee.property.type === "Identifier" &&
callExpression.callee.property.name === "useRef");

if (isUseRefCall && callExpression.arguments.length === 0) {
changedSome = true;
callExpression.arguments = [t.identifier("undefined")];
}
},
});

// Otherwise some files will be marked as "modified" because formatting changed
if (changedSome) {
return ast.toSource();
}
return file.source;
};

module.exports = useRefRequiredInitialTransform;
6 changes: 5 additions & 1 deletion transforms/preset-19.js
Expand Up @@ -3,6 +3,7 @@ const deprecatedReactTextTransform = require("./deprecated-react-text");
const deprecatedVoidFunctionComponentTransform = require("./deprecated-void-function-component");
const refobjectDefaultsTransform = require("./experimental-refobject-defaults");
const scopedJsxTransform = require("./scoped-jsx");
const useRefRequiredInitialTransform = require("./experimental-useRef-required-initial");

/**
* @type {import('jscodeshift').Transform}
Expand All @@ -24,12 +25,15 @@ const transform = (file, api, options) => {
if (transformNames.has("deprecated-void-function-component")) {
transforms.push(deprecatedVoidFunctionComponentTransform);
}
if (transformNames.has("plain-refs")) {
if (transformNames.has("experimental-refobject-defaults")) {
transforms.push(refobjectDefaultsTransform);
}
if (transformNames.has("scoped-jsx")) {
transforms.push(scopedJsxTransform);
}
if (transformNames.has("experimental-useRef-required-initial")) {
transforms.push(useRefRequiredInitialTransform);
}

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

0 comments on commit 0047404

Please sign in to comment.