From d8bc38350f6d6ddeaec7407c158ddcd5b73d269a Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 7 Apr 2017 13:06:55 -0700 Subject: [PATCH] Replace React.propTypes with PropTypes from prop-types --- transforms/React-propTypes-to-prop-types.js | 172 ++++++++++++++++++ .../default-and-named-import.input.js | 17 ++ .../default-and-named-import.output.js | 19 ++ .../default-import.input.js | 17 ++ .../default-import.output.js | 19 ++ .../no-change-import.input.js | 18 ++ .../no-change-import.output.js | 0 .../no-change-require.input.js | 18 ++ .../no-change-require.output.js | 0 .../require-destructured-multi.input.js | 18 ++ .../require-destructured-multi.output.js | 21 +++ .../require-destructured-only.input.js | 18 ++ .../require-destructured-only.output.js | 18 ++ .../require.input.js | 17 ++ .../require.output.js | 19 ++ .../React-propTypes-to-prop-types-test.js | 34 ++++ 16 files changed, 425 insertions(+) create mode 100644 transforms/React-propTypes-to-prop-types.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/default-and-named-import.input.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/default-and-named-import.output.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/default-import.input.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/default-import.output.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-import.input.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-import.output.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-require.input.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-require.output.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-multi.input.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-multi.output.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-only.input.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-only.output.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/require.input.js create mode 100644 transforms/__testfixtures__/React-propTypes-to-prop-types/require.output.js create mode 100644 transforms/__tests__/React-propTypes-to-prop-types-test.js diff --git a/transforms/React-propTypes-to-prop-types.js b/transforms/React-propTypes-to-prop-types.js new file mode 100644 index 00000000..83e8e8b3 --- /dev/null +++ b/transforms/React-propTypes-to-prop-types.js @@ -0,0 +1,172 @@ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +'use strict'; + +// import React from 'react'; +const isReactImport = path => ( + path.node.specifiers.some(specifier => ( + specifier.type === 'ImportDefaultSpecifier' && + specifier.local.name === 'React' + )) +); + +// const React = require('react'); +const isReactRequire = path => ( + path.node.callee.type === 'Identifier' && + path.parent.node.type === 'VariableDeclarator' && + ( + path.parent.node.id.type === 'Identifier' && + path.parent.node.id.name === 'React' || + path.parent.node.id.type === 'ObjectPattern' && + path.parent.node.id.properties.some( + property => property.value.name === 'React' + ) + ) +); + +// React.PropTypes +const isReactPropTypes = path => ( + path.node.name === 'PropTypes' && + path.parent.node.type === 'MemberExpression' && + path.parent.node.object.name === 'React' +); + +// If any PropTypes references exist, add a 'prop-types' import (or require) +function addPropTypesImport(j, root) { + if (useImportSyntax(j, root)) { + const importStatement = j.importDeclaration( + [j.importDefaultSpecifier(j.identifier('PropTypes'))], + j.literal('prop-types') + ); + + root + .find(j.ImportDeclaration) + .filter(isReactImport) + .forEach(path => { + j(path).insertAfter(importStatement); + }); + } else { + const requireStatement = useVar(j, root) + ? j.template.statement`var PropTypes = require('prop-types');` + : j.template.statement`const PropTypes = require('prop-types');`; + + root + .find(j.CallExpression, {callee: {name: 'require'}}) + .filter(isReactRequire) + .forEach(path => { + j(path.parent.parent).insertAfter(requireStatement); + }); + } +} + +// Remove PropTypes destructure statements (eg const { ProptTypes } = React) +function removeDestructuredPropTypeStatements(j, root) { + let hasModifications = false; + + root + .find(j.ObjectPattern) + .filter(path => ( + path.parent.node.init.name === 'React' && + path.node.properties.some( + property => property.key.name === 'PropTypes' + ) + )) + .forEach(path => { + hasModifications = true; + + // Remove the PropTypes key + path.node.properties = path.node.properties.filter( + property => property.key.name !== 'PropTypes' + ); + + // If this was the only property, remove the entire statement. + if (path.node.properties.length === 0) { + path.parent.parent.replace(''); + } + }); + + return hasModifications; +} + +// Remove old { PropTypes } imports +function removePropTypesImport(j, root) { + let hasModifications = false; + + root + .find(j.Identifier) + .filter(path => ( + path.node.name === 'PropTypes' && + path.parent.node.type === 'ImportSpecifier' + )) + .forEach(path => { + hasModifications = true; + + const importDeclaration = path.parent.parent.node; + importDeclaration.specifiers = importDeclaration.specifiers.filter( + specifier => ( + !specifier.imported || + specifier.imported.name !== 'PropTypes' + ) + ); + }); + + return hasModifications; +} + +// Replace all React.PropTypes instances with PropTypes +function replacePropTypesReferences(j, root) { + let hasModifications = false; + + root + .find(j.Identifier) + .filter(isReactPropTypes) + .forEach(path => { + hasModifications = true; + + j(path.parent).replaceWith( + j.identifier('PropTypes') + ); + }); + + return hasModifications; +} + +// Program uses ES import syntax +function useImportSyntax(j, root) { + return root + .find(j.CallExpression, {callee: {name: 'require'}}) + .length === 0; +} + +// Program uses var keywords +function useVar(j, root) { + return root + .find(j.VariableDeclaration, {kind: 'const'}) + .length === 0; +} + +module.exports = function(file, api, options) { + const j = api.jscodeshift; + const root = j(file.source); + + let hasModifications = false; + hasModifications = replacePropTypesReferences(j, root) || hasModifications; + hasModifications = removePropTypesImport(j, root) || hasModifications; + hasModifications = removeDestructuredPropTypeStatements(j, root) || hasModifications; + + if (hasModifications) { + addPropTypesImport(j, root); + } + + return hasModifications + ? root.toSource({ quote: 'single' }) + : null; +}; diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/default-and-named-import.input.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/default-and-named-import.input.js new file mode 100644 index 00000000..466c1e22 --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/default-and-named-import.input.js @@ -0,0 +1,17 @@ +import React, { Component, PropTypes } from 'react'; + +class ClassComponent extends Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/default-and-named-import.output.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/default-and-named-import.output.js new file mode 100644 index 00000000..21acdaf0 --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/default-and-named-import.output.js @@ -0,0 +1,19 @@ +import React, { Component } from 'react'; + +import PropTypes from 'prop-types'; + +class ClassComponent extends Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/default-import.input.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/default-import.input.js new file mode 100644 index 00000000..c950c7ec --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/default-import.input.js @@ -0,0 +1,17 @@ +import React from 'React'; + +class ClassComponent extends React.Component { + static propTypes = { + text: React.PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: React.PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/default-import.output.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/default-import.output.js new file mode 100644 index 00000000..7ca68a8f --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/default-import.output.js @@ -0,0 +1,19 @@ +import React from 'React'; + +import PropTypes from 'prop-types'; + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-import.input.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-import.input.js new file mode 100644 index 00000000..41d7403a --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-import.input.js @@ -0,0 +1,18 @@ +import React from 'React'; +import PropTypes from 'prop-types' + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-import.output.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-import.output.js new file mode 100644 index 00000000..e69de29b diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-require.input.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-require.input.js new file mode 100644 index 00000000..32d21c42 --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-require.input.js @@ -0,0 +1,18 @@ +const React = require('React'); +const PropTypes = require('prop-types'); + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-require.output.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/no-change-require.output.js new file mode 100644 index 00000000..e69de29b diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-multi.input.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-multi.input.js new file mode 100644 index 00000000..2db70ad9 --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-multi.input.js @@ -0,0 +1,18 @@ +const React = require('react'); +const { Component, PropTypes } = React; + +class ClassComponent extends Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-multi.output.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-multi.output.js new file mode 100644 index 00000000..98e1e1d8 --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-multi.output.js @@ -0,0 +1,21 @@ +const React = require('react'); +const PropTypes = require('prop-types'); +const { + Component +} = React; + +class ClassComponent extends Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-only.input.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-only.input.js new file mode 100644 index 00000000..c3e42141 --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-only.input.js @@ -0,0 +1,18 @@ +const React = require('react'); +const { PropTypes } = React; + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-only.output.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-only.output.js new file mode 100644 index 00000000..b46acbaa --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/require-destructured-only.output.js @@ -0,0 +1,18 @@ +const React = require('react'); +const PropTypes = require('prop-types'); + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/require.input.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/require.input.js new file mode 100644 index 00000000..b0bd1ef4 --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/require.input.js @@ -0,0 +1,17 @@ +const React = require('React'); + +class ClassComponent extends React.Component { + static propTypes = { + text: React.PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: React.PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-propTypes-to-prop-types/require.output.js b/transforms/__testfixtures__/React-propTypes-to-prop-types/require.output.js new file mode 100644 index 00000000..b1b1cbd0 --- /dev/null +++ b/transforms/__testfixtures__/React-propTypes-to-prop-types/require.output.js @@ -0,0 +1,19 @@ +const React = require('React'); + +const PropTypes = require('prop-types'); + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__tests__/React-propTypes-to-prop-types-test.js b/transforms/__tests__/React-propTypes-to-prop-types-test.js new file mode 100644 index 00000000..3382bd0b --- /dev/null +++ b/transforms/__tests__/React-propTypes-to-prop-types-test.js @@ -0,0 +1,34 @@ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +'use strict'; + +const tests = [ + 'default-and-named-import', + 'default-import', + 'no-change-import', + 'no-change-require', + 'require-destructured-multi', + 'require-destructured-only', + 'require', +]; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +describe('React-propTypes-to-prop-types', () => { + tests.forEach(test => + defineTest( + __dirname, + 'React-propTypes-to-prop-types', + null, + `React-propTypes-to-prop-types/${ test }` + ) + ); +});