Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add codemod for new RefObject behavior #208

Merged
merged 10 commits into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fifty-pumpkins-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"types-react-codemod": minor
---

Add codemod for new `useRef` behavior
eps1lon marked this conversation as resolved.
Show resolved Hide resolved

TODO link `@types/react` PR that landed this change
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ types-react-codemod <codemod> <paths...>
Positionals:
codemod [string] [required] [choices: "context-any", "deprecated-react-child",
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
"deprecated-sfc", "deprecated-stateless-component", "implicit-children",
"deprecated-sfc", "deprecated-stateless-component",
"deprecated-void-function-component", "implicit-children", "plain-refs",
"preset-18", "preset-19", "useCallback-implicit-any"]
paths [string] [required]

Expand Down Expand Up @@ -215,7 +216,7 @@ You should select all and audit the changed files regardless.
}
```

#### `deprecated-react-text` false-negative pattern A
#### `deprecated-react-child` false-negative pattern A

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

Expand Down Expand Up @@ -262,6 +263,29 @@ In earlier versions of `@types/react` this codemod would change the typings.
+const Component: React.FunctionComponent = () => {}
```

### `plain-refs`

WARNING: Only apply to codebases using `@types/react@^18.0.0`.

`RefObject` no longer makes `current` nullable by default

```diff
import * as React from "react";
-const myRef: React.RefObject<View>
+const myRef: React.RefObject<View | null>
```

#### `plain-refs` false-negative pattern A

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

```tsx
import { RefObject as MyRefObject } from "react";

// not transformed
const myRef: MyRefObject<View>;
```

## Supported platforms

The following list contains officially supported runtimes.
Expand Down
4 changes: 2 additions & 2 deletions bin/__tests__/types-react-codemod.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ describe("types-react-codemod", () => {
codemod [string] [required] [choices: "context-any", "deprecated-react-child",
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
"deprecated-sfc", "deprecated-stateless-component",
"deprecated-void-function-component", "implicit-children", "preset-18",
"preset-19", "useCallback-implicit-any"]
"deprecated-void-function-component", "implicit-children", "plain-refs",
"preset-18", "preset-19", "useCallback-implicit-any"]
paths [string] [required]

Options:
Expand Down
111 changes: 111 additions & 0 deletions transforms/__tests__/plain-refs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const { describe, expect, test } = require("@jest/globals");
const dedent = require("dedent");
const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils");
const deprecatedReactChildTransform = require("../plain-refs");

function applyTransform(source, options = {}) {
return JscodeshiftTestUtils.applyTransform(
deprecatedReactChildTransform,
options,
{
path: "test.d.ts",
source: dedent(source),
}
);
}

describe("transform plain-refs", () => {
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 { RefObject } from 'react';
const myRef: RefObject<View> = createRef();
`)
).toMatchInlineSnapshot(`
"import { RefObject } from 'react';
const myRef: RefObject<View | null> = createRef();"
`);
});

test("false-negative named renamed import", () => {
expect(
applyTransform(`
import { RefObject as MyRefObject } from 'react';
const myRef: MyRefObject<View> = createRef();
`)
).toMatchInlineSnapshot(`
"import { RefObject as MyRefObject } from 'react';
const myRef: MyRefObject<View> = createRef();"
`);
});

test("namespace import", () => {
expect(
applyTransform(`
import * as React from 'react';
const myRef: React.RefObject<View> = createRef();
`)
).toMatchInlineSnapshot(`
"import * as React from 'react';
const myRef: React.RefObject<View | null> = createRef();"
`);
});

test("unions", () => {
expect(
applyTransform(`
import * as React from 'react';
const myRef: React.RefObject<number | string> = createRef();
`)
).toMatchInlineSnapshot(`
"import * as React from 'react';
const myRef: React.RefObject<number | string | null> = createRef();"
`);
});

test("no change on apparent nullable", () => {
expect(
applyTransform(`
import * as React from 'react';
const myRef: React.RefObject<null | number> = createRef();
`)
).toMatchInlineSnapshot(`
"import * as React from 'react';
const myRef: React.RefObject<null | number> = createRef();"
`);
});

test("no change on apparent any", () => {
expect(
applyTransform(`
import * as React from 'react';
const anyRef: React.RefObject<any> = createRef();
const stillAnyRef: React.RefObject<any | number> = createRef();
type AnyAlias = any;
const notApparentAny: React.RefObject<AnyAlias> = createRef();
`)
).toMatchInlineSnapshot(`
"import * as React from 'react';
const anyRef: React.RefObject<any> = createRef();
const stillAnyRef: React.RefObject<any | number> = createRef();
type AnyAlias = any;
const notApparentAny: React.RefObject<AnyAlias | null> = createRef();"
`);
});
});
79 changes: 79 additions & 0 deletions transforms/plain-refs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const parseSync = require("./utils/parseSync");

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

let changedSome = false;

ast
.find(j.TSTypeReference, (typeReference) => {
const { typeName } = typeReference;
if (typeName.type === "TSTypeParameter") {
// TODO: What code produces this AST?
return false;
} else {
const identifier =
typeName.type === "TSQualifiedName"
? /** @type {any} */ (typeName.right)
: typeName;
return ["RefObject"].includes(identifier.name);
}
})
.forEach((typeReference) => {
/**
* @type {import('ast-types').namedTypes.TSTypeParameterInstantiation['params'] | undefined}
*/
const params = typeReference.get("typeParameters").get("params").value;
if (params !== undefined) {
const [typeNode] = params;

/**
* @type {typeof typeNode | undefined}
*/
let nullableType;
if (typeNode.type === "TSUnionType") {
const typeIsApparentAny = typeNode.types.some((unionMember) => {
return unionMember.type === "TSAnyKeyword";
});
if (!typeIsApparentAny) {
const unionIsApparentlyNullable = typeNode.types.some(
(unionMember) => {
return unionMember.type === "TSNullKeyword";
}
);

nullableType = unionIsApparentlyNullable
? typeNode
: j.tsUnionType([...typeNode.types, j.tsNullKeyword()]);
}
} else {
if (typeNode.type !== "TSAnyKeyword") {
nullableType = j.tsUnionType([typeNode, j.tsNullKeyword()]);
}
}

if (nullableType !== undefined && nullableType !== typeNode) {
// Ideally we'd clone the `typeReference` path and add `typeParameters`.
// But I don't know if there's an API or better pattern for it.
typeReference.value.typeParameters = j.tsTypeParameterInstantiation([
nullableType,
]);
changedSome = true;
}
}
});

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

module.exports = plainRefsTransform;
4 changes: 4 additions & 0 deletions transforms/preset-19.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const deprecatedReactChildTransform = require("./deprecated-react-child");
const deprecatedReactTextTransform = require("./deprecated-react-text");
const deprecatedVoidFunctionComponentTransform = require("./deprecated-void-function-component");
const plainRefsTransform = require("./plain-refs");

/**
* @type {import('jscodeshift').Transform}
Expand All @@ -22,6 +23,9 @@ const transform = (file, api, options) => {
if (transformNames.has("deprecated-void-function-component")) {
transforms.push(deprecatedVoidFunctionComponentTransform);
}
if (transformNames.has("plain-refs")) {
transforms.push(plainRefsTransform);
}

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