require-default-prop.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /**
  2. * @fileoverview Require default value for props
  3. * @author Michał Sajnóg <msajnog93@gmail.com> (https://github.com/michalsnik)
  4. */
  5. 'use strict'
  6. /**
  7. * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp
  8. * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
  9. * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
  10. * @typedef {ComponentObjectProp & { value: ObjectExpression} } ComponentObjectPropObject
  11. */
  12. const utils = require('../utils')
  13. const { isDef } = require('../utils')
  14. const NATIVE_TYPES = new Set([
  15. 'String',
  16. 'Number',
  17. 'Boolean',
  18. 'Function',
  19. 'Object',
  20. 'Array',
  21. 'Symbol'
  22. ])
  23. // ------------------------------------------------------------------------------
  24. // Rule Definition
  25. // ------------------------------------------------------------------------------
  26. module.exports = {
  27. meta: {
  28. type: 'suggestion',
  29. docs: {
  30. description: 'require default value for props',
  31. categories: ['vue3-strongly-recommended', 'strongly-recommended'],
  32. url: 'https://eslint.vuejs.org/rules/require-default-prop.html'
  33. },
  34. fixable: null, // or "code" or "whitespace"
  35. schema: [],
  36. messages: {
  37. missingDefault: `Prop '{{propName}}' requires default value to be set.`
  38. }
  39. },
  40. /** @param {RuleContext} context */
  41. create(context) {
  42. // ----------------------------------------------------------------------
  43. // Helpers
  44. // ----------------------------------------------------------------------
  45. /**
  46. * Checks if the passed prop is required
  47. * @param {ObjectExpression} propValue - ObjectExpression AST node for a single prop
  48. * @return {boolean}
  49. */
  50. function propIsRequired(propValue) {
  51. const propRequiredNode = propValue.properties.find(
  52. (p) =>
  53. p.type === 'Property' &&
  54. utils.getStaticPropertyName(p) === 'required' &&
  55. p.value.type === 'Literal' &&
  56. p.value.value === true
  57. )
  58. return Boolean(propRequiredNode)
  59. }
  60. /**
  61. * Checks if the passed prop has a default value
  62. * @param {ObjectExpression} propValue - ObjectExpression AST node for a single prop
  63. * @return {boolean}
  64. */
  65. function propHasDefault(propValue) {
  66. const propDefaultNode = propValue.properties.find(
  67. (p) =>
  68. p.type === 'Property' && utils.getStaticPropertyName(p) === 'default'
  69. )
  70. return Boolean(propDefaultNode)
  71. }
  72. /**
  73. * Checks whether the given props that don't have a default value
  74. * @param {ComponentObjectProp} prop Vue component's "props" node
  75. * @return {boolean}
  76. */
  77. function isWithoutDefaultValue(prop) {
  78. if (prop.value.type !== 'ObjectExpression') {
  79. if (prop.value.type === 'Identifier') {
  80. return NATIVE_TYPES.has(prop.value.name)
  81. }
  82. if (
  83. prop.value.type === 'CallExpression' ||
  84. prop.value.type === 'MemberExpression'
  85. ) {
  86. // OK
  87. return false
  88. }
  89. // NG
  90. return true
  91. }
  92. return !propIsRequired(prop.value) && !propHasDefault(prop.value)
  93. }
  94. /**
  95. * Detects whether given value node is a Boolean type
  96. * @param {Expression} value
  97. * @return {boolean}
  98. */
  99. function isValueNodeOfBooleanType(value) {
  100. if (value.type === 'Identifier' && value.name === 'Boolean') {
  101. return true
  102. }
  103. if (value.type === 'ArrayExpression') {
  104. const elements = value.elements.filter(isDef)
  105. return (
  106. elements.length === 1 &&
  107. elements[0].type === 'Identifier' &&
  108. elements[0].name === 'Boolean'
  109. )
  110. }
  111. return false
  112. }
  113. /**
  114. * Detects whether given prop node is a Boolean
  115. * @param {ComponentObjectProp} prop
  116. * @return {Boolean}
  117. */
  118. function isBooleanProp(prop) {
  119. const value = utils.skipTSAsExpression(prop.value)
  120. return (
  121. isValueNodeOfBooleanType(value) ||
  122. (value.type === 'ObjectExpression' &&
  123. value.properties.some(
  124. (p) =>
  125. p.type === 'Property' &&
  126. p.key.type === 'Identifier' &&
  127. p.key.name === 'type' &&
  128. isValueNodeOfBooleanType(p.value)
  129. ))
  130. )
  131. }
  132. /**
  133. * @param {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} props
  134. * @param {boolean} [withDefaults]
  135. * @param { { [key: string]: Expression | undefined } } [withDefaultsExpressions]
  136. */
  137. function processProps(props, withDefaults, withDefaultsExpressions) {
  138. for (const prop of props) {
  139. if (prop.type === 'object' && !prop.node.shorthand) {
  140. if (!isWithoutDefaultValue(prop)) {
  141. continue
  142. }
  143. if (isBooleanProp(prop)) {
  144. continue
  145. }
  146. const propName =
  147. prop.propName != null
  148. ? prop.propName
  149. : `[${context.getSourceCode().getText(prop.node.key)}]`
  150. context.report({
  151. node: prop.node,
  152. messageId: `missingDefault`,
  153. data: {
  154. propName
  155. }
  156. })
  157. } else if (
  158. prop.type === 'type' &&
  159. withDefaults &&
  160. withDefaultsExpressions
  161. ) {
  162. if (prop.required) {
  163. continue
  164. }
  165. if (prop.types.length === 1 && prop.types[0] === 'Boolean') {
  166. continue
  167. }
  168. if (!withDefaultsExpressions[prop.propName]) {
  169. context.report({
  170. node: prop.node,
  171. messageId: `missingDefault`,
  172. data: {
  173. propName: prop.propName
  174. }
  175. })
  176. }
  177. }
  178. }
  179. }
  180. // ----------------------------------------------------------------------
  181. // Public
  182. // ----------------------------------------------------------------------
  183. return utils.compositingVisitors(
  184. utils.defineScriptSetupVisitor(context, {
  185. onDefinePropsEnter(node, props) {
  186. processProps(
  187. props,
  188. utils.hasWithDefaults(node),
  189. utils.getWithDefaultsPropExpressions(node)
  190. )
  191. }
  192. }),
  193. utils.executeOnVue(context, (obj) => {
  194. processProps(utils.getComponentProps(obj))
  195. })
  196. )
  197. }
  198. }