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

feat: Add codemod to transform string refs to arrow-functions #309

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,76 @@ guide](https://github.com/airbnb/javascript/blob/7684892951ef663e1c4e62ad57d662e
npx react-codemod sort-comp <path>
```

#### `string-refs`

WARNING: Only apply this codemod if you've fixed all warnings like this:

```
Warning: Component "div" contains the string ref "inner". Support for string refs will be removed in a future major release. We recommend using useRef() or createRef() instead.
```

This codemod will convert deprecated string refs to callback refs.

Input:

```jsx
import * as React from "react";

class ParentComponent extends React.Component {
render() {
return <div ref="refComponent" />;
}
}
```

Output:

```jsx
import * as React from "react";

class ParentComponent extends React.Component {
render() {
return (
<div
ref={(current) => {
this.refs["refComponent"] = current;
}}
/>
);
}
}
```

Note that this only works for string literals.
Referring to the ref with a variable will not trigger the transform:
Input:

```jsx
import * as React from "react";

const refName = "refComponent";

class ParentComponent extends React.Component {
render() {
return <div ref={refName} />;
}
}
```

Output (nothing changed):

```jsx
import * as React from "react";

const refName = "refComponent";

class ParentComponent extends React.Component {
render() {
return <div ref={refName} />;
}
}
```

#### `update-react-imports`

[As of Babel 7.9.0](https://babeljs.io/blog/2020/03/16/7.9.0#a-new-jsx-transform-11154-https-githubcom-babel-babel-pull-11154), when using `runtime: automatic` in `@babel/preset-react` or `@babel/plugin-transform-react-jsx`, you will not need to explicitly import React for compiling jsx. This codemod removes the redundant import statements. It also converts default imports (`import React from 'react'`) to named imports (e.g. `import { useState } from 'react'`).
Expand Down
5 changes: 5 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ const TRANSFORMER_INQUIRER_CHOICES = [
'Reorders React component methods to match the ESLint react/sort-comp rule.',
value: 'sort-comp'
},
{
name:
'string-refs: Converts deprecated string refs to callback refs.',
value: 'string-refs'
},
{
name: 'update-react-imports: Removes redundant import statements from explicitly importing React to compile JSX and converts default imports to destructured named imports',
value: 'update-react-imports',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from "react";

class ParentComponent extends React.Component {
render() {
return (
<div ref="P" id="P">
<div ref="P_P1" id="P_P1">
<span ref="P_P1_C1" id="P_P1_C1" />
<span ref="P_P1_C2" id="P_P1_C2" />
</div>
<div ref="P_OneOff" id="P_OneOff" />
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from "react";

class ParentComponent extends React.Component {
render() {
return (
<div ref={current => {
this.refs['P'] = current;
}} id="P">
<div ref={current => {
this.refs['P_P1'] = current;
}} id="P_P1">
<span ref={current => {
this.refs['P_P1_C1'] = current;
}} id="P_P1_C1" />
<span ref={current => {
this.refs['P_P1_C2'] = current;
}} id="P_P1_C2" />
</div>
<div ref={current => {
this.refs['P_OneOff'] = current;
}} id="P_OneOff" />
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as React from "react";

<div ref="bad" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as React from "react";

<div ref={current => {
this.refs['bad'] = current;
}} />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from "react";

class ParentComponent extends React.Component {
// Actual code probably has more accurate types.
// Codemod might cause TypeScript errors but these are good errors since they reveal unsound code.
refs: Record<string, any>;

render() {
return (
<div ref="P" id="P">
<div ref="P_P1" id="P_P1">
<span ref="P_P1_C1" id="P_P1_C1" />
<span ref="P_P1_C2" id="P_P1_C2" />
</div>
<div ref="P_OneOff" id="P_OneOff" />
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from "react";

class ParentComponent extends React.Component {
// Actual code probably has more accurate types.
// Codemod might cause TypeScript errors but these are good errors since they reveal unsound code.
refs: Record<string, any>;

render() {
return (
<div ref={current => {
this.refs['P'] = current;
}} id="P">
<div ref={current => {
this.refs['P_P1'] = current;
}} id="P_P1">
<span ref={current => {
this.refs['P_P1_C1'] = current;
}} id="P_P1_C1" />
<span ref={current => {
this.refs['P_P1_C2'] = current;
}} id="P_P1_C2" />
</div>
<div ref={current => {
this.refs['P_OneOff'] = current;
}} id="P_OneOff" />
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from "react";

class ParentComponent extends React.Component {
render() {
const refName = "P";
// Giving up. Would need to implement scope tracking.
return <div ref={refName} id="P"></div>;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from "react";

class ParentComponent extends React.Component {
render() {
const refName = "P";
// Giving up. Would need to implement scope tracking.
return <div ref={refName} id="P"></div>;
}
}
41 changes: 41 additions & 0 deletions transforms/__tests__/string-refs-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

"use strict";

const flowTests = [
"literal-with-owner",
"literal-without-owner",
"value-with-owner",
];

const typescriptTests = ["literal-with-owner"];

const defineTest = require("jscodeshift/dist/testUtils").defineTest;

describe("string-refs", () => {
describe("flow", () => {
flowTests.forEach((test) =>
defineTest(__dirname, "string-refs", null, `string-refs/${test}`, {
parser: "flow",
})
);
});

describe("typescript", () => {
typescriptTests.forEach((test) =>
defineTest(
__dirname,
"string-refs",
null,
`string-refs/typescript/${test}`,
{ parser: "tsx" }
)
);
});
});
69 changes: 69 additions & 0 deletions transforms/string-refs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

"use strict";

export default (file, api, options) => {
const j = api.jscodeshift;

const printOptions = options.printOptions || {
quote: "single",
trailingComma: true,
};

const root = j(file.source);

let hasModifications = false;

root
.find(j.JSXAttribute, (node) => {
return node.name.name === "ref";
})
.forEach((jsxAttributePath) => {
const valuePath = jsxAttributePath.get("value");
if (
// Flow parser
valuePath.value.type === "Literal" ||
// TSX parser
valuePath.value.type === "StringLiteral"
) {
hasModifications = true;
// This might shadow existing variables.
// But this should be safe since we control what identifiers we're reading in this block.
// It will trigger ESLint's `no-shadow` though.
// Babel has a helper to get a identifier that doesn't shadow existing vars.
// Maybe JSCodeShift has such a helper as well?
const currentIdentifierName = "current";
valuePath.replace(
// {(current) => { this.refs[valuePath.node.value] = current }}
j.jsxExpressionContainer(
j.arrowFunctionExpression(
[j.identifier(currentIdentifierName)],
j.blockStatement([
j.expressionStatement(
j.assignmentExpression(
"=",
j.memberExpression(
j.memberExpression(
j.thisExpression(),
j.identifier("refs")
),
j.literal(valuePath.node.value)
),
j.identifier(currentIdentifierName)
)
),
])
)
)
);
}
});

return hasModifications ? root.toSource(printOptions) : file.source;
};