valid-v-slot.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. /**
  2. * @author Toru Nagashima
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. /**
  8. * @typedef { { expr: VForExpression, variables: VVariable[] } } VSlotVForVariables
  9. */
  10. /**
  11. * Get all `v-slot` directives on a given element.
  12. * @param {VElement} node The VElement node to check.
  13. * @returns {VDirective[]} The array of `v-slot` directives.
  14. */
  15. function getSlotDirectivesOnElement(node) {
  16. return utils.getDirectives(node, 'slot')
  17. }
  18. /**
  19. * Get all `v-slot` directives on the children of a given element.
  20. * @param {VElement} node The VElement node to check.
  21. * @returns {VDirective[][]}
  22. * The array of the group of `v-slot` directives.
  23. * The group bundles `v-slot` directives of element sequence which is connected
  24. * by `v-if`/`v-else-if`/`v-else`.
  25. */
  26. function getSlotDirectivesOnChildren(node) {
  27. /** @type {VDirective[][]} */
  28. const groups = []
  29. for (const group of utils.iterateChildElementsChains(node)) {
  30. const slotDirs = group
  31. .map((childElement) =>
  32. childElement.name === 'template'
  33. ? utils.getDirective(childElement, 'slot')
  34. : null
  35. )
  36. .filter(utils.isDef)
  37. if (slotDirs.length > 0) {
  38. groups.push(slotDirs)
  39. }
  40. }
  41. return groups
  42. }
  43. /**
  44. * Get the normalized name of a given `v-slot` directive node with modifiers after `v-slot:` directive.
  45. * @param {VDirective} node The `v-slot` directive node.
  46. * @param {SourceCode} sourceCode The source code.
  47. * @returns {string} The normalized name.
  48. */
  49. function getNormalizedName(node, sourceCode) {
  50. if (node.key.argument == null) {
  51. return 'default'
  52. }
  53. return node.key.modifiers.length === 0
  54. ? sourceCode.getText(node.key.argument)
  55. : sourceCode.text.slice(node.key.argument.range[0], node.key.range[1])
  56. }
  57. /**
  58. * Get all `v-slot` directives which are distributed to the same slot as a given `v-slot` directive node.
  59. * @param {VDirective[][]} vSlotGroups The result of `getAllNamedSlotElements()`.
  60. * @param {VDirective} currentVSlot The current `v-slot` directive node.
  61. * @param {VSlotVForVariables | null} currentVSlotVForVars The current `v-for` variables.
  62. * @param {SourceCode} sourceCode The source code.
  63. * @param {ParserServices.TokenStore} tokenStore The token store.
  64. * @returns {VDirective[][]} The array of the group of `v-slot` directives.
  65. */
  66. function filterSameSlot(
  67. vSlotGroups,
  68. currentVSlot,
  69. currentVSlotVForVars,
  70. sourceCode,
  71. tokenStore
  72. ) {
  73. const currentName = getNormalizedName(currentVSlot, sourceCode)
  74. return vSlotGroups
  75. .map((vSlots) =>
  76. vSlots.filter((vSlot) => {
  77. if (getNormalizedName(vSlot, sourceCode) !== currentName) {
  78. return false
  79. }
  80. const vForExpr = getVSlotVForVariableIfUsingIterationVars(
  81. vSlot,
  82. utils.getDirective(vSlot.parent.parent, 'for')
  83. )
  84. if (!currentVSlotVForVars || !vForExpr) {
  85. return !currentVSlotVForVars && !vForExpr
  86. }
  87. if (
  88. !equalVSlotVForVariables(currentVSlotVForVars, vForExpr, tokenStore)
  89. ) {
  90. return false
  91. }
  92. //
  93. return true
  94. })
  95. )
  96. .filter((slots) => slots.length >= 1)
  97. }
  98. /**
  99. * Determines whether the two given `v-slot` variables are considered to be equal.
  100. * @param {VSlotVForVariables} a First element.
  101. * @param {VSlotVForVariables} b Second element.
  102. * @param {ParserServices.TokenStore} tokenStore The token store.
  103. * @returns {boolean} `true` if the elements are considered to be equal.
  104. */
  105. function equalVSlotVForVariables(a, b, tokenStore) {
  106. if (a.variables.length !== b.variables.length) {
  107. return false
  108. }
  109. if (!equal(a.expr.right, b.expr.right)) {
  110. return false
  111. }
  112. const checkedVarNames = new Set()
  113. const len = Math.min(a.expr.left.length, b.expr.left.length)
  114. for (let index = 0; index < len; index++) {
  115. const aPtn = a.expr.left[index]
  116. const bPtn = b.expr.left[index]
  117. const aVar = a.variables.find(
  118. (v) => aPtn.range[0] <= v.id.range[0] && v.id.range[1] <= aPtn.range[1]
  119. )
  120. const bVar = b.variables.find(
  121. (v) => bPtn.range[0] <= v.id.range[0] && v.id.range[1] <= bPtn.range[1]
  122. )
  123. if (aVar && bVar) {
  124. if (aVar.id.name !== bVar.id.name) {
  125. return false
  126. }
  127. if (!equal(aPtn, bPtn)) {
  128. return false
  129. }
  130. checkedVarNames.add(aVar.id.name)
  131. } else if (aVar || bVar) {
  132. return false
  133. }
  134. }
  135. for (const v of a.variables) {
  136. if (!checkedVarNames.has(v.id.name)) {
  137. if (b.variables.every((bv) => v.id.name !== bv.id.name)) {
  138. return false
  139. }
  140. }
  141. }
  142. return true
  143. /**
  144. * Determines whether the two given nodes are considered to be equal.
  145. * @param {ASTNode} a First node.
  146. * @param {ASTNode} b Second node.
  147. * @returns {boolean} `true` if the nodes are considered to be equal.
  148. */
  149. function equal(a, b) {
  150. if (a.type !== b.type) {
  151. return false
  152. }
  153. return utils.equalTokens(a, b, tokenStore)
  154. }
  155. }
  156. /**
  157. * Gets the `v-for` directive and variable that provide the variables used by the given` v-slot` directive.
  158. * @param {VDirective} vSlot The current `v-slot` directive node.
  159. * @param {VDirective | null} [vFor] The current `v-for` directive node.
  160. * @returns { VSlotVForVariables | null } The VSlotVForVariable.
  161. */
  162. function getVSlotVForVariableIfUsingIterationVars(vSlot, vFor) {
  163. const expr =
  164. vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression)
  165. const variables =
  166. expr && getUsingIterationVars(vSlot.key.argument, vSlot.parent.parent)
  167. return expr && variables && variables.length ? { expr, variables } : null
  168. }
  169. /**
  170. * Gets iterative variables if a given argument node is using iterative variables that the element defined.
  171. * @param {VExpressionContainer|VIdentifier|null} argument The argument node to check.
  172. * @param {VElement} element The element node which has the argument.
  173. * @returns {VVariable[]} The argument node is using iteration variables.
  174. */
  175. function getUsingIterationVars(argument, element) {
  176. const vars = []
  177. if (argument && argument.type === 'VExpressionContainer') {
  178. for (const { variable } of argument.references) {
  179. if (
  180. variable != null &&
  181. variable.kind === 'v-for' &&
  182. variable.id.range[0] > element.startTag.range[0] &&
  183. variable.id.range[1] < element.startTag.range[1]
  184. ) {
  185. vars.push(variable)
  186. }
  187. }
  188. }
  189. return vars
  190. }
  191. /**
  192. * Check whether a given argument node is using an scope variable that the directive defined.
  193. * @param {VDirective} vSlot The `v-slot` directive to check.
  194. * @returns {boolean} `true` if that argument node is using a scope variable the directive defined.
  195. */
  196. function isUsingScopeVar(vSlot) {
  197. const argument = vSlot.key.argument
  198. const value = vSlot.value
  199. if (argument && value && argument.type === 'VExpressionContainer') {
  200. for (const { variable } of argument.references) {
  201. if (
  202. variable != null &&
  203. variable.kind === 'scope' &&
  204. variable.id.range[0] > value.range[0] &&
  205. variable.id.range[1] < value.range[1]
  206. ) {
  207. return true
  208. }
  209. }
  210. }
  211. return false
  212. }
  213. /**
  214. * If `allowModifiers` option is set to `true`, check whether a given argument node has invalid modifiers like `v-slot.foo`.
  215. * Otherwise, check whether a given argument node has at least one modifier.
  216. * @param {VDirective} vSlot The `v-slot` directive to check.
  217. * @param {boolean} allowModifiers `allowModifiers` option in context.
  218. * @return {boolean} `true` if that argument node has invalid modifiers like `v-slot.foo`.
  219. */
  220. function hasInvalidModifiers(vSlot, allowModifiers) {
  221. return allowModifiers
  222. ? vSlot.key.argument == null && vSlot.key.modifiers.length >= 1
  223. : vSlot.key.modifiers.length >= 1
  224. }
  225. module.exports = {
  226. meta: {
  227. type: 'problem',
  228. docs: {
  229. description: 'enforce valid `v-slot` directives',
  230. categories: ['vue3-essential', 'essential'],
  231. url: 'https://eslint.vuejs.org/rules/valid-v-slot.html'
  232. },
  233. fixable: null,
  234. schema: [
  235. {
  236. type: 'object',
  237. properties: {
  238. allowModifiers: {
  239. type: 'boolean'
  240. }
  241. },
  242. additionalProperties: false
  243. }
  244. ],
  245. messages: {
  246. ownerMustBeCustomElement:
  247. "'v-slot' directive must be owned by a custom element, but '{{name}}' is not.",
  248. namedSlotMustBeOnTemplate:
  249. "Named slots must use '<template>' on a custom element.",
  250. defaultSlotMustBeOnTemplate:
  251. "Default slot must use '<template>' on a custom element when there are other named slots.",
  252. disallowDuplicateSlotsOnElement:
  253. "An element cannot have multiple 'v-slot' directives.",
  254. disallowDuplicateSlotsOnChildren:
  255. "An element cannot have multiple '<template>' elements which are distributed to the same slot.",
  256. disallowArgumentUseSlotParams:
  257. "Dynamic argument of 'v-slot' directive cannot use that slot parameter.",
  258. disallowAnyModifier: "'v-slot' directive doesn't support any modifier.",
  259. requireAttributeValue:
  260. "'v-slot' directive on a custom element requires that attribute value."
  261. }
  262. },
  263. /** @param {RuleContext} context */
  264. create(context) {
  265. const sourceCode = context.getSourceCode()
  266. const tokenStore =
  267. context.parserServices.getTemplateBodyTokenStore &&
  268. context.parserServices.getTemplateBodyTokenStore()
  269. const options = context.options[0] || {}
  270. const allowModifiers = options.allowModifiers === true
  271. return utils.defineTemplateBodyVisitor(context, {
  272. /** @param {VDirective} node */
  273. "VAttribute[directive=true][key.name.name='slot']"(node) {
  274. const isDefaultSlot =
  275. node.key.argument == null ||
  276. (node.key.argument.type === 'VIdentifier' &&
  277. node.key.argument.name === 'default')
  278. const element = node.parent.parent
  279. const parentElement = element.parent
  280. const ownerElement =
  281. element.name === 'template' ? parentElement : element
  282. if (ownerElement.type === 'VDocumentFragment') {
  283. return
  284. }
  285. const vSlotsOnElement = getSlotDirectivesOnElement(element)
  286. const vSlotGroupsOnChildren = getSlotDirectivesOnChildren(ownerElement)
  287. // Verify location.
  288. if (!utils.isCustomComponent(ownerElement)) {
  289. context.report({
  290. node,
  291. messageId: 'ownerMustBeCustomElement',
  292. data: { name: ownerElement.rawName }
  293. })
  294. }
  295. if (!isDefaultSlot && element.name !== 'template') {
  296. context.report({
  297. node,
  298. messageId: 'namedSlotMustBeOnTemplate'
  299. })
  300. }
  301. if (ownerElement === element && vSlotGroupsOnChildren.length >= 1) {
  302. context.report({
  303. node,
  304. messageId: 'defaultSlotMustBeOnTemplate'
  305. })
  306. }
  307. // Verify duplication.
  308. if (vSlotsOnElement.length >= 2 && vSlotsOnElement[0] !== node) {
  309. // E.g., <my-component #one #two>
  310. context.report({
  311. node,
  312. messageId: 'disallowDuplicateSlotsOnElement'
  313. })
  314. }
  315. if (ownerElement === parentElement) {
  316. const vFor = utils.getDirective(element, 'for')
  317. const vSlotVForVar = getVSlotVForVariableIfUsingIterationVars(
  318. node,
  319. vFor
  320. )
  321. const vSlotGroupsOfSameSlot = filterSameSlot(
  322. vSlotGroupsOnChildren,
  323. node,
  324. vSlotVForVar,
  325. sourceCode,
  326. tokenStore
  327. )
  328. if (
  329. vSlotGroupsOfSameSlot.length >= 2 &&
  330. !vSlotGroupsOfSameSlot[0].includes(node)
  331. ) {
  332. // E.g., <template #one></template>
  333. // <template #one></template>
  334. context.report({
  335. node,
  336. messageId: 'disallowDuplicateSlotsOnChildren'
  337. })
  338. }
  339. if (vFor && !vSlotVForVar) {
  340. // E.g., <template v-for="x of xs" #one></template>
  341. context.report({
  342. node,
  343. messageId: 'disallowDuplicateSlotsOnChildren'
  344. })
  345. }
  346. }
  347. // Verify argument.
  348. if (isUsingScopeVar(node)) {
  349. context.report({
  350. node,
  351. messageId: 'disallowArgumentUseSlotParams'
  352. })
  353. }
  354. // Verify modifiers.
  355. if (hasInvalidModifiers(node, allowModifiers)) {
  356. // E.g., <template v-slot.foo>
  357. context.report({
  358. node,
  359. loc: {
  360. start: node.key.modifiers[0].loc.start,
  361. end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
  362. },
  363. messageId: 'disallowAnyModifier'
  364. })
  365. }
  366. // Verify value.
  367. if (
  368. ownerElement === element &&
  369. isDefaultSlot &&
  370. (!node.value ||
  371. utils.isEmptyValueDirective(node, context) ||
  372. utils.isEmptyExpressionValueDirective(node, context))
  373. ) {
  374. context.report({
  375. node,
  376. messageId: 'requireAttributeValue'
  377. })
  378. }
  379. }
  380. })
  381. }
  382. }