require-default-props.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. /**
  2. * @fileOverview Enforce a defaultProps definition for every prop that is not a required prop.
  3. * @author Vitor Balocco
  4. */
  5. 'use strict';
  6. const entries = require('object.entries');
  7. const values = require('object.values');
  8. const Components = require('../util/Components');
  9. const docsUrl = require('../util/docsUrl');
  10. const astUtil = require('../util/ast');
  11. const report = require('../util/report');
  12. // ------------------------------------------------------------------------------
  13. // Rule Definition
  14. // ------------------------------------------------------------------------------
  15. const messages = {
  16. noDefaultWithRequired: 'propType "{{name}}" is required and should not have a defaultProps declaration.',
  17. shouldHaveDefault: 'propType "{{name}}" is not required, but has no corresponding defaultProps declaration.',
  18. noDefaultPropsWithFunction: 'Don’t use defaultProps with function components.',
  19. shouldAssignObjectDefault: 'propType "{{name}}" is not required, but has no corresponding default argument value.',
  20. destructureInSignature: 'Must destructure props in the function signature to initialize an optional prop.',
  21. };
  22. module.exports = {
  23. meta: {
  24. docs: {
  25. description: 'Enforce a defaultProps definition for every prop that is not a required prop',
  26. category: 'Best Practices',
  27. url: docsUrl('require-default-props'),
  28. },
  29. messages,
  30. schema: [{
  31. type: 'object',
  32. properties: {
  33. forbidDefaultForRequired: {
  34. type: 'boolean',
  35. },
  36. classes: {
  37. allow: {
  38. enum: ['defaultProps', 'ignore'],
  39. },
  40. },
  41. functions: {
  42. allow: {
  43. enum: ['defaultArguments', 'defaultProps', 'ignore'],
  44. },
  45. },
  46. /**
  47. * @deprecated
  48. */
  49. ignoreFunctionalComponents: {
  50. type: 'boolean',
  51. },
  52. },
  53. additionalProperties: false,
  54. }],
  55. },
  56. create: Components.detect((context, components) => {
  57. const configuration = context.options[0] || {};
  58. const forbidDefaultForRequired = configuration.forbidDefaultForRequired || false;
  59. const classes = configuration.classes || 'defaultProps';
  60. /**
  61. * @todo
  62. * - Remove ignoreFunctionalComponents
  63. * - Change default to 'defaultArguments'
  64. */
  65. const functions = configuration.ignoreFunctionalComponents
  66. ? 'ignore'
  67. : configuration.functions || 'defaultProps';
  68. /**
  69. * Reports all propTypes passed in that don't have a defaultProps counterpart.
  70. * @param {Object[]} propTypes List of propTypes to check.
  71. * @param {Object} defaultProps Object of defaultProps to check. Keys are the props names.
  72. * @return {void}
  73. */
  74. function reportPropTypesWithoutDefault(propTypes, defaultProps) {
  75. entries(propTypes).forEach((propType) => {
  76. const propName = propType[0];
  77. const prop = propType[1];
  78. if (!prop.node) {
  79. return;
  80. }
  81. if (prop.isRequired) {
  82. if (forbidDefaultForRequired && defaultProps[propName]) {
  83. report(context, messages.noDefaultWithRequired, 'noDefaultWithRequired', {
  84. node: prop.node,
  85. data: { name: propName },
  86. });
  87. }
  88. return;
  89. }
  90. if (defaultProps[propName]) {
  91. return;
  92. }
  93. report(context, messages.shouldHaveDefault, 'shouldHaveDefault', {
  94. node: prop.node,
  95. data: { name: propName },
  96. });
  97. });
  98. }
  99. /**
  100. * If functions option is 'defaultArguments', reports defaultProps is used and all params that doesn't initialized.
  101. * @param {Object} componentNode Node of component.
  102. * @param {Object[]} declaredPropTypes List of propTypes to check `isRequired`.
  103. * @param {Object} defaultProps Object of defaultProps to check used.
  104. */
  105. function reportFunctionComponent(componentNode, declaredPropTypes, defaultProps) {
  106. if (defaultProps) {
  107. report(context, messages.noDefaultPropsWithFunction, 'noDefaultPropsWithFunction', {
  108. node: componentNode,
  109. });
  110. }
  111. const props = componentNode.params[0];
  112. const propTypes = declaredPropTypes;
  113. if (!props) {
  114. return;
  115. }
  116. if (props.type === 'Identifier') {
  117. const hasOptionalProp = values(propTypes).some((propType) => !propType.isRequired);
  118. if (hasOptionalProp) {
  119. report(context, messages.destructureInSignature, 'destructureInSignature', {
  120. node: props,
  121. });
  122. }
  123. } else if (props.type === 'ObjectPattern') {
  124. props.properties.filter((prop) => {
  125. if (prop.type === 'RestElement' || prop.type === 'ExperimentalRestProperty') {
  126. return false;
  127. }
  128. const propType = propTypes[prop.key.name];
  129. if (!propType || propType.isRequired) {
  130. return false;
  131. }
  132. return prop.value.type !== 'AssignmentPattern';
  133. }).forEach((prop) => {
  134. report(context, messages.shouldAssignObjectDefault, 'shouldAssignObjectDefault', {
  135. node: prop,
  136. data: { name: prop.key.name },
  137. });
  138. });
  139. }
  140. }
  141. // --------------------------------------------------------------------------
  142. // Public API
  143. // --------------------------------------------------------------------------
  144. return {
  145. 'Program:exit'() {
  146. const list = components.list();
  147. values(list).filter((component) => {
  148. if (functions === 'ignore' && astUtil.isFunctionLike(component.node)) {
  149. return false;
  150. }
  151. if (classes === 'ignore' && astUtil.isClass(component.node)) {
  152. return false;
  153. }
  154. // If this defaultProps is "unresolved", then we should ignore this component and not report
  155. // any errors for it, to avoid false-positives with e.g. external defaultProps declarations or spread operators.
  156. if (component.defaultProps === 'unresolved') {
  157. return false;
  158. }
  159. return component.declaredPropTypes !== undefined;
  160. }).forEach((component) => {
  161. if (functions === 'defaultArguments' && astUtil.isFunctionLike(component.node)) {
  162. reportFunctionComponent(
  163. component.node,
  164. component.declaredPropTypes,
  165. component.defaultProps
  166. );
  167. } else {
  168. reportPropTypesWithoutDefault(
  169. component.declaredPropTypes,
  170. component.defaultProps || {}
  171. );
  172. }
  173. });
  174. },
  175. };
  176. }),
  177. };