diff --git a/docs/rules/valid-v-slot.md b/docs/rules/valid-v-slot.md
new file mode 100644
index 000000000..edb17c8df
--- /dev/null
+++ b/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. `
`
+- The directive is a named slot and is on a custom element directly. E.g. ``
+- The directive is the default slot, is on a custom element directly, and there are other named slots. E.g. ``
+- The element which has the directive has another `v-slot` directive. E.g. ``
+- The element which has the directive has another `v-slot` directive that is distributed to the same slot. E.g. ``
+- The directive has a dynamic argument which uses the scope properties that the directive defined. E.g. ``
+- The directive has any modifier. E.g. ``
+- The directive is the default slot, is on a custom element directly, and has no value. E.g. ``
+
+
+
+```vue
+
+
+
+ {{data}}
+
+
+
+ default
+
+
+ one
+
+
+ two
+
+
+
+
+
+ {{data}}
+
+
+
+ one
+
+
+
+
+ {{data}}
+
+
+ {{data}}
+
+ one
+
+
+
+
+ one and two
+
+
+
+ one 1
+
+
+ one 2
+
+
+
+
+
+ dynamic?
+
+
+
+
+ {{data}}
+
+
+
+ content
+
+
+```
+
+
+
+::: 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)
diff --git a/lib/rules/valid-v-slot.js b/lib/rules/valid-v-slot.js
new file mode 100644
index 000000000..567e3b251
--- /dev/null
+++ b/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 '' on a custom element.",
+ defaultSlotMustBeOnTemplate: "Default slot must use '' 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 '' 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.,
+ 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.,
+ //
+ context.report({
+ node,
+ messageId: 'disallowDuplicateSlotsOnChildren'
+ })
+ }
+ if (vFor && !isUsingIterationVar(node.key.argument, element)) {
+ // E.g.,
+ 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'
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/tests/lib/rules/valid-v-slot.js b/tests/lib/rules/valid-v-slot.js
new file mode 100644
index 000000000..36f709581
--- /dev/null
+++ b/tests/lib/rules/valid-v-slot.js
@@ -0,0 +1,297 @@
+/**
+ * @author Toru Nagashima
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/valid-v-slot')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+ parser: 'vue-eslint-parser',
+ parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('valid-v-slot', rule, {
+ valid: [
+ `
+ {{data}}
+ `,
+ `
+ {{data}}
+ `,
+ `
+ {{data}}
+ `,
+ `
+
+ default
+
+ `,
+ `
+
+ default
+
+ `,
+ `
+
+ named
+
+ `,
+ `
+
+ default
+ named
+
+ `,
+ `
+
+ one
+ two
+
+ `,
+ `
+
+ one
+ two
+ one
+ two
+
+ `,
+ `
+
+ foo
+ bar
+
+ `,
+ `
+
+ foo
+ bar
+ baz
+
+ `,
+ `
+
+ {{value}}
+
+ `
+ ],
+ invalid: [
+ // Verify location.
+ {
+ code: `
+
+ {{data}}
+
+ `,
+ errors: [{ messageId: 'ownerMustBeCustomElement', data: { name: 'div' }}]
+ },
+ {
+ code: `
+
+
+
+ `,
+ errors: [{ messageId: 'ownerMustBeCustomElement', data: { name: 'template' }}]
+ },
+ {
+ code: `
+
+ {{data}}
+
+ `,
+ errors: [
+ { messageId: 'ownerMustBeCustomElement', data: { name: 'div' }},
+ { messageId: 'namedSlotMustBeOnTemplate' }
+ ]
+ },
+ {
+ code: `
+
+
+
+ `,
+ errors: [{ messageId: 'ownerMustBeCustomElement', data: { name: 'div' }}]
+ },
+ {
+ code: `
+
+ {{data}}
+
+ `,
+ errors: [{ messageId: 'namedSlotMustBeOnTemplate' }]
+ },
+ {
+ code: `
+
+
+ {{data}}
+
+
+
+ `,
+ errors: [{ messageId: 'defaultSlotMustBeOnTemplate' }]
+ },
+
+ // Verify duplication.
+ {
+ code: `
+
+ {{data}}
+
+ `,
+ errors: [
+ { messageId: 'namedSlotMustBeOnTemplate' },
+ { messageId: 'disallowDuplicateSlotsOnElement' }
+ ]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'disallowDuplicateSlotsOnElement' }]
+ },
+ {
+ code: `
+
+
+ 1st
+ 2nd
+
+
+ `,
+ errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
+ },
+ {
+ code: `
+
+
+ 1st
+ 2nd
+
+
+ `,
+ errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
+ },
+ {
+ code: `
+
+
+ 1st
+ 2nd
+
+
+ `,
+ errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
+ },
+ {
+ code: `
+
+
+ 1st
+ 2nd
+
+
+ `,
+ errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
+ },
+ {
+ code: `
+
+
+ 1st
+ 2nd
+
+
+ `,
+ errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
+ },
+ {
+ code: `
+
+
+ 1st
+ 2st
+ 3rd
+
+
+ `,
+ errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
+ },
+ {
+ code: `
+
+
+ {{value}}
+
+
+ `,
+ errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
+ },
+ {
+ code: `
+
+
+ {{value}}
+
+
+ `,
+ errors: [{ messageId: 'disallowDuplicateSlotsOnChildren' }]
+ },
+
+ // Verify arguments.
+ {
+ code: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'disallowArgumentUseSlotParams' }]
+ },
+
+ // Verify modifiers.
+ {
+ code: `
+
+ {{data}}
+
+ `,
+ errors: [{ messageId: 'disallowAnyModifier' }]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'disallowAnyModifier' }]
+ },
+
+ // Verify value.
+ {
+ code: `
+
+ content
+
+ `,
+ errors: [{ messageId: 'requireAttributeValue' }]
+ }
+ ]
+})