no-unused-state.js 15 KB

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