defaultProps.js 8.3 KB

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