Components.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. /**
  2. * @fileoverview Utility class and functions for React components detection
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. /**
  7. * Components
  8. * @class
  9. */
  10. function Components() {
  11. this.list = {};
  12. this.getId = function (node) {
  13. return node && node.range.join(':');
  14. };
  15. }
  16. /**
  17. * Add a node to the components list, or update it if it's already in the list
  18. *
  19. * @param {ASTNode} node The AST node being added.
  20. * @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes)
  21. */
  22. Components.prototype.add = function (node, confidence) {
  23. const id = this.getId(node);
  24. if (this.list[id]) {
  25. if (confidence === 0 || this.list[id].confidence === 0) {
  26. this.list[id].confidence = 0;
  27. } else {
  28. this.list[id].confidence = Math.max(this.list[id].confidence, confidence);
  29. }
  30. return;
  31. }
  32. this.list[id] = {
  33. node: node,
  34. confidence: confidence,
  35. };
  36. };
  37. /**
  38. * Find a component in the list using its node
  39. *
  40. * @param {ASTNode} node The AST node being searched.
  41. * @returns {Object} Component object, undefined if the component is not found
  42. */
  43. Components.prototype.get = function (node) {
  44. const id = this.getId(node);
  45. return this.list[id];
  46. };
  47. /**
  48. * Update a component in the list
  49. *
  50. * @param {ASTNode} node The AST node being updated.
  51. * @param {Object} props Additional properties to add to the component.
  52. */
  53. Components.prototype.set = function (node, props) {
  54. let currentNode = node;
  55. while (currentNode && !this.list[this.getId(currentNode)]) {
  56. currentNode = node.parent;
  57. }
  58. if (!currentNode) {
  59. return;
  60. }
  61. const id = this.getId(currentNode);
  62. this.list[id] = { ...this.list[id], ...props };
  63. };
  64. /**
  65. * Return the components list
  66. * Components for which we are not confident are not returned
  67. *
  68. * @returns {Object} Components list
  69. */
  70. Components.prototype.all = function () {
  71. const list = {};
  72. Object.keys(this.list).forEach((i) => {
  73. if ({}.hasOwnProperty.call(this.list, i) && this.list[i].confidence >= 2) {
  74. list[i] = this.list[i];
  75. }
  76. });
  77. return list;
  78. };
  79. /**
  80. * Return the length of the components list
  81. * Components for which we are not confident are not counted
  82. *
  83. * @returns {Number} Components list length
  84. */
  85. Components.prototype.length = function () {
  86. let length = 0;
  87. Object.keys(this.list).forEach((i) => {
  88. if ({}.hasOwnProperty.call(this.list, i) && this.list[i].confidence >= 2) {
  89. length += 1;
  90. }
  91. });
  92. return length;
  93. };
  94. function componentRule(rule, context) {
  95. const sourceCode = context.getSourceCode();
  96. const components = new Components();
  97. // Utilities for component detection
  98. const utils = {
  99. /**
  100. * Check if the node is a React ES5 component
  101. *
  102. * @param {ASTNode} node The AST node being checked.
  103. * @returns {Boolean} True if the node is a React ES5 component, false if not
  104. */
  105. isES5Component: function (node) {
  106. if (!node.parent) {
  107. return false;
  108. }
  109. return /^(React\.)?createClass$/.test(sourceCode.getText(node.parent.callee));
  110. },
  111. /**
  112. * Check if the node is a React ES6 component
  113. *
  114. * @param {ASTNode} node The AST node being checked.
  115. * @returns {Boolean} True if the node is a React ES6 component, false if not
  116. */
  117. isES6Component: function (node) {
  118. if (!node.superClass) {
  119. return false;
  120. }
  121. return /^(React\.)?(Pure)?Component$/.test(sourceCode.getText(node.superClass));
  122. },
  123. /**
  124. * Check if the node is returning JSX
  125. *
  126. * @param {ASTNode} node The AST node being checked (must be a ReturnStatement).
  127. * @returns {Boolean} True if the node is returning JSX, false if not
  128. */
  129. isReturningJSX: function (node) {
  130. let property;
  131. switch (node.type) {
  132. case 'ReturnStatement':
  133. property = 'argument';
  134. break;
  135. case 'ArrowFunctionExpression':
  136. property = 'body';
  137. break;
  138. default:
  139. return false;
  140. }
  141. const returnsJSX = node[property]
  142. && (node[property].type === 'JSXElement' || node[property].type === 'JSXFragment');
  143. const returnsReactCreateElement = node[property]
  144. && node[property].callee
  145. && node[property].callee.property
  146. && node[property].callee.property.name === 'createElement';
  147. return Boolean(returnsJSX || returnsReactCreateElement);
  148. },
  149. /**
  150. * Get the parent component node from the current scope
  151. *
  152. * @returns {ASTNode} component node, null if we are not in a component
  153. */
  154. getParentComponent: function () {
  155. return (
  156. utils.getParentES6Component()
  157. || utils.getParentES5Component()
  158. || utils.getParentStatelessComponent()
  159. );
  160. },
  161. /**
  162. * Get the parent ES5 component node from the current scope
  163. *
  164. * @returns {ASTNode} component node, null if we are not in a component
  165. */
  166. getParentES5Component: function () {
  167. // eslint-disable-next-line react/destructuring-assignment
  168. let scope = context.getScope();
  169. while (scope) {
  170. const node = scope.block && scope.block.parent && scope.block.parent.parent;
  171. if (node && utils.isES5Component(node)) {
  172. return node;
  173. }
  174. scope = scope.upper;
  175. }
  176. return null;
  177. },
  178. /**
  179. * Get the parent ES6 component node from the current scope
  180. *
  181. * @returns {ASTNode} component node, null if we are not in a component
  182. */
  183. getParentES6Component: function () {
  184. let scope = context.getScope();
  185. while (scope && scope.type !== 'class') {
  186. scope = scope.upper;
  187. }
  188. const node = scope && scope.block;
  189. if (!node || !utils.isES6Component(node)) {
  190. return null;
  191. }
  192. return node;
  193. },
  194. /**
  195. * Get the parent stateless component node from the current scope
  196. *
  197. * @returns {ASTNode} component node, null if we are not in a component
  198. */
  199. getParentStatelessComponent: function () {
  200. // eslint-disable-next-line react/destructuring-assignment
  201. let scope = context.getScope();
  202. while (scope) {
  203. const node = scope.block;
  204. // Ignore non functions
  205. const isFunction = /Function/.test(node.type);
  206. // Ignore classes methods
  207. const isNotMethod = !node.parent || node.parent.type !== 'MethodDefinition';
  208. // Ignore arguments (callback, etc.)
  209. const isNotArgument = !node.parent || node.parent.type !== 'CallExpression';
  210. if (isFunction && isNotMethod && isNotArgument) {
  211. return node;
  212. }
  213. scope = scope.upper;
  214. }
  215. return null;
  216. },
  217. /**
  218. * Get the related component from a node
  219. *
  220. * @param {ASTNode} node The AST node being checked (must be a MemberExpression).
  221. * @returns {ASTNode} component node, null if we cannot find the component
  222. */
  223. getRelatedComponent: function (node) {
  224. let currentNode = node;
  225. let i;
  226. let j;
  227. let k;
  228. let l;
  229. // Get the component path
  230. const componentPath = [];
  231. while (currentNode) {
  232. if (currentNode.property && currentNode.property.type === 'Identifier') {
  233. componentPath.push(currentNode.property.name);
  234. }
  235. if (currentNode.object && currentNode.object.type === 'Identifier') {
  236. componentPath.push(currentNode.object.name);
  237. }
  238. currentNode = currentNode.object;
  239. }
  240. componentPath.reverse();
  241. // Find the variable in the current scope
  242. const variableName = componentPath.shift();
  243. if (!variableName) {
  244. return null;
  245. }
  246. let variableInScope;
  247. const { variables } = context.getScope();
  248. for (i = 0, j = variables.length; i < j; i++) { // eslint-disable-line no-plusplus
  249. if (variables[i].name === variableName) {
  250. variableInScope = variables[i];
  251. break;
  252. }
  253. }
  254. if (!variableInScope) {
  255. return null;
  256. }
  257. // Find the variable declaration
  258. let defInScope;
  259. const { defs } = variableInScope;
  260. for (i = 0, j = defs.length; i < j; i++) { // eslint-disable-line no-plusplus
  261. if (
  262. defs[i].type === 'ClassName'
  263. || defs[i].type === 'FunctionName'
  264. || defs[i].type === 'Variable'
  265. ) {
  266. defInScope = defs[i];
  267. break;
  268. }
  269. }
  270. if (!defInScope) {
  271. return null;
  272. }
  273. currentNode = defInScope.node.init || defInScope.node;
  274. // Traverse the node properties to the component declaration
  275. for (i = 0, j = componentPath.length; i < j; i++) { // eslint-disable-line no-plusplus
  276. if (!currentNode.properties) {
  277. continue; // eslint-disable-line no-continue
  278. }
  279. for (k = 0, l = currentNode.properties.length; k < l; k++) { // eslint-disable-line no-plusplus, max-len
  280. if (currentNode.properties[k].key.name === componentPath[i]) {
  281. currentNode = currentNode.properties[k];
  282. break;
  283. }
  284. }
  285. if (!currentNode) {
  286. return null;
  287. }
  288. currentNode = currentNode.value;
  289. }
  290. // Return the component
  291. return components.get(currentNode);
  292. },
  293. };
  294. // Component detection instructions
  295. const detectionInstructions = {
  296. ClassDeclaration: function (node) {
  297. if (!utils.isES6Component(node)) {
  298. return;
  299. }
  300. components.add(node, 2);
  301. },
  302. ClassProperty: function () {
  303. const node = utils.getParentComponent();
  304. if (!node) {
  305. return;
  306. }
  307. components.add(node, 2);
  308. },
  309. ObjectExpression: function (node) {
  310. if (!utils.isES5Component(node)) {
  311. return;
  312. }
  313. components.add(node, 2);
  314. },
  315. FunctionExpression: function () {
  316. const node = utils.getParentComponent();
  317. if (!node) {
  318. return;
  319. }
  320. components.add(node, 1);
  321. },
  322. FunctionDeclaration: function () {
  323. const node = utils.getParentComponent();
  324. if (!node) {
  325. return;
  326. }
  327. components.add(node, 1);
  328. },
  329. ArrowFunctionExpression: function () {
  330. const node = utils.getParentComponent();
  331. if (!node) {
  332. return;
  333. }
  334. if (node.expression && utils.isReturningJSX(node)) {
  335. components.add(node, 2);
  336. } else {
  337. components.add(node, 1);
  338. }
  339. },
  340. ThisExpression: function () {
  341. const node = utils.getParentComponent();
  342. if (!node || !/Function/.test(node.type)) {
  343. return;
  344. }
  345. // Ban functions with a ThisExpression
  346. components.add(node, 0);
  347. },
  348. ReturnStatement: function (node) {
  349. if (!utils.isReturningJSX(node)) {
  350. return;
  351. }
  352. const parentNode = utils.getParentComponent();
  353. if (!parentNode) {
  354. return;
  355. }
  356. components.add(parentNode, 2);
  357. },
  358. };
  359. // Update the provided rule instructions to add the component detection
  360. const ruleInstructions = rule(context, components, utils);
  361. const updatedRuleInstructions = { ...ruleInstructions };
  362. Object.keys(detectionInstructions).forEach((instruction) => {
  363. updatedRuleInstructions[instruction] = (node) => {
  364. detectionInstructions[instruction](node);
  365. return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : undefined;
  366. };
  367. });
  368. // Return the updated rule instructions
  369. return updatedRuleInstructions;
  370. }
  371. Components.detect = function (rule) {
  372. return componentRule.bind(this, rule);
  373. };
  374. module.exports = Components;