require-default-props.js 7.2 KB

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