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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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. module.exports = {
  86. meta: {
  87. docs: {
  88. description: 'Disallow declaring unused methods of component class',
  89. category: 'Best Practices',
  90. recommended: false,
  91. url: docsUrl('no-unused-class-component-methods'),
  92. },
  93. messages,
  94. schema: [],
  95. },
  96. create: ((context) => {
  97. let classInfo = null;
  98. // Takes an ObjectExpression node and adds all named Property nodes to the
  99. // current set of properties.
  100. function addProperty(node) {
  101. classInfo.properties.add(node);
  102. }
  103. // Adds the name of the given node as a used property if the node is an
  104. // Identifier or a Literal. Other node types are ignored.
  105. function addUsedProperty(node) {
  106. const name = getName(node);
  107. if (name) {
  108. classInfo.usedProperties.add(name);
  109. }
  110. }
  111. function reportUnusedProperties() {
  112. // Report all unused properties.
  113. for (const node of classInfo.properties) { // eslint-disable-line no-restricted-syntax
  114. const name = getName(node);
  115. if (
  116. !classInfo.usedProperties.has(name)
  117. && !LIFECYCLE_METHODS.has(name)
  118. && (classInfo.isClass ? !ES6_LIFECYCLE.has(name) : !ES5_LIFECYCLE.has(name))
  119. ) {
  120. const className = (classInfo.classNode.id && classInfo.classNode.id.name) || '';
  121. const messageID = className ? 'unusedWithClass' : 'unused';
  122. report(
  123. context,
  124. messages[messageID],
  125. messageID,
  126. {
  127. node,
  128. data: {
  129. name,
  130. className,
  131. },
  132. }
  133. );
  134. }
  135. }
  136. }
  137. function exitMethod() {
  138. if (!classInfo || !classInfo.inStatic) {
  139. return;
  140. }
  141. classInfo.inStatic = false;
  142. }
  143. return {
  144. ClassDeclaration(node) {
  145. if (componentUtil.isES6Component(node, context)) {
  146. classInfo = getInitialClassInfo(node, true);
  147. }
  148. },
  149. ObjectExpression(node) {
  150. if (componentUtil.isES5Component(node, context)) {
  151. classInfo = getInitialClassInfo(node, false);
  152. }
  153. },
  154. 'ClassDeclaration:exit'() {
  155. if (!classInfo) {
  156. return;
  157. }
  158. reportUnusedProperties();
  159. classInfo = null;
  160. },
  161. 'ObjectExpression:exit'(node) {
  162. if (!classInfo || classInfo.classNode !== node) {
  163. return;
  164. }
  165. reportUnusedProperties();
  166. classInfo = null;
  167. },
  168. Property(node) {
  169. if (!classInfo || classInfo.classNode !== node.parent) {
  170. return;
  171. }
  172. if (isKeyLiteralLike(node, node.key)) {
  173. addProperty(node.key);
  174. }
  175. },
  176. 'ClassProperty, MethodDefinition, PropertyDefinition'(node) {
  177. if (!classInfo) {
  178. return;
  179. }
  180. if (node.static) {
  181. classInfo.inStatic = true;
  182. return;
  183. }
  184. if (isKeyLiteralLike(node, node.key)) {
  185. addProperty(node.key);
  186. }
  187. },
  188. 'ClassProperty:exit': exitMethod,
  189. 'MethodDefinition:exit': exitMethod,
  190. 'PropertyDefinition:exit': exitMethod,
  191. MemberExpression(node) {
  192. if (!classInfo || classInfo.inStatic) {
  193. return;
  194. }
  195. if (isThisExpression(node.object) && isKeyLiteralLike(node, node.property)) {
  196. if (node.parent.type === 'AssignmentExpression' && node.parent.left === node) {
  197. // detect `this.property = xxx`
  198. addProperty(node.property);
  199. } else {
  200. // detect `this.property()`, `x = this.property`, etc.
  201. addUsedProperty(node.property);
  202. }
  203. }
  204. },
  205. VariableDeclarator(node) {
  206. if (!classInfo || classInfo.inStatic) {
  207. return;
  208. }
  209. // detect `{ foo, bar: baz } = this`
  210. if (node.init && isThisExpression(node.init) && node.id.type === 'ObjectPattern') {
  211. node.id.properties
  212. .filter((prop) => prop.type === 'Property' && isKeyLiteralLike(prop, prop.key))
  213. .forEach((prop) => {
  214. addUsedProperty(prop.key);
  215. });
  216. }
  217. },
  218. };
  219. }),
  220. };