componentUtil.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. 'use strict';
  2. const doctrine = require('doctrine');
  3. const pragmaUtil = require('./pragma');
  4. const eslintUtil = require('./eslint');
  5. const getScope = eslintUtil.getScope;
  6. const getSourceCode = eslintUtil.getSourceCode;
  7. const getText = eslintUtil.getText;
  8. // eslint-disable-next-line valid-jsdoc
  9. /**
  10. * @template {(_: object) => any} T
  11. * @param {T} fn
  12. * @returns {T}
  13. */
  14. function memoize(fn) {
  15. const cache = new WeakMap();
  16. // @ts-ignore
  17. return function memoizedFn(arg) {
  18. const cachedValue = cache.get(arg);
  19. if (cachedValue !== undefined) {
  20. return cachedValue;
  21. }
  22. const v = fn(arg);
  23. cache.set(arg, v);
  24. return v;
  25. };
  26. }
  27. const getPragma = memoize(pragmaUtil.getFromContext);
  28. const getCreateClass = memoize(pragmaUtil.getCreateClassFromContext);
  29. /**
  30. * @param {ASTNode} node
  31. * @param {Context} context
  32. * @returns {boolean}
  33. */
  34. function isES5Component(node, context) {
  35. const pragma = getPragma(context);
  36. const createClass = getCreateClass(context);
  37. if (!node.parent || !node.parent.callee) {
  38. return false;
  39. }
  40. const callee = node.parent.callee;
  41. // React.createClass({})
  42. if (callee.type === 'MemberExpression') {
  43. return callee.object.name === pragma && callee.property.name === createClass;
  44. }
  45. // createClass({})
  46. if (callee.type === 'Identifier') {
  47. return callee.name === createClass;
  48. }
  49. return false;
  50. }
  51. /**
  52. * Check if the node is explicitly declared as a descendant of a React Component
  53. * @param {any} node
  54. * @param {Context} context
  55. * @returns {boolean}
  56. */
  57. function isExplicitComponent(node, context) {
  58. const sourceCode = getSourceCode(context);
  59. let comment;
  60. // Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes.
  61. // Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27
  62. // eslint-disable-next-line no-warning-comments
  63. // FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented.
  64. try {
  65. comment = sourceCode.getJSDocComment(node);
  66. } catch (e) {
  67. comment = null;
  68. }
  69. if (comment === null) {
  70. return false;
  71. }
  72. let commentAst;
  73. try {
  74. commentAst = doctrine.parse(comment.value, {
  75. unwrap: true,
  76. tags: ['extends', 'augments'],
  77. });
  78. } catch (e) {
  79. // handle a bug in the archived `doctrine`, see #2596
  80. return false;
  81. }
  82. const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent');
  83. return relevantTags.length > 0;
  84. }
  85. /**
  86. * @param {ASTNode} node
  87. * @param {Context} context
  88. * @returns {boolean}
  89. */
  90. function isES6Component(node, context) {
  91. const pragma = getPragma(context);
  92. if (isExplicitComponent(node, context)) {
  93. return true;
  94. }
  95. if (!node.superClass) {
  96. return false;
  97. }
  98. if (node.superClass.type === 'MemberExpression') {
  99. return node.superClass.object.name === pragma
  100. && /^(Pure)?Component$/.test(node.superClass.property.name);
  101. }
  102. if (node.superClass.type === 'Identifier') {
  103. return /^(Pure)?Component$/.test(node.superClass.name);
  104. }
  105. return false;
  106. }
  107. /**
  108. * Get the parent ES5 component node from the current scope
  109. * @param {Context} context
  110. * @param {ASTNode} node
  111. * @returns {ASTNode|null}
  112. */
  113. function getParentES5Component(context, node) {
  114. let scope = getScope(context, node);
  115. while (scope) {
  116. // @ts-ignore
  117. node = scope.block && scope.block.parent && scope.block.parent.parent;
  118. if (node && isES5Component(node, context)) {
  119. return node;
  120. }
  121. scope = scope.upper;
  122. }
  123. return null;
  124. }
  125. /**
  126. * Get the parent ES6 component node from the current scope
  127. * @param {Context} context
  128. * @param {ASTNode} node
  129. * @returns {ASTNode | null}
  130. */
  131. function getParentES6Component(context, node) {
  132. let scope = getScope(context, node);
  133. while (scope && scope.type !== 'class') {
  134. scope = scope.upper;
  135. }
  136. node = scope && scope.block;
  137. if (!node || !isES6Component(node, context)) {
  138. return null;
  139. }
  140. return node;
  141. }
  142. /**
  143. * Checks if a component extends React.PureComponent
  144. * @param {ASTNode} node
  145. * @param {Context} context
  146. * @returns {boolean}
  147. */
  148. function isPureComponent(node, context) {
  149. const pragma = getPragma(context);
  150. if (node.superClass) {
  151. return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(getText(context, node.superClass));
  152. }
  153. return false;
  154. }
  155. /**
  156. * @param {ASTNode} node
  157. * @returns {boolean}
  158. */
  159. function isStateMemberExpression(node) {
  160. return node.type === 'MemberExpression'
  161. && node.object.type === 'ThisExpression'
  162. && node.property.name === 'state';
  163. }
  164. module.exports = {
  165. isES5Component,
  166. isES6Component,
  167. getParentES5Component,
  168. getParentES6Component,
  169. isExplicitComponent,
  170. isPureComponent,
  171. isStateMemberExpression,
  172. };