Components.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. /**
  2. * @fileoverview Utility class and functions for React components detection
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. const doctrine = require('doctrine');
  7. const arrayIncludes = require('array-includes');
  8. const values = require('object.values');
  9. const variableUtil = require('./variable');
  10. const pragmaUtil = require('./pragma');
  11. const astUtil = require('./ast');
  12. const propTypesUtil = require('./propTypes');
  13. const jsxUtil = require('./jsx');
  14. const usedPropTypesUtil = require('./usedPropTypes');
  15. const defaultPropsUtil = require('./defaultProps');
  16. const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');
  17. function getId(node) {
  18. return node && node.range.join(':');
  19. }
  20. function usedPropTypesAreEquivalent(propA, propB) {
  21. if (propA.name === propB.name) {
  22. if (!propA.allNames && !propB.allNames) {
  23. return true;
  24. }
  25. if (Array.isArray(propA.allNames) && Array.isArray(propB.allNames) && propA.allNames.join('') === propB.allNames.join('')) {
  26. return true;
  27. }
  28. return false;
  29. }
  30. return false;
  31. }
  32. function mergeUsedPropTypes(propsList, newPropsList) {
  33. const propsToAdd = [];
  34. newPropsList.forEach((newProp) => {
  35. const newPropisAlreadyInTheList = propsList.some((prop) => usedPropTypesAreEquivalent(prop, newProp));
  36. if (!newPropisAlreadyInTheList) {
  37. propsToAdd.push(newProp);
  38. }
  39. });
  40. return propsList.concat(propsToAdd);
  41. }
  42. function isReturnsConditionalJSX(node, property, strict) {
  43. const returnsConditionalJSXConsequent = node[property]
  44. && node[property].type === 'ConditionalExpression'
  45. && jsxUtil.isJSX(node[property].consequent);
  46. const returnsConditionalJSXAlternate = node[property]
  47. && node[property].type === 'ConditionalExpression'
  48. && jsxUtil.isJSX(node[property].alternate);
  49. return strict
  50. ? (returnsConditionalJSXConsequent && returnsConditionalJSXAlternate)
  51. : (returnsConditionalJSXConsequent || returnsConditionalJSXAlternate);
  52. }
  53. function isReturnsLogicalJSX(node, property, strict) {
  54. const returnsLogicalJSXLeft = node[property]
  55. && node[property].type === 'LogicalExpression'
  56. && jsxUtil.isJSX(node[property].left);
  57. const returnsLogicalJSXRight = node[property]
  58. && node[property].type === 'LogicalExpression'
  59. && jsxUtil.isJSX(node[property].right);
  60. return strict
  61. ? (returnsLogicalJSXLeft && returnsLogicalJSXRight)
  62. : (returnsLogicalJSXLeft || returnsLogicalJSXRight);
  63. }
  64. function isReturnsSequentialJSX(node, property) {
  65. return node[property]
  66. && node[property].type === 'SequenceExpression'
  67. && jsxUtil.isJSX(node[property].expressions[node[property].expressions.length - 1]);
  68. }
  69. const Lists = new WeakMap();
  70. /**
  71. * Components
  72. */
  73. class Components {
  74. constructor() {
  75. Lists.set(this, {});
  76. }
  77. /**
  78. * Add a node to the components list, or update it if it's already in the list
  79. *
  80. * @param {ASTNode} node The AST node being added.
  81. * @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes)
  82. * @returns {Object} Added component object
  83. */
  84. add(node, confidence) {
  85. const id = getId(node);
  86. const list = Lists.get(this);
  87. if (list[id]) {
  88. if (confidence === 0 || list[id].confidence === 0) {
  89. list[id].confidence = 0;
  90. } else {
  91. list[id].confidence = Math.max(list[id].confidence, confidence);
  92. }
  93. return list[id];
  94. }
  95. list[id] = {
  96. node,
  97. confidence
  98. };
  99. return list[id];
  100. }
  101. /**
  102. * Find a component in the list using its node
  103. *
  104. * @param {ASTNode} node The AST node being searched.
  105. * @returns {Object} Component object, undefined if the component is not found or has confidence value of 0.
  106. */
  107. get(node) {
  108. const id = getId(node);
  109. const item = Lists.get(this)[id];
  110. if (item && item.confidence >= 1) {
  111. return item;
  112. }
  113. return null;
  114. }
  115. /**
  116. * Update a component in the list
  117. *
  118. * @param {ASTNode} node The AST node being updated.
  119. * @param {Object} props Additional properties to add to the component.
  120. */
  121. set(node, props) {
  122. const list = Lists.get(this);
  123. let component = list[getId(node)];
  124. while (!component) {
  125. node = node.parent;
  126. if (!node) {
  127. return;
  128. }
  129. component = list[getId(node)];
  130. }
  131. Object.assign(
  132. component,
  133. props,
  134. {
  135. usedPropTypes: mergeUsedPropTypes(
  136. component.usedPropTypes || [],
  137. props.usedPropTypes || []
  138. )
  139. }
  140. );
  141. }
  142. /**
  143. * Return the components list
  144. * Components for which we are not confident are not returned
  145. *
  146. * @returns {Object} Components list
  147. */
  148. list() {
  149. const thisList = Lists.get(this);
  150. const list = {};
  151. const usedPropTypes = {};
  152. // Find props used in components for which we are not confident
  153. Object.keys(thisList).filter((i) => thisList[i].confidence < 2).forEach((i) => {
  154. let component = null;
  155. let node = null;
  156. node = thisList[i].node;
  157. while (!component && node.parent) {
  158. node = node.parent;
  159. // Stop moving up if we reach a decorator
  160. if (node.type === 'Decorator') {
  161. break;
  162. }
  163. component = this.get(node);
  164. }
  165. if (component) {
  166. const newUsedProps = (thisList[i].usedPropTypes || []).filter((propType) => !propType.node || propType.node.kind !== 'init');
  167. const componentId = getId(component.node);
  168. usedPropTypes[componentId] = mergeUsedPropTypes(usedPropTypes[componentId] || [], newUsedProps);
  169. }
  170. });
  171. // Assign used props in not confident components to the parent component
  172. Object.keys(thisList).filter((j) => thisList[j].confidence >= 2).forEach((j) => {
  173. const id = getId(thisList[j].node);
  174. list[j] = thisList[j];
  175. if (usedPropTypes[id]) {
  176. list[j].usedPropTypes = mergeUsedPropTypes(list[j].usedPropTypes || [], usedPropTypes[id]);
  177. }
  178. });
  179. return list;
  180. }
  181. /**
  182. * Return the length of the components list
  183. * Components for which we are not confident are not counted
  184. *
  185. * @returns {Number} Components list length
  186. */
  187. length() {
  188. const list = Lists.get(this);
  189. return Object.keys(list).filter((i) => list[i].confidence >= 2).length;
  190. }
  191. }
  192. function getWrapperFunctions(context, pragma) {
  193. const componentWrapperFunctions = context.settings.componentWrapperFunctions || [];
  194. // eslint-disable-next-line arrow-body-style
  195. return componentWrapperFunctions.map((wrapperFunction) => {
  196. return typeof wrapperFunction === 'string'
  197. ? {property: wrapperFunction}
  198. : Object.assign({}, wrapperFunction, {
  199. object: wrapperFunction.object === '<pragma>' ? pragma : wrapperFunction.object
  200. });
  201. }).concat([
  202. {property: 'forwardRef', object: pragma},
  203. {property: 'memo', object: pragma}
  204. ]);
  205. }
  206. function componentRule(rule, context) {
  207. const createClass = pragmaUtil.getCreateClassFromContext(context);
  208. const pragma = pragmaUtil.getFromContext(context);
  209. const sourceCode = context.getSourceCode();
  210. const components = new Components();
  211. const wrapperFunctions = getWrapperFunctions(context, pragma);
  212. // Utilities for component detection
  213. const utils = {
  214. /**
  215. * Check if the node is a React ES5 component
  216. *
  217. * @param {ASTNode} node The AST node being checked.
  218. * @returns {Boolean} True if the node is a React ES5 component, false if not
  219. */
  220. isES5Component(node) {
  221. if (!node.parent) {
  222. return false;
  223. }
  224. return new RegExp(`^(${pragma}\\.)?${createClass}$`).test(sourceCode.getText(node.parent.callee));
  225. },
  226. /**
  227. * Check if the node is a React ES6 component
  228. *
  229. * @param {ASTNode} node The AST node being checked.
  230. * @returns {Boolean} True if the node is a React ES6 component, false if not
  231. */
  232. isES6Component(node) {
  233. if (utils.isExplicitComponent(node)) {
  234. return true;
  235. }
  236. if (!node.superClass) {
  237. return false;
  238. }
  239. return new RegExp(`^(${pragma}\\.)?(Pure)?Component$`).test(sourceCode.getText(node.superClass));
  240. },
  241. /**
  242. * Check if the node is explicitly declared as a descendant of a React Component
  243. *
  244. * @param {ASTNode} node The AST node being checked (can be a ReturnStatement or an ArrowFunctionExpression).
  245. * @returns {Boolean} True if the node is explicitly declared as a descendant of a React Component, false if not
  246. */
  247. isExplicitComponent(node) {
  248. let comment;
  249. // Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes.
  250. // Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27
  251. // eslint-disable-next-line no-warning-comments
  252. // FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented.
  253. try {
  254. comment = sourceCode.getJSDocComment(node);
  255. } catch (e) {
  256. comment = null;
  257. }
  258. if (comment === null) {
  259. return false;
  260. }
  261. const commentAst = doctrine.parse(comment.value, {
  262. unwrap: true,
  263. tags: ['extends', 'augments']
  264. });
  265. const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent');
  266. return relevantTags.length > 0;
  267. },
  268. /**
  269. * Checks to see if our component extends React.PureComponent
  270. *
  271. * @param {ASTNode} node The AST node being checked.
  272. * @returns {Boolean} True if node extends React.PureComponent, false if not
  273. */
  274. isPureComponent(node) {
  275. if (node.superClass) {
  276. return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass));
  277. }
  278. return false;
  279. },
  280. /**
  281. * Check if variable is destructured from pragma import
  282. *
  283. * @param {string} variable The variable name to check
  284. * @returns {Boolean} True if createElement is destructured from the pragma
  285. */
  286. isDestructuredFromPragmaImport(variable) {
  287. const variables = variableUtil.variablesInScope(context);
  288. const variableInScope = variableUtil.getVariable(variables, variable);
  289. if (variableInScope) {
  290. const latestDef = variableUtil.getLatestVariableDefinition(variableInScope);
  291. if (latestDef) {
  292. // check if latest definition is a variable declaration: 'variable = value'
  293. if (latestDef.node.type === 'VariableDeclarator' && latestDef.node.init) {
  294. // check for: 'variable = pragma.variable'
  295. if (
  296. latestDef.node.init.type === 'MemberExpression'
  297. && latestDef.node.init.object.type === 'Identifier'
  298. && latestDef.node.init.object.name === pragma
  299. ) {
  300. return true;
  301. }
  302. // check for: '{variable} = pragma'
  303. if (
  304. latestDef.node.init.type === 'Identifier'
  305. && latestDef.node.init.name === pragma
  306. ) {
  307. return true;
  308. }
  309. // "require('react')"
  310. let requireExpression = null;
  311. // get "require('react')" from: "{variable} = require('react')"
  312. if (latestDef.node.init.type === 'CallExpression') {
  313. requireExpression = latestDef.node.init;
  314. }
  315. // get "require('react')" from: "variable = require('react').variable"
  316. if (
  317. !requireExpression
  318. && latestDef.node.init.type === 'MemberExpression'
  319. && latestDef.node.init.object.type === 'CallExpression'
  320. ) {
  321. requireExpression = latestDef.node.init.object;
  322. }
  323. // check proper require.
  324. if (
  325. requireExpression
  326. && requireExpression.callee
  327. && requireExpression.callee.name === 'require'
  328. && requireExpression.arguments[0]
  329. && requireExpression.arguments[0].value === pragma.toLocaleLowerCase()
  330. ) {
  331. return true;
  332. }
  333. return false;
  334. }
  335. // latest definition is an import declaration: import {<variable>} from 'react'
  336. if (
  337. latestDef.parent
  338. && latestDef.parent.type === 'ImportDeclaration'
  339. && latestDef.parent.source.value === pragma.toLocaleLowerCase()
  340. ) {
  341. return true;
  342. }
  343. }
  344. }
  345. return false;
  346. },
  347. /**
  348. * Checks to see if node is called within createElement from pragma
  349. *
  350. * @param {ASTNode} node The AST node being checked.
  351. * @returns {Boolean} True if createElement called from pragma
  352. */
  353. isCreateElement(node) {
  354. // match `React.createElement()`
  355. if (
  356. node
  357. && node.callee
  358. && node.callee.object
  359. && node.callee.object.name === pragma
  360. && node.callee.property
  361. && node.callee.property.name === 'createElement'
  362. ) {
  363. return true;
  364. }
  365. // match `createElement()`
  366. if (
  367. node
  368. && node.callee
  369. && node.callee.name === 'createElement'
  370. && this.isDestructuredFromPragmaImport('createElement')
  371. ) {
  372. return true;
  373. }
  374. return false;
  375. },
  376. /**
  377. * Check if we are in a class constructor
  378. * @return {boolean} true if we are in a class constructor, false if not
  379. */
  380. inConstructor() {
  381. let scope = context.getScope();
  382. while (scope) {
  383. if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
  384. return true;
  385. }
  386. scope = scope.upper;
  387. }
  388. return false;
  389. },
  390. /**
  391. * Determine if the node is MemberExpression of `this.state`
  392. * @param {Object} node The node to process
  393. * @returns {Boolean}
  394. */
  395. isStateMemberExpression(node) {
  396. return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state';
  397. },
  398. getReturnPropertyAndNode(ASTnode) {
  399. let property;
  400. let node = ASTnode;
  401. switch (node.type) {
  402. case 'ReturnStatement':
  403. property = 'argument';
  404. break;
  405. case 'ArrowFunctionExpression':
  406. property = 'body';
  407. if (node[property] && node[property].type === 'BlockStatement') {
  408. node = utils.findReturnStatement(node);
  409. property = 'argument';
  410. }
  411. break;
  412. default:
  413. node = utils.findReturnStatement(node);
  414. property = 'argument';
  415. }
  416. return {
  417. node,
  418. property
  419. };
  420. },
  421. /**
  422. * Check if the node is returning JSX
  423. *
  424. * @param {ASTNode} ASTnode The AST node being checked
  425. * @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases
  426. * @returns {Boolean} True if the node is returning JSX, false if not
  427. */
  428. isReturningJSX(ASTnode, strict) {
  429. const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
  430. const node = nodeAndProperty.node;
  431. const property = nodeAndProperty.property;
  432. if (!node) {
  433. return false;
  434. }
  435. const returnsConditionalJSX = isReturnsConditionalJSX(node, property, strict);
  436. const returnsLogicalJSX = isReturnsLogicalJSX(node, property, strict);
  437. const returnsSequentialJSX = isReturnsSequentialJSX(node, property);
  438. const returnsJSX = node[property] && jsxUtil.isJSX(node[property]);
  439. const returnsPragmaCreateElement = this.isCreateElement(node[property]);
  440. return !!(
  441. returnsConditionalJSX
  442. || returnsLogicalJSX
  443. || returnsSequentialJSX
  444. || returnsJSX
  445. || returnsPragmaCreateElement
  446. );
  447. },
  448. /**
  449. * Check if the node is returning null
  450. *
  451. * @param {ASTNode} ASTnode The AST node being checked
  452. * @returns {Boolean} True if the node is returning null, false if not
  453. */
  454. isReturningNull(ASTnode) {
  455. const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
  456. const property = nodeAndProperty.property;
  457. const node = nodeAndProperty.node;
  458. if (!node) {
  459. return false;
  460. }
  461. return node[property] && node[property].value === null;
  462. },
  463. /**
  464. * Check if the node is returning JSX or null
  465. *
  466. * @param {ASTNode} ASTNode The AST node being checked
  467. * @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases
  468. * @returns {Boolean} True if the node is returning JSX or null, false if not
  469. */
  470. isReturningJSXOrNull(ASTNode, strict) {
  471. return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode);
  472. },
  473. getPragmaComponentWrapper(node) {
  474. let isPragmaComponentWrapper;
  475. let currentNode = node;
  476. let prevNode;
  477. do {
  478. currentNode = currentNode.parent;
  479. isPragmaComponentWrapper = this.isPragmaComponentWrapper(currentNode);
  480. if (isPragmaComponentWrapper) {
  481. prevNode = currentNode;
  482. }
  483. } while (isPragmaComponentWrapper);
  484. return prevNode;
  485. },
  486. getComponentNameFromJSXElement(node) {
  487. if (node.type !== 'JSXElement') {
  488. return null;
  489. }
  490. if (node.openingElement && node.openingElement.name && node.openingElement.name.name) {
  491. return node.openingElement.name.name;
  492. }
  493. return null;
  494. },
  495. /**
  496. * Getting the first JSX element's name.
  497. * @param {object} node
  498. * @returns {string | null}
  499. */
  500. getNameOfWrappedComponent(node) {
  501. if (node.length < 1) {
  502. return null;
  503. }
  504. const body = node[0].body;
  505. if (!body) {
  506. return null;
  507. }
  508. if (body.type === 'JSXElement') {
  509. return this.getComponentNameFromJSXElement(body);
  510. }
  511. if (body.type === 'BlockStatement') {
  512. const jsxElement = body.body.find((item) => item.type === 'ReturnStatement');
  513. return jsxElement
  514. && jsxElement.argument
  515. && this.getComponentNameFromJSXElement(jsxElement.argument);
  516. }
  517. return null;
  518. },
  519. /**
  520. * Get the list of names of components created till now
  521. * @returns {string | boolean}
  522. */
  523. getDetectedComponents() {
  524. const list = components.list();
  525. return values(list).filter((val) => {
  526. if (val.node.type === 'ClassDeclaration') {
  527. return true;
  528. }
  529. if (
  530. val.node.type === 'ArrowFunctionExpression'
  531. && val.node.parent
  532. && val.node.parent.type === 'VariableDeclarator'
  533. && val.node.parent.id
  534. ) {
  535. return true;
  536. }
  537. return false;
  538. }).map((val) => {
  539. if (val.node.type === 'ArrowFunctionExpression') return val.node.parent.id.name;
  540. return val.node.id && val.node.id.name;
  541. });
  542. },
  543. /**
  544. * It will check wheater memo/forwardRef is wrapping existing component or
  545. * creating a new one.
  546. * @param {object} node
  547. * @returns {boolean}
  548. */
  549. nodeWrapsComponent(node) {
  550. const childComponent = this.getNameOfWrappedComponent(node.arguments);
  551. const componentList = this.getDetectedComponents();
  552. return !!childComponent && arrayIncludes(componentList, childComponent);
  553. },
  554. isPragmaComponentWrapper(node) {
  555. if (!node || node.type !== 'CallExpression') {
  556. return false;
  557. }
  558. return wrapperFunctions.some((wrapperFunction) => {
  559. if (node.callee.type === 'MemberExpression') {
  560. return wrapperFunction.object
  561. && wrapperFunction.object === node.callee.object.name
  562. && wrapperFunction.property === node.callee.property.name
  563. && !this.nodeWrapsComponent(node);
  564. }
  565. return wrapperFunction.property === node.callee.name
  566. && (!wrapperFunction.object
  567. // Functions coming from the current pragma need special handling
  568. || (wrapperFunction.object === pragma && this.isDestructuredFromPragmaImport(node.callee.name))
  569. );
  570. });
  571. },
  572. /**
  573. * Find a return statment in the current node
  574. *
  575. * @param {ASTNode} ASTnode The AST node being checked
  576. */
  577. findReturnStatement: astUtil.findReturnStatement,
  578. /**
  579. * Get the parent component node from the current scope
  580. *
  581. * @returns {ASTNode} component node, null if we are not in a component
  582. */
  583. getParentComponent() {
  584. return (
  585. utils.getParentES6Component()
  586. || utils.getParentES5Component()
  587. || utils.getParentStatelessComponent()
  588. );
  589. },
  590. /**
  591. * Get the parent ES5 component node from the current scope
  592. *
  593. * @returns {ASTNode} component node, null if we are not in a component
  594. */
  595. getParentES5Component() {
  596. let scope = context.getScope();
  597. while (scope) {
  598. const node = scope.block && scope.block.parent && scope.block.parent.parent;
  599. if (node && utils.isES5Component(node)) {
  600. return node;
  601. }
  602. scope = scope.upper;
  603. }
  604. return null;
  605. },
  606. /**
  607. * Get the parent ES6 component node from the current scope
  608. *
  609. * @returns {ASTNode} component node, null if we are not in a component
  610. */
  611. getParentES6Component() {
  612. let scope = context.getScope();
  613. while (scope && scope.type !== 'class') {
  614. scope = scope.upper;
  615. }
  616. const node = scope && scope.block;
  617. if (!node || !utils.isES6Component(node)) {
  618. return null;
  619. }
  620. return node;
  621. },
  622. /**
  623. * @param {ASTNode} node
  624. * @returns {boolean}
  625. */
  626. isInAllowedPositionForComponent(node) {
  627. switch (node.parent.type) {
  628. case 'VariableDeclarator':
  629. case 'AssignmentExpression':
  630. case 'Property':
  631. case 'ReturnStatement':
  632. case 'ExportDefaultDeclaration':
  633. case 'ArrowFunctionExpression': {
  634. return true;
  635. }
  636. case 'SequenceExpression': {
  637. return utils.isInAllowedPositionForComponent(node.parent)
  638. && node === node.parent.expressions[node.parent.expressions.length - 1];
  639. }
  640. default:
  641. return false;
  642. }
  643. },
  644. /**
  645. * Get node if node is a stateless component, or node.parent in cases like
  646. * `React.memo` or `React.forwardRef`. Otherwise returns `undefined`.
  647. * @param {ASTNode} node
  648. * @returns {ASTNode | undefined}
  649. */
  650. getStatelessComponent(node) {
  651. if (
  652. node.type === 'FunctionDeclaration'
  653. && (!node.id || isFirstLetterCapitalized(node.id.name))
  654. && utils.isReturningJSXOrNull(node)
  655. ) {
  656. return node;
  657. }
  658. if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
  659. if (node.parent.type === 'VariableDeclarator' && utils.isReturningJSXOrNull(node)) {
  660. if (isFirstLetterCapitalized(node.parent.id.name)) {
  661. return node;
  662. }
  663. return undefined;
  664. }
  665. if (utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node)) {
  666. if (!node.id || isFirstLetterCapitalized(node.id.name)) {
  667. return node;
  668. }
  669. return undefined;
  670. }
  671. // Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
  672. const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
  673. if (pragmaComponentWrapper) {
  674. return pragmaComponentWrapper;
  675. }
  676. }
  677. return undefined;
  678. },
  679. /**
  680. * Get the parent stateless component node from the current scope
  681. *
  682. * @returns {ASTNode} component node, null if we are not in a component
  683. */
  684. getParentStatelessComponent() {
  685. let scope = context.getScope();
  686. while (scope) {
  687. const node = scope.block;
  688. const statelessComponent = utils.getStatelessComponent(node);
  689. if (statelessComponent) {
  690. return statelessComponent;
  691. }
  692. scope = scope.upper;
  693. }
  694. return null;
  695. },
  696. /**
  697. * Get the related component from a node
  698. *
  699. * @param {ASTNode} node The AST node being checked (must be a MemberExpression).
  700. * @returns {ASTNode} component node, null if we cannot find the component
  701. */
  702. getRelatedComponent(node) {
  703. let i;
  704. let j;
  705. let k;
  706. let l;
  707. let componentNode;
  708. // Get the component path
  709. const componentPath = [];
  710. while (node) {
  711. if (node.property && node.property.type === 'Identifier') {
  712. componentPath.push(node.property.name);
  713. }
  714. if (node.object && node.object.type === 'Identifier') {
  715. componentPath.push(node.object.name);
  716. }
  717. node = node.object;
  718. }
  719. componentPath.reverse();
  720. const componentName = componentPath.slice(0, componentPath.length - 1).join('.');
  721. // Find the variable in the current scope
  722. const variableName = componentPath.shift();
  723. if (!variableName) {
  724. return null;
  725. }
  726. let variableInScope;
  727. const variables = variableUtil.variablesInScope(context);
  728. for (i = 0, j = variables.length; i < j; i++) {
  729. if (variables[i].name === variableName) {
  730. variableInScope = variables[i];
  731. break;
  732. }
  733. }
  734. if (!variableInScope) {
  735. return null;
  736. }
  737. // Try to find the component using variable references
  738. const refs = variableInScope.references;
  739. refs.some((ref) => {
  740. let refId = ref.identifier;
  741. if (refId.parent && refId.parent.type === 'MemberExpression') {
  742. refId = refId.parent;
  743. }
  744. if (sourceCode.getText(refId) !== componentName) {
  745. return false;
  746. }
  747. if (refId.type === 'MemberExpression') {
  748. componentNode = refId.parent.right;
  749. } else if (
  750. refId.parent
  751. && refId.parent.type === 'VariableDeclarator'
  752. && refId.parent.init
  753. && refId.parent.init.type !== 'Identifier'
  754. ) {
  755. componentNode = refId.parent.init;
  756. }
  757. return true;
  758. });
  759. if (componentNode) {
  760. // Return the component
  761. return components.add(componentNode, 1);
  762. }
  763. // Try to find the component using variable declarations
  764. const defs = variableInScope.defs;
  765. const defInScope = defs.find((def) => (
  766. def.type === 'ClassName'
  767. || def.type === 'FunctionName'
  768. || def.type === 'Variable'
  769. ));
  770. if (!defInScope || !defInScope.node) {
  771. return null;
  772. }
  773. componentNode = defInScope.node.init || defInScope.node;
  774. // Traverse the node properties to the component declaration
  775. for (i = 0, j = componentPath.length; i < j; i++) {
  776. if (!componentNode.properties) {
  777. continue; // eslint-disable-line no-continue
  778. }
  779. for (k = 0, l = componentNode.properties.length; k < l; k++) {
  780. if (componentNode.properties[k].key && componentNode.properties[k].key.name === componentPath[i]) {
  781. componentNode = componentNode.properties[k];
  782. break;
  783. }
  784. }
  785. if (!componentNode || !componentNode.value) {
  786. return null;
  787. }
  788. componentNode = componentNode.value;
  789. }
  790. // Return the component
  791. return components.add(componentNode, 1);
  792. }
  793. };
  794. // Component detection instructions
  795. const detectionInstructions = {
  796. CallExpression(node) {
  797. if (!utils.isPragmaComponentWrapper(node)) {
  798. return;
  799. }
  800. if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
  801. components.add(node, 2);
  802. }
  803. },
  804. ClassExpression(node) {
  805. if (!utils.isES6Component(node)) {
  806. return;
  807. }
  808. components.add(node, 2);
  809. },
  810. ClassDeclaration(node) {
  811. if (!utils.isES6Component(node)) {
  812. return;
  813. }
  814. components.add(node, 2);
  815. },
  816. ClassProperty(node) {
  817. node = utils.getParentComponent();
  818. if (!node) {
  819. return;
  820. }
  821. components.add(node, 2);
  822. },
  823. ObjectExpression(node) {
  824. if (!utils.isES5Component(node)) {
  825. return;
  826. }
  827. components.add(node, 2);
  828. },
  829. FunctionExpression(node) {
  830. if (node.async) {
  831. components.add(node, 0);
  832. return;
  833. }
  834. const component = utils.getParentComponent();
  835. if (
  836. !component
  837. || (component.parent && component.parent.type === 'JSXExpressionContainer')
  838. ) {
  839. // Ban the node if we cannot find a parent component
  840. components.add(node, 0);
  841. return;
  842. }
  843. components.add(component, 1);
  844. },
  845. FunctionDeclaration(node) {
  846. if (node.async) {
  847. components.add(node, 0);
  848. return;
  849. }
  850. node = utils.getParentComponent();
  851. if (!node) {
  852. return;
  853. }
  854. components.add(node, 1);
  855. },
  856. ArrowFunctionExpression(node) {
  857. if (node.async) {
  858. components.add(node, 0);
  859. return;
  860. }
  861. const component = utils.getParentComponent();
  862. if (
  863. !component
  864. || (component.parent && component.parent.type === 'JSXExpressionContainer')
  865. ) {
  866. // Ban the node if we cannot find a parent component
  867. components.add(node, 0);
  868. return;
  869. }
  870. if (component.expression && utils.isReturningJSX(component)) {
  871. components.add(component, 2);
  872. } else {
  873. components.add(component, 1);
  874. }
  875. },
  876. ThisExpression(node) {
  877. const component = utils.getParentComponent();
  878. if (!component || !/Function/.test(component.type) || !node.parent.property) {
  879. return;
  880. }
  881. // Ban functions accessing a property on a ThisExpression
  882. components.add(node, 0);
  883. },
  884. ReturnStatement(node) {
  885. if (!utils.isReturningJSX(node)) {
  886. return;
  887. }
  888. node = utils.getParentComponent();
  889. if (!node) {
  890. const scope = context.getScope();
  891. components.add(scope.block, 1);
  892. return;
  893. }
  894. components.add(node, 2);
  895. }
  896. };
  897. // Update the provided rule instructions to add the component detection
  898. const ruleInstructions = rule(context, components, utils);
  899. const updatedRuleInstructions = Object.assign({}, ruleInstructions);
  900. const propTypesInstructions = propTypesUtil(context, components, utils);
  901. const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils);
  902. const defaultPropsInstructions = defaultPropsUtil(context, components, utils);
  903. const allKeys = new Set(Object.keys(detectionInstructions).concat(
  904. Object.keys(propTypesInstructions),
  905. Object.keys(usedPropTypesInstructions),
  906. Object.keys(defaultPropsInstructions)
  907. ));
  908. allKeys.forEach((instruction) => {
  909. updatedRuleInstructions[instruction] = (node) => {
  910. if (instruction in detectionInstructions) {
  911. detectionInstructions[instruction](node);
  912. }
  913. if (instruction in propTypesInstructions) {
  914. propTypesInstructions[instruction](node);
  915. }
  916. if (instruction in usedPropTypesInstructions) {
  917. usedPropTypesInstructions[instruction](node);
  918. }
  919. if (instruction in defaultPropsInstructions) {
  920. defaultPropsInstructions[instruction](node);
  921. }
  922. if (ruleInstructions[instruction]) {
  923. return ruleInstructions[instruction](node);
  924. }
  925. };
  926. });
  927. // Return the updated rule instructions
  928. return updatedRuleInstructions;
  929. }
  930. module.exports = Object.assign(Components, {
  931. detect(rule) {
  932. return componentRule.bind(this, rule);
  933. }
  934. });