Skip to content

Commit

Permalink
New: valid-v-slot rule (fixes #802) (#837)
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea committed Mar 4, 2019
1 parent 9f2cec1 commit 263076a
Show file tree
Hide file tree
Showing 3 changed files with 661 additions and 0 deletions.
112 changes: 112 additions & 0 deletions docs/rules/valid-v-slot.md
@@ -0,0 +1,112 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/valid-v-slot
description: enforce valid `v-slot` directives
---
# vue/valid-v-slot
> enforce valid `v-slot` directives
This rule checks whether every `v-slot` directive is valid.

## :book: Rule Details

This rule reports `v-slot` directives in the following cases:

- The directive is not owned by a custom element. E.g. `<div v-slot=""></div>`
- The directive is a named slot and is on a custom element directly. E.g. `<my-component v-slot:foo></my-component>`
- The directive is the default slot, is on a custom element directly, and there are other named slots. E.g. `<my-component v-slot=""><template v-slot:foo></template></my-component>`
- The element which has the directive has another `v-slot` directive. E.g. `<my-component v-slot:one v-slot:two></my-component>`
- The element which has the directive has another `v-slot` directive that is distributed to the same slot. E.g. `<my-component><template v-slot:foo></template><template v-slot:foo></template></my-component>`
- The directive has a dynamic argument which uses the scope properties that the directive defined. E.g. `<my-component><template v-slot:[data]="data"></template></my-component>`
- The directive has any modifier. E.g. `<my-component v-slot.foo></my-component>`
- The directive is the default slot, is on a custom element directly, and has no value. E.g. `<my-component v-slot></my-component>`

<eslint-code-block :rules="{'vue/valid-v-slot': ['error']}">

```vue
<template>
<!-- ✓ GOOD -->
<my-component v-slot="data">
{{data}}
</my-component>
<my-component>
<template v-slot:default>
default
</template>
<template v-slot:one>
one
</template>
<template v-slot:two>
two
</template>
</my-component>
<!-- ✗ BAD -->
<div v-slot="data">
{{data}}
</div>
<div>
<template v-slot:one>
one
</template>
</div>
<my-component v-slot:one="data">
{{data}}
</my-component>
<my-component v-slot="data">
{{data}}
<template v-slot:one>
one
</template>
</my-component>
<my-component v-slot:one v-slot:two>
one and two
</my-component>
<my-component>
<template v-slot:one>
one 1
</template>
<template v-slot:one>
one 2
</template>
</my-component>
<my-component>
<template v-slot:[data]="data">
dynamic?
</template>
</my-component>
<my-component v-slot.mod="data">
{{data}}
</my-component>
<my-component v-slot>
content
</my-component>
</template>
```

</eslint-code-block>

::: warning Note
This rule does not check syntax errors in directives because it's checked by [no-parsing-error] rule.
:::

## :wrench: Options

Nothing.

## :couple: Related rules

- [no-parsing-error]

[no-parsing-error]: no-parsing-error.md

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-v-slot.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-v-slot.js)
252 changes: 252 additions & 0 deletions lib/rules/valid-v-slot.js
@@ -0,0 +1,252 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
'use strict'

const utils = require('../utils')

/**
* Get all `v-slot` directives on a given element.
* @param {VElement} node The VElement node to check.
* @returns {VAttribute[]} The array of `v-slot` directives.
*/
function getSlotDirectivesOnElement (node) {
return node.startTag.attributes.filter(attribute =>
attribute.directive &&
attribute.key.name.name === 'slot'
)
}

/**
* Get all `v-slot` directives on the children of a given element.
* @param {VElement} node The VElement node to check.
* @returns {VAttribute[][]}
* The array of the group of `v-slot` directives.
* The group bundles `v-slot` directives of element sequence which is connected
* by `v-if`/`v-else-if`/`v-else`.
*/
function getSlotDirectivesOnChildren (node) {
return node.children
.reduce(({ groups, vIf }, childNode) => {
if (childNode.type === 'VElement') {
let connected
if (utils.hasDirective(childNode, 'if')) {
connected = false
vIf = true
} else if (utils.hasDirective(childNode, 'else-if')) {
connected = vIf
vIf = true
} else if (utils.hasDirective(childNode, 'else')) {
connected = vIf
vIf = false
} else {
connected = false
vIf = false
}

if (connected) {
groups[groups.length - 1].push(childNode)
} else {
groups.push([childNode])
}
} else if (childNode.type !== 'VText' || childNode.value.trim() !== '') {
vIf = false
}
return { groups, vIf }
}, { groups: [], vIf: false })
.groups
.map(group =>
group
.map(childElement =>
childElement.name === 'template'
? utils.getDirective(childElement, 'slot')
: null
)
.filter(Boolean)
)
.filter(group => group.length >= 1)
}

/**
* Get the normalized name of a given `v-slot` directive node.
* @param {VAttribute} node The `v-slot` directive node.
* @returns {string} The normalized name.
*/
function getNormalizedName (node, sourceCode) {
return node.key.argument == null ? 'default' : sourceCode.getText(node.key.argument)
}

/**
* Get all `v-slot` directives which are distributed to the same slot as a given `v-slot` directive node.
* @param {VAttribute[][]} vSlotGroups The result of `getAllNamedSlotElements()`.
* @param {VElement} currentVSlot The current `v-slot` directive node.
* @returns {VAttribute[][]} The array of the group of `v-slot` directives.
*/
function filterSameSlot (vSlotGroups, currentVSlot, sourceCode) {
const currentName = getNormalizedName(currentVSlot, sourceCode)
return vSlotGroups
.map(vSlots =>
vSlots.filter(vSlot => getNormalizedName(vSlot, sourceCode) === currentName)
)
.filter(slots => slots.length >= 1)
}

/**
* Check whether a given argument node is using an iteration variable that the element defined.
* @param {VExpressionContainer|VIdentifier|null} argument The argument node to check.
* @param {VElement} element The element node which has the argument.
* @returns {boolean} `true` if the argument node is using the iteration variable.
*/
function isUsingIterationVar (argument, element) {
if (argument && argument.type === 'VExpressionContainer') {
for (const { variable } of argument.references) {
if (
variable != null &&
variable.kind === 'v-for' &&
variable.id.range[0] > element.startTag.range[0] &&
variable.id.range[1] < element.startTag.range[1]
) {
return true
}
}
}
return false
}

/**
* Check whether a given argument node is using an scope variable that the directive defined.
* @param {VAttribute} vSlot The `v-slot` directive to check.
* @returns {boolean} `true` if that argument node is using a scope variable the directive defined.
*/
function isUsingScopeVar (vSlot) {
const argument = vSlot.key.argument
const value = vSlot.value

if (argument && value && argument.type === 'VExpressionContainer') {
for (const { variable } of argument.references) {
if (
variable != null &&
variable.kind === 'scope' &&
variable.id.range[0] > value.range[0] &&
variable.id.range[1] < value.range[1]
) {
return true
}
}
}
}

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `v-slot` directives',
category: undefined, // essential
url: 'https://eslint.vuejs.org/rules/valid-v-slot.html'
},
fixable: null,
schema: [],
messages: {
ownerMustBeCustomElement: "'v-slot' directive must be owned by a custom element, but '{{name}}' is not.",
namedSlotMustBeOnTemplate: "Named slots must use '<template>' on a custom element.",
defaultSlotMustBeOnTemplate: "Default slot must use '<template>' on a custom element when there are other named slots.",
disallowDuplicateSlotsOnElement: "An element cannot have multiple 'v-slot' directives.",
disallowDuplicateSlotsOnChildren: "An element cannot have multiple '<template>' elements which are distributed to the same slot.",
disallowArgumentUseSlotParams: "Dynamic argument of 'v-slot' directive cannot use that slot parameter.",
disallowAnyModifier: "'v-slot' directive doesn't support any modifier.",
requireAttributeValue: "'v-slot' directive on a custom element requires that attribute value."
}
},

create (context) {
const sourceCode = context.getSourceCode()

return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='slot']" (node) {
const isDefaultSlot = node.key.argument == null || node.key.argument.name === 'default'
const element = node.parent.parent
const parentElement = element.parent
const ownerElement = element.name === 'template' ? parentElement : element
const vSlotsOnElement = getSlotDirectivesOnElement(element)
const vSlotGroupsOnChildren = getSlotDirectivesOnChildren(ownerElement)

// Verify location.
if (!utils.isCustomComponent(ownerElement)) {
context.report({
node,
messageId: 'ownerMustBeCustomElement',
data: { name: ownerElement.rawName }
})
}
if (!isDefaultSlot && element.name !== 'template') {
context.report({
node,
messageId: 'namedSlotMustBeOnTemplate'
})
}
if (ownerElement === element && vSlotGroupsOnChildren.length >= 1) {
context.report({
node,
messageId: 'defaultSlotMustBeOnTemplate'
})
}

// Verify duplication.
if (vSlotsOnElement.length >= 2 && vSlotsOnElement[0] !== node) {
// E.g., <my-component #one #two>
context.report({
node,
messageId: 'disallowDuplicateSlotsOnElement'
})
}
if (ownerElement === parentElement) {
const vSlotGroupsOfSameSlot = filterSameSlot(vSlotGroupsOnChildren, node, sourceCode)
const vFor = utils.getDirective(element, 'for')
if (
vSlotGroupsOfSameSlot.length >= 2 &&
!vSlotGroupsOfSameSlot[0].includes(node)
) {
// E.g., <template #one></template>
// <template #one></template>
context.report({
node,
messageId: 'disallowDuplicateSlotsOnChildren'
})
}
if (vFor && !isUsingIterationVar(node.key.argument, element)) {
// E.g., <template v-for="x of xs" #one></template>
context.report({
node,
messageId: 'disallowDuplicateSlotsOnChildren'
})
}
}

// Verify argument.
if (isUsingScopeVar(node)) {
context.report({
node,
messageId: 'disallowArgumentUseSlotParams'
})
}

// Verify modifiers.
if (node.key.modifiers.length >= 1) {
context.report({
node,
messageId: 'disallowAnyModifier'
})
}

// Verify value.
if (ownerElement === element && isDefaultSlot && !utils.hasAttributeValue(node)) {
context.report({
node,
messageId: 'requireAttributeValue'
})
}
}
})
}
}

0 comments on commit 263076a

Please sign in to comment.