Skip to content

Commit aa423d9

Browse files
committed
Add no-unused-state
This adds a new rule, react/no-unused-state, which discovers state fields in a React component and warns if any of them are never read. It was developed internally at Facebook by @rjbailey and has been in use throughout Facebook's JS for a couple months now. It was written against a modern version of node, so has been rebased on @jlharb's branch dropping support for node < 4. It currently supports es2015 classes extending `React.Component` (no support currently for `React.createClass()`) and can detect when state is as `this.state = {}`, assigning state in a property initializer, and when calling `this.setState()`.
1 parent b646485 commit aa423d9

File tree

4 files changed

+716
-1
lines changed

4 files changed

+716
-1
lines changed

.eslintrc

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"env": {
3+
"es6": true,
34
"node": true
45
},
56
ecmaFeatures: {

index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ var allRules = {
6363
'no-comment-textnodes': require('./lib/rules/no-comment-textnodes'),
6464
'require-extension': require('./lib/rules/require-extension'),
6565
'wrap-multilines': require('./lib/rules/wrap-multilines'),
66-
'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing')
66+
'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing'),
67+
'no-unused-state': require('./lib/rules/no-unused-state')
6768
};
6869

6970
function filterRules(rules, predicate) {

lib/rules/no-unused-state.js

+299
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
/**
2+
* @fileoverview Attempts to discover all state fields in a React component and
3+
* warn if any of them are never read.
4+
*
5+
* State field definitions are collected from `this.state = {}` assignments in
6+
* the constructor, objects passed to `this.setState()`, and `state = {}` class
7+
* property assignments.
8+
*/
9+
10+
'use strict';
11+
12+
// Descend through all wrapping TypeCastExpressions and return the expression
13+
// that was cast.
14+
function uncast(node) {
15+
while (node.type === 'TypeCastExpression') {
16+
node = node.expression;
17+
}
18+
return node;
19+
}
20+
21+
// Return the name of an identifier or the string value of a literal. Useful
22+
// anywhere that a literal may be used as a key (e.g., member expressions,
23+
// method definitions, ObjectExpression property keys).
24+
function getName(node) {
25+
node = uncast(node);
26+
if (node.type === 'Identifier') {
27+
return node.name;
28+
} else if (node.type === 'Literal') {
29+
return String(node.value);
30+
}
31+
return null;
32+
}
33+
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+
43+
function isThisExpression(node) {
44+
return uncast(node).type === 'ThisExpression';
45+
}
46+
47+
module.exports = {
48+
meta: {
49+
docs: {
50+
description: 'Prevent definition of unused state fields',
51+
category: 'Best Practices',
52+
recommended: false
53+
},
54+
schema: []
55+
},
56+
57+
create: function(context) {
58+
// Non-null when we are inside a React component ClassDeclaration and we have
59+
// not yet encountered any use of this.state which we have chosen not to
60+
// analyze. If we encounter any such usage (like this.state being spread as
61+
// JSX attributes), then this is again set to null.
62+
let classInfo = null;
63+
64+
// Returns true if the given node is possibly a reference to `this.state`.
65+
function isStateReference(node) {
66+
node = uncast(node);
67+
68+
const isDirectStateReference =
69+
node.type === 'MemberExpression' &&
70+
isThisExpression(node.object) &&
71+
node.property.name === 'state';
72+
73+
const isAliasedStateReference =
74+
node.type === 'Identifier' &&
75+
classInfo.aliases &&
76+
classInfo.aliases.has(node.name);
77+
78+
return isDirectStateReference || isAliasedStateReference;
79+
}
80+
81+
// Takes an ObjectExpression node and adds all named Property nodes to the
82+
// current set of state fields.
83+
function addStateFields(node) {
84+
for (let prop of node.properties) {
85+
if (prop.type === 'Property' && getName(prop.key) !== null) {
86+
classInfo.stateFields.add(prop);
87+
}
88+
}
89+
}
90+
91+
// Adds the name of the given node as a used state field if the node is an
92+
// Identifier or a Literal. Other node types are ignored.
93+
function addUsedStateField(node) {
94+
let name = getName(node);
95+
if (name) {
96+
classInfo.usedStateFields.add(name);
97+
}
98+
}
99+
100+
// Records used state fields and new aliases for an ObjectPattern which
101+
// destructures `this.state`.
102+
function handleStateDestructuring(node) {
103+
for (let prop of node.properties) {
104+
if (prop.type === 'Property') {
105+
addUsedStateField(prop.key);
106+
} else if (
107+
prop.type === 'ExperimentalRestProperty' &&
108+
classInfo.aliases
109+
) {
110+
classInfo.aliases.add(getName(prop.argument));
111+
}
112+
}
113+
}
114+
115+
// Used to record used state fields and new aliases for both
116+
// AssignmentExpressions and VariableDeclarators.
117+
function handleAssignment(left, right) {
118+
switch (left.type) {
119+
case 'Identifier':
120+
if (isStateReference(right) && classInfo.aliases) {
121+
classInfo.aliases.add(left.name);
122+
}
123+
break;
124+
case 'ObjectPattern':
125+
if (isStateReference(right)) {
126+
handleStateDestructuring(left);
127+
} else if (isThisExpression(right) && classInfo.aliases) {
128+
for (let prop of left.properties) {
129+
if (prop.type === 'Property' && getName(prop.key) === 'state') {
130+
let name = getName(prop.value);
131+
if (name) {
132+
classInfo.aliases.add(name);
133+
} else if (prop.value.type === 'ObjectPattern') {
134+
handleStateDestructuring(prop.value);
135+
}
136+
}
137+
}
138+
}
139+
break;
140+
default:
141+
// pass
142+
}
143+
}
144+
145+
return {
146+
ClassDeclaration(node) {
147+
// Simple heuristic for determining whether we're in a React component.
148+
const isReactComponent = node.body.body.some(
149+
child => isMethodDefinitionWithName(child, 'render')
150+
);
151+
152+
if (isReactComponent) {
153+
classInfo = {
154+
// Set of nodes where state fields were defined.
155+
stateFields: new Set(),
156+
157+
// Set of names of state fields that we've seen used.
158+
usedStateFields: new Set(),
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+
}
168+
},
169+
170+
'ClassDeclaration:exit'() {
171+
if (!classInfo) {
172+
return;
173+
}
174+
// Report all unused state fields.
175+
for (let node of classInfo.stateFields) {
176+
let name = getName(node.key);
177+
if (!classInfo.usedStateFields.has(name)) {
178+
context.report(node, `Unused state field: '${name}'`);
179+
}
180+
}
181+
classInfo = null;
182+
},
183+
184+
CallExpression(node) {
185+
if (!classInfo) {
186+
return;
187+
}
188+
// If we're looking at a `this.setState({})` invocation, record all the
189+
// properties as state fields.
190+
if (
191+
node.callee.type === 'MemberExpression' &&
192+
isThisExpression(node.callee.object) &&
193+
getName(node.callee.property) === 'setState' &&
194+
node.arguments.length > 0 &&
195+
node.arguments[0].type === 'ObjectExpression'
196+
) {
197+
addStateFields(node.arguments[0]);
198+
}
199+
},
200+
201+
ClassProperty(node) {
202+
if (!classInfo) {
203+
return;
204+
}
205+
// If we see state being assigned as a class property using an object
206+
// expression, record all the fields of that object as state fields.
207+
if (
208+
getName(node.key) === 'state' &&
209+
!node.static &&
210+
node.value &&
211+
node.value.type === 'ObjectExpression'
212+
) {
213+
addStateFields(node.value);
214+
}
215+
},
216+
217+
MethodDefinition() {
218+
if (!classInfo) {
219+
return;
220+
}
221+
// Create a new set for this.state aliases local to this method.
222+
classInfo.aliases = new Set();
223+
},
224+
225+
'MethodDefinition:exit'() {
226+
if (!classInfo) {
227+
return;
228+
}
229+
// Forget our set of local aliases.
230+
classInfo.aliases = null;
231+
},
232+
233+
AssignmentExpression(node) {
234+
if (!classInfo) {
235+
return;
236+
}
237+
// Check for assignments like `this.state = {}`
238+
if (
239+
node.left.type === 'MemberExpression' &&
240+
isThisExpression(node.left.object) &&
241+
getName(node.left.property) === 'state' &&
242+
node.right.type === 'ObjectExpression'
243+
) {
244+
// Find the nearest function expression containing this assignment.
245+
let fn = node;
246+
while (fn.type !== 'FunctionExpression' && fn.parent) {
247+
fn = fn.parent;
248+
}
249+
// If the nearest containing function is the constructor, then we want
250+
// to record all the assigned properties as state fields.
251+
if (
252+
fn.parent &&
253+
fn.parent.type === 'MethodDefinition' &&
254+
fn.parent.kind === 'constructor'
255+
) {
256+
addStateFields(node.right);
257+
}
258+
} else {
259+
// Check for assignments like `alias = this.state` and record the alias.
260+
handleAssignment(node.left, node.right);
261+
}
262+
},
263+
264+
VariableDeclarator(node) {
265+
if (!classInfo || !node.init) {
266+
return;
267+
}
268+
handleAssignment(node.id, node.init);
269+
},
270+
271+
MemberExpression(node) {
272+
if (!classInfo) {
273+
return;
274+
}
275+
if (isStateReference(node.object)) {
276+
// If we see this.state[foo] access, give up.
277+
if (node.computed && node.property.type !== 'Literal') {
278+
classInfo = null;
279+
return;
280+
}
281+
// Otherwise, record that we saw this property being accessed.
282+
addUsedStateField(node.property);
283+
}
284+
},
285+
286+
JSXSpreadAttribute(node) {
287+
if (classInfo && isStateReference(node.argument)) {
288+
classInfo = null;
289+
}
290+
},
291+
292+
ExperimentalSpreadProperty(node) {
293+
if (classInfo && isStateReference(node.argument)) {
294+
classInfo = null;
295+
}
296+
}
297+
};
298+
}
299+
};

0 commit comments

Comments
 (0)