can-convert-to-v-slot.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const utils = require('../../../utils')
  7. /**
  8. * @typedef {object} SlotVForVariables
  9. * @property {VForExpression} expr
  10. * @property {VVariable[]} variables
  11. */
  12. /**
  13. * @typedef {object} SlotContext
  14. * @property {VElement} element
  15. * @property {VAttribute | VDirective | null} slot
  16. * @property {VDirective | null} vFor
  17. * @property {SlotVForVariables | null} slotVForVars
  18. * @property {string} normalizedName
  19. */
  20. /**
  21. * Checks whether the given element can use v-slot.
  22. * @param {VElement} element
  23. * @param {SourceCode} sourceCode
  24. * @param {ParserServices.TokenStore} tokenStore
  25. */
  26. module.exports = function canConvertToVSlot(element, sourceCode, tokenStore) {
  27. const ownerElement = element.parent
  28. if (
  29. ownerElement.type === 'VDocumentFragment' ||
  30. !utils.isCustomComponent(ownerElement) ||
  31. ownerElement.name === 'component'
  32. ) {
  33. return false
  34. }
  35. const slot = getSlotContext(element, sourceCode)
  36. if (slot.vFor && !slot.slotVForVars) {
  37. // E.g., <template v-for="x of xs" #one></template>
  38. return false
  39. }
  40. if (hasSameSlotDirective(ownerElement, slot, sourceCode, tokenStore)) {
  41. return false
  42. }
  43. return true
  44. }
  45. /**
  46. * @param {VElement} element
  47. * @param {SourceCode} sourceCode
  48. * @returns {SlotContext}
  49. */
  50. function getSlotContext(element, sourceCode) {
  51. const slot =
  52. utils.getAttribute(element, 'slot') ||
  53. utils.getDirective(element, 'bind', 'slot')
  54. const vFor = utils.getDirective(element, 'for')
  55. const slotVForVars = getSlotVForVariableIfUsingIterationVars(slot, vFor)
  56. return {
  57. element,
  58. slot,
  59. vFor,
  60. slotVForVars,
  61. normalizedName: getNormalizedName(slot, sourceCode)
  62. }
  63. }
  64. /**
  65. * Gets the `v-for` directive and variable that provide the variables used by the given `slot` attribute.
  66. * @param {VAttribute | VDirective | null} slot The current `slot` attribute node.
  67. * @param {VDirective | null} [vFor] The current `v-for` directive node.
  68. * @returns { SlotVForVariables | null } The SlotVForVariables.
  69. */
  70. function getSlotVForVariableIfUsingIterationVars(slot, vFor) {
  71. if (!slot || !slot.directive) {
  72. return null
  73. }
  74. const expr =
  75. vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression)
  76. const variables =
  77. expr && getUsingIterationVars(slot.value, slot.parent.parent)
  78. return expr && variables && variables.length > 0 ? { expr, variables } : null
  79. }
  80. /**
  81. * Gets iterative variables if a given expression node is using iterative variables that the element defined.
  82. * @param {VExpressionContainer|null} expression The expression node to check.
  83. * @param {VElement} element The element node which has the expression.
  84. * @returns {VVariable[]} The expression node is using iteration variables.
  85. */
  86. function getUsingIterationVars(expression, element) {
  87. const vars = []
  88. if (expression && expression.type === 'VExpressionContainer') {
  89. for (const { variable } of expression.references) {
  90. if (
  91. variable != null &&
  92. variable.kind === 'v-for' &&
  93. variable.id.range[0] > element.startTag.range[0] &&
  94. variable.id.range[1] < element.startTag.range[1]
  95. ) {
  96. vars.push(variable)
  97. }
  98. }
  99. }
  100. return vars
  101. }
  102. /**
  103. * Get the normalized name of a given `slot` attribute node.
  104. * @param {VAttribute | VDirective | null} slotAttr node of `slot`
  105. * @param {SourceCode} sourceCode The source code.
  106. * @returns {string} The normalized name.
  107. */
  108. function getNormalizedName(slotAttr, sourceCode) {
  109. if (!slotAttr) {
  110. return 'default'
  111. }
  112. if (!slotAttr.directive) {
  113. return slotAttr.value ? slotAttr.value.value : 'default'
  114. }
  115. return slotAttr.value ? `[${sourceCode.getText(slotAttr.value)}]` : '[null]'
  116. }
  117. /**
  118. * Checks whether parent element has the same slot as the given slot.
  119. * @param {VElement} ownerElement The parent element.
  120. * @param {SlotContext} targetSlot The SlotContext with a slot to check if they are the same.
  121. * @param {SourceCode} sourceCode
  122. * @param {ParserServices.TokenStore} tokenStore
  123. */
  124. function hasSameSlotDirective(
  125. ownerElement,
  126. targetSlot,
  127. sourceCode,
  128. tokenStore
  129. ) {
  130. for (const group of utils.iterateChildElementsChains(ownerElement)) {
  131. if (group.includes(targetSlot.element)) {
  132. continue
  133. }
  134. for (const childElement of group) {
  135. const slot = getSlotContext(childElement, sourceCode)
  136. if (!targetSlot.slotVForVars || !slot.slotVForVars) {
  137. if (
  138. !targetSlot.slotVForVars &&
  139. !slot.slotVForVars &&
  140. targetSlot.normalizedName === slot.normalizedName
  141. ) {
  142. return true
  143. }
  144. continue
  145. }
  146. if (
  147. equalSlotVForVariables(
  148. targetSlot.slotVForVars,
  149. slot.slotVForVars,
  150. tokenStore
  151. )
  152. ) {
  153. return true
  154. }
  155. }
  156. }
  157. return false
  158. }
  159. /**
  160. * Determines whether the two given `v-slot` variables are considered to be equal.
  161. * @param {SlotVForVariables} a First element.
  162. * @param {SlotVForVariables} b Second element.
  163. * @param {ParserServices.TokenStore} tokenStore The token store.
  164. * @returns {boolean} `true` if the elements are considered to be equal.
  165. */
  166. function equalSlotVForVariables(a, b, tokenStore) {
  167. if (a.variables.length !== b.variables.length) {
  168. return false
  169. }
  170. if (!equal(a.expr.right, b.expr.right)) {
  171. return false
  172. }
  173. const checkedVarNames = new Set()
  174. const len = Math.min(a.expr.left.length, b.expr.left.length)
  175. for (let index = 0; index < len; index++) {
  176. const aPtn = a.expr.left[index]
  177. const bPtn = b.expr.left[index]
  178. const aVar = a.variables.find(
  179. (v) => aPtn.range[0] <= v.id.range[0] && v.id.range[1] <= aPtn.range[1]
  180. )
  181. const bVar = b.variables.find(
  182. (v) => bPtn.range[0] <= v.id.range[0] && v.id.range[1] <= bPtn.range[1]
  183. )
  184. if (aVar && bVar) {
  185. if (aVar.id.name !== bVar.id.name) {
  186. return false
  187. }
  188. if (!equal(aPtn, bPtn)) {
  189. return false
  190. }
  191. checkedVarNames.add(aVar.id.name)
  192. } else if (aVar || bVar) {
  193. return false
  194. }
  195. }
  196. return a.variables.every(
  197. (v) =>
  198. checkedVarNames.has(v.id.name) ||
  199. b.variables.some((bv) => v.id.name === bv.id.name)
  200. )
  201. /**
  202. * Determines whether the two given nodes are considered to be equal.
  203. * @param {ASTNode} a First node.
  204. * @param {ASTNode} b Second node.
  205. * @returns {boolean} `true` if the nodes are considered to be equal.
  206. */
  207. function equal(a, b) {
  208. if (a.type !== b.type) {
  209. return false
  210. }
  211. return utils.equalTokens(a, b, tokenStore)
  212. }
  213. }