componentUtil.js 4.5 KB

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