diff --git a/README.md b/README.md index e3958cf..20a8070 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,76 @@ guide](https://github.com/airbnb/javascript/blob/7684892951ef663e1c4e62ad57d662e npx react-codemod sort-comp ``` +#### `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
; + } +} +``` + +Output: + +```jsx +import * as React from "react"; + +class ParentComponent extends React.Component { + render() { + return ( +
{ + 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
; + } +} +``` + +Output (nothing changed): + +```jsx +import * as React from "react"; + +const refName = "refComponent"; + +class ParentComponent extends React.Component { + render() { + return
; + } +} +``` + #### `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'`). diff --git a/bin/cli.js b/bin/cli.js index 6640b37..1231ddd 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -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', diff --git a/transforms/__testfixtures__/string-refs/literal-with-owner.input.js b/transforms/__testfixtures__/string-refs/literal-with-owner.input.js new file mode 100644 index 0000000..6b29faa --- /dev/null +++ b/transforms/__testfixtures__/string-refs/literal-with-owner.input.js @@ -0,0 +1,15 @@ +import * as React from "react"; + +class ParentComponent extends React.Component { + render() { + return ( +
+
+ + +
+
+
+ ); + } +} diff --git a/transforms/__testfixtures__/string-refs/literal-with-owner.output.js b/transforms/__testfixtures__/string-refs/literal-with-owner.output.js new file mode 100644 index 0000000..dd79b41 --- /dev/null +++ b/transforms/__testfixtures__/string-refs/literal-with-owner.output.js @@ -0,0 +1,25 @@ +import * as React from "react"; + +class ParentComponent extends React.Component { + render() { + return ( +
{ + this.refs['P'] = current; + }} id="P"> +
{ + this.refs['P_P1'] = current; + }} id="P_P1"> + { + this.refs['P_P1_C1'] = current; + }} id="P_P1_C1" /> + { + this.refs['P_P1_C2'] = current; + }} id="P_P1_C2" /> +
+
{ + this.refs['P_OneOff'] = current; + }} id="P_OneOff" /> +
+ ); + } +} diff --git a/transforms/__testfixtures__/string-refs/literal-without-owner.input.js b/transforms/__testfixtures__/string-refs/literal-without-owner.input.js new file mode 100644 index 0000000..708d8af --- /dev/null +++ b/transforms/__testfixtures__/string-refs/literal-without-owner.input.js @@ -0,0 +1,3 @@ +import * as React from "react"; + +
; diff --git a/transforms/__testfixtures__/string-refs/literal-without-owner.output.js b/transforms/__testfixtures__/string-refs/literal-without-owner.output.js new file mode 100644 index 0000000..48af0cc --- /dev/null +++ b/transforms/__testfixtures__/string-refs/literal-without-owner.output.js @@ -0,0 +1,5 @@ +import * as React from "react"; + +
{ + this.refs['bad'] = current; +}} />; diff --git a/transforms/__testfixtures__/string-refs/typescript/literal-with-owner.input.tsx b/transforms/__testfixtures__/string-refs/typescript/literal-with-owner.input.tsx new file mode 100644 index 0000000..34b1177 --- /dev/null +++ b/transforms/__testfixtures__/string-refs/typescript/literal-with-owner.input.tsx @@ -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; + + render() { + return ( +
+
+ + +
+
+
+ ); + } +} diff --git a/transforms/__testfixtures__/string-refs/typescript/literal-with-owner.output.tsx b/transforms/__testfixtures__/string-refs/typescript/literal-with-owner.output.tsx new file mode 100644 index 0000000..d74412f --- /dev/null +++ b/transforms/__testfixtures__/string-refs/typescript/literal-with-owner.output.tsx @@ -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; + + render() { + return ( +
{ + this.refs['P'] = current; + }} id="P"> +
{ + this.refs['P_P1'] = current; + }} id="P_P1"> + { + this.refs['P_P1_C1'] = current; + }} id="P_P1_C1" /> + { + this.refs['P_P1_C2'] = current; + }} id="P_P1_C2" /> +
+
{ + this.refs['P_OneOff'] = current; + }} id="P_OneOff" /> +
+ ); + } +} diff --git a/transforms/__testfixtures__/string-refs/value-with-owner.input.js b/transforms/__testfixtures__/string-refs/value-with-owner.input.js new file mode 100644 index 0000000..38ded0f --- /dev/null +++ b/transforms/__testfixtures__/string-refs/value-with-owner.input.js @@ -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
; + } +} diff --git a/transforms/__testfixtures__/string-refs/value-with-owner.output.js b/transforms/__testfixtures__/string-refs/value-with-owner.output.js new file mode 100644 index 0000000..38ded0f --- /dev/null +++ b/transforms/__testfixtures__/string-refs/value-with-owner.output.js @@ -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
; + } +} diff --git a/transforms/__tests__/string-refs-test.js b/transforms/__tests__/string-refs-test.js new file mode 100644 index 0000000..e9fe935 --- /dev/null +++ b/transforms/__tests__/string-refs-test.js @@ -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" } + ) + ); + }); +}); diff --git a/transforms/string-refs.js b/transforms/string-refs.js new file mode 100644 index 0000000..aa36a16 --- /dev/null +++ b/transforms/string-refs.js @@ -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; +};