Skip to content

Commit cc8450d

Browse files
authored
Add vue/no-lone-template rule (#1238)
1 parent c37f953 commit cc8450d

File tree

7 files changed

+397
-0
lines changed

7 files changed

+397
-0
lines changed

Diff for: docs/rules/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
148148
|:--------|:------------|:---|
149149
| [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
150150
| [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements | |
151+
| [vue/no-lone-template](./no-lone-template.md) | disallow unnecessary `<template>` | |
151152
| [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots | |
152153
| [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack | |
153154
| [vue/order-in-components](./order-in-components.md) | enforce order of properties in components | :wrench: |
@@ -256,6 +257,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
256257
|:--------|:------------|:---|
257258
| [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
258259
| [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements | |
260+
| [vue/no-lone-template](./no-lone-template.md) | disallow unnecessary `<template>` | |
259261
| [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots | |
260262
| [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack | |
261263
| [vue/order-in-components](./order-in-components.md) | enforce order of properties in components | :wrench: |

Diff for: docs/rules/no-lone-template.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-lone-template
5+
description: disallow unnecessary `<template>`
6+
---
7+
# vue/no-lone-template
8+
> disallow unnecessary `<template>`
9+
10+
- :gear: This rule is included in `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
11+
12+
## :book: Rule Details
13+
14+
This rule aims to eliminate unnecessary and potentially confusing `<template>`.
15+
In Vue.js 2.x, the `<template>` elements that have no specific directives have no effect.
16+
In Vue.js 3.x, the `<template>` elements that have no specific directives render the `<template>` elements as is, but in most cases this may not be what you intended.
17+
18+
<eslint-code-block :rules="{'vue/no-lone-template': ['error']}">
19+
20+
```vue
21+
<template>
22+
<!-- ✓ GOOD -->
23+
<template v-if="foo">...</template>
24+
<template v-else-if="bar">...</template>
25+
<template v-else>...</template>
26+
<template v-for="e in list">...</template>
27+
<template v-slot>...</template>
28+
29+
<!-- ✗ BAD -->
30+
<template>...</template>
31+
<template/>
32+
</template>
33+
```
34+
35+
</eslint-code-block>
36+
37+
## :wrench: Options
38+
39+
```json
40+
{
41+
"vue/no-lone-template": ["error", {
42+
"ignoreAccessible": false
43+
}]
44+
}
45+
```
46+
47+
- `ignoreAccessible` ... If `true`, ignore accessible `<template>` elements. default `false`.
48+
Note: this option is useless if you are using Vue.js 2.x.
49+
50+
### `"ignoreAccessible": true`
51+
52+
<eslint-code-block :rules="{'vue/no-lone-template': ['error', { 'ignoreAccessible': true }]}">
53+
54+
```vue
55+
<template>
56+
<!-- ✓ GOOD -->
57+
<template ref="foo">...</template>
58+
<template id="bar">...</template>
59+
60+
<!-- ✗ BAD -->
61+
<template class="baz">...</template>
62+
</template>
63+
```
64+
65+
</eslint-code-block>
66+
67+
## :mute: When Not To Use It
68+
69+
If you are using Vue.js 3.x and want to define the `<template>` element intentionally, you will have to turn this rule off or use `"ignoreAccessible"` option.
70+
71+
## :couple: Related rules
72+
73+
- [vue/no-template-key]
74+
- [no-lone-blocks]
75+
76+
[no-lone-blocks]: https://eslint.org/docs/rules/no-lone-blocks
77+
[vue/no-template-key]: ./no-template-key.md
78+
79+
## :mag: Implementation
80+
81+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-lone-template.js)
82+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-lone-template.js)

Diff for: lib/configs/recommended.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module.exports = {
88
rules: {
99
'vue/attributes-order': 'warn',
1010
'vue/component-tags-order': 'warn',
11+
'vue/no-lone-template': 'warn',
1112
'vue/no-multiple-slot-args': 'warn',
1213
'vue/no-v-html': 'warn',
1314
'vue/order-in-components': 'warn',

Diff for: lib/configs/vue3-recommended.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module.exports = {
88
rules: {
99
'vue/attributes-order': 'warn',
1010
'vue/component-tags-order': 'warn',
11+
'vue/no-lone-template': 'warn',
1112
'vue/no-multiple-slot-args': 'warn',
1213
'vue/no-v-html': 'warn',
1314
'vue/order-in-components': 'warn',

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ module.exports = {
7474
'no-extra-parens': require('./rules/no-extra-parens'),
7575
'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
7676
'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'),
77+
'no-lone-template': require('./rules/no-lone-template'),
7778
'no-multi-spaces': require('./rules/no-multi-spaces'),
7879
'no-multiple-objects-in-class': require('./rules/no-multiple-objects-in-class'),
7980
'no-multiple-slot-args': require('./rules/no-multiple-slot-args'),

Diff for: lib/rules/no-lone-template.js

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// ------------------------------------------------------------------------------
14+
// Helpers
15+
// ------------------------------------------------------------------------------
16+
17+
// https://github.com/vuejs/vue-next/blob/64e2f4643602c5980361e66674141e61ba60ef70/packages/compiler-core/src/parse.ts#L405
18+
const SPECIAL_TEMPLATE_DIRECTIVES = new Set([
19+
'if',
20+
'else',
21+
'else-if',
22+
'for',
23+
'slot'
24+
])
25+
26+
// ------------------------------------------------------------------------------
27+
// Rule Definition
28+
// ------------------------------------------------------------------------------
29+
30+
module.exports = {
31+
meta: {
32+
type: 'problem',
33+
docs: {
34+
description: 'disallow unnecessary `<template>`',
35+
categories: ['vue3-recommended', 'recommended'],
36+
url: 'https://eslint.vuejs.org/rules/no-lone-template.html'
37+
},
38+
fixable: null,
39+
schema: [
40+
{
41+
type: 'object',
42+
properties: {
43+
ignoreAccessible: {
44+
type: 'boolean'
45+
}
46+
},
47+
additionalProperties: false
48+
}
49+
],
50+
messages: {
51+
requireDirective: '`<template>` require directive.'
52+
}
53+
},
54+
/** @param {RuleContext} context */
55+
create(context) {
56+
const options = context.options[0] || {}
57+
const ignoreAccessible = options.ignoreAccessible === true
58+
59+
/**
60+
* @param {VAttribute | VDirective} attr
61+
*/
62+
function getKeyName(attr) {
63+
if (attr.directive) {
64+
if (attr.key.name.name !== 'bind') {
65+
// no v-bind
66+
return null
67+
}
68+
if (
69+
!attr.key.argument ||
70+
attr.key.argument.type === 'VExpressionContainer'
71+
) {
72+
// unknown
73+
return null
74+
}
75+
return attr.key.argument.name
76+
}
77+
return attr.key.name
78+
}
79+
80+
return utils.defineTemplateBodyVisitor(context, {
81+
/** @param {VStartTag} node */
82+
"VElement[name='template'][parent.type='VElement'] > VStartTag"(node) {
83+
if (
84+
node.attributes.some((attr) => {
85+
if (attr.directive) {
86+
const directiveName = attr.key.name.name
87+
if (SPECIAL_TEMPLATE_DIRECTIVES.has(directiveName)) {
88+
return true
89+
}
90+
if (directiveName === 'slot-scope') {
91+
// `slot-scope` is deprecated in Vue.js 2.6
92+
return true
93+
}
94+
if (directiveName === 'scope') {
95+
// `scope` is deprecated in Vue.js 2.5
96+
return true
97+
}
98+
}
99+
100+
const keyName = getKeyName(attr)
101+
if (keyName === 'slot') {
102+
// `slot` is deprecated in Vue.js 2.6
103+
return true
104+
}
105+
106+
return false
107+
})
108+
) {
109+
return
110+
}
111+
112+
if (
113+
ignoreAccessible &&
114+
node.attributes.some((attr) => {
115+
const keyName = getKeyName(attr)
116+
return keyName === 'id' || keyName === 'ref'
117+
})
118+
) {
119+
return
120+
}
121+
122+
context.report({
123+
node,
124+
messageId: 'requireDirective'
125+
})
126+
}
127+
})
128+
}
129+
}

0 commit comments

Comments
 (0)