Components.js 29 KB

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