no-unused-state.js 14 KB

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