require-default-prop.js 6.1 KB

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