require-default-prop.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  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('vue-eslint-parser').AST.ESLintExpression} Expression
  8. * @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression
  9. * @typedef {import('vue-eslint-parser').AST.ESLintPattern} Pattern
  10. */
  11. /**
  12. * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
  13. * @typedef {ComponentObjectProp & { value: ObjectExpression} } ComponentObjectPropObject
  14. */
  15. const utils = require('../utils')
  16. const NATIVE_TYPES = new Set([
  17. 'String',
  18. 'Number',
  19. 'Boolean',
  20. 'Function',
  21. 'Object',
  22. 'Array',
  23. 'Symbol'
  24. ])
  25. // ------------------------------------------------------------------------------
  26. // Rule Definition
  27. // ------------------------------------------------------------------------------
  28. module.exports = {
  29. meta: {
  30. type: 'suggestion',
  31. docs: {
  32. description: 'require default value for props',
  33. category: 'strongly-recommended',
  34. url: 'https://eslint.vuejs.org/rules/require-default-prop.html'
  35. },
  36. fixable: null, // or "code" or "whitespace"
  37. schema: []
  38. },
  39. create (context) {
  40. // ----------------------------------------------------------------------
  41. // Helpers
  42. // ----------------------------------------------------------------------
  43. /**
  44. * Checks if the passed prop is required
  45. * @param {ComponentObjectPropObject} prop - Property AST node for a single prop
  46. * @return {boolean}
  47. */
  48. function propIsRequired (prop) {
  49. const propRequiredNode = prop.value.properties
  50. .find(p =>
  51. p.type === 'Property' &&
  52. utils.getStaticPropertyName(p) === 'required' &&
  53. p.value.type === 'Literal' &&
  54. p.value.value === true
  55. )
  56. return Boolean(propRequiredNode)
  57. }
  58. /**
  59. * Checks if the passed prop has a default value
  60. * @param {ComponentObjectPropObject} prop - Property AST node for a single prop
  61. * @return {boolean}
  62. */
  63. function propHasDefault (prop) {
  64. const propDefaultNode = prop.value.properties
  65. .find(p =>
  66. p.type === 'Property' && utils.getStaticPropertyName(p) === 'default'
  67. )
  68. return Boolean(propDefaultNode)
  69. }
  70. /**
  71. * Finds all props that don't have a default value set
  72. * @param {ComponentObjectProp[]} props - Vue component's "props" node
  73. * @return {ComponentObjectProp[]} Array of props without "default" value
  74. */
  75. function findPropsWithoutDefaultValue (props) {
  76. return props
  77. .filter(prop => {
  78. if (prop.value.type !== 'ObjectExpression') {
  79. return (prop.value.type !== 'CallExpression' && prop.value.type !== 'Identifier') ||
  80. (prop.value.type === 'Identifier' && NATIVE_TYPES.has(prop.value.name))
  81. }
  82. return !propIsRequired(/** @type {ComponentObjectPropObject} */(prop)) && !propHasDefault(/** @type {ComponentObjectPropObject} */(prop))
  83. })
  84. }
  85. /**
  86. * Detects whether given value node is a Boolean type
  87. * @param {Expression | Pattern} value
  88. * @return {Boolean}
  89. */
  90. function isValueNodeOfBooleanType (value) {
  91. return (
  92. value.type === 'Identifier' &&
  93. value.name === 'Boolean'
  94. ) || (
  95. value.type === 'ArrayExpression' &&
  96. value.elements.length === 1 &&
  97. value.elements[0].type === 'Identifier' &&
  98. value.elements[0].name === 'Boolean'
  99. )
  100. }
  101. /**
  102. * Detects whether given prop node is a Boolean
  103. * @param {ComponentObjectProp} prop
  104. * @return {Boolean}
  105. */
  106. function isBooleanProp (prop) {
  107. const value = utils.unwrapTypes(prop.value)
  108. return isValueNodeOfBooleanType(value) || (
  109. value.type === 'ObjectExpression' &&
  110. value.properties.some(p =>
  111. p.type === 'Property' &&
  112. p.key.type === 'Identifier' &&
  113. p.key.name === 'type' &&
  114. isValueNodeOfBooleanType(p.value)
  115. )
  116. )
  117. }
  118. /**
  119. * Excludes purely Boolean props from the Array
  120. * @param {ComponentObjectProp[]} props - Array with props
  121. * @return {ComponentObjectProp[]}
  122. */
  123. function excludeBooleanProps (props) {
  124. return props.filter(prop => !isBooleanProp(prop))
  125. }
  126. // ----------------------------------------------------------------------
  127. // Public
  128. // ----------------------------------------------------------------------
  129. return utils.executeOnVue(context, (obj) => {
  130. const props = utils.getComponentProps(obj)
  131. .filter(prop => prop.key && prop.value && !(prop.node.type === 'Property' && prop.node.shorthand))
  132. const propsWithoutDefault = findPropsWithoutDefaultValue(/** @type {ComponentObjectProp[]} */(props))
  133. const propsToReport = excludeBooleanProps(propsWithoutDefault)
  134. for (const prop of propsToReport) {
  135. const propName = prop.propName != null ? prop.propName : `[${context.getSourceCode().getText(prop.key)}]`
  136. context.report({
  137. node: prop.node,
  138. message: `Prop '{{propName}}' requires default value to be set.`,
  139. data: {
  140. propName
  141. }
  142. })
  143. }
  144. })
  145. }
  146. }