Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

721 wiersze
22 KiB

  1. /**
  2. * @fileoverview Utility class and functions for React components detection
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. const has = require('has');
  7. const util = require('util');
  8. const doctrine = require('doctrine');
  9. const variableUtil = require('./variable');
  10. const pragmaUtil = require('./pragma');
  11. const astUtil = require('./ast');
  12. const propTypes = require('./propTypes');
  13. function getId(node) {
  14. return node && node.range.join(':');
  15. }
  16. function usedPropTypesAreEquivalent(propA, propB) {
  17. if (propA.name === propB.name) {
  18. if (!propA.allNames && !propB.allNames) {
  19. return true;
  20. } else if (Array.isArray(propA.allNames) && Array.isArray(propB.allNames) && propA.allNames.join('') === propB.allNames.join('')) {
  21. return true;
  22. }
  23. return false;
  24. }
  25. return false;
  26. }
  27. function mergeUsedPropTypes(propsList, newPropsList) {
  28. const propsToAdd = [];
  29. newPropsList.forEach(newProp => {
  30. const newPropisAlreadyInTheList = propsList.some(prop => usedPropTypesAreEquivalent(prop, newProp));
  31. if (!newPropisAlreadyInTheList) {
  32. propsToAdd.push(newProp);
  33. }
  34. });
  35. return propsList.concat(propsToAdd);
  36. }
  37. /**
  38. * Components
  39. */
  40. class Components {
  41. constructor() {
  42. this._list = {};
  43. }
  44. /**
  45. * Add a node to the components list, or update it if it's already in the list
  46. *
  47. * @param {ASTNode} node The AST node being added.
  48. * @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes)
  49. * @returns {Object} Added component object
  50. */
  51. add(node, confidence) {
  52. const id = getId(node);
  53. if (this._list[id]) {
  54. if (confidence === 0 || this._list[id].confidence === 0) {
  55. this._list[id].confidence = 0;
  56. } else {
  57. this._list[id].confidence = Math.max(this._list[id].confidence, confidence);
  58. }
  59. return this._list[id];
  60. }
  61. this._list[id] = {
  62. node: node,
  63. confidence: confidence
  64. };
  65. return this._list[id];
  66. }
  67. /**
  68. * Find a component in the list using its node
  69. *
  70. * @param {ASTNode} node The AST node being searched.
  71. * @returns {Object} Component object, undefined if the component is not found or has confidence value of 0.
  72. */
  73. get(node) {
  74. const id = getId(node);
  75. if (this._list[id] && this._list[id].confidence >= 1) {
  76. return this._list[id];
  77. }
  78. return null;
  79. }
  80. /**
  81. * Update a component in the list
  82. *
  83. * @param {ASTNode} node The AST node being updated.
  84. * @param {Object} props Additional properties to add to the component.
  85. */
  86. set(node, props) {
  87. while (node && !this._list[getId(node)]) {
  88. node = node.parent;
  89. }
  90. if (!node) {
  91. return;
  92. }
  93. const id = getId(node);
  94. let copyUsedPropTypes;
  95. if (this._list[id]) {
  96. // usedPropTypes is an array. _extend replaces existing array with a new one which caused issue #1309.
  97. // preserving original array so it can be merged later on.
  98. copyUsedPropTypes = this._list[id].usedPropTypes && this._list[id].usedPropTypes.slice();
  99. }
  100. this._list[id] = util._extend(this._list[id], props);
  101. if (this._list[id] && props.usedPropTypes) {
  102. this._list[id].usedPropTypes = mergeUsedPropTypes(copyUsedPropTypes || [], props.usedPropTypes);
  103. }
  104. }
  105. /**
  106. * Return the components list
  107. * Components for which we are not confident are not returned
  108. *
  109. * @returns {Object} Components list
  110. */
  111. list() {
  112. const list = {};
  113. const usedPropTypes = {};
  114. // Find props used in components for which we are not confident
  115. for (const i in this._list) {
  116. if (!has(this._list, i) || this._list[i].confidence >= 2) {
  117. continue;
  118. }
  119. let component = null;
  120. let node = null;
  121. node = this._list[i].node;
  122. while (!component && node.parent) {
  123. node = node.parent;
  124. // Stop moving up if we reach a decorator
  125. if (node.type === 'Decorator') {
  126. break;
  127. }
  128. component = this.get(node);
  129. }
  130. if (component) {
  131. const newUsedProps = (this._list[i].usedPropTypes || []).filter(propType => !propType.node || propType.node.kind !== 'init');
  132. const componentId = getId(component.node);
  133. usedPropTypes[componentId] = (usedPropTypes[componentId] || []).concat(newUsedProps);
  134. }
  135. }
  136. // Assign used props in not confident components to the parent component
  137. for (const j in this._list) {
  138. if (!has(this._list, j) || this._list[j].confidence < 2) {
  139. continue;
  140. }
  141. const id = getId(this._list[j].node);
  142. list[j] = this._list[j];
  143. if (usedPropTypes[id]) {
  144. list[j].usedPropTypes = (list[j].usedPropTypes || []).concat(usedPropTypes[id]);
  145. }
  146. }
  147. return list;
  148. }
  149. /**
  150. * Return the length of the components list
  151. * Components for which we are not confident are not counted
  152. *
  153. * @returns {Number} Components list length
  154. */
  155. length() {
  156. let length = 0;
  157. for (const i in this._list) {
  158. if (!has(this._list, i) || this._list[i].confidence < 2) {
  159. continue;
  160. }
  161. length++;
  162. }
  163. return length;
  164. }
  165. }
  166. function componentRule(rule, context) {
  167. const createClass = pragmaUtil.getCreateClassFromContext(context);
  168. const pragma = pragmaUtil.getFromContext(context);
  169. const sourceCode = context.getSourceCode();
  170. const components = new Components();
  171. // Utilities for component detection
  172. const utils = {
  173. /**
  174. * Check if the node is a React ES5 component
  175. *
  176. * @param {ASTNode} node The AST node being checked.
  177. * @returns {Boolean} True if the node is a React ES5 component, false if not
  178. */
  179. isES5Component: function(node) {
  180. if (!node.parent) {
  181. return false;
  182. }
  183. return new RegExp(`^(${pragma}\\.)?${createClass}$`).test(sourceCode.getText(node.parent.callee));
  184. },
  185. /**
  186. * Check if the node is a React ES6 component
  187. *
  188. * @param {ASTNode} node The AST node being checked.
  189. * @returns {Boolean} True if the node is a React ES6 component, false if not
  190. */
  191. isES6Component: function(node) {
  192. if (utils.isExplicitComponent(node)) {
  193. return true;
  194. }
  195. if (!node.superClass) {
  196. return false;
  197. }
  198. return new RegExp(`^(${pragma}\\.)?(Pure)?Component$`).test(sourceCode.getText(node.superClass));
  199. },
  200. /**
  201. * Check if the node is explicitly declared as a descendant of a React Component
  202. *
  203. * @param {ASTNode} node The AST node being checked (can be a ReturnStatement or an ArrowFunctionExpression).
  204. * @returns {Boolean} True if the node is explicitly declared as a descendant of a React Component, false if not
  205. */
  206. isExplicitComponent: function(node) {
  207. let comment;
  208. // Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes.
  209. // Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27
  210. // eslint-disable-next-line no-warning-comments
  211. // FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented.
  212. try {
  213. comment = sourceCode.getJSDocComment(node);
  214. } catch (e) {
  215. comment = null;
  216. }
  217. if (comment === null) {
  218. return false;
  219. }
  220. const commentAst = doctrine.parse(comment.value, {
  221. unwrap: true,
  222. tags: ['extends', 'augments']
  223. });
  224. const relevantTags = commentAst.tags.filter(tag => tag.name === 'React.Component' || tag.name === 'React.PureComponent');
  225. return relevantTags.length > 0;
  226. },
  227. /**
  228. * Checks to see if our component extends React.PureComponent
  229. *
  230. * @param {ASTNode} node The AST node being checked.
  231. * @returns {Boolean} True if node extends React.PureComponent, false if not
  232. */
  233. isPureComponent: function (node) {
  234. if (node.superClass) {
  235. return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass));
  236. }
  237. return false;
  238. },
  239. /**
  240. * Check if createElement is destructured from React import
  241. *
  242. * @returns {Boolean} True if createElement is destructured from React
  243. */
  244. hasDestructuredReactCreateElement: function() {
  245. const variables = variableUtil.variablesInScope(context);
  246. const variable = variableUtil.getVariable(variables, 'createElement');
  247. if (variable) {
  248. const map = variable.scope.set;
  249. if (map.has('React')) {
  250. return true;
  251. }
  252. }
  253. return false;
  254. },
  255. /**
  256. * Checks to see if node is called within React.createElement
  257. *
  258. * @param {ASTNode} node The AST node being checked.
  259. * @returns {Boolean} True if React.createElement called
  260. */
  261. isReactCreateElement: function(node) {
  262. const calledOnReact = (
  263. node &&
  264. node.callee &&
  265. node.callee.object &&
  266. node.callee.object.name === 'React' &&
  267. node.callee.property &&
  268. node.callee.property.name === 'createElement'
  269. );
  270. const calledDirectly = (
  271. node &&
  272. node.callee &&
  273. node.callee.name === 'createElement'
  274. );
  275. if (this.hasDestructuredReactCreateElement()) {
  276. return calledDirectly || calledOnReact;
  277. }
  278. return calledOnReact;
  279. },
  280. getReturnPropertyAndNode(ASTnode) {
  281. let property;
  282. let node = ASTnode;
  283. switch (node.type) {
  284. case 'ReturnStatement':
  285. property = 'argument';
  286. break;
  287. case 'ArrowFunctionExpression':
  288. property = 'body';
  289. if (node[property] && node[property].type === 'BlockStatement') {
  290. node = utils.findReturnStatement(node);
  291. property = 'argument';
  292. }
  293. break;
  294. default:
  295. node = utils.findReturnStatement(node);
  296. property = 'argument';
  297. }
  298. return {
  299. node: node,
  300. property: property
  301. };
  302. },
  303. /**
  304. * Check if the node is returning JSX
  305. *
  306. * @param {ASTNode} ASTnode The AST node being checked
  307. * @param {Boolean} strict If true, in a ternary condition the node must return JSX in both cases
  308. * @returns {Boolean} True if the node is returning JSX, false if not
  309. */
  310. isReturningJSX: function(ASTnode, strict) {
  311. const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
  312. const node = nodeAndProperty.node;
  313. const property = nodeAndProperty.property;
  314. if (!node) {
  315. return false;
  316. }
  317. const returnsConditionalJSXConsequent =
  318. node[property] &&
  319. node[property].type === 'ConditionalExpression' &&
  320. node[property].consequent.type === 'JSXElement'
  321. ;
  322. const returnsConditionalJSXAlternate =
  323. node[property] &&
  324. node[property].type === 'ConditionalExpression' &&
  325. node[property].alternate.type === 'JSXElement'
  326. ;
  327. const returnsConditionalJSX =
  328. strict ?
  329. (returnsConditionalJSXConsequent && returnsConditionalJSXAlternate) :
  330. (returnsConditionalJSXConsequent || returnsConditionalJSXAlternate);
  331. const returnsJSX =
  332. node[property] &&
  333. node[property].type === 'JSXElement'
  334. ;
  335. const returnsReactCreateElement = this.isReactCreateElement(node[property]);
  336. return Boolean(
  337. returnsConditionalJSX ||
  338. returnsJSX ||
  339. returnsReactCreateElement
  340. );
  341. },
  342. /**
  343. * Check if the node is returning null
  344. *
  345. * @param {ASTNode} ASTnode The AST node being checked
  346. * @returns {Boolean} True if the node is returning null, false if not
  347. */
  348. isReturningNull(ASTnode) {
  349. const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode);
  350. const property = nodeAndProperty.property;
  351. const node = nodeAndProperty.node;
  352. if (!node) {
  353. return false;
  354. }
  355. return node[property] && node[property].value === null;
  356. },
  357. /**
  358. * Check if the node is returning JSX or null
  359. *
  360. * @param {ASTNode} ASTnode The AST node being checked
  361. * @param {Boolean} strict If true, in a ternary condition the node must return JSX in both cases
  362. * @returns {Boolean} True if the node is returning JSX or null, false if not
  363. */
  364. isReturningJSXOrNull(ASTNode, strict) {
  365. return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode);
  366. },
  367. /**
  368. * Find a return statment in the current node
  369. *
  370. * @param {ASTNode} ASTnode The AST node being checked
  371. */
  372. findReturnStatement: astUtil.findReturnStatement,
  373. /**
  374. * Get the parent component node from the current scope
  375. *
  376. * @returns {ASTNode} component node, null if we are not in a component
  377. */
  378. getParentComponent: function() {
  379. return (
  380. utils.getParentES6Component() ||
  381. utils.getParentES5Component() ||
  382. utils.getParentStatelessComponent()
  383. );
  384. },
  385. /**
  386. * Get the parent ES5 component node from the current scope
  387. *
  388. * @returns {ASTNode} component node, null if we are not in a component
  389. */
  390. getParentES5Component: function() {
  391. let scope = context.getScope();
  392. while (scope) {
  393. const node = scope.block && scope.block.parent && scope.block.parent.parent;
  394. if (node && utils.isES5Component(node)) {
  395. return node;
  396. }
  397. scope = scope.upper;
  398. }
  399. return null;
  400. },
  401. /**
  402. * Get the parent ES6 component node from the current scope
  403. *
  404. * @returns {ASTNode} component node, null if we are not in a component
  405. */
  406. getParentES6Component: function() {
  407. let scope = context.getScope();
  408. while (scope && scope.type !== 'class') {
  409. scope = scope.upper;
  410. }
  411. const node = scope && scope.block;
  412. if (!node || !utils.isES6Component(node)) {
  413. return null;
  414. }
  415. return node;
  416. },
  417. /**
  418. * Get the parent stateless component node from the current scope
  419. *
  420. * @returns {ASTNode} component node, null if we are not in a component
  421. */
  422. getParentStatelessComponent: function() {
  423. let scope = context.getScope();
  424. while (scope) {
  425. const node = scope.block;
  426. const isClass = node.type === 'ClassExpression';
  427. const isFunction = /Function/.test(node.type); // Functions
  428. const isMethod = node.parent && node.parent.type === 'MethodDefinition'; // Classes methods
  429. const isArgument = node.parent && node.parent.type === 'CallExpression'; // Arguments (callback, etc.)
  430. // Attribute Expressions inside JSX Elements (<button onClick={() => props.handleClick()}></button>)
  431. const isJSXExpressionContainer = node.parent && node.parent.type === 'JSXExpressionContainer';
  432. // Stop moving up if we reach a class or an argument (like a callback)
  433. if (isClass || isArgument) {
  434. return null;
  435. }
  436. // Return the node if it is a function that is not a class method and is not inside a JSX Element
  437. if (isFunction && !isMethod && !isJSXExpressionContainer && utils.isReturningJSXOrNull(node)) {
  438. return node;
  439. }
  440. scope = scope.upper;
  441. }
  442. return null;
  443. },
  444. /**
  445. * Get the related component from a node
  446. *
  447. * @param {ASTNode} node The AST node being checked (must be a MemberExpression).
  448. * @returns {ASTNode} component node, null if we cannot find the component
  449. */
  450. getRelatedComponent: function(node) {
  451. let i;
  452. let j;
  453. let k;
  454. let l;
  455. let componentNode;
  456. // Get the component path
  457. const componentPath = [];
  458. while (node) {
  459. if (node.property && node.property.type === 'Identifier') {
  460. componentPath.push(node.property.name);
  461. }
  462. if (node.object && node.object.type === 'Identifier') {
  463. componentPath.push(node.object.name);
  464. }
  465. node = node.object;
  466. }
  467. componentPath.reverse();
  468. const componentName = componentPath.slice(0, componentPath.length - 1).join('.');
  469. // Find the variable in the current scope
  470. const variableName = componentPath.shift();
  471. if (!variableName) {
  472. return null;
  473. }
  474. let variableInScope;
  475. const variables = variableUtil.variablesInScope(context);
  476. for (i = 0, j = variables.length; i < j; i++) {
  477. if (variables[i].name === variableName) {
  478. variableInScope = variables[i];
  479. break;
  480. }
  481. }
  482. if (!variableInScope) {
  483. return null;
  484. }
  485. // Try to find the component using variable references
  486. const refs = variableInScope.references;
  487. let refId;
  488. for (i = 0, j = refs.length; i < j; i++) {
  489. refId = refs[i].identifier;
  490. if (refId.parent && refId.parent.type === 'MemberExpression') {
  491. refId = refId.parent;
  492. }
  493. if (sourceCode.getText(refId) !== componentName) {
  494. continue;
  495. }
  496. if (refId.type === 'MemberExpression') {
  497. componentNode = refId.parent.right;
  498. } else if (refId.parent && refId.parent.type === 'VariableDeclarator') {
  499. componentNode = refId.parent.init;
  500. }
  501. break;
  502. }
  503. if (componentNode) {
  504. // Return the component
  505. return components.add(componentNode, 1);
  506. }
  507. // Try to find the component using variable declarations
  508. let defInScope;
  509. const defs = variableInScope.defs;
  510. for (i = 0, j = defs.length; i < j; i++) {
  511. if (defs[i].type === 'ClassName' || defs[i].type === 'FunctionName' || defs[i].type === 'Variable') {
  512. defInScope = defs[i];
  513. break;
  514. }
  515. }
  516. if (!defInScope || !defInScope.node) {
  517. return null;
  518. }
  519. componentNode = defInScope.node.init || defInScope.node;
  520. // Traverse the node properties to the component declaration
  521. for (i = 0, j = componentPath.length; i < j; i++) {
  522. if (!componentNode.properties) {
  523. continue;
  524. }
  525. for (k = 0, l = componentNode.properties.length; k < l; k++) {
  526. if (componentNode.properties[k].key && componentNode.properties[k].key.name === componentPath[i]) {
  527. componentNode = componentNode.properties[k];
  528. break;
  529. }
  530. }
  531. if (!componentNode || !componentNode.value) {
  532. return null;
  533. }
  534. componentNode = componentNode.value;
  535. }
  536. // Return the component
  537. return components.add(componentNode, 1);
  538. }
  539. };
  540. // Component detection instructions
  541. const detectionInstructions = {
  542. ClassExpression: function(node) {
  543. if (!utils.isES6Component(node)) {
  544. return;
  545. }
  546. components.add(node, 2);
  547. },
  548. ClassDeclaration: function(node) {
  549. if (!utils.isES6Component(node)) {
  550. return;
  551. }
  552. components.add(node, 2);
  553. },
  554. ClassProperty: function(node) {
  555. node = utils.getParentComponent();
  556. if (!node) {
  557. return;
  558. }
  559. components.add(node, 2);
  560. },
  561. ObjectExpression: function(node) {
  562. if (!utils.isES5Component(node)) {
  563. return;
  564. }
  565. components.add(node, 2);
  566. },
  567. FunctionExpression: function(node) {
  568. if (node.async) {
  569. components.add(node, 0);
  570. return;
  571. }
  572. const component = utils.getParentComponent();
  573. if (
  574. !component ||
  575. (component.parent && component.parent.type === 'JSXExpressionContainer')
  576. ) {
  577. // Ban the node if we cannot find a parent component
  578. components.add(node, 0);
  579. return;
  580. }
  581. components.add(component, 1);
  582. },
  583. FunctionDeclaration: function(node) {
  584. if (node.async) {
  585. components.add(node, 0);
  586. return;
  587. }
  588. node = utils.getParentComponent();
  589. if (!node) {
  590. return;
  591. }
  592. components.add(node, 1);
  593. },
  594. ArrowFunctionExpression: function(node) {
  595. if (node.async) {
  596. components.add(node, 0);
  597. return;
  598. }
  599. const component = utils.getParentComponent();
  600. if (
  601. !component ||
  602. (component.parent && component.parent.type === 'JSXExpressionContainer')
  603. ) {
  604. // Ban the node if we cannot find a parent component
  605. components.add(node, 0);
  606. return;
  607. }
  608. if (component.expression && utils.isReturningJSX(component)) {
  609. components.add(component, 2);
  610. } else {
  611. components.add(component, 1);
  612. }
  613. },
  614. ThisExpression: function(node) {
  615. const component = utils.getParentComponent();
  616. if (!component || !/Function/.test(component.type) || !node.parent.property) {
  617. return;
  618. }
  619. // Ban functions accessing a property on a ThisExpression
  620. components.add(node, 0);
  621. },
  622. ReturnStatement: function(node) {
  623. if (!utils.isReturningJSX(node)) {
  624. return;
  625. }
  626. node = utils.getParentComponent();
  627. if (!node) {
  628. const scope = context.getScope();
  629. components.add(scope.block, 1);
  630. return;
  631. }
  632. components.add(node, 2);
  633. }
  634. };
  635. // Update the provided rule instructions to add the component detection
  636. const ruleInstructions = rule(context, components, utils);
  637. const updatedRuleInstructions = util._extend({}, ruleInstructions);
  638. const propTypesInstructions = propTypes(context, components, utils);
  639. const allKeys = new Set(Object.keys(detectionInstructions).concat(Object.keys(propTypesInstructions)));
  640. allKeys.forEach(instruction => {
  641. updatedRuleInstructions[instruction] = function(node) {
  642. if (instruction in detectionInstructions) {
  643. detectionInstructions[instruction](node);
  644. }
  645. if (instruction in propTypesInstructions) {
  646. propTypesInstructions[instruction](node);
  647. }
  648. return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : void 0;
  649. };
  650. });
  651. // Return the updated rule instructions
  652. return updatedRuleInstructions;
  653. }
  654. module.exports = Object.assign(Components, {
  655. detect(rule) {
  656. return componentRule.bind(this, rule);
  657. }
  658. });