require-default-prop.js 6.4 KB

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