Skip to content

Commit 17f9566

Browse files
committed
Implement React.createClass support and use Component util
1 parent 4ddb0ff commit 17f9566

File tree

2 files changed

+178
-38
lines changed

2 files changed

+178
-38
lines changed

lib/rules/no-unused-state.js

+77-38
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
'use strict';
1111

12+
var Components = require('../util/Components');
13+
1214
// Descend through all wrapping TypeCastExpressions and return the expression
1315
// that was cast.
1416
function uncast(node) {
@@ -31,19 +33,27 @@ function getName(node) {
3133
return null;
3234
}
3335

34-
function isMethodDefinitionWithName(node, name, isStatic) {
35-
isStatic = isStatic || false;
36-
return (
37-
node.type === 'MethodDefinition' &&
38-
node.static === isStatic &&
39-
getName(node.key) === name
40-
);
41-
}
42-
4336
function isThisExpression(node) {
4437
return uncast(node).type === 'ThisExpression';
4538
}
4639

40+
function getInitialClassInfo() {
41+
return {
42+
// Set of nodes where state fields were defined.
43+
stateFields: [],
44+
45+
// Set of names of state fields that we've seen used.
46+
usedStateFields: [],
47+
48+
// Names of local variables that may be pointing to this.state. To
49+
// track this properly, we would need to keep track of all locals,
50+
// shadowing, assignments, etc. To keep things simple, we only
51+
// maintain one set of aliases per method and accept that it will
52+
// produce some false negatives.
53+
aliases: null
54+
};
55+
}
56+
4757
module.exports = {
4858
meta: {
4959
docs: {
@@ -54,7 +64,7 @@ module.exports = {
5464
schema: []
5565
},
5666

57-
create: function(context) {
67+
create: Components.detect(function(context, components, utils) {
5868
// Non-null when we are inside a React component ClassDeclaration and we have
5969
// not yet encountered any use of this.state which we have chosen not to
6070
// analyze. If we encounter any such usage (like this.state being spread as
@@ -142,42 +152,45 @@ module.exports = {
142152
}
143153
}
144154

155+
function reportUnusedFields() {
156+
// Report all unused state fields.
157+
classInfo.stateFields.forEach(function(node) {
158+
var name = getName(node.key);
159+
if (classInfo.usedStateFields.indexOf(name) < 0) {
160+
context.report(node, 'Unused state field: \'' + name + '\'');
161+
}
162+
});
163+
}
164+
145165
return {
146166
ClassDeclaration: function(node) {
147-
// Simple heuristic for determining whether we're in a React component.
148-
var isReactComponent = node.body.body.some(function(child) {
149-
return isMethodDefinitionWithName(child, 'render');
150-
});
151-
152-
if (isReactComponent) {
153-
classInfo = {
154-
// Set of nodes where state fields were defined.
155-
stateFields: [],
156-
157-
// Set of names of state fields that we've seen used.
158-
usedStateFields: [],
159-
160-
// Names of local variables that may be pointing to this.state. To
161-
// track this properly, we would need to keep track of all locals,
162-
// shadowing, assignments, etc. To keep things simple, we only
163-
// maintain one set of aliases per method and accept that it will
164-
// produce some false negatives.
165-
aliases: null
166-
};
167+
if (utils.isES6Component(node)) {
168+
classInfo = getInitialClassInfo();
169+
}
170+
},
171+
172+
ObjectExpression: function(node) {
173+
if (utils.isES5Component(node)) {
174+
classInfo = getInitialClassInfo();
175+
}
176+
},
177+
178+
'ObjectExpression:exit': function(node) {
179+
if (!classInfo) {
180+
return;
181+
}
182+
183+
if (utils.isES5Component(node)) {
184+
reportUnusedFields();
185+
classInfo = null;
167186
}
168187
},
169188

170189
'ClassDeclaration:exit': function() {
171190
if (!classInfo) {
172191
return;
173192
}
174-
// Report all unused state fields.
175-
classInfo.stateFields.forEach(function(node) {
176-
var name = getName(node.key);
177-
if (classInfo.usedStateFields.indexOf(name) < 0) {
178-
context.report(node, 'Unused state field: \'' + name + '\'');
179-
}
180-
});
193+
reportUnusedFields();
181194
classInfo = null;
182195
},
183196

@@ -230,6 +243,32 @@ module.exports = {
230243
classInfo.aliases = null;
231244
},
232245

246+
FunctionExpression: function(node) {
247+
if (!classInfo) {
248+
return;
249+
}
250+
251+
var parent = node.parent;
252+
if (!utils.isES5Component(parent.parent)) {
253+
return;
254+
}
255+
256+
if (parent.key.name === 'getInitialState') {
257+
var body = node.body.body;
258+
var lastBodyNode = body[body.length - 1];
259+
260+
if (
261+
lastBodyNode.type === 'ReturnStatement' &&
262+
lastBodyNode.argument.type === 'ObjectExpression'
263+
) {
264+
addStateFields(lastBodyNode.argument);
265+
}
266+
} else {
267+
// Create a new set for this.state aliases local to this method.
268+
classInfo.aliases = [];
269+
}
270+
},
271+
233272
AssignmentExpression: function(node) {
234273
if (!classInfo) {
235274
return;
@@ -295,5 +334,5 @@ module.exports = {
295334
}
296335
}
297336
};
298-
}
337+
})
299338
};

tests/lib/rules/no-unused-state.js

+101
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,68 @@ function getErrorMessages(unusedFields) {
2929

3030
eslintTester.run('no-unused-state', rule, {
3131
valid: [
32+
[
33+
'function StatelessFnUnaffectedTest(props) {',
34+
' return <SomeComponent foo={props.foo} />;',
35+
'};'
36+
].join('\n'),
37+
[
38+
'var NoStateTest = React.createClass({',
39+
' render: function() {',
40+
' return <SomeComponent />;',
41+
' }',
42+
'});'
43+
].join('\n'),
44+
[
45+
'var NoStateMethodTest = React.createClass({',
46+
' render() {',
47+
' return <SomeComponent />;',
48+
' }',
49+
'});'
50+
].join('\n'),
51+
[
52+
'var GetInitialStateTest = React.createClass({',
53+
' getInitialState: function() {',
54+
' return { foo: 0 };',
55+
' },',
56+
' render: function() {',
57+
' return <SomeComponent foo={this.state.foo} />;',
58+
' }',
59+
'});'
60+
].join('\n'),
61+
[
62+
'var GetInitialStateMethodTest = React.createClass({',
63+
' getInitialState() {',
64+
' return { foo: 0 };',
65+
' },',
66+
' render() {',
67+
' return <SomeComponent foo={this.state.foo} />;',
68+
' }',
69+
'});'
70+
].join('\n'),
71+
[
72+
'var SetStateTest = React.createClass({',
73+
' onFooChange(newFoo) {',
74+
' this.setState({ foo: newFoo });',
75+
' },',
76+
' render() {',
77+
' return <SomeComponent foo={this.state.foo} />;',
78+
' }',
79+
'});'
80+
].join('\n'),
81+
[
82+
'var MultipleSetState = React.createClass({',
83+
' getInitialState() {',
84+
' return { foo: 0 };',
85+
' },',
86+
' update() {',
87+
' this.setState({foo: 1});',
88+
' },',
89+
' render() {',
90+
' return <SomeComponent onClick={this.update} foo={this.state.foo} />;',
91+
' }',
92+
'});'
93+
].join('\n'),
3294
[
3395
'class NoStateTest extends React.Component {',
3496
' render() {',
@@ -262,6 +324,45 @@ eslintTester.run('no-unused-state', rule, {
262324
],
263325

264326
invalid: [
327+
{
328+
code: [
329+
'var UnusedGetInitialStateTest = React.createClass({',
330+
' getInitialState: function() {',
331+
' return { foo: 0 };',
332+
' },',
333+
' render: function() {',
334+
' return <SomeComponent />;',
335+
' }',
336+
'})'
337+
].join('\n'),
338+
errors: getErrorMessages(['foo'])
339+
},
340+
{
341+
code: [
342+
'var UnusedGetInitialStateMethodTest = React.createClass({',
343+
' getInitialState() {',
344+
' return { foo: 0 };',
345+
' },',
346+
' render() {',
347+
' return <SomeComponent />;',
348+
' }',
349+
'})'
350+
].join('\n'),
351+
errors: getErrorMessages(['foo'])
352+
},
353+
{
354+
code: [
355+
'var UnusedSetStateTest = React.createClass({',
356+
' onFooChange(newFoo) {',
357+
' this.setState({ foo: newFoo });',
358+
' },',
359+
' render() {',
360+
' return <SomeComponent />;',
361+
' }',
362+
'});'
363+
].join('\n'),
364+
errors: getErrorMessages(['foo'])
365+
},
265366
{
266367
code: [
267368
'class UnusedCtorStateTest extends React.Component {',

0 commit comments

Comments
 (0)