Skip to content

Commit

Permalink
New: v-slot-style rule (fixes #801) (#836)
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea authored Mar 4, 2019
1 parent 263076a commit b7ee76e
Show file tree
Hide file tree
Showing 3 changed files with 699 additions and 0 deletions.
110 changes: 110 additions & 0 deletions docs/rules/v-slot-style.md
Original file line number Diff line number Diff line change
@@ -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.

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

```vue
<template>
<!-- ✓ GOOD -->
<my-component v-slot="data">
{{data}}
</my-component>
<my-component>
<template #default>content</template>
<template #one>content</template>
<template #two>content</template>
</my-component>
<!-- ✗ BAD -->
<my-component #default="data">
{{data}}
</my-component>
<my-component>
<template v-slot>content</template>
<template v-slot:one>content</template>
<template v-slot:two>content</template>
</my-component>
</template>
```

</eslint-code-block>

## :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. `<my-component v-slot="">`).
| `default` | `"shorthand"` \| `"longform"` \| `"v-slot"` | `"shorthand"` | The style for the default slot at template wrappers (E.g. `<template #default="">`).
| `named` | `"shorthand"` \| `"longform"` | `"shorthand"` | The style for named slots (E.g. `<template #named="">`).

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"`

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

```vue
<template>
<!-- ✓ GOOD -->
<my-component v-slot:default="data">
{{data}}
</my-component>
<my-component>
<template v-slot:default>content</template>
<template v-slot:one>content</template>
<template v-slot:two>content</template>
</my-component>
<!-- ✗ BAD -->
<my-component v-slot="data">
{{data}}
</my-component>
<my-component>
<template #default>content</template>
<template #one>content</template>
<template #two>content</template>
</my-component>
</template>
```

</eslint-code-block>

## :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)
146 changes: 146 additions & 0 deletions lib/rules/v-slot-style.js
Original file line number Diff line number Diff line change
@@ -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
}
}
})
}
})
}
}
Loading

0 comments on commit b7ee76e

Please sign in to comment.