Skip to content

Commit c7136e2

Browse files
author
Keyan Zhang
committed
prune unused requires safely and add support for primitives as class properties
1 parent 747dc5b commit c7136e2

File tree

8 files changed

+125
-26
lines changed

8 files changed

+125
-26
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const React = require('react');
2+
3+
const Component1 = React.createClass({
4+
statics: {
5+
booleanPrim: true,
6+
numberPrim: 12,
7+
stringPrim: 'foo',
8+
nullPrim: null,
9+
undefinedPrim: undefined,
10+
},
11+
booleanPrim: true,
12+
numberPrim: 12,
13+
stringPrim: 'foo',
14+
nullPrim: null,
15+
undefinedPrim: undefined,
16+
17+
foobar: function() {
18+
return 123;
19+
},
20+
21+
componentDidMount: function() {
22+
console.log('hello');
23+
},
24+
25+
render: function() {
26+
return <div />;
27+
},
28+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const React = require('react');
2+
3+
class Component1 extends React.Component {
4+
static booleanPrim = true;
5+
static numberPrim = 12;
6+
static stringPrim = 'foo';
7+
static nullPrim = null;
8+
static undefinedPrim = undefined;
9+
booleanPrim = true;
10+
numberPrim = 12;
11+
stringPrim = 'foo';
12+
nullPrim = null;
13+
undefinedPrim = undefined;
14+
15+
foobar = () => {
16+
return 123;
17+
};
18+
19+
componentDidMount() {
20+
console.log('hello');
21+
}
22+
23+
render() {
24+
return <div />;
25+
}
26+
}

transforms/__testfixtures__/class-pure-mixin2.input.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'React';
22
import WhateverYouCallIt from 'react-addons-pure-render-mixin';
3+
import dontPruneMe from 'foobar';
34

45
var ComponentWithOnlyPureRenderMixin = React.createClass({
56
mixins: [WhateverYouCallIt],
@@ -11,6 +12,7 @@ var ComponentWithOnlyPureRenderMixin = React.createClass({
1112
},
1213

1314
render: function() {
15+
dontPruneMe();
1416
return (
1517
<div>{this.state.counter}</div>
1618
);

transforms/__testfixtures__/class-pure-mixin2.output.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'React';
2+
import dontPruneMe from 'foobar';
23

34
class ComponentWithOnlyPureRenderMixin extends React.PureComponent {
45
constructor(props, context) {
@@ -10,6 +11,7 @@ class ComponentWithOnlyPureRenderMixin extends React.PureComponent {
1011
}
1112

1213
render() {
14+
dontPruneMe();
1315
return (
1416
<div>{this.state.counter}</div>
1517
);

transforms/__testfixtures__/class-pure-mixin3.input.js

Whitespace-only changes.

transforms/__testfixtures__/class-pure-mixin3.output.js

Whitespace-only changes.

transforms/__tests__/class-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ defineTest(__dirname, 'class', pureMixinAlternativeOption, 'class-test2');
2121
defineTest(__dirname, 'class', null, 'export-default-class');
2222
defineTest(__dirname, 'class', pureMixinAlternativeOption, 'class-pure-mixin1');
2323
defineTest(__dirname, 'class', null, 'class-pure-mixin2');
24+
defineTest(__dirname, 'class', null, 'class-property-field');

transforms/class.js

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ module.exports = (file, api, options) => {
9292
!filterDefaultPropsField(prop) &&
9393
!filterGetInitialStateField(prop) &&
9494
!isFunctionExpression(prop) &&
95+
!isPrimExpression(prop) &&
9596
MIXIN_KEY != prop.key.name
9697
)
9798
));
@@ -152,6 +153,16 @@ module.exports = (file, api, options) => {
152153
node.value.type === 'FunctionExpression'
153154
);
154155

156+
const isPrimExpression = node => (
157+
node.key &&
158+
node.key.type === 'Identifier' &&
159+
node.value && (
160+
node.value.type === 'Literal' || ( // TODO this might change in babylon v6
161+
node.value.type === 'Identifier' &&
162+
node.value.name === 'undefined'
163+
))
164+
);
165+
155166
// Collects `childContextTypes`, `contextTypes`, `displayName`, and `propTypes`;
156167
// simplifies `getDefaultProps` or converts it to an IIFE;
157168
// and collects everything else in the `statics` property object.
@@ -172,25 +183,20 @@ module.exports = (file, api, options) => {
172183
return result;
173184
};
174185

175-
const collectFunctions = specPath => specPath.properties
186+
const collectProperties = specPath => specPath.properties
176187
.filter(prop =>
177188
!(filterDefaultPropsField(prop) || filterGetInitialStateField(prop))
178189
)
179-
.filter(isFunctionExpression);
190+
.filter(prop => isFunctionExpression(prop) || isPrimExpression(prop));
180191

181192
const findRequirePathAndBinding = (moduleName) => {
182193
let result = null;
183194

184195
const requireStatement = root.find(j.VariableDeclarator, {
196+
id: {type: 'Identifier'},
185197
init: {
186-
type: 'CallExpression',
187-
callee: {
188-
type: 'Identifier',
189-
name: 'require',
190-
},
191-
arguments: [{
192-
value: moduleName,
193-
}],
198+
callee: {name: 'require'},
199+
arguments: [{value: moduleName}],
194200
},
195201
});
196202

@@ -344,13 +350,21 @@ module.exports = (file, api, options) => {
344350
false
345351
), fn);
346352

347-
const createArrowPropertyFromMethod = method =>
353+
const createArrowProperty = prop =>
354+
withComments(j.classProperty(
355+
j.identifier(prop.key.name),
356+
createArrowFunctionExpression(prop.value),
357+
null,
358+
false
359+
), prop);
360+
361+
const createClassProperty = prop =>
348362
withComments(j.classProperty(
349-
j.identifier(method.key.name),
350-
createArrowFunctionExpression(method.value),
363+
j.identifier(prop.key.name),
364+
prop.value,
351365
null,
352366
false
353-
), method);
367+
), prop);
354368

355369
// if there's no `getInitialState` or the `getInitialState` function is simple
356370
// (i.e., it does not reference `this.props`) then we don't need a constructor.
@@ -368,7 +382,7 @@ module.exports = (file, api, options) => {
368382
baseClassName,
369383
staticProperties,
370384
getInitialState,
371-
methods,
385+
rawProperties,
372386
comments
373387
) => {
374388
let maybeConstructor = [];
@@ -382,11 +396,15 @@ module.exports = (file, api, options) => {
382396
maybeConstructor = createConstructor(getInitialState);
383397
}
384398

385-
const arrowBindFunctionsAndMethods = methods.map(method =>
386-
AUTOBIND_IGNORE_KEYS[method.key.name] ?
387-
method :
388-
createArrowPropertyFromMethod(method)
389-
);
399+
const propertiesAndMethods = rawProperties.map(prop => {
400+
if (isPrimExpression(prop)) {
401+
return createClassProperty(prop);
402+
} else if (AUTOBIND_IGNORE_KEYS[prop.key.name]) {
403+
return createMethodDefinition(prop);
404+
}
405+
406+
return createArrowProperty(prop);
407+
});
390408

391409
return withComments(j.classDeclaration(
392410
name ? j.identifier(name) : null,
@@ -395,7 +413,7 @@ module.exports = (file, api, options) => {
395413
staticProperties,
396414
maybeConstructor,
397415
initialStateProperty,
398-
arrowBindFunctionsAndMethods
416+
propertiesAndMethods
399417
)
400418
),
401419
j.memberExpression(
@@ -449,11 +467,30 @@ module.exports = (file, api, options) => {
449467
return null;
450468
};
451469

470+
const findUnusedVariables = (path, varName) => j(path)
471+
.closestScope()
472+
.find(j.Identifier, {name: varName})
473+
// Ignore require vars
474+
.filter(identifierPath => identifierPath.value !== path.value.id)
475+
// Ignore import bindings
476+
.filter(identifierPath => !(
477+
path.value.type === 'ImportDeclaration' &&
478+
path.value.specifiers.some(specifier => specifier.local === identifierPath.value)
479+
))
480+
// Ignore properties in MemberExpressions
481+
.filter(identifierPath => {
482+
const parent = identifierPath.parent.value;
483+
return !(
484+
j.MemberExpression.check(parent) &&
485+
parent.property === identifierPath.value
486+
);
487+
});
488+
452489
const updateToClass = (classPath, type) => {
453490
const specPath = ReactUtils.getReactCreateClassSpec(classPath);
454491
const name = ReactUtils.getComponentName(classPath);
455492
const statics = collectStatics(specPath);
456-
const functions = collectFunctions(specPath);
493+
const properties = collectProperties(specPath);
457494
const comments = getComments(classPath);
458495

459496
const getInitialState = findGetInitialState(specPath);
@@ -478,7 +515,7 @@ module.exports = (file, api, options) => {
478515
baseClassName,
479516
staticProperties,
480517
getInitialState,
481-
functions.map(createMethodDefinition),
518+
properties,
482519
comments
483520
)
484521
);
@@ -496,7 +533,8 @@ module.exports = (file, api, options) => {
496533
if (!ReactUtils.hasMixins(classPath)) {
497534
return true;
498535
} else if (pureRenderMixinPathAndBinding) {
499-
if (areMixinsConvertible([pureRenderMixinPathAndBinding.binding], classPath)) {
536+
const {binding} = pureRenderMixinPathAndBinding;
537+
if (areMixinsConvertible([binding], classPath)) {
500538
return true;
501539
}
502540
}
@@ -526,8 +564,10 @@ module.exports = (file, api, options) => {
526564
if (didTransform) {
527565
// prune removed requires
528566
if (pureRenderMixinPathAndBinding) {
529-
// FIXME check the scope before removing stuff
530-
j(pureRenderMixinPathAndBinding.path).remove();
567+
const {binding, path} = pureRenderMixinPathAndBinding;
568+
if (findUnusedVariables(path, binding).size() === 0) {
569+
j(path).remove();
570+
}
531571
}
532572

533573
return root.toSource(printOptions);

0 commit comments

Comments
 (0)