require-explicit-slots.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. /**
  2. * @author Mussin Benarbia
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. /**
  8. * @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TypeNode
  9. * @typedef {import('@typescript-eslint/types').TSESTree.TypeElement} TypeElement
  10. */
  11. /**
  12. * @param {TypeElement} node
  13. * @return {string | null}
  14. */
  15. function getSlotsName(node) {
  16. if (
  17. node.type !== 'TSMethodSignature' &&
  18. node.type !== 'TSPropertySignature'
  19. ) {
  20. return null
  21. }
  22. const key = node.key
  23. if (key.type === 'Literal') {
  24. return typeof key.value === 'string' ? key.value : null
  25. }
  26. if (key.type === 'Identifier') {
  27. return key.name
  28. }
  29. return null
  30. }
  31. module.exports = {
  32. meta: {
  33. type: 'problem',
  34. docs: {
  35. description: 'require slots to be explicitly defined',
  36. categories: undefined,
  37. url: 'https://eslint.vuejs.org/rules/require-explicit-slots.html'
  38. },
  39. fixable: null,
  40. schema: [],
  41. messages: {
  42. requireExplicitSlots: 'Slots must be explicitly defined.',
  43. alreadyDefinedSlot: 'Slot {{slotName}} is already defined.'
  44. }
  45. },
  46. /** @param {RuleContext} context */
  47. create(context) {
  48. const sourceCode = context.getSourceCode()
  49. const documentFragment =
  50. sourceCode.parserServices.getDocumentFragment &&
  51. sourceCode.parserServices.getDocumentFragment()
  52. if (!documentFragment) {
  53. return {}
  54. }
  55. const scripts = documentFragment.children.filter(
  56. /** @returns {element is VElement} */
  57. (element) => utils.isVElement(element) && element.name === 'script'
  58. )
  59. if (scripts.every((script) => !utils.hasAttribute(script, 'lang', 'ts'))) {
  60. return {}
  61. }
  62. const slotsDefined = new Set()
  63. return utils.compositingVisitors(
  64. utils.defineScriptSetupVisitor(context, {
  65. onDefineSlotsEnter(node) {
  66. const typeArguments =
  67. 'typeArguments' in node ? node.typeArguments : node.typeParameters
  68. const param = /** @type {TypeNode|undefined} */ (
  69. typeArguments?.params[0]
  70. )
  71. if (!param) return
  72. if (param.type === 'TSTypeLiteral') {
  73. for (const memberNode of param.members) {
  74. const slotName = getSlotsName(memberNode)
  75. if (!slotName) continue
  76. if (slotsDefined.has(slotName)) {
  77. context.report({
  78. node: memberNode,
  79. messageId: 'alreadyDefinedSlot',
  80. data: {
  81. slotName
  82. }
  83. })
  84. } else {
  85. slotsDefined.add(slotName)
  86. }
  87. }
  88. }
  89. }
  90. }),
  91. utils.executeOnVue(context, (obj) => {
  92. const slotsProperty = utils.findProperty(obj, 'slots')
  93. if (!slotsProperty) return
  94. const slotsTypeHelper =
  95. slotsProperty.value.type === 'TSAsExpression' &&
  96. slotsProperty.value.typeAnnotation?.typeName.name === 'SlotsType' &&
  97. slotsProperty.value.typeAnnotation
  98. if (!slotsTypeHelper) return
  99. const typeArguments =
  100. 'typeArguments' in slotsTypeHelper
  101. ? slotsTypeHelper.typeArguments
  102. : slotsTypeHelper.typeParameters
  103. const param = /** @type {TypeNode|undefined} */ (
  104. typeArguments?.params[0]
  105. )
  106. if (!param) return
  107. if (param.type === 'TSTypeLiteral') {
  108. for (const memberNode of param.members) {
  109. const slotName = getSlotsName(memberNode)
  110. if (!slotName) continue
  111. if (slotsDefined.has(slotName)) {
  112. context.report({
  113. node: memberNode,
  114. messageId: 'alreadyDefinedSlot',
  115. data: {
  116. slotName
  117. }
  118. })
  119. } else {
  120. slotsDefined.add(slotName)
  121. }
  122. }
  123. }
  124. }),
  125. utils.defineTemplateBodyVisitor(context, {
  126. "VElement[name='slot']"(node) {
  127. let slotName = 'default'
  128. const slotNameAttr = utils.getAttribute(node, 'name')
  129. if (slotNameAttr?.value) {
  130. slotName = slotNameAttr.value.value
  131. }
  132. if (!slotsDefined.has(slotName)) {
  133. context.report({
  134. node,
  135. messageId: 'requireExplicitSlots'
  136. })
  137. }
  138. }
  139. })
  140. )
  141. }
  142. }