Components.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946
  1. /**
  2. * @fileoverview Utility class and functions for React components detection
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. const arrayIncludes = require('array-includes');
  7. const fromEntries = require('object.fromentries');
  8. const values = require('object.values');
  9. const iterFrom = require('es-iterator-helpers/Iterator.from');
  10. const map = require('es-iterator-helpers/Iterator.prototype.map');
  11. const variableUtil = require('./variable');
  12. const pragmaUtil = require('./pragma');
  13. const astUtil = require('./ast');
  14. const componentUtil = require('./componentUtil');
  15. const propTypesUtil = require('./propTypes');
  16. const jsxUtil = require('./jsx');
  17. const usedPropTypesUtil = require('./usedPropTypes');
  18. const defaultPropsUtil = require('./defaultProps');
  19. const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');
  20. const isDestructuredFromPragmaImport = require('./isDestructuredFromPragmaImport');
  21. function getId(node) {
  22. return node ? `${node.range[0]}:${node.range[1]}` : '';
  23. }
  24. function usedPropTypesAreEquivalent(propA, propB) {
  25. if (propA.name === propB.name) {
  26. if (!propA.allNames && !propB.allNames) {
  27. return true;
  28. }
  29. if (Array.isArray(propA.allNames) && Array.isArray(propB.allNames) && propA.allNames.join('') === propB.allNames.join('')) {
  30. return true;
  31. }
  32. return false;
  33. }
  34. return false;
  35. }
  36. function mergeUsedPropTypes(propsList, newPropsList) {
  37. const propsToAdd = newPropsList.filter((newProp) => {
  38. const newPropIsAlreadyInTheList = propsList.some((prop) => usedPropTypesAreEquivalent(prop, newProp));
  39. return !newPropIsAlreadyInTheList;
  40. });
  41. return propsList.concat(propsToAdd);
  42. }
  43. const USE_HOOK_PREFIX_REGEX = /^use[A-Z]/;
  44. const Lists = new WeakMap();
  45. const ReactImports = new WeakMap();
  46. /**
  47. * Components
  48. */
  49. class Components {
  50. constructor() {
  51. Lists.set(this, {});
  52. ReactImports.set(this, {});
  53. }
  54. /**
  55. * Add a node to the components list, or update it if it's already in the list
  56. *
  57. * @param {ASTNode} node The AST node being added.
  58. * @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes)
  59. * @returns {Object} Added component object
  60. */
  61. add(node, confidence) {
  62. const id = getId(node);
  63. const list = Lists.get(this);
  64. if (list[id]) {
  65. if (confidence === 0 || list[id].confidence === 0) {
  66. list[id].confidence = 0;
  67. } else {
  68. list[id].confidence = Math.max(list[id].confidence, confidence);
  69. }
  70. return list[id];
  71. }
  72. list[id] = {
  73. node,
  74. confidence,
  75. };
  76. return list[id];
  77. }
  78. /**
  79. * Find a component in the list using its node
  80. *
  81. * @param {ASTNode} node The AST node being searched.
  82. * @returns {Object} Component object, undefined if the component is not found or has confidence value of 0.
  83. */
  84. get(node) {
  85. const id = getId(node);
  86. const item = Lists.get(this)[id];
  87. if (item && item.confidence >= 1) {
  88. return item;
  89. }
  90. return null;
  91. }
  92. /**
  93. * Update a component in the list
  94. *
  95. * @param {ASTNode} node The AST node being updated.
  96. * @param {Object} props Additional properties to add to the component.
  97. */
  98. set(node, props) {
  99. const list = Lists.get(this);
  100. let component = list[getId(node)];
  101. while (!component || component.confidence < 1) {
  102. node = node.parent;
  103. if (!node) {
  104. return;
  105. }
  106. component = list[getId(node)];
  107. }
  108. Object.assign(
  109. component,
  110. props,
  111. {
  112. usedPropTypes: mergeUsedPropTypes(
  113. component.usedPropTypes || [],
  114. props.usedPropTypes || []
  115. ),
  116. }
  117. );
  118. }
  119. /**
  120. * Return the components list
  121. * Components for which we are not confident are not returned
  122. *
  123. * @returns {Object} Components list
  124. */
  125. list() {
  126. const thisList = Lists.get(this);
  127. const list = {};
  128. const usedPropTypes = {};
  129. // Find props used in components for which we are not confident
  130. Object.keys(thisList).filter((i) => thisList[i].confidence < 2).forEach((i) => {
  131. let component = null;
  132. let node = null;
  133. node = thisList[i].node;
  134. while (!component && node.parent) {
  135. node = node.parent;
  136. // Stop moving up if we reach a decorator
  137. if (node.type === 'Decorator') {
  138. break;
  139. }
  140. component = this.get(node);
  141. }
  142. if (component) {
  143. const newUsedProps = (thisList[i].usedPropTypes || []).filter((propType) => !propType.node || propType.node.kind !== 'init');
  144. const componentId = getId(component.node);
  145. usedPropTypes[componentId] = mergeUsedPropTypes(usedPropTypes[componentId] || [], newUsedProps);
  146. }
  147. });
  148. // Assign used props in not confident components to the parent component
  149. Object.keys(thisList).filter((j) => thisList[j].confidence >= 2).forEach((j) => {
  150. const id = getId(thisList[j].node);
  151. list[j] = thisList[j];
  152. if (usedPropTypes[id]) {
  153. list[j].usedPropTypes = mergeUsedPropTypes(list[j].usedPropTypes || [], usedPropTypes[id]);
  154. }
  155. });
  156. return list;
  157. }
  158. /**
  159. * Return the length of the components list
  160. * Components for which we are not confident are not counted
  161. *
  162. * @returns {Number} Components list length
  163. */
  164. length() {
  165. const list = Lists.get(this);
  166. return values(list).filter((component) => component.confidence >= 2).length;
  167. }
  168. /**
  169. * Return the node naming the default React import
  170. * It can be used to determine the local name of import, even if it's imported
  171. * with an unusual name.
  172. *
  173. * @returns {ASTNode} React default import node
  174. */
  175. getDefaultReactImports() {
  176. return ReactImports.get(this).defaultReactImports;
  177. }
  178. /**
  179. * Return the nodes of all React named imports
  180. *
  181. * @returns {Object} The list of React named imports
  182. */
  183. getNamedReactImports() {
  184. return ReactImports.get(this).namedReactImports;
  185. }
  186. /**
  187. * Add the default React import specifier to the scope
  188. *
  189. * @param {ASTNode} specifier The AST Node of the default React import
  190. * @returns {void}
  191. */
  192. addDefaultReactImport(specifier) {
  193. const info = ReactImports.get(this);
  194. ReactImports.set(this, Object.assign({}, info, {
  195. defaultReactImports: (info.defaultReactImports || []).concat(specifier),
  196. }));
  197. }
  198. /**
  199. * Add a named React import specifier to the scope
  200. *
  201. * @param {ASTNode} specifier The AST Node of a named React import
  202. * @returns {void}
  203. */
  204. addNamedReactImport(specifier) {
  205. const info = ReactImports.get(this);
  206. ReactImports.set(this, Object.assign({}, info, {
  207. namedReactImports: (info.namedReactImports || []).concat(specifier),
  208. }));
  209. }
  210. }
  211. function getWrapperFunctions(context, pragma) {
  212. const componentWrapperFunctions = context.settings.componentWrapperFunctions || [];
  213. // eslint-disable-next-line arrow-body-style
  214. return componentWrapperFunctions.map((wrapperFunction) => {
  215. return typeof wrapperFunction === 'string'
  216. ? { property: wrapperFunction }
  217. : Object.assign({}, wrapperFunction, {
  218. object: wrapperFunction.object === '<pragma>' ? pragma : wrapperFunction.object,
  219. });
  220. }).concat([
  221. { property: 'forwardRef', object: pragma },
  222. { property: 'memo', object: pragma },
  223. ]);
  224. }
  225. // eslint-disable-next-line valid-jsdoc
  226. /**
  227. * Merge many eslint rules into one
  228. * @param {{[_: string]: Function}[]} rules the returned values for eslint rule.create(context)
  229. * @returns {{[_: string]: Function}} merged rule
  230. */
  231. function mergeRules(rules) {
  232. /** @type {Map<string, Function[]>} */
  233. const handlersByKey = new Map();
  234. rules.forEach((rule) => {
  235. Object.keys(rule).forEach((key) => {
  236. const fns = handlersByKey.get(key);
  237. if (!fns) {
  238. handlersByKey.set(key, [rule[key]]);
  239. } else {
  240. fns.push(rule[key]);
  241. }
  242. });
  243. });
  244. /** @type {{ [key: string]: Function }} */
  245. return fromEntries(map(iterFrom(handlersByKey), (entry) => [
  246. entry[0],
  247. function mergedHandler(node) {
  248. entry[1].forEach((fn) => {
  249. fn(node);
  250. });
  251. },
  252. ]));
  253. }
  254. function componentRule(rule, context) {
  255. const pragma = pragmaUtil.getFromContext(context);
  256. const sourceCode = context.getSourceCode();
  257. const components = new Components();
  258. const wrapperFunctions = getWrapperFunctions(context, pragma);
  259. // Utilities for component detection
  260. const utils = {
  261. /**
  262. * Check if variable is destructured from pragma import
  263. *
  264. * @param {string} variable The variable name to check
  265. * @returns {Boolean} True if createElement is destructured from the pragma
  266. */
  267. isDestructuredFromPragmaImport(variable) {
  268. return isDestructuredFromPragmaImport(variable, context);
  269. },
  270. isReturningJSX(ASTNode, strict) {
  271. return jsxUtil.isReturningJSX(ASTNode, context, strict, true);
  272. },
  273. isReturningJSXOrNull(ASTNode, strict) {
  274. return jsxUtil.isReturningJSX(ASTNode, context, strict);
  275. },
  276. isReturningOnlyNull(ASTNode) {
  277. return jsxUtil.isReturningOnlyNull(ASTNode, context);
  278. },
  279. getPragmaComponentWrapper(node) {
  280. let isPragmaComponentWrapper;
  281. let currentNode = node;
  282. let prevNode;
  283. do {
  284. currentNode = currentNode.parent;
  285. isPragmaComponentWrapper = this.isPragmaComponentWrapper(currentNode);
  286. if (isPragmaComponentWrapper) {
  287. prevNode = currentNode;
  288. }
  289. } while (isPragmaComponentWrapper);
  290. return prevNode;
  291. },
  292. getComponentNameFromJSXElement(node) {
  293. if (node.type !== 'JSXElement') {
  294. return null;
  295. }
  296. if (node.openingElement && node.openingElement.name && node.openingElement.name.name) {
  297. return node.openingElement.name.name;
  298. }
  299. return null;
  300. },
  301. /**
  302. * Getting the first JSX element's name.
  303. * @param {object} node
  304. * @returns {string | null}
  305. */
  306. getNameOfWrappedComponent(node) {
  307. if (node.length < 1) {
  308. return null;
  309. }
  310. const body = node[0].body;
  311. if (!body) {
  312. return null;
  313. }
  314. if (body.type === 'JSXElement') {
  315. return this.getComponentNameFromJSXElement(body);
  316. }
  317. if (body.type === 'BlockStatement') {
  318. const jsxElement = body.body.find((item) => item.type === 'ReturnStatement');
  319. return jsxElement
  320. && jsxElement.argument
  321. && this.getComponentNameFromJSXElement(jsxElement.argument);
  322. }
  323. return null;
  324. },
  325. /**
  326. * Get the list of names of components created till now
  327. * @returns {string | boolean}
  328. */
  329. getDetectedComponents() {
  330. const list = components.list();
  331. return values(list).filter((val) => {
  332. if (val.node.type === 'ClassDeclaration') {
  333. return true;
  334. }
  335. if (
  336. val.node.type === 'ArrowFunctionExpression'
  337. && val.node.parent
  338. && val.node.parent.type === 'VariableDeclarator'
  339. && val.node.parent.id
  340. ) {
  341. return true;
  342. }
  343. return false;
  344. }).map((val) => {
  345. if (val.node.type === 'ArrowFunctionExpression') return val.node.parent.id.name;
  346. return val.node.id && val.node.id.name;
  347. });
  348. },
  349. /**
  350. * It will check whether memo/forwardRef is wrapping existing component or
  351. * creating a new one.
  352. * @param {object} node
  353. * @returns {boolean}
  354. */
  355. nodeWrapsComponent(node) {
  356. const childComponent = this.getNameOfWrappedComponent(node.arguments);
  357. const componentList = this.getDetectedComponents();
  358. return !!childComponent && arrayIncludes(componentList, childComponent);
  359. },
  360. isPragmaComponentWrapper(node) {
  361. if (!node || node.type !== 'CallExpression') {
  362. return false;
  363. }
  364. return wrapperFunctions.some((wrapperFunction) => {
  365. if (node.callee.type === 'MemberExpression') {
  366. return wrapperFunction.object
  367. && wrapperFunction.object === node.callee.object.name
  368. && wrapperFunction.property === node.callee.property.name
  369. && !this.nodeWrapsComponent(node);
  370. }
  371. return wrapperFunction.property === node.callee.name
  372. && (!wrapperFunction.object
  373. // Functions coming from the current pragma need special handling
  374. || (wrapperFunction.object === pragma && this.isDestructuredFromPragmaImport(node.callee.name))
  375. );
  376. });
  377. },
  378. /**
  379. * Find a return statement in the current node
  380. *
  381. * @param {ASTNode} node The AST node being checked
  382. */
  383. findReturnStatement: astUtil.findReturnStatement,
  384. /**
  385. * Get the parent component node from the current scope
  386. *
  387. * @returns {ASTNode} component node, null if we are not in a component
  388. */
  389. getParentComponent() {
  390. return (
  391. componentUtil.getParentES6Component(context)
  392. || componentUtil.getParentES5Component(context)
  393. || utils.getParentStatelessComponent()
  394. );
  395. },
  396. /**
  397. * @param {ASTNode} node
  398. * @returns {boolean}
  399. */
  400. isInAllowedPositionForComponent(node) {
  401. switch (node.parent.type) {
  402. case 'VariableDeclarator':
  403. case 'AssignmentExpression':
  404. case 'Property':
  405. case 'ReturnStatement':
  406. case 'ExportDefaultDeclaration':
  407. case 'ArrowFunctionExpression': {
  408. return true;
  409. }
  410. case 'SequenceExpression': {
  411. return utils.isInAllowedPositionForComponent(node.parent)
  412. && node === node.parent.expressions[node.parent.expressions.length - 1];
  413. }
  414. default:
  415. return false;
  416. }
  417. },
  418. /**
  419. * Get node if node is a stateless component, or node.parent in cases like
  420. * `React.memo` or `React.forwardRef`. Otherwise returns `undefined`.
  421. * @param {ASTNode} node
  422. * @returns {ASTNode | undefined}
  423. */
  424. getStatelessComponent(node) {
  425. const parent = node.parent;
  426. if (
  427. node.type === 'FunctionDeclaration'
  428. && (!node.id || isFirstLetterCapitalized(node.id.name))
  429. && utils.isReturningJSXOrNull(node)
  430. ) {
  431. return node;
  432. }
  433. if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
  434. const isPropertyAssignment = parent.type === 'AssignmentExpression'
  435. && parent.left.type === 'MemberExpression';
  436. const isModuleExportsAssignment = isPropertyAssignment
  437. && parent.left.object.name === 'module'
  438. && parent.left.property.name === 'exports';
  439. if (node.parent.type === 'ExportDefaultDeclaration') {
  440. if (utils.isReturningJSX(node)) {
  441. return node;
  442. }
  443. return undefined;
  444. }
  445. if (node.parent.type === 'VariableDeclarator' && utils.isReturningJSXOrNull(node)) {
  446. if (isFirstLetterCapitalized(node.parent.id.name)) {
  447. return node;
  448. }
  449. return undefined;
  450. }
  451. // case: const any = () => { return (props) => null }
  452. // case: const any = () => (props) => null
  453. if (
  454. (node.parent.type === 'ReturnStatement' || (node.parent.type === 'ArrowFunctionExpression' && node.parent.expression))
  455. && !utils.isReturningJSX(node)
  456. ) {
  457. return undefined;
  458. }
  459. // case: any = () => { return => null }
  460. // case: any = () => null
  461. if (node.parent.type === 'AssignmentExpression' && !isPropertyAssignment && utils.isReturningJSXOrNull(node)) {
  462. if (isFirstLetterCapitalized(node.parent.left.name)) {
  463. return node;
  464. }
  465. return undefined;
  466. }
  467. // case: any = () => () => null
  468. if (node.parent.type === 'ArrowFunctionExpression' && node.parent.parent.type === 'AssignmentExpression' && !isPropertyAssignment && utils.isReturningJSXOrNull(node)) {
  469. if (isFirstLetterCapitalized(node.parent.parent.left.name)) {
  470. return node;
  471. }
  472. return undefined;
  473. }
  474. // case: { any: () => () => null }
  475. if (node.parent.type === 'ArrowFunctionExpression' && node.parent.parent.type === 'Property' && !isPropertyAssignment && utils.isReturningJSXOrNull(node)) {
  476. if (isFirstLetterCapitalized(node.parent.parent.key.name)) {
  477. return node;
  478. }
  479. return undefined;
  480. }
  481. // case: any = function() {return function() {return null;};}
  482. if (node.parent.type === 'ReturnStatement') {
  483. if (isFirstLetterCapitalized(node.id && node.id.name)) {
  484. return node;
  485. }
  486. const functionExpr = node.parent.parent.parent;
  487. if (functionExpr.parent.type === 'AssignmentExpression' && !isPropertyAssignment && utils.isReturningJSXOrNull(node)) {
  488. if (isFirstLetterCapitalized(functionExpr.parent.left.name)) {
  489. return node;
  490. }
  491. return undefined;
  492. }
  493. }
  494. // case: { any: function() {return function() {return null;};} }
  495. if (node.parent.type === 'ReturnStatement') {
  496. const functionExpr = node.parent.parent.parent;
  497. if (functionExpr.parent.type === 'Property' && !isPropertyAssignment && utils.isReturningJSXOrNull(node)) {
  498. if (isFirstLetterCapitalized(functionExpr.parent.key.name)) {
  499. return node;
  500. }
  501. return undefined;
  502. }
  503. }
  504. // for case abc = { [someobject.somekey]: props => { ... return not-jsx } }
  505. if (node.parent && node.parent.key && node.parent.key.type === 'MemberExpression' && !utils.isReturningJSX(node) && !utils.isReturningOnlyNull(node)) {
  506. return undefined;
  507. }
  508. if (
  509. node.parent.type === 'Property' && (
  510. (node.parent.method && !node.parent.computed) // case: { f() { return ... } }
  511. || (!node.id && !node.parent.computed) // case: { f: () => ... }
  512. )
  513. ) {
  514. if (isFirstLetterCapitalized(node.parent.key.name) && utils.isReturningJSX(node)) {
  515. return node;
  516. }
  517. return undefined;
  518. }
  519. // Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
  520. const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
  521. if (pragmaComponentWrapper && utils.isReturningJSXOrNull(node)) {
  522. return pragmaComponentWrapper;
  523. }
  524. if (!(utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node))) {
  525. return undefined;
  526. }
  527. if (utils.isParentComponentNotStatelessComponent(node)) {
  528. return undefined;
  529. }
  530. if (node.id) {
  531. return isFirstLetterCapitalized(node.id.name) ? node : undefined;
  532. }
  533. if (
  534. isPropertyAssignment
  535. && !isModuleExportsAssignment
  536. && !isFirstLetterCapitalized(parent.left.property.name)
  537. ) {
  538. return undefined;
  539. }
  540. if (parent.type === 'Property' && utils.isReturningOnlyNull(node)) {
  541. return undefined;
  542. }
  543. return node;
  544. }
  545. return undefined;
  546. },
  547. /**
  548. * Get the parent stateless component node from the current scope
  549. *
  550. * @returns {ASTNode} component node, null if we are not in a component
  551. */
  552. getParentStatelessComponent() {
  553. let scope = context.getScope();
  554. while (scope) {
  555. const node = scope.block;
  556. const statelessComponent = utils.getStatelessComponent(node);
  557. if (statelessComponent) {
  558. return statelessComponent;
  559. }
  560. scope = scope.upper;
  561. }
  562. return null;
  563. },
  564. /**
  565. * Get the related component from a node
  566. *
  567. * @param {ASTNode} node The AST node being checked (must be a MemberExpression).
  568. * @returns {ASTNode} component node, null if we cannot find the component
  569. */
  570. getRelatedComponent(node) {
  571. let i;
  572. let j;
  573. let k;
  574. let l;
  575. let componentNode;
  576. // Get the component path
  577. const componentPath = [];
  578. while (node) {
  579. if (node.property && node.property.type === 'Identifier') {
  580. componentPath.push(node.property.name);
  581. }
  582. if (node.object && node.object.type === 'Identifier') {
  583. componentPath.push(node.object.name);
  584. }
  585. node = node.object;
  586. }
  587. componentPath.reverse();
  588. const componentName = componentPath.slice(0, componentPath.length - 1).join('.');
  589. // Find the variable in the current scope
  590. const variableName = componentPath.shift();
  591. if (!variableName) {
  592. return null;
  593. }
  594. let variableInScope;
  595. const variables = variableUtil.variablesInScope(context);
  596. for (i = 0, j = variables.length; i < j; i++) {
  597. if (variables[i].name === variableName) {
  598. variableInScope = variables[i];
  599. break;
  600. }
  601. }
  602. if (!variableInScope) {
  603. return null;
  604. }
  605. // Try to find the component using variable references
  606. const refs = variableInScope.references;
  607. refs.some((ref) => {
  608. let refId = ref.identifier;
  609. if (refId.parent && refId.parent.type === 'MemberExpression') {
  610. refId = refId.parent;
  611. }
  612. if (sourceCode.getText(refId) !== componentName) {
  613. return false;
  614. }
  615. if (refId.type === 'MemberExpression') {
  616. componentNode = refId.parent.right;
  617. } else if (
  618. refId.parent
  619. && refId.parent.type === 'VariableDeclarator'
  620. && refId.parent.init
  621. && refId.parent.init.type !== 'Identifier'
  622. ) {
  623. componentNode = refId.parent.init;
  624. }
  625. return true;
  626. });
  627. if (componentNode) {
  628. // Return the component
  629. return components.add(componentNode, 1);
  630. }
  631. // Try to find the component using variable declarations
  632. const defs = variableInScope.defs;
  633. const defInScope = defs.find((def) => (
  634. def.type === 'ClassName'
  635. || def.type === 'FunctionName'
  636. || def.type === 'Variable'
  637. ));
  638. if (!defInScope || !defInScope.node) {
  639. return null;
  640. }
  641. componentNode = defInScope.node.init || defInScope.node;
  642. // Traverse the node properties to the component declaration
  643. for (i = 0, j = componentPath.length; i < j; i++) {
  644. if (!componentNode.properties) {
  645. continue; // eslint-disable-line no-continue
  646. }
  647. for (k = 0, l = componentNode.properties.length; k < l; k++) {
  648. if (componentNode.properties[k].key && componentNode.properties[k].key.name === componentPath[i]) {
  649. componentNode = componentNode.properties[k];
  650. break;
  651. }
  652. }
  653. if (!componentNode || !componentNode.value) {
  654. return null;
  655. }
  656. componentNode = componentNode.value;
  657. }
  658. // Return the component
  659. return components.add(componentNode, 1);
  660. },
  661. isParentComponentNotStatelessComponent(node) {
  662. return !!(
  663. node.parent
  664. && node.parent.key
  665. && node.parent.key.type === 'Identifier'
  666. // custom component functions must start with a capital letter (returns false otherwise)
  667. && node.parent.key.name.charAt(0) === node.parent.key.name.charAt(0).toLowerCase()
  668. // react render function cannot have params
  669. && !!(node.params || []).length
  670. );
  671. },
  672. /**
  673. * Identify whether a node (CallExpression) is a call to a React hook
  674. *
  675. * @param {ASTNode} node The AST node being searched. (expects CallExpression)
  676. * @param {('useCallback'|'useContext'|'useDebugValue'|'useEffect'|'useImperativeHandle'|'useLayoutEffect'|'useMemo'|'useReducer'|'useRef'|'useState')[]} [expectedHookNames] React hook names to which search is limited.
  677. * @returns {Boolean} True if the node is a call to a React hook
  678. */
  679. isReactHookCall(node, expectedHookNames) {
  680. if (node.type !== 'CallExpression') {
  681. return false;
  682. }
  683. const defaultReactImports = components.getDefaultReactImports();
  684. const namedReactImports = components.getNamedReactImports();
  685. const defaultReactImportName = defaultReactImports
  686. && defaultReactImports[0]
  687. && defaultReactImports[0].local.name;
  688. const reactHookImportSpecifiers = namedReactImports
  689. && namedReactImports.filter((specifier) => USE_HOOK_PREFIX_REGEX.test(specifier.imported.name));
  690. const reactHookImportNames = reactHookImportSpecifiers
  691. && fromEntries(reactHookImportSpecifiers.map((specifier) => [specifier.local.name, specifier.imported.name]));
  692. const isPotentialReactHookCall = defaultReactImportName
  693. && node.callee.type === 'MemberExpression'
  694. && node.callee.object.type === 'Identifier'
  695. && node.callee.object.name === defaultReactImportName
  696. && node.callee.property.type === 'Identifier'
  697. && node.callee.property.name.match(USE_HOOK_PREFIX_REGEX);
  698. const isPotentialHookCall = reactHookImportNames
  699. && node.callee.type === 'Identifier'
  700. && node.callee.name.match(USE_HOOK_PREFIX_REGEX);
  701. const scope = (isPotentialReactHookCall || isPotentialHookCall) && context.getScope();
  702. const reactResolvedDefs = isPotentialReactHookCall
  703. && scope.references
  704. && scope.references.find(
  705. (reference) => reference.identifier.name === defaultReactImportName
  706. ).resolved.defs;
  707. const isReactShadowed = isPotentialReactHookCall && reactResolvedDefs
  708. && reactResolvedDefs.some((reactDef) => reactDef.type !== 'ImportBinding');
  709. const potentialHookReference = isPotentialHookCall
  710. && scope.references
  711. && scope.references.find(
  712. (reference) => reactHookImportNames[reference.identifier.name]
  713. );
  714. const hookResolvedDefs = potentialHookReference && potentialHookReference.resolved.defs;
  715. const localHookName = (isPotentialReactHookCall && node.callee.property.name)
  716. || (isPotentialHookCall && potentialHookReference && node.callee.name);
  717. const isHookShadowed = isPotentialHookCall
  718. && hookResolvedDefs
  719. && hookResolvedDefs.some(
  720. (hookDef) => hookDef.name.name === localHookName
  721. && hookDef.type !== 'ImportBinding'
  722. );
  723. const isHookCall = (isPotentialReactHookCall && !isReactShadowed)
  724. || (isPotentialHookCall && localHookName && !isHookShadowed);
  725. if (!isHookCall) {
  726. return false;
  727. }
  728. if (!expectedHookNames) {
  729. return true;
  730. }
  731. return arrayIncludes(
  732. expectedHookNames,
  733. (reactHookImportNames && reactHookImportNames[localHookName]) || localHookName
  734. );
  735. },
  736. };
  737. // Component detection instructions
  738. const detectionInstructions = {
  739. CallExpression(node) {
  740. if (!utils.isPragmaComponentWrapper(node)) {
  741. return;
  742. }
  743. if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
  744. components.add(node, 2);
  745. }
  746. },
  747. ClassExpression(node) {
  748. if (!componentUtil.isES6Component(node, context)) {
  749. return;
  750. }
  751. components.add(node, 2);
  752. },
  753. ClassDeclaration(node) {
  754. if (!componentUtil.isES6Component(node, context)) {
  755. return;
  756. }
  757. components.add(node, 2);
  758. },
  759. ObjectExpression(node) {
  760. if (!componentUtil.isES5Component(node, context)) {
  761. return;
  762. }
  763. components.add(node, 2);
  764. },
  765. FunctionExpression(node) {
  766. if (node.async) {
  767. components.add(node, 0);
  768. return;
  769. }
  770. const component = utils.getStatelessComponent(node);
  771. if (!component) {
  772. return;
  773. }
  774. components.add(component, 2);
  775. },
  776. FunctionDeclaration(node) {
  777. if (node.async) {
  778. components.add(node, 0);
  779. return;
  780. }
  781. node = utils.getStatelessComponent(node);
  782. if (!node) {
  783. return;
  784. }
  785. components.add(node, 2);
  786. },
  787. ArrowFunctionExpression(node) {
  788. if (node.async) {
  789. components.add(node, 0);
  790. return;
  791. }
  792. const component = utils.getStatelessComponent(node);
  793. if (!component) {
  794. return;
  795. }
  796. components.add(component, 2);
  797. },
  798. ThisExpression(node) {
  799. const component = utils.getParentStatelessComponent();
  800. if (!component || !/Function/.test(component.type) || !node.parent.property) {
  801. return;
  802. }
  803. // Ban functions accessing a property on a ThisExpression
  804. components.add(node, 0);
  805. },
  806. };
  807. // Detect React import specifiers
  808. const reactImportInstructions = {
  809. ImportDeclaration(node) {
  810. const isReactImported = node.source.type === 'Literal' && node.source.value === 'react';
  811. if (!isReactImported) {
  812. return;
  813. }
  814. node.specifiers.forEach((specifier) => {
  815. if (specifier.type === 'ImportDefaultSpecifier') {
  816. components.addDefaultReactImport(specifier);
  817. }
  818. if (specifier.type === 'ImportSpecifier') {
  819. components.addNamedReactImport(specifier);
  820. }
  821. });
  822. },
  823. };
  824. const ruleInstructions = rule(context, components, utils);
  825. const propTypesInstructions = propTypesUtil(context, components, utils);
  826. const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils);
  827. const defaultPropsInstructions = defaultPropsUtil(context, components, utils);
  828. const mergedRule = mergeRules([
  829. detectionInstructions,
  830. propTypesInstructions,
  831. usedPropTypesInstructions,
  832. defaultPropsInstructions,
  833. reactImportInstructions,
  834. ruleInstructions,
  835. ]);
  836. return mergedRule;
  837. }
  838. module.exports = Object.assign(Components, {
  839. detect(rule) {
  840. return componentRule.bind(this, rule);
  841. },
  842. });