require-default-prop.js 5.4 KB

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