no-unused-state.js 16 KB

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