require-default-prop.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  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').ComponentObjectProp} ComponentObjectProp
  8. * @typedef {ComponentObjectProp & { value: ObjectExpression} } ComponentObjectPropObject
  9. */
  10. const utils = require('../utils')
  11. const { isDef } = require('../utils')
  12. const NATIVE_TYPES = new Set([
  13. 'String',
  14. 'Number',
  15. 'Boolean',
  16. 'Function',
  17. 'Object',
  18. 'Array',
  19. 'Symbol'
  20. ])
  21. // ------------------------------------------------------------------------------
  22. // Rule Definition
  23. // ------------------------------------------------------------------------------
  24. module.exports = {
  25. meta: {
  26. type: 'suggestion',
  27. docs: {
  28. description: 'require default value for props',
  29. categories: ['vue3-strongly-recommended', 'strongly-recommended'],
  30. url: 'https://eslint.vuejs.org/rules/require-default-prop.html'
  31. },
  32. fixable: null, // or "code" or "whitespace"
  33. schema: []
  34. },
  35. /** @param {RuleContext} context */
  36. create(context) {
  37. // ----------------------------------------------------------------------
  38. // Helpers
  39. // ----------------------------------------------------------------------
  40. /**
  41. * Checks if the passed prop is required
  42. * @param {ComponentObjectPropObject} prop - Property AST node for a single prop
  43. * @return {boolean}
  44. */
  45. function propIsRequired(prop) {
  46. const propRequiredNode = prop.value.properties.find(
  47. (p) =>
  48. p.type === 'Property' &&
  49. utils.getStaticPropertyName(p) === 'required' &&
  50. p.value.type === 'Literal' &&
  51. p.value.value === true
  52. )
  53. return Boolean(propRequiredNode)
  54. }
  55. /**
  56. * Checks if the passed prop has a default value
  57. * @param {ComponentObjectPropObject} prop - Property AST node for a single prop
  58. * @return {boolean}
  59. */
  60. function propHasDefault(prop) {
  61. const propDefaultNode = prop.value.properties.find(
  62. (p) =>
  63. p.type === 'Property' && utils.getStaticPropertyName(p) === 'default'
  64. )
  65. return Boolean(propDefaultNode)
  66. }
  67. /**
  68. * Finds all props that don't have a default value set
  69. * @param {ComponentObjectProp[]} props - Vue component's "props" node
  70. * @return {ComponentObjectProp[]} Array of props without "default" value
  71. */
  72. function findPropsWithoutDefaultValue(props) {
  73. return props.filter((prop) => {
  74. if (prop.value.type !== 'ObjectExpression') {
  75. if (prop.value.type === 'Identifier') {
  76. return NATIVE_TYPES.has(prop.value.name)
  77. }
  78. if (
  79. prop.value.type === 'CallExpression' ||
  80. prop.value.type === 'MemberExpression'
  81. ) {
  82. // OK
  83. return false
  84. }
  85. // NG
  86. return true
  87. }
  88. return (
  89. !propIsRequired(/** @type {ComponentObjectPropObject} */ (prop)) &&
  90. !propHasDefault(/** @type {ComponentObjectPropObject} */ (prop))
  91. )
  92. })
  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. * Excludes purely Boolean props from the Array
  134. * @param {ComponentObjectProp[]} props - Array with props
  135. * @return {ComponentObjectProp[]}
  136. */
  137. function excludeBooleanProps(props) {
  138. return props.filter((prop) => !isBooleanProp(prop))
  139. }
  140. // ----------------------------------------------------------------------
  141. // Public
  142. // ----------------------------------------------------------------------
  143. return utils.executeOnVue(context, (obj) => {
  144. const props = utils
  145. .getComponentProps(obj)
  146. .filter(
  147. (prop) =>
  148. prop.value &&
  149. !(prop.node.type === 'Property' && prop.node.shorthand)
  150. )
  151. const propsWithoutDefault = findPropsWithoutDefaultValue(
  152. /** @type {ComponentObjectProp[]} */ (props)
  153. )
  154. const propsToReport = excludeBooleanProps(propsWithoutDefault)
  155. for (const prop of propsToReport) {
  156. const propName =
  157. prop.propName != null
  158. ? prop.propName
  159. : `[${context.getSourceCode().getText(prop.node.key)}]`
  160. context.report({
  161. node: prop.node,
  162. message: `Prop '{{propName}}' requires default value to be set.`,
  163. data: {
  164. propName
  165. }
  166. })
  167. }
  168. })
  169. }
  170. }