slot-attribute.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const canConvertToVSlot = require('./utils/can-convert-to-v-slot')
  7. const casing = require('../../utils/casing')
  8. module.exports = {
  9. deprecated: '2.6.0',
  10. supported: '<3.0.0',
  11. /** @param {RuleContext} context @returns {TemplateListener} */
  12. createTemplateBodyVisitor(context) {
  13. const options = context.options[0] || {}
  14. /** @type {Set<string>} */
  15. const ignore = new Set(options.ignore)
  16. const sourceCode = context.getSourceCode()
  17. const tokenStore =
  18. sourceCode.parserServices.getTemplateBodyTokenStore &&
  19. sourceCode.parserServices.getTemplateBodyTokenStore()
  20. /**
  21. * Checks whether the given node can convert to the `v-slot`.
  22. * @param {VAttribute} slotAttr node of `slot`
  23. * @returns {boolean} `true` if the given node can convert to the `v-slot`
  24. */
  25. function canConvertFromSlotToVSlot(slotAttr) {
  26. if (!canConvertToVSlot(slotAttr.parent.parent, sourceCode, tokenStore)) {
  27. return false
  28. }
  29. if (!slotAttr.value) {
  30. return true
  31. }
  32. const slotName = slotAttr.value.value
  33. // If other than alphanumeric, underscore and hyphen characters are included it can not be converted.
  34. return !/[^\w\-]/u.test(slotName)
  35. }
  36. /**
  37. * Checks whether the given node can convert to the `v-slot`.
  38. * @param {VDirective} slotAttr node of `v-bind:slot`
  39. * @returns {boolean} `true` if the given node can convert to the `v-slot`
  40. */
  41. function canConvertFromVBindSlotToVSlot(slotAttr) {
  42. if (!canConvertToVSlot(slotAttr.parent.parent, sourceCode, tokenStore)) {
  43. return false
  44. }
  45. if (!slotAttr.value) {
  46. return true
  47. }
  48. if (!slotAttr.value.expression) {
  49. // parse error or empty expression
  50. return false
  51. }
  52. return slotAttr.value.expression.type === 'Identifier'
  53. }
  54. /**
  55. * Convert to `v-slot`.
  56. * @param {RuleFixer} fixer fixer
  57. * @param {VAttribute|VDirective} slotAttr node of `slot`
  58. * @param {string | null} slotName name of `slot`
  59. * @param {boolean} vBind `true` if `slotAttr` is `v-bind:slot`
  60. * @returns {IterableIterator<Fix>} fix data
  61. */
  62. function* fixSlotToVSlot(fixer, slotAttr, slotName, vBind) {
  63. const startTag = slotAttr.parent
  64. const scopeAttr = startTag.attributes.find(
  65. (attr) =>
  66. attr.directive === true &&
  67. attr.key.name &&
  68. (attr.key.name.name === 'slot-scope' ||
  69. attr.key.name.name === 'scope')
  70. )
  71. let nameArgument = ''
  72. if (slotName) {
  73. nameArgument = vBind ? `:[${slotName}]` : `:${slotName}`
  74. }
  75. const scopeValue =
  76. scopeAttr && scopeAttr.value
  77. ? `=${sourceCode.getText(scopeAttr.value)}`
  78. : ''
  79. const replaceText = `v-slot${nameArgument}${scopeValue}`
  80. const element = startTag.parent
  81. if (element.name === 'template') {
  82. yield fixer.replaceText(slotAttr || scopeAttr, replaceText)
  83. if (slotAttr && scopeAttr) {
  84. yield fixer.remove(scopeAttr)
  85. }
  86. } else {
  87. yield fixer.remove(slotAttr || scopeAttr)
  88. if (slotAttr && scopeAttr) {
  89. yield fixer.remove(scopeAttr)
  90. }
  91. const vFor = startTag.attributes.find(
  92. (attr) => attr.directive && attr.key.name.name === 'for'
  93. )
  94. const vForText = vFor ? `${sourceCode.getText(vFor)} ` : ''
  95. if (vFor) {
  96. yield fixer.remove(vFor)
  97. }
  98. yield fixer.insertTextBefore(
  99. element,
  100. `<template ${vForText}${replaceText}>\n`
  101. )
  102. yield fixer.insertTextAfter(element, `\n</template>`)
  103. }
  104. }
  105. /**
  106. * Reports `slot` node
  107. * @param {VAttribute} slotAttr node of `slot`
  108. * @returns {void}
  109. */
  110. function reportSlot(slotAttr) {
  111. const componentName = slotAttr.parent.parent.rawName
  112. if (
  113. ignore.has(componentName) ||
  114. ignore.has(casing.pascalCase(componentName)) ||
  115. ignore.has(casing.kebabCase(componentName))
  116. ) {
  117. return
  118. }
  119. context.report({
  120. node: slotAttr.key,
  121. messageId: 'forbiddenSlotAttribute',
  122. // fix to use `v-slot`
  123. *fix(fixer) {
  124. if (!canConvertFromSlotToVSlot(slotAttr)) {
  125. return
  126. }
  127. const slotName = slotAttr.value && slotAttr.value.value
  128. yield* fixSlotToVSlot(fixer, slotAttr, slotName, false)
  129. }
  130. })
  131. }
  132. /**
  133. * Reports `v-bind:slot` node
  134. * @param {VDirective} slotAttr node of `v-bind:slot`
  135. * @returns {void}
  136. */
  137. function reportVBindSlot(slotAttr) {
  138. context.report({
  139. node: slotAttr.key,
  140. messageId: 'forbiddenSlotAttribute',
  141. // fix to use `v-slot`
  142. *fix(fixer) {
  143. if (!canConvertFromVBindSlotToVSlot(slotAttr)) {
  144. return
  145. }
  146. const slotName =
  147. slotAttr.value &&
  148. slotAttr.value.expression &&
  149. sourceCode.getText(slotAttr.value.expression).trim()
  150. yield* fixSlotToVSlot(fixer, slotAttr, slotName, true)
  151. }
  152. })
  153. }
  154. return {
  155. "VAttribute[directive=false][key.name='slot']": reportSlot,
  156. "VAttribute[directive=true][key.name.name='bind'][key.argument.name='slot']":
  157. reportVBindSlot
  158. }
  159. }
  160. }