defaultProps.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. /**
  2. * @fileoverview Common defaultProps detection functionality.
  3. */
  4. 'use strict';
  5. const fromEntries = require('object.fromentries');
  6. const astUtil = require('./ast');
  7. const propsUtil = require('./props');
  8. const variableUtil = require('./variable');
  9. const propWrapperUtil = require('./propWrapper');
  10. const QUOTES_REGEX = /^["']|["']$/g;
  11. module.exports = function defaultPropsInstructions(context, components, utils) {
  12. const sourceCode = context.getSourceCode();
  13. /**
  14. * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
  15. * an Identifier, then the node is simply returned.
  16. * @param {ASTNode} node The node to resolve.
  17. * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
  18. */
  19. function resolveNodeValue(node) {
  20. if (node.type === 'Identifier') {
  21. return variableUtil.findVariableByName(context, node.name);
  22. }
  23. if (
  24. node.type === 'CallExpression'
  25. && propWrapperUtil.isPropWrapperFunction(context, node.callee.name)
  26. && node.arguments && node.arguments[0]
  27. ) {
  28. return resolveNodeValue(node.arguments[0]);
  29. }
  30. return node;
  31. }
  32. /**
  33. * Extracts a DefaultProp from an ObjectExpression node.
  34. * @param {ASTNode} objectExpression ObjectExpression node.
  35. * @returns {Object|string} Object representation of a defaultProp, to be consumed by
  36. * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
  37. * from this ObjectExpression can't be resolved.
  38. */
  39. function getDefaultPropsFromObjectExpression(objectExpression) {
  40. const hasSpread = objectExpression.properties.find((property) => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement');
  41. if (hasSpread) {
  42. return 'unresolved';
  43. }
  44. return objectExpression.properties.map((defaultProp) => ({
  45. name: sourceCode.getText(defaultProp.key).replace(QUOTES_REGEX, ''),
  46. node: defaultProp
  47. }));
  48. }
  49. /**
  50. * Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is
  51. * marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations
  52. * without risking false negatives.
  53. * @param {Object} component The component to mark.
  54. * @returns {void}
  55. */
  56. function markDefaultPropsAsUnresolved(component) {
  57. components.set(component.node, {
  58. defaultProps: 'unresolved'
  59. });
  60. }
  61. /**
  62. * Adds defaultProps to the component passed in.
  63. * @param {ASTNode} component The component to add the defaultProps to.
  64. * @param {Object[]|'unresolved'} defaultProps defaultProps to add to the component or the string "unresolved"
  65. * if this component has defaultProps that can't be resolved.
  66. * @returns {void}
  67. */
  68. function addDefaultPropsToComponent(component, defaultProps) {
  69. // Early return if this component's defaultProps is already marked as "unresolved".
  70. if (component.defaultProps === 'unresolved') {
  71. return;
  72. }
  73. if (defaultProps === 'unresolved') {
  74. markDefaultPropsAsUnresolved(component);
  75. return;
  76. }
  77. const defaults = component.defaultProps || {};
  78. const newDefaultProps = Object.assign(
  79. {},
  80. defaults,
  81. fromEntries(defaultProps.map((prop) => [prop.name, prop]))
  82. );
  83. components.set(component.node, {
  84. defaultProps: newDefaultProps
  85. });
  86. }
  87. return {
  88. MemberExpression(node) {
  89. const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node);
  90. if (!isDefaultProp) {
  91. return;
  92. }
  93. // find component this defaultProps belongs to
  94. const component = utils.getRelatedComponent(node);
  95. if (!component) {
  96. return;
  97. }
  98. // e.g.:
  99. // MyComponent.propTypes = {
  100. // foo: React.PropTypes.string.isRequired,
  101. // bar: React.PropTypes.string
  102. // };
  103. //
  104. // or:
  105. //
  106. // MyComponent.propTypes = myPropTypes;
  107. if (node.parent.type === 'AssignmentExpression') {
  108. const expression = resolveNodeValue(node.parent.right);
  109. if (!expression || expression.type !== 'ObjectExpression') {
  110. // If a value can't be found, we mark the defaultProps declaration as "unresolved", because
  111. // we should ignore this component and not report any errors for it, to avoid false-positives
  112. // with e.g. external defaultProps declarations.
  113. if (isDefaultProp) {
  114. markDefaultPropsAsUnresolved(component);
  115. }
  116. return;
  117. }
  118. addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
  119. return;
  120. }
  121. // e.g.:
  122. // MyComponent.propTypes.baz = React.PropTypes.string;
  123. if (node.parent.type === 'MemberExpression' && node.parent.parent
  124. && node.parent.parent.type === 'AssignmentExpression') {
  125. addDefaultPropsToComponent(component, [{
  126. name: node.parent.property.name,
  127. node: node.parent.parent
  128. }]);
  129. }
  130. },
  131. // e.g.:
  132. // class Hello extends React.Component {
  133. // static get defaultProps() {
  134. // return {
  135. // name: 'Dean'
  136. // };
  137. // }
  138. // render() {
  139. // return <div>Hello {this.props.name}</div>;
  140. // }
  141. // }
  142. MethodDefinition(node) {
  143. if (!node.static || node.kind !== 'get') {
  144. return;
  145. }
  146. if (!propsUtil.isDefaultPropsDeclaration(node)) {
  147. return;
  148. }
  149. // find component this propTypes/defaultProps belongs to
  150. const component = components.get(utils.getParentES6Component());
  151. if (!component) {
  152. return;
  153. }
  154. const returnStatement = utils.findReturnStatement(node);
  155. if (!returnStatement) {
  156. return;
  157. }
  158. const expression = resolveNodeValue(returnStatement.argument);
  159. if (!expression || expression.type !== 'ObjectExpression') {
  160. return;
  161. }
  162. addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
  163. },
  164. // e.g.:
  165. // class Greeting extends React.Component {
  166. // render() {
  167. // return (
  168. // <h1>Hello, {this.props.foo} {this.props.bar}</h1>
  169. // );
  170. // }
  171. // static defaultProps = {
  172. // foo: 'bar',
  173. // bar: 'baz'
  174. // };
  175. // }
  176. ClassProperty(node) {
  177. if (!(node.static && node.value)) {
  178. return;
  179. }
  180. const propName = astUtil.getPropertyName(node);
  181. const isDefaultProp = propName === 'defaultProps' || propName === 'getDefaultProps';
  182. if (!isDefaultProp) {
  183. return;
  184. }
  185. // find component this propTypes/defaultProps belongs to
  186. const component = components.get(utils.getParentES6Component());
  187. if (!component) {
  188. return;
  189. }
  190. const expression = resolveNodeValue(node.value);
  191. if (!expression || expression.type !== 'ObjectExpression') {
  192. return;
  193. }
  194. addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
  195. },
  196. // e.g.:
  197. // React.createClass({
  198. // render: function() {
  199. // return <div>{this.props.foo}</div>;
  200. // },
  201. // getDefaultProps: function() {
  202. // return {
  203. // foo: 'default'
  204. // };
  205. // }
  206. // });
  207. ObjectExpression(node) {
  208. // find component this propTypes/defaultProps belongs to
  209. const component = utils.isES5Component(node) && components.get(node);
  210. if (!component) {
  211. return;
  212. }
  213. // Search for the proptypes declaration
  214. node.properties.forEach((property) => {
  215. if (property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement') {
  216. return;
  217. }
  218. const isDefaultProp = propsUtil.isDefaultPropsDeclaration(property);
  219. if (isDefaultProp && property.value.type === 'FunctionExpression') {
  220. const returnStatement = utils.findReturnStatement(property);
  221. if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') {
  222. return;
  223. }
  224. addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument));
  225. }
  226. });
  227. }
  228. };
  229. };