Skip to content

Commit 8bfafe1

Browse files
authored
Merge pull request #1103 from wbinnssmith/wbinnssmith/no-unused-state
Add no-unused-state
2 parents 067882b + ba1dfc1 commit 8bfafe1

File tree

3 files changed

+786
-1
lines changed

3 files changed

+786
-1
lines changed

index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ const allRules = {
6666
'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing'),
6767
'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update'),
6868
'boolean-prop-naming': require('./lib/rules/boolean-prop-naming'),
69-
'no-typos': require('./lib/rules/no-typos')
69+
'no-typos': require('./lib/rules/no-typos'),
70+
'no-unused-state': require('./lib/rules/no-unused-state')
7071
};
7172

7273
function filterRules(rules, predicate) {

lib/rules/no-unused-state.js

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

0 commit comments

Comments
 (0)