diff --git a/docs/rules/v-slot-style.md b/docs/rules/v-slot-style.md
new file mode 100644
index 000000000..41b5ea6b4
--- /dev/null
+++ b/docs/rules/v-slot-style.md
@@ -0,0 +1,110 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/v-slot-style
+description: enforce `v-slot` directive style
+---
+# vue/v-slot-style
+> enforce `v-slot` directive style
+
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule enforces `v-slot` directive style which you should use shorthand or long form.
+
+
+
+```vue
+
+
+
+ {{data}}
+
+
+ content
+ content
+ content
+
+
+
+
+ {{data}}
+
+
+ content
+ content
+ content
+
+
+```
+
+
+
+## :wrench: Options
+
+```json
+{
+ "vue/v-slot-style": ["error", {
+ "atComponent": "shorthand" | "longform" | "v-slot",
+ "default": "shorthand" | "longform" | "v-slot",
+ "named": "shorthand" | "longform",
+ }]
+}
+```
+
+| Name | Type | Default Value | Description
+|:-----|:-----|:--------------|:------------
+| `atComponent` | `"shorthand"` \| `"longform"` \| `"v-slot"` | `"v-slot"` | The style for the default slot at custom components directly (E.g. ``).
+| `default` | `"shorthand"` \| `"longform"` \| `"v-slot"` | `"shorthand"` | The style for the default slot at template wrappers (E.g. ``).
+| `named` | `"shorthand"` \| `"longform"` | `"shorthand"` | The style for named slots (E.g. ``).
+
+Each value means:
+
+- `"shorthand"` ... use `#` shorthand. E.g. `#default`, `#named`, ...
+- `"longform"` ... use `v-slot:` directive notation. E.g. `v-slot:default`, `v-slot:named`, ...
+- `"v-slot"` ... use `v-slot` without that argument. This is shorter than `#default` shorthand.
+
+And a string option is supported to be consistent to similar `vue/v-bind-style` and `vue/v-on-style`.
+
+- `["error", "longform"]` is same as `["error", { atComponent: "longform", default: "longform", named: "longform" }]`.
+- `["error", "shorthand"]` is same as `["error", { atComponent: "shorthand", default: "shorthand", named: "shorthand" }]`.
+
+### `"longform"`
+
+
+
+```vue
+
+
+
+ {{data}}
+
+
+ content
+ content
+ content
+
+
+
+
+ {{data}}
+
+
+ content
+ content
+ content
+
+
+```
+
+
+
+## :books: Further reading
+
+- [Style guide - Directive shorthands](https://vuejs.org/v2/style-guide/#Directive-shorthands-strongly-recommended)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/v-slot-style.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/v-slot-style.js)
diff --git a/lib/rules/v-slot-style.js b/lib/rules/v-slot-style.js
new file mode 100644
index 000000000..e55dec95f
--- /dev/null
+++ b/lib/rules/v-slot-style.js
@@ -0,0 +1,146 @@
+/**
+ * @author Toru Nagashima
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { pascalCase } = require('../utils/casing')
+const utils = require('../utils')
+
+/**
+ * @typedef {Object} Options
+ * @property {"shorthand" | "longform" | "v-slot"} atComponent The style for the default slot at a custom component directly.
+ * @property {"shorthand" | "longform" | "v-slot"} default The style for the default slot at a template wrapper.
+ * @property {"shorthand" | "longform"} named The style for named slots at a template wrapper.
+ */
+
+/**
+ * Normalize options.
+ * @param {any} options The raw options to normalize.
+ * @returns {Options} The normalized options.
+ */
+function normalizeOptions (options) {
+ const normalized = {
+ atComponent: 'v-slot',
+ default: 'shorthand',
+ named: 'shorthand'
+ }
+
+ if (typeof options === 'string') {
+ normalized.atComponent = normalized.default = normalized.named = options
+ } else if (options != null) {
+ for (const key of ['atComponent', 'default', 'named']) {
+ if (options[key] != null) {
+ normalized[key] = options[key]
+ }
+ }
+ }
+
+ return normalized
+}
+
+/**
+ * Get the expected style.
+ * @param {Options} options The options that defined expected types.
+ * @param {VAttribute} node The `v-slot` node to check.
+ * @returns {"shorthand" | "longform" | "v-slot"} The expected style.
+ */
+function getExpectedStyle (options, node) {
+ const { argument } = node.key
+
+ if (argument == null || (argument.type === 'VIdentifier' && argument.name === 'default')) {
+ const element = node.parent.parent
+ return element.name === 'template' ? options.default : options.atComponent
+ }
+ return options.named
+}
+
+/**
+ * Get the expected style.
+ * @param {VAttribute} node The `v-slot` node to check.
+ * @returns {"shorthand" | "longform" | "v-slot"} The expected style.
+ */
+function getActualStyle (node) {
+ const { name, argument } = node.key
+
+ if (name.rawName === '#') {
+ return 'shorthand'
+ }
+ if (argument != null) {
+ return 'longform'
+ }
+ return 'v-slot'
+}
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'enforce `v-slot` directive style',
+ category: undefined, // strongly-recommended
+ url: 'https://eslint.vuejs.org/rules/v-slot-style.html'
+ },
+ fixable: 'code',
+ schema: [
+ {
+ anyOf: [
+ { enum: ['shorthand', 'longform'] },
+ {
+ type: 'object',
+ properties: {
+ atComponent: { enum: ['shorthand', 'longform', 'v-slot'] },
+ default: { enum: ['shorthand', 'longform', 'v-slot'] },
+ named: { enum: ['shorthand', 'longform'] }
+ },
+ additionalProperties: false
+ }
+ ]
+ }
+ ],
+ messages: {
+ expectedShorthand: "Expected '#{{argument}}' instead of '{{actual}}'.",
+ expectedLongform: "Expected 'v-slot:{{argument}}' instead of '{{actual}}'.",
+ expectedVSlot: "Expected 'v-slot' instead of '{{actual}}'."
+ }
+ },
+
+ create (context) {
+ const sourceCode = context.getSourceCode()
+ const options = normalizeOptions(context.options[0])
+
+ return utils.defineTemplateBodyVisitor(context, {
+ "VAttribute[directive=true][key.name.name='slot']" (node) {
+ const expected = getExpectedStyle(options, node)
+ const actual = getActualStyle(node)
+ if (actual === expected) {
+ return
+ }
+
+ const { name, argument } = node.key
+ const range = [name.range[0], (argument || name).range[1]]
+ const argumentText = argument ? sourceCode.getText(argument) : 'default'
+ context.report({
+ node,
+ messageId: `expected${pascalCase(expected)}`,
+ data: {
+ actual: sourceCode.text.slice(range[0], range[1]),
+ argument: argumentText
+ },
+
+ fix (fixer) {
+ switch (expected) {
+ case 'shorthand':
+ return fixer.replaceTextRange(range, `#${argumentText}`)
+ case 'longform':
+ return fixer.replaceTextRange(range, `v-slot:${argumentText}`)
+ case 'v-slot':
+ return fixer.replaceTextRange(range, 'v-slot')
+ default:
+ return null
+ }
+ }
+ })
+ }
+ })
+ }
+}
diff --git a/tests/lib/rules/v-slot-style.js b/tests/lib/rules/v-slot-style.js
new file mode 100644
index 000000000..2a9ccfb67
--- /dev/null
+++ b/tests/lib/rules/v-slot-style.js
@@ -0,0 +1,443 @@
+/**
+ * @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/v-slot-style')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+ parser: 'vue-eslint-parser',
+ parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('v-slot-style', rule, {
+ valid: [
+ `
+
+
+
+
+
+
+ `,
+ {
+ code: `
+
+
+
+
+
+
+
+
+ `,
+ options: ['shorthand']
+ },
+ {
+ code: `
+
+
+
+
+
+
+
+
+ `,
+ options: ['longform']
+ },
+
+ {
+ code: `
+
+
+
+
+
+
+
+
+ `,
+ options: [{ atComponent: 'shorthand' }]
+ },
+ {
+ code: `
+
+
+
+
+
+
+
+
+ `,
+ options: [{ atComponent: 'longform' }]
+ },
+ {
+ code: `
+
+
+
+
+
+
+
+
+ `,
+ options: [{ atComponent: 'v-slot' }]
+ },
+
+ {
+ code: `
+
+
+
+
+
+
+
+
+ `,
+ options: [{ default: 'shorthand' }]
+ },
+ {
+ code: `
+
+
+
+
+
+
+
+
+ `,
+ options: [{ default: 'longform' }]
+ },
+ {
+ code: `
+
+
+
+
+
+
+
+
+ `,
+ options: [{ default: 'v-slot' }]
+ },
+
+ {
+ code: `
+
+
+
+
+
+
+
+
+ `,
+ options: [{ named: 'shorthand' }]
+ },
+ {
+ code: `
+
+
+
+
+
+
+
+
+ `,
+ options: [{ named: 'longform' }]
+ },
+ {
+ code: `
+
+
+
+
+
+
+
+ `,
+ options: [{ named: 'longform' }]
+ }
+ ],
+
+ invalid: [
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: [{ messageId: 'expectedVSlot', data: { actual: '#default', argument: 'default' }}]
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: [{ messageId: 'expectedVSlot', data: { actual: 'v-slot:default', argument: 'default' }}]
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: [{ messageId: 'expectedShorthand', data: { actual: 'v-slot:default', argument: 'default' }}],
+ options: [{ atComponent: 'shorthand' }]
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: [{ messageId: 'expectedShorthand', data: { actual: 'v-slot', argument: 'default' }}],
+ options: [{ atComponent: 'shorthand' }]
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: [{ messageId: 'expectedLongform', data: { actual: '#default', argument: 'default' }}],
+ options: [{ atComponent: 'longform' }]
+ },
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+ `,
+ errors: [{ messageId: 'expectedLongform', data: { actual: 'v-slot', argument: 'default' }}],
+ options: [{ atComponent: 'longform' }]
+ },
+
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'expectedShorthand', data: { actual: 'v-slot', argument: 'default' }}]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'expectedShorthand', data: { actual: 'v-slot:default', argument: 'default' }}]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'expectedLongform', data: { actual: '#default', argument: 'default' }}],
+ options: [{ default: 'longform' }]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'expectedLongform', data: { actual: 'v-slot', argument: 'default' }}],
+ options: [{ default: 'longform' }]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'expectedVSlot', data: { actual: '#default', argument: 'default' }}],
+ options: [{ default: 'v-slot' }]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'expectedVSlot', data: { actual: 'v-slot:default', argument: 'default' }}],
+ options: [{ default: 'v-slot' }]
+ },
+
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'expectedShorthand', data: { actual: 'v-slot:foo', argument: 'foo' }}]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'expectedLongform', data: { actual: '#foo', argument: 'foo' }}],
+ options: [{ named: 'longform' }]
+ },
+
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'expectedShorthand', data: { actual: 'v-slot:[foo]', argument: '[foo]' }}]
+ },
+ {
+ code: `
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [{ messageId: 'expectedLongform', data: { actual: '#[foo]', argument: '[foo]' }}],
+ options: [{ named: 'longform' }]
+ }
+ ]
+})