Skip to content
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,15 @@ jscodeshift -t react-codemod/transforms/manual-bind-to-arrow.js <path>

#### `pure-component`

Converts ES6 classes that only have a render method, only have safe properties
(statics and props), and do not have refs to Stateless Functional Components.

Option `useArrows` converts to arrow function. Converts to `function` by default.
Option `destructuring` will destructure props in the argument where it is safe to do so.
Note these options must be passed on the command line as `--useArrows=true` (`--useArrows` won't work)

```sh
jscodeshift -t react-codemod/transforms/pure-component.js <path>
jscodeshift -t react-codemod/transforms/pure-component.js <path> [--useArrows=true --destructuring=true]
```

#### `pure-render-mixin`
Expand Down
51 changes: 51 additions & 0 deletions transforms/__testfixtures__/pure-component-destructuring.input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';

var React = require('React');

const shadow = 'shadow';

function doSomething(props) { return props; }

class ShouldDestsructure extends React.Component {
render() {
return <div className={this.props.foo} />;
}
}

class ShouldDestructureAndRemoveDuplicateDeclaration extends React.Component {
render() {
const fizz = { buzz: 'buzz' };
const bar = this.props.bar;
const baz = this.props.bizzaz;
const buzz = fizz.buzz;
return <div className={this.props.foo} bar={bar} baz={baz} buzz={buzz} />;
}
}

class UsesThisDotProps extends React.Component {
render() {
doSomething(this.props);
return <div className={this.props.foo} />;
}
}

class DestructuresThisDotProps extends React.Component {
// would be nice to destructure in this case
render() {
const { bar } = this.props;
return <div className={this.props.foo} bar={bar} />;
}
}

class HasShadowProps extends React.Component {
render() {
return <div shadow={shadow} propsShadow={this.props.shadow} />;
}
}

class PureWithTypes extends React.Component {
props: { foo: string };
render() {
return <div className={this.props.foo} />;
}
}
50 changes: 50 additions & 0 deletions transforms/__testfixtures__/pure-component-destructuring.output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

var React = require('React');

const shadow = 'shadow';

function doSomething(props) { return props; }

function ShouldDestsructure(
{
foo,
},
) {
return <div className={foo} />;
}

function ShouldDestructureAndRemoveDuplicateDeclaration(
{
bar,
bizzaz,
foo,
},
) {
const fizz = { buzz: 'buzz' };
const baz = bizzaz;
const buzz = fizz.buzz;
return <div className={foo} bar={bar} baz={baz} buzz={buzz} />;
}

function UsesThisDotProps(props) {
doSomething(props);
return <div className={props.foo} />;
}

function DestructuresThisDotProps(props) {
const { bar } = props;
return <div className={props.foo} bar={bar} />;
}

function HasShadowProps(props) {
return <div shadow={shadow} propsShadow={props.shadow} />;
}

function PureWithTypes(
{
foo: string,
},
) {
return <div className={foo} />;
}
3 changes: 2 additions & 1 deletion transforms/__tests__/pure-component-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@

const defineTest = require('jscodeshift/dist/testUtils').defineTest;
defineTest(__dirname, 'pure-component');
defineTest(__dirname, 'pure-component', {useArrows: true}, 'pure-component2');
defineTest(__dirname, 'pure-component', { useArrows: true }, 'pure-component2');
defineTest(__dirname, 'pure-component', { destructuring: true }, 'pure-component-destructuring');
157 changes: 138 additions & 19 deletions transforms/pure-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = function(file, api, options) {
const ReactUtils = require('./utils/ReactUtils')(j);

const useArrows = options.useArrows || false;
const destructuringEnabled = options.destructuring || false;
const silenceWarnings = options.silenceWarnings || false;
const printOptions = options.printOptions || {
quote: 'single',
Expand Down Expand Up @@ -85,31 +86,144 @@ module.exports = function(file, api, options) {
return identifier;
};

const isDuplicateDeclaration = (path, pre) => {
if (path && path.value && path.value.id && path.value.init) {
const initName = pre ? path.value.init.property && path.value.init.property.name :
path.value.init.name;
return path.value.id.name === initName;
}
return false;
};

const needsThisDotProps = path =>
path.find(j.Identifier, {
name: 'props'
})
.filter(p => p.parentPath.parentPath.value.type !== 'MemberExpression')
.size() > 0;

const getPropNames = path => {
const propNames = new Set();
path.find(j.MemberExpression, {
object: {
property: {
name: 'props',
},
},
})
.forEach(p => {
propNames.add(p.value.property.name);
});
return propNames;
};

const getDuplicateNames = path => {
const duplicates = new Set();
path
.find(j.VariableDeclarator)
.filter(p => isDuplicateDeclaration(p, true))
.forEach(p => {
duplicates.add(p.value.id.name);
});
return duplicates;
};

const getAssignmentNames = path => {
const assignmentNames = new Set();
path
.find(j.Identifier)
.filter(p => {
if (p.value.type === 'JSXIdentifier') { return false; }
if (!(p.parentPath.value.object && p.parentPath.value.object.property)) {
return true;
}
return p.parentPath.value.object.property.name !== 'props';
})
.forEach(p => {
assignmentNames.add(p.value.name);
});
return assignmentNames;
};

const hasAssignmentsThatShadowProps = path => {
const propNames = getPropNames(path);
const assignmentNames = getAssignmentNames(path);
const duplicates = getDuplicateNames(path);
return (Array.from(propNames).some(prop => !duplicates.has(prop) && assignmentNames.has(prop)));
};

const canDestructure = path =>
!needsThisDotProps(path) && !hasAssignmentsThatShadowProps(path);

const createShorthandProperty = (j, typeAnnotation) => prop => {
const property = j.property('init', j.identifier(prop), j.identifier(prop));
property.shorthand = true;
if (typeAnnotation) {
typeAnnotation.properties.forEach(t => {
if (t.key.name === prop) {
property.key.typeAnnotation = j.typeAnnotation(t.value);
}
});
}
return property;
};

const destructureProps = (body, typeAnnotation) => {
const toDestructure = body.find(j.MemberExpression, {
object: {
name: 'props'
}
});
if (toDestructure) {
const propNames = new Set();
toDestructure.replaceWith(path => {
const propName = path.value.property.name;
propNames.add(propName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this approach (adding names to the set while replacing) is efficient, it introduces some potential shadowing issues. For example:

Input:

class Shadowing extends React.Component { render() { let style = { color: 'black' }; return <div style={{...style, ...this.props.style}} />; } }

Output:

function Shadowing( { style, }, ) { let style = { color: 'black' }; return <div style={{...style, ...style}} />; }

I think a better way here is to scan the whole function twice (using forEach), once for identifiers and once for prop property accesses; then we can compare the two sets to see if it's possible to apply destructuring here.

return j.identifier(propName);
});
if (propNames.size > 0) {
const assignments = body.find(j.VariableDeclarator);
const duplicateAssignments = assignments.filter(a => isDuplicateDeclaration(a, false));
duplicateAssignments.remove();
return j.objectExpression(Array.from(propNames).map(createShorthandProperty(j, typeAnnotation)));
}
}
return false;
};

const findPropsTypeAnnotation = body => {
const property = body.find(isPropsProperty);

return property && property.typeAnnotation.typeAnnotation;
};

const buildPureComponentFunction = (name, body, typeAnnotation) =>
j.functionDeclaration(
j.identifier(name),
[buildIdentifierWithTypeAnnotation('props', typeAnnotation)],
const build = useArrows => (name, body, typeAnnotation, destructure) => {
const identifier = j.identifier(name);
const propsIdentifier = buildIdentifierWithTypeAnnotation('props', typeAnnotation);
const propsArg = [(destructure && destructureProps(j(body), typeAnnotation)) || propsIdentifier];
if (useArrows) {
return j.variableDeclaration(
'const', [
j.variableDeclarator(
identifier,
j.arrowFunctionExpression(
propsArg,
body
)
),
]
);
}
return j.functionDeclaration(
identifier,
propsArg,
body
);
};

const buildPureComponentArrowFunction = (name, body, typeAnnotation) =>
j.variableDeclaration(
'const', [
j.variableDeclarator(
j.identifier(name),
j.arrowFunctionExpression(
[buildIdentifierWithTypeAnnotation('props', typeAnnotation)],
body
)
),
]
);
const buildPureComponentFunction = build();

const buildPureComponentArrowFunction = build(true);

const buildStatics = (name, properties) => properties.map(prop => (
j.expressionStatement(
Expand Down Expand Up @@ -154,17 +268,22 @@ module.exports = function(file, api, options) {
const renderBody = renderMethod.value.body;
const propsTypeAnnotation = findPropsTypeAnnotation(p.value.body.body);
const statics = p.value.body.body.filter(isStaticProperty);
const destructure = destructuringEnabled && canDestructure(j(renderMethod));

replaceThisProps(renderBody);
if (destructuringEnabled && !destructure) {
console.warn(`Unable to destructure ${name} props.`);
}

replaceThisProps(renderBody);

if (useArrows) {
return [
buildPureComponentArrowFunction(name, renderBody, propsTypeAnnotation),
buildPureComponentArrowFunction(name, renderBody, propsTypeAnnotation, destructure),
...buildStatics(name, statics)
];
} else {
return [
buildPureComponentFunction(name, renderBody, propsTypeAnnotation),
buildPureComponentFunction(name, renderBody, propsTypeAnnotation, destructure),
...buildStatics(name, statics)
];
}
Expand Down