Skip to content

Commit

Permalink
fix: Find and replace type usage in type parameters of call expressio…
Browse files Browse the repository at this point in the history
…ns (#344)
  • Loading branch information
eps1lon committed Feb 15, 2024
1 parent f05624f commit 8c27551
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 49 deletions.
15 changes: 15 additions & 0 deletions .changeset/short-zoos-type.md
@@ -0,0 +1,15 @@
---
"types-react-codemod": patch
---

Find and replace type usage in type parameters of call expressions

Now we properly detect that e.g. `JSX` is used in `someFunctionWithTypeParameters<JSX>()`.

Affected codemods:

- `deprecated-react-child`
- `deprecated-react-fragment`
- `deprecated-react-node-array`
- `deprecated-react-text`
- `scoped-jsx`
12 changes: 12 additions & 0 deletions transforms/__tests__/deprecated-react-child.js
Expand Up @@ -78,4 +78,16 @@ describe("transform deprecated-react-child", () => {
}"
`);
});

test("as type parameter", () => {
expect(
applyTransform(`
import * as React from 'react';
createAction<React.ReactChild>()
`),
).toMatchInlineSnapshot(`
"import * as React from 'react';
createAction<React.ReactElement | number | string>()"
`);
});
});
12 changes: 12 additions & 0 deletions transforms/__tests__/deprecated-react-fragment.js
Expand Up @@ -126,4 +126,16 @@ describe("transform deprecated-react-node-array", () => {
}"
`);
});

test("as type parameter", () => {
expect(
applyTransform(`
import * as React from 'react';
createComponent<React.ReactFragment>();
`),
).toMatchInlineSnapshot(`
"import * as React from 'react';
createComponent<Iterable<React.ReactNode>>();"
`);
});
});
12 changes: 12 additions & 0 deletions transforms/__tests__/deprecated-react-node-array.js
Expand Up @@ -126,4 +126,16 @@ describe("transform deprecated-react-node-array", () => {
}"
`);
});

test("in type parameters", () => {
expect(
applyTransform(`
import * as React from 'react';
createComponent<ReactNodeArray>();
`),
).toMatchInlineSnapshot(`
"import * as React from 'react';
createComponent<ReadonlyArray<ReactNode>>();"
`);
});
});
12 changes: 12 additions & 0 deletions transforms/__tests__/deprecated-react-text.js
Expand Up @@ -78,4 +78,16 @@ describe("transform deprecated-react-text", () => {
}"
`);
});

test("in type parameters", () => {
expect(
applyTransform(`
import * as React from 'react';
createComponent<React.ReactText>();
`),
).toMatchInlineSnapshot(`
"import * as React from 'react';
createComponent<number | string>();"
`);
});
});
18 changes: 17 additions & 1 deletion transforms/__tests__/scoped-jsx.js
Expand Up @@ -5,7 +5,7 @@ const scopedJSXTransform = require("../scoped-jsx");

function applyTransform(source, options = {}) {
return JscodeshiftTestUtils.applyTransform(scopedJSXTransform, options, {
path: "test.d.ts",
path: "test.tsx",
source: dedent(source),
});
}
Expand Down Expand Up @@ -175,4 +175,20 @@ describe("transform scoped-jsx", () => {
declare const attributes: JSX.LibraryManagedAttributes<A, B>;"
`);
});

test("detects usage in type parameters", () => {
expect(
applyTransform(`
import React, { useMemo } from 'react'
[].reduce<JSX.Element[]>((acc, component, i) => {});
[].reduce((acc, component, i) => {})
`),
).toMatchInlineSnapshot(`
"import React, { useMemo, type JSX } from 'react';
[].reduce<JSX.Element[]>((acc, component, i) => {});
[].reduce((acc, component, i) => {})"
`);
});
});
23 changes: 18 additions & 5 deletions transforms/deprecated-react-child.js
@@ -1,4 +1,7 @@
const parseSync = require("./utils/parseSync");
const {
findTSTypeReferenceCollections,
} = require("./utils/jscodeshift-bugfixes");

/**
* @type {import('jscodeshift').Transform}
Expand All @@ -7,8 +10,10 @@ const deprecatedReactChildTransform = (file, api) => {
const j = api.jscodeshift;
const ast = parseSync(file);

const changedIdentifiers = ast
.find(j.TSTypeReference, (node) => {
const reactChildTypeReferences = findTSTypeReferenceCollections(
j,
ast,
(node) => {
const { typeName } = node;
/**
* @type {import('jscodeshift').Identifier | null}
Expand All @@ -24,8 +29,12 @@ const deprecatedReactChildTransform = (file, api) => {
}

return identifier !== null && identifier.name === "ReactChild";
})
.replaceWith(() => {
},
);

let didChangeIdentifiers = false;
for (const typeReferences of reactChildTypeReferences) {
const changedIdentifiers = typeReferences.replaceWith(() => {
// `React.ReactElement | number | string`
return j.tsUnionType([
// React.ReactElement
Expand All @@ -39,9 +48,13 @@ const deprecatedReactChildTransform = (file, api) => {
j.tsStringKeyword(),
]);
});
if (changedIdentifiers.length > 0) {
didChangeIdentifiers = true;
}
}

// Otherwise some files will be marked as "modified" because formatting changed
if (changedIdentifiers.length > 0) {
if (didChangeIdentifiers) {
return ast.toSource();
}
return file.source;
Expand Down
46 changes: 33 additions & 13 deletions transforms/deprecated-react-fragment.js
@@ -1,11 +1,16 @@
const parseSync = require("./utils/parseSync");
const {
findTSTypeReferenceCollections,
} = require("./utils/jscodeshift-bugfixes");
/**
* @type {import('jscodeshift').Transform}
*/
const deprecatedReactFragmentTransform = (file, api) => {
const j = api.jscodeshift;
const ast = parseSync(file);

let hasChanges = false;

const hasReactNodeImport = ast.find(j.ImportSpecifier, (node) => {
const { imported, local } = node;
return (
Expand All @@ -24,6 +29,9 @@ const deprecatedReactFragmentTransform = (file, api) => {
(local == null || local.name === "ReactFragment")
);
});
if (reactFragmentImports.length > 0) {
hasChanges = true;
}

if (hasReactNodeImport.length > 0) {
reactFragmentImports.remove();
Expand All @@ -40,15 +48,19 @@ const deprecatedReactFragmentTransform = (file, api) => {
});
}

const changedIdentifiers = ast
.find(j.TSTypeReference, (node) => {
const reactFragmentTypeReferences = findTSTypeReferenceCollections(
j,
ast,
(node) => {
const { typeName } = node;

return (
typeName.type === "Identifier" && typeName.name === "ReactFragment"
);
})
.replaceWith(() => {
},
);
for (const typeReferences of reactFragmentTypeReferences) {
const changedIdentifiers = typeReferences.replaceWith(() => {
// `Iterable<ReactNode>`
return j.tsTypeReference(
j.identifier("Iterable"),
Expand All @@ -57,18 +69,26 @@ const deprecatedReactFragmentTransform = (file, api) => {
]),
);
});
if (changedIdentifiers.length > 0) {
hasChanges = true;
}
}

const changedQualifiedNames = ast
.find(j.TSTypeReference, (node) => {
const reactFragmentQualifiedNamesReferences = findTSTypeReferenceCollections(
j,
ast,
(node) => {
const { typeName } = node;

return (
typeName.type === "TSQualifiedName" &&
typeName.right.type === "Identifier" &&
typeName.right.name === "ReactFragment"
);
})
.replaceWith((path) => {
},
);
for (const typeReferences of reactFragmentQualifiedNamesReferences) {
const changedQualifiedNames = typeReferences.replaceWith((path) => {
const { node } = path;
const typeName = /** @type {import('jscodeshift').TSQualifiedName} */ (
node.typeName
Expand All @@ -83,13 +103,13 @@ const deprecatedReactFragmentTransform = (file, api) => {
]),
);
});
if (changedQualifiedNames.length > 0) {
hasChanges = true;
}
}

// Otherwise some files will be marked as "modified" because formatting changed
if (
changedIdentifiers.length > 0 ||
changedQualifiedNames.length > 0 ||
reactFragmentImports.length > 0
) {
if (hasChanges) {
return ast.toSource();
}
return file.source;
Expand Down
48 changes: 35 additions & 13 deletions transforms/deprecated-react-node-array.js
@@ -1,4 +1,7 @@
const parseSync = require("./utils/parseSync");
const {
findTSTypeReferenceCollections,
} = require("./utils/jscodeshift-bugfixes");

/**
* @type {import('jscodeshift').Transform}
Expand All @@ -7,6 +10,8 @@ const deprecatedReactNodeArrayTransform = (file, api) => {
const j = api.jscodeshift;
const ast = parseSync(file);

let hasChanges = false;

const hasReactNodeImport = ast.find(j.ImportSpecifier, (node) => {
const { imported, local } = node;
return (
Expand All @@ -25,6 +30,9 @@ const deprecatedReactNodeArrayTransform = (file, api) => {
(local == null || local.name === "ReactNodeArray")
);
});
if (reactNodeArrayImports.length > 0) {
hasChanges = true;
}

if (hasReactNodeImport.length > 0) {
reactNodeArrayImports.remove();
Expand All @@ -42,15 +50,19 @@ const deprecatedReactNodeArrayTransform = (file, api) => {
});
}

const changedIdentifiers = ast
.find(j.TSTypeReference, (node) => {
const reactNodeArrayTypeReferences = findTSTypeReferenceCollections(
j,
ast,
(node) => {
const { typeName } = node;

return (
typeName.type === "Identifier" && typeName.name === "ReactNodeArray"
);
})
.replaceWith(() => {
},
);
for (const typeReferences of reactNodeArrayTypeReferences) {
const changedIdentifiers = typeReferences.replaceWith(() => {
// `ReadonlyArray<ReactNode>`
return j.tsTypeReference(
j.identifier("ReadonlyArray"),
Expand All @@ -60,17 +72,26 @@ const deprecatedReactNodeArrayTransform = (file, api) => {
);
});

const changedQualifiedNames = ast
.find(j.TSTypeReference, (node) => {
if (changedIdentifiers.length > 0) {
hasChanges = true;
}
}

const reactNodeArrayQualifiedTypeReferences = findTSTypeReferenceCollections(
j,
ast,
(node) => {
const { typeName } = node;

return (
typeName.type === "TSQualifiedName" &&
typeName.right.type === "Identifier" &&
typeName.right.name === "ReactNodeArray"
);
})
.replaceWith((path) => {
},
);
for (const typeReferences of reactNodeArrayQualifiedTypeReferences) {
const changedQualifiedNames = typeReferences.replaceWith((path) => {
const { node } = path;
const typeName = /** @type {import('jscodeshift').TSQualifiedName} */ (
node.typeName
Expand All @@ -86,12 +107,13 @@ const deprecatedReactNodeArrayTransform = (file, api) => {
);
});

if (changedQualifiedNames.length > 0) {
hasChanges = true;
}
}

// Otherwise some files will be marked as "modified" because formatting changed
if (
changedIdentifiers.length > 0 ||
changedQualifiedNames.length > 0 ||
reactNodeArrayImports.length > 0
) {
if (hasChanges) {
return ast.toSource();
}
return file.source;
Expand Down

0 comments on commit 8c27551

Please sign in to comment.