25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

385 satır
11 KiB

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