no-unused-class-component-methods.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /**
  2. * @fileoverview Prevent declaring unused methods and properties of component class
  3. * @author Paweł Nowak, Berton Zhu
  4. */
  5. 'use strict';
  6. const docsUrl = require('../util/docsUrl');
  7. const componentUtil = require('../util/componentUtil');
  8. const report = require('../util/report');
  9. // ------------------------------------------------------------------------------
  10. // Rule Definition
  11. // ------------------------------------------------------------------------------
  12. const LIFECYCLE_METHODS = new Set([
  13. 'constructor',
  14. 'componentDidCatch',
  15. 'componentDidMount',
  16. 'componentDidUpdate',
  17. 'componentWillMount',
  18. 'componentWillReceiveProps',
  19. 'componentWillUnmount',
  20. 'componentWillUpdate',
  21. 'getChildContext',
  22. 'getSnapshotBeforeUpdate',
  23. 'render',
  24. 'shouldComponentUpdate',
  25. 'UNSAFE_componentWillMount',
  26. 'UNSAFE_componentWillReceiveProps',
  27. 'UNSAFE_componentWillUpdate',
  28. ]);
  29. const ES6_LIFECYCLE = new Set([
  30. 'state',
  31. ]);
  32. const ES5_LIFECYCLE = new Set([
  33. 'getInitialState',
  34. 'getDefaultProps',
  35. 'mixins',
  36. ]);
  37. function isKeyLiteralLike(node, property) {
  38. return property.type === 'Literal'
  39. || (property.type === 'TemplateLiteral' && property.expressions.length === 0)
  40. || (node.computed === false && property.type === 'Identifier');
  41. }
  42. // Descend through all wrapping TypeCastExpressions and return the expression
  43. // that was cast.
  44. function uncast(node) {
  45. while (node.type === 'TypeCastExpression') {
  46. node = node.expression;
  47. }
  48. return node;
  49. }
  50. // Return the name of an identifier or the string value of a literal. Useful
  51. // anywhere that a literal may be used as a key (e.g., member expressions,
  52. // method definitions, ObjectExpression property keys).
  53. function getName(node) {
  54. node = uncast(node);
  55. const type = node.type;
  56. if (type === 'Identifier') {
  57. return node.name;
  58. }
  59. if (type === 'Literal') {
  60. return String(node.value);
  61. }
  62. if (type === 'TemplateLiteral' && node.expressions.length === 0) {
  63. return node.quasis[0].value.raw;
  64. }
  65. return null;
  66. }
  67. function isThisExpression(node) {
  68. return uncast(node).type === 'ThisExpression';
  69. }
  70. function getInitialClassInfo(node, isClass) {
  71. return {
  72. classNode: node,
  73. isClass,
  74. // Set of nodes where properties were defined.
  75. properties: new Set(),
  76. // Set of names of properties that we've seen used.
  77. usedProperties: new Set(),
  78. inStatic: false,
  79. };
  80. }
  81. const messages = {
  82. unused: 'Unused method or property "{{name}}"',
  83. unusedWithClass: 'Unused method or property "{{name}}" of class "{{className}}"',
  84. };
  85. /** @type {import('eslint').Rule.RuleModule} */
  86. module.exports = {
  87. meta: {
  88. docs: {
  89. description: 'Disallow declaring unused methods of component class',
  90. category: 'Best Practices',
  91. recommended: false,
  92. url: docsUrl('no-unused-class-component-methods'),
  93. },
  94. messages,
  95. schema: [],
  96. },
  97. create: ((context) => {
  98. let classInfo = null;
  99. // Takes an ObjectExpression node and adds all named Property nodes to the
  100. // current set of properties.
  101. function addProperty(node) {
  102. classInfo.properties.add(node);
  103. }
  104. // Adds the name of the given node as a used property if the node is an
  105. // Identifier or a Literal. Other node types are ignored.
  106. function addUsedProperty(node) {
  107. const name = getName(node);
  108. if (name) {
  109. classInfo.usedProperties.add(name);
  110. }
  111. }
  112. function reportUnusedProperties() {
  113. // Report all unused properties.
  114. for (const node of classInfo.properties) { // eslint-disable-line no-restricted-syntax
  115. const name = getName(node);
  116. if (
  117. !classInfo.usedProperties.has(name)
  118. && !LIFECYCLE_METHODS.has(name)
  119. && (classInfo.isClass ? !ES6_LIFECYCLE.has(name) : !ES5_LIFECYCLE.has(name))
  120. ) {
  121. const className = (classInfo.classNode.id && classInfo.classNode.id.name) || '';
  122. const messageID = className ? 'unusedWithClass' : 'unused';
  123. report(
  124. context,
  125. messages[messageID],
  126. messageID,
  127. {
  128. node,
  129. data: {
  130. name,
  131. className,
  132. },
  133. }
  134. );
  135. }
  136. }
  137. }
  138. function exitMethod() {
  139. if (!classInfo || !classInfo.inStatic) {
  140. return;
  141. }
  142. classInfo.inStatic = false;
  143. }
  144. return {
  145. ClassDeclaration(node) {
  146. if (componentUtil.isES6Component(node, context)) {
  147. classInfo = getInitialClassInfo(node, true);
  148. }
  149. },
  150. ObjectExpression(node) {
  151. if (componentUtil.isES5Component(node, context)) {
  152. classInfo = getInitialClassInfo(node, false);
  153. }
  154. },
  155. 'ClassDeclaration:exit'() {
  156. if (!classInfo) {
  157. return;
  158. }
  159. reportUnusedProperties();
  160. classInfo = null;
  161. },
  162. 'ObjectExpression:exit'(node) {
  163. if (!classInfo || classInfo.classNode !== node) {
  164. return;
  165. }
  166. reportUnusedProperties();
  167. classInfo = null;
  168. },
  169. Property(node) {
  170. if (!classInfo || classInfo.classNode !== node.parent) {
  171. return;
  172. }
  173. if (isKeyLiteralLike(node, node.key)) {
  174. addProperty(node.key);
  175. }
  176. },
  177. 'ClassProperty, MethodDefinition, PropertyDefinition'(node) {
  178. if (!classInfo) {
  179. return;
  180. }
  181. if (node.static) {
  182. classInfo.inStatic = true;
  183. return;
  184. }
  185. if (isKeyLiteralLike(node, node.key)) {
  186. addProperty(node.key);
  187. }
  188. },
  189. 'ClassProperty:exit': exitMethod,
  190. 'MethodDefinition:exit': exitMethod,
  191. 'PropertyDefinition:exit': exitMethod,
  192. MemberExpression(node) {
  193. if (!classInfo || classInfo.inStatic) {
  194. return;
  195. }
  196. if (isThisExpression(node.object) && isKeyLiteralLike(node, node.property)) {
  197. if (node.parent.type === 'AssignmentExpression' && node.parent.left === node) {
  198. // detect `this.property = xxx`
  199. addProperty(node.property);
  200. } else {
  201. // detect `this.property()`, `x = this.property`, etc.
  202. addUsedProperty(node.property);
  203. }
  204. }
  205. },
  206. VariableDeclarator(node) {
  207. if (!classInfo || classInfo.inStatic) {
  208. return;
  209. }
  210. // detect `{ foo, bar: baz } = this`
  211. if (node.init && isThisExpression(node.init) && node.id.type === 'ObjectPattern') {
  212. node.id.properties
  213. .filter((prop) => prop.type === 'Property' && isKeyLiteralLike(prop, prop.key))
  214. .forEach((prop) => {
  215. addUsedProperty('key' in prop ? prop.key : undefined);
  216. });
  217. }
  218. },
  219. };
  220. }),
  221. };