valid-v-slot.js 14 KB

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