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