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

main
Last change on this file since 0c6b92a was 0c6b92a, checked in by stefan toskovski <stefantoska84@…>, 5 weeks ago

Pred finalna verzija

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