source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/no-unused-state.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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