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

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