Skip to content
Permalink
Browse files

Pure component destructuring (#94)

* Add description of `pure-component` transform

* Basic destructuring works

Needs to be tested
Only works if there are no references to `this.props` by itself
Would be nice if it would work for (replace) in-render destructuring

* Remove duplicate variable declarations

If `bar` is assigned to `this.props.bar` we need to remove that, as it's available as a destrctured arg

* Test various scenarios

* Update readme with destructure option

* Undo accidental indentation change

* Made propNames be a set to make sure they are unique

* Type Arrays to Array

* Rename option to `destructuring`

* Simplify build argument

* Identify shadowing and do not destructure

* Retain type annotations

* Change option name in readme

destructure -> destructuring
  • Loading branch information...
andrewdushane authored and keyz committed Jul 10, 2017
1 parent 0936fac commit b1628643a9166fee2da03b24f33df04e0098fd57
@@ -46,8 +46,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`
@@ -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} />;
}
}
@@ -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} />;
}
@@ -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');
@@ -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',
@@ -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);
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(
@@ -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)
];
}

0 comments on commit b162864

Please sign in to comment.
You can’t perform that action at this time.