require-explicit-slots.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  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. /**
  32. * @param {VElement} node
  33. * @return {VAttribute | VDirective | undefined}
  34. */
  35. function getSlotNameNode(node) {
  36. return node.startTag.attributes.find(
  37. (node) =>
  38. (!node.directive && node.key.name === 'name') ||
  39. (node.directive &&
  40. node.key.name.name === 'bind' &&
  41. node.key.argument?.type === 'VIdentifier' &&
  42. node.key.argument?.name === 'name')
  43. )
  44. }
  45. module.exports = {
  46. meta: {
  47. type: 'problem',
  48. docs: {
  49. description: 'require slots to be explicitly defined',
  50. categories: undefined,
  51. url: 'https://eslint.vuejs.org/rules/require-explicit-slots.html'
  52. },
  53. fixable: null,
  54. schema: [],
  55. messages: {
  56. requireExplicitSlots: 'Slots must be explicitly defined.',
  57. alreadyDefinedSlot: 'Slot {{slotName}} is already defined.'
  58. }
  59. },
  60. /** @param {RuleContext} context */
  61. create(context) {
  62. const sourceCode = context.getSourceCode()
  63. const documentFragment =
  64. sourceCode.parserServices.getDocumentFragment &&
  65. sourceCode.parserServices.getDocumentFragment()
  66. if (!documentFragment) {
  67. return {}
  68. }
  69. const scripts = documentFragment.children.filter(
  70. /** @returns {element is VElement} */
  71. (element) => utils.isVElement(element) && element.name === 'script'
  72. )
  73. if (scripts.every((script) => !utils.hasAttribute(script, 'lang', 'ts'))) {
  74. return {}
  75. }
  76. const slotsDefined = new Set()
  77. /**
  78. * @param {VElement} node
  79. * @param {string | undefined} slotName
  80. */
  81. function reportMissingSlot(node, slotName) {
  82. if (!slotsDefined.has(slotName)) {
  83. context.report({
  84. node,
  85. messageId: 'requireExplicitSlots'
  86. })
  87. }
  88. }
  89. return utils.compositingVisitors(
  90. utils.defineScriptSetupVisitor(context, {
  91. onDefineSlotsEnter(_node, slots) {
  92. for (const slot of slots) {
  93. if (!slot.slotName) {
  94. continue
  95. }
  96. if (slotsDefined.has(slot.slotName)) {
  97. context.report({
  98. node: slot.node,
  99. messageId: 'alreadyDefinedSlot',
  100. data: {
  101. slotName: slot.slotName
  102. }
  103. })
  104. } else {
  105. slotsDefined.add(slot.slotName)
  106. }
  107. }
  108. }
  109. }),
  110. utils.executeOnVue(context, (obj) => {
  111. const slotsProperty = utils.findProperty(obj, 'slots')
  112. if (!slotsProperty) return
  113. const slotsTypeHelper =
  114. slotsProperty.value.type === 'TSAsExpression' &&
  115. slotsProperty.value.typeAnnotation?.typeName.name === 'SlotsType' &&
  116. slotsProperty.value.typeAnnotation
  117. if (!slotsTypeHelper) return
  118. const typeArguments =
  119. 'typeArguments' in slotsTypeHelper
  120. ? slotsTypeHelper.typeArguments
  121. : slotsTypeHelper.typeParameters
  122. const param = /** @type {TypeNode|undefined} */ (
  123. typeArguments?.params[0]
  124. )
  125. if (!param) return
  126. if (param.type === 'TSTypeLiteral') {
  127. for (const memberNode of param.members) {
  128. const slotName = getSlotsName(memberNode)
  129. if (!slotName) continue
  130. if (slotsDefined.has(slotName)) {
  131. context.report({
  132. node: memberNode,
  133. messageId: 'alreadyDefinedSlot',
  134. data: {
  135. slotName
  136. }
  137. })
  138. } else {
  139. slotsDefined.add(slotName)
  140. }
  141. }
  142. }
  143. }),
  144. utils.defineTemplateBodyVisitor(context, {
  145. /** @param {VElement} node */
  146. "VElement[name='slot']"(node) {
  147. const nameNode = getSlotNameNode(node)
  148. // if no slot name is declared, default to 'default'
  149. if (!nameNode) {
  150. reportMissingSlot(node, 'default')
  151. return
  152. }
  153. if (nameNode.directive) {
  154. const expression = nameNode.value?.expression
  155. // ignore attribute binding except string literal
  156. if (!expression || !utils.isStringLiteral(expression)) {
  157. return
  158. }
  159. const name = utils.getStringLiteralValue(expression) || undefined
  160. reportMissingSlot(node, name)
  161. } else {
  162. reportMissingSlot(node, nameNode.value?.value)
  163. }
  164. }
  165. })
  166. )
  167. }
  168. }