UNPKG

14 kBJavaScriptView Raw
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
12const Components = require('../util/Components');
13const docsUrl = require('../util/docsUrl');
14const ast = require('../util/ast');
15
16// Descend through all wrapping TypeCastExpressions and return the expression
17// that was cast.
18function uncast(node) {
19 while (node.type === 'TypeCastExpression') {
20 node = node.expression;
21 }
22 return node;
23}
24
25// Return the name of an identifier or the string value of a literal. Useful
26// anywhere that a literal may be used as a key (e.g., member expressions,
27// method definitions, ObjectExpression property keys).
28function getName(node) {
29 node = uncast(node);
30 const type = node.type;
31
32 if (type === 'Identifier') {
33 return node.name;
34 }
35 if (type === 'Literal') {
36 return String(node.value);
37 }
38 if (type === 'TemplateLiteral' && node.expressions.length === 0) {
39 return node.quasis[0].value.raw;
40 }
41 return null;
42}
43
44function isThisExpression(node) {
45 return ast.unwrapTSAsExpression(uncast(node)).type === 'ThisExpression';
46}
47
48function getInitialClassInfo() {
49 return {
50 // Set of nodes where state fields were defined.
51 stateFields: new Set(),
52
53 // Set of names of state fields that we've seen used.
54 usedStateFields: new Set(),
55
56 // Names of local variables that may be pointing to this.state. To
57 // track this properly, we would need to keep track of all locals,
58 // shadowing, assignments, etc. To keep things simple, we only
59 // maintain one set of aliases per method and accept that it will
60 // produce some false negatives.
61 aliases: null
62 };
63}
64
65function isSetStateCall(node) {
66 const unwrappedCalleeNode = ast.unwrapTSAsExpression(node.callee);
67
68 return (
69 unwrappedCalleeNode.type === 'MemberExpression'
70 && isThisExpression(unwrappedCalleeNode.object)
71 && getName(unwrappedCalleeNode.property) === 'setState'
72 );
73}
74
75module.exports = {
76 meta: {
77 docs: {
78 description: 'Prevent definition of unused state fields',
79 category: 'Best Practices',
80 recommended: false,
81 url: docsUrl('no-unused-state')
82 },
83 schema: []
84 },
85
86 create: Components.detect((context, components, utils) => {
87 // Non-null when we are inside a React component ClassDeclaration and we have
88 // not yet encountered any use of this.state which we have chosen not to
89 // analyze. If we encounter any such usage (like this.state being spread as
90 // JSX attributes), then this is again set to null.
91 let classInfo = null;
92
93 function isStateParameterReference(node) {
94 const classMethods = [
95 'shouldComponentUpdate',
96 'componentWillUpdate',
97 'UNSAFE_componentWillUpdate',
98 'getSnapshotBeforeUpdate',
99 'componentDidUpdate'
100 ];
101
102 let scope = context.getScope();
103 while (scope) {
104 const parent = scope.block && scope.block.parent;
105 if (
106 parent
107 && parent.type === 'MethodDefinition' && (
108 parent.static && parent.key.name === 'getDerivedStateFromProps'
109 || classMethods.indexOf(parent.key.name) !== -1
110 )
111 && parent.value.type === 'FunctionExpression'
112 && parent.value.params[1]
113 && parent.value.params[1].name === node.name
114 ) {
115 return true;
116 }
117 scope = scope.upper;
118 }
119
120 return false;
121 }
122
123 // Returns true if the given node is possibly a reference to `this.state` or the state parameter of
124 // a lifecycle method.
125 function isStateReference(node) {
126 node = uncast(node);
127
128 const isDirectStateReference = node.type === 'MemberExpression'
129 && isThisExpression(node.object)
130 && node.property.name === 'state';
131
132 const isAliasedStateReference = node.type === 'Identifier'
133 && classInfo.aliases
134 && classInfo.aliases.has(node.name);
135
136 return isDirectStateReference || isAliasedStateReference || isStateParameterReference(node);
137 }
138
139 // Takes an ObjectExpression node and adds all named Property nodes to the
140 // current set of state fields.
141 function addStateFields(node) {
142 node.properties.filter((prop) => (
143 prop.type === 'Property'
144 && (prop.key.type === 'Literal'
145 || (prop.key.type === 'TemplateLiteral' && prop.key.expressions.length === 0)
146 || (prop.computed === false && prop.key.type === 'Identifier'))
147 && getName(prop.key) !== null
148 )).forEach((prop) => {
149 classInfo.stateFields.add(prop);
150 });
151 }
152
153 // Adds the name of the given node as a used state field if the node is an
154 // Identifier or a Literal. Other node types are ignored.
155 function addUsedStateField(node) {
156 const name = getName(node);
157 if (name) {
158 classInfo.usedStateFields.add(name);
159 }
160 }
161
162 // Records used state fields and new aliases for an ObjectPattern which
163 // destructures `this.state`.
164 function handleStateDestructuring(node) {
165 for (const prop of node.properties) {
166 if (prop.type === 'Property') {
167 addUsedStateField(prop.key);
168 } else if (
169 (prop.type === 'ExperimentalRestProperty' || prop.type === 'RestElement')
170 && classInfo.aliases
171 ) {
172 classInfo.aliases.add(getName(prop.argument));
173 }
174 }
175 }
176
177 // Used to record used state fields and new aliases for both
178 // AssignmentExpressions and VariableDeclarators.
179 function handleAssignment(left, right) {
180 const unwrappedRight = ast.unwrapTSAsExpression(right);
181
182 switch (left.type) {
183 case 'Identifier':
184 if (isStateReference(unwrappedRight) && classInfo.aliases) {
185 classInfo.aliases.add(left.name);
186 }
187 break;
188 case 'ObjectPattern':
189 if (isStateReference(unwrappedRight)) {
190 handleStateDestructuring(left);
191 } else if (isThisExpression(unwrappedRight) && classInfo.aliases) {
192 for (const prop of left.properties) {
193 if (prop.type === 'Property' && getName(prop.key) === 'state') {
194 const name = getName(prop.value);
195 if (name) {
196 classInfo.aliases.add(name);
197 } else if (prop.value.type === 'ObjectPattern') {
198 handleStateDestructuring(prop.value);
199 }
200 }
201 }
202 }
203 break;
204 default:
205 // pass
206 }
207 }
208
209 function reportUnusedFields() {
210 // Report all unused state fields.
211 for (const node of classInfo.stateFields) {
212 const name = getName(node.key);
213 if (!classInfo.usedStateFields.has(name)) {
214 context.report({
215 node,
216 message: `Unused state field: '${name}'`
217 });
218 }
219 }
220 }
221
222 function handleES6ComponentEnter(node) {
223 if (utils.isES6Component(node)) {
224 classInfo = getInitialClassInfo();
225 }
226 }
227
228 function handleES6ComponentExit() {
229 if (!classInfo) {
230 return;
231 }
232 reportUnusedFields();
233 classInfo = null;
234 }
235
236 return {
237 ClassDeclaration: handleES6ComponentEnter,
238
239 'ClassDeclaration:exit': handleES6ComponentExit,
240
241 ClassExpression: handleES6ComponentEnter,
242
243 'ClassExpression:exit': handleES6ComponentExit,
244
245 ObjectExpression(node) {
246 if (utils.isES5Component(node)) {
247 classInfo = getInitialClassInfo();
248 }
249 },
250
251 'ObjectExpression:exit'(node) {
252 if (!classInfo) {
253 return;
254 }
255
256 if (utils.isES5Component(node)) {
257 reportUnusedFields();
258 classInfo = null;
259 }
260 },
261
262 CallExpression(node) {
263 if (!classInfo) {
264 return;
265 }
266
267 const unwrappedNode = ast.unwrapTSAsExpression(node);
268 const unwrappedArgumentNode = ast.unwrapTSAsExpression(unwrappedNode.arguments[0]);
269
270 // If we're looking at a `this.setState({})` invocation, record all the
271 // properties as state fields.
272 if (
273 isSetStateCall(unwrappedNode)
274 && unwrappedNode.arguments.length > 0
275 && unwrappedArgumentNode.type === 'ObjectExpression'
276 ) {
277 addStateFields(unwrappedArgumentNode);
278 } else if (
279 isSetStateCall(unwrappedNode)
280 && unwrappedNode.arguments.length > 0
281 && unwrappedArgumentNode.type === 'ArrowFunctionExpression'
282 ) {
283 const unwrappedBodyNode = ast.unwrapTSAsExpression(unwrappedArgumentNode.body);
284
285 if (unwrappedBodyNode.type === 'ObjectExpression') {
286 addStateFields(unwrappedBodyNode);
287 }
288 if (unwrappedArgumentNode.params.length > 0 && classInfo.aliases) {
289 const firstParam = unwrappedArgumentNode.params[0];
290 if (firstParam.type === 'ObjectPattern') {
291 handleStateDestructuring(firstParam);
292 } else {
293 classInfo.aliases.add(getName(firstParam));
294 }
295 }
296 }
297 },
298
299 ClassProperty(node) {
300 if (!classInfo) {
301 return;
302 }
303 // If we see state being assigned as a class property using an object
304 // expression, record all the fields of that object as state fields.
305 const unwrappedValueNode = ast.unwrapTSAsExpression(node.value);
306
307 if (
308 getName(node.key) === 'state'
309 && !node.static
310 && unwrappedValueNode
311 && unwrappedValueNode.type === 'ObjectExpression'
312 ) {
313 addStateFields(unwrappedValueNode);
314 }
315
316 if (
317 !node.static
318 && unwrappedValueNode
319 && unwrappedValueNode.type === 'ArrowFunctionExpression'
320 ) {
321 // Create a new set for this.state aliases local to this method.
322 classInfo.aliases = new Set();
323 }
324 },
325
326 'ClassProperty:exit'(node) {
327 if (
328 classInfo
329 && !node.static
330 && node.value
331 && node.value.type === 'ArrowFunctionExpression'
332 ) {
333 // Forget our set of local aliases.
334 classInfo.aliases = null;
335 }
336 },
337
338 MethodDefinition() {
339 if (!classInfo) {
340 return;
341 }
342 // Create a new set for this.state aliases local to this method.
343 classInfo.aliases = new Set();
344 },
345
346 'MethodDefinition:exit'() {
347 if (!classInfo) {
348 return;
349 }
350 // Forget our set of local aliases.
351 classInfo.aliases = null;
352 },
353
354 FunctionExpression(node) {
355 if (!classInfo) {
356 return;
357 }
358
359 const parent = node.parent;
360 if (!utils.isES5Component(parent.parent)) {
361 return;
362 }
363
364 if (parent.key.name === 'getInitialState') {
365 const body = node.body.body;
366 const lastBodyNode = body[body.length - 1];
367
368 if (
369 lastBodyNode.type === 'ReturnStatement'
370 && lastBodyNode.argument.type === 'ObjectExpression'
371 ) {
372 addStateFields(lastBodyNode.argument);
373 }
374 } else {
375 // Create a new set for this.state aliases local to this method.
376 classInfo.aliases = new Set();
377 }
378 },
379
380 AssignmentExpression(node) {
381 if (!classInfo) {
382 return;
383 }
384
385 const unwrappedLeft = ast.unwrapTSAsExpression(node.left);
386 const unwrappedRight = ast.unwrapTSAsExpression(node.right);
387
388 // Check for assignments like `this.state = {}`
389 if (
390 unwrappedLeft.type === 'MemberExpression'
391 && isThisExpression(unwrappedLeft.object)
392 && getName(unwrappedLeft.property) === 'state'
393 && unwrappedRight.type === 'ObjectExpression'
394 ) {
395 // Find the nearest function expression containing this assignment.
396 let fn = node;
397 while (fn.type !== 'FunctionExpression' && fn.parent) {
398 fn = fn.parent;
399 }
400 // If the nearest containing function is the constructor, then we want
401 // to record all the assigned properties as state fields.
402 if (
403 fn.parent
404 && fn.parent.type === 'MethodDefinition'
405 && fn.parent.kind === 'constructor'
406 ) {
407 addStateFields(unwrappedRight);
408 }
409 } else {
410 // Check for assignments like `alias = this.state` and record the alias.
411 handleAssignment(unwrappedLeft, unwrappedRight);
412 }
413 },
414
415 VariableDeclarator(node) {
416 if (!classInfo || !node.init) {
417 return;
418 }
419 handleAssignment(node.id, node.init);
420 },
421
422 'MemberExpression, OptionalMemberExpression'(node) {
423 if (!classInfo) {
424 return;
425 }
426 if (isStateReference(ast.unwrapTSAsExpression(node.object))) {
427 // If we see this.state[foo] access, give up.
428 if (node.computed && node.property.type !== 'Literal') {
429 classInfo = null;
430 return;
431 }
432 // Otherwise, record that we saw this property being accessed.
433 addUsedStateField(node.property);
434 // If we see a `this.state` access in a CallExpression, give up.
435 } else if (isStateReference(node) && node.parent.type === 'CallExpression') {
436 classInfo = null;
437 }
438 },
439
440 JSXSpreadAttribute(node) {
441 if (classInfo && isStateReference(node.argument)) {
442 classInfo = null;
443 }
444 },
445
446 'ExperimentalSpreadProperty, SpreadElement'(node) {
447 if (classInfo && isStateReference(node.argument)) {
448 classInfo = null;
449 }
450 }
451 };
452 })
453};