Skip to content

Commit e644855

Browse files
authored
Add vue/no-useless-v-bind rule (#1186)
1 parent e5c835e commit e644855

File tree

5 files changed

+391
-0
lines changed

5 files changed

+391
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ For example:
292292
| [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | |
293293
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
294294
| [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties | |
295+
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
295296
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
296297
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
297298
| [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` | |

docs/rules/no-useless-v-bind.md

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-useless-v-bind
5+
description: disallow unnecessary `v-bind` directives
6+
---
7+
# vue/no-useless-v-bind
8+
> disallow unnecessary `v-bind` directives
9+
10+
- :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.
11+
12+
## :book: Rule Details
13+
14+
This rule reports `v-bind` with a string literal value.
15+
The `v-bind` with a string literal value can be changed to a static attribute definition.
16+
17+
<eslint-code-block fix :rules="{'vue/no-useless-v-bind': ['error']}">
18+
19+
```vue
20+
<template>
21+
<!-- ✓ GOOD -->
22+
<div foo="bar"/>
23+
<div :foo="bar"/>
24+
25+
<!-- ✗ BAD -->
26+
<div v-bind:foo="'bar'"/>
27+
<div :foo="'bar'"/>
28+
</template>
29+
```
30+
31+
</eslint-code-block>
32+
33+
## :wrench: Options
34+
35+
```js
36+
{
37+
"vue/no-useless-v-bind": ["error", {
38+
"ignoreIncludesComment": false,
39+
"ignoreStringEscape": false
40+
}]
41+
}
42+
```
43+
44+
- `ignoreIncludesComment` ... If `true`, do not report expressions containing comments. default `false`.
45+
- `ignoreStringEscape` ... If `true`, do not report string literals with useful escapes. default `false`.
46+
47+
### `"ignoreIncludesComment": true`
48+
49+
<eslint-code-block fix :rules="{'vue/no-useless-v-bind': ['error', {ignoreIncludesComment: true}]}">
50+
51+
```vue
52+
<template>
53+
<!-- ✓ GOOD -->
54+
<div v-bind:foo="'bar'/* comment */"/>
55+
56+
<!-- ✗ BAD -->
57+
<div v-bind:foo="'bar'"/>
58+
</template>
59+
```
60+
61+
</eslint-code-block>
62+
63+
### `"ignoreStringEscape": true`
64+
65+
<eslint-code-block fix :rules="{'vue/no-useless-v-bind': ['error', {ignoreStringEscape: true}]}">
66+
67+
```vue
68+
<template>
69+
<!-- ✓ GOOD -->
70+
<div v-bind:foo="'bar\nbaz'"/>
71+
</template>
72+
```
73+
74+
</eslint-code-block>
75+
76+
## :couple: Related rules
77+
78+
- [vue/no-useless-mustaches]
79+
- [vue/no-useless-concat]
80+
81+
[vue/no-useless-mustaches]: ./no-useless-mustaches.md
82+
[vue/no-useless-concat]: ./no-useless-concat.md
83+
84+
## :mag: Implementation
85+
86+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-useless-v-bind.js)
87+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-useless-v-bind.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ module.exports = {
9797
'no-unused-vars': require('./rules/no-unused-vars'),
9898
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
9999
'no-useless-concat': require('./rules/no-useless-concat'),
100+
'no-useless-v-bind': require('./rules/no-useless-v-bind'),
100101
'no-v-html': require('./rules/no-v-html'),
101102
'no-v-model-argument': require('./rules/no-v-model-argument'),
102103
'no-watch-after-await': require('./rules/no-watch-after-await'),

lib/rules/no-useless-v-bind.js

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
const DOUBLE_QUOTES_RE = /"/gu
10+
const SINGLE_QUOTES_RE = /'/gu
11+
12+
/**
13+
* @typedef {import('eslint').Rule.RuleContext} RuleContext
14+
* @typedef {import('vue-eslint-parser').AST.VDirective} VDirective
15+
*/
16+
17+
module.exports = {
18+
meta: {
19+
docs: {
20+
description: 'disallow unnecessary `v-bind` directives',
21+
categories: undefined,
22+
url: 'https://eslint.vuejs.org/rules/no-useless-v-bind.html'
23+
},
24+
fixable: 'code',
25+
messages: {
26+
unexpected: 'Unexpected `v-bind` with a string literal value.'
27+
},
28+
schema: [
29+
{
30+
type: 'object',
31+
properties: {
32+
ignoreIncludesComment: {
33+
type: 'boolean'
34+
},
35+
ignoreStringEscape: {
36+
type: 'boolean'
37+
}
38+
}
39+
}
40+
],
41+
type: 'suggestion'
42+
},
43+
/** @param {RuleContext} context */
44+
create(context) {
45+
const opts = context.options[0] || {}
46+
const ignoreIncludesComment = opts.ignoreIncludesComment
47+
const ignoreStringEscape = opts.ignoreStringEscape
48+
const sourceCode = context.getSourceCode()
49+
50+
/**
51+
* Report if the value expression is string literals
52+
* @param {VDirective} node the node to check
53+
*/
54+
function verify(node) {
55+
if (!node.value || node.key.modifiers.length) {
56+
return
57+
}
58+
const { expression } = node.value
59+
if (!expression) {
60+
return
61+
}
62+
let strValue, rawValue
63+
if (expression.type === 'Literal') {
64+
if (typeof expression.value !== 'string') {
65+
return
66+
}
67+
strValue = expression.value
68+
rawValue = expression.raw.slice(1, -1)
69+
} else if (expression.type === 'TemplateLiteral') {
70+
if (expression.expressions.length > 0) {
71+
return
72+
}
73+
strValue = expression.quasis[0].value.cooked
74+
rawValue = expression.quasis[0].value.raw
75+
} else {
76+
return
77+
}
78+
79+
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
80+
const hasComment = tokenStore
81+
.getTokens(node.value, { includeComments: true })
82+
.some((t) => t.type === 'Block' || t.type === 'Line')
83+
if (ignoreIncludesComment && hasComment) {
84+
return
85+
}
86+
87+
let hasEscape = false
88+
if (rawValue !== strValue) {
89+
// check escapes
90+
const chars = [...rawValue]
91+
let c = chars.shift()
92+
while (c) {
93+
if (c === '\\') {
94+
c = chars.shift()
95+
if (
96+
c == null ||
97+
// ignore "\\", '"', "'", "`" and "$"
98+
'nrvtbfux'.includes(c)
99+
) {
100+
// has useful escape.
101+
hasEscape = true
102+
break
103+
}
104+
}
105+
c = chars.shift()
106+
}
107+
}
108+
if (ignoreStringEscape && hasEscape) {
109+
return
110+
}
111+
112+
context.report({
113+
// @ts-ignore
114+
node,
115+
messageId: 'unexpected',
116+
fix(fixer) {
117+
if (hasComment || hasEscape) {
118+
// cannot fix
119+
return null
120+
}
121+
const text = sourceCode.getText(node.value)
122+
const quoteChar = text[0]
123+
124+
const shorthand = node.key.name.rawName === ':'
125+
/** @type { [number, number] } */
126+
const keyDirectiveRange = [
127+
node.key.name.range[0],
128+
node.key.name.range[1] + (shorthand ? 0 : 1)
129+
]
130+
131+
let attrValue
132+
if (quoteChar === '"') {
133+
attrValue = strValue.replace(DOUBLE_QUOTES_RE, '&quot;')
134+
} else if (quoteChar === "'") {
135+
attrValue = strValue.replace(SINGLE_QUOTES_RE, '&apos;')
136+
} else {
137+
attrValue = strValue
138+
.replace(DOUBLE_QUOTES_RE, '&quot;')
139+
.replace(SINGLE_QUOTES_RE, '&apos;')
140+
}
141+
return [
142+
fixer.removeRange(keyDirectiveRange),
143+
fixer.replaceText(expression, attrValue)
144+
]
145+
}
146+
})
147+
}
148+
149+
return utils.defineTemplateBodyVisitor(context, {
150+
"VAttribute[directive=true][key.name.name='bind'][key.argument!=null]": verify
151+
})
152+
}
153+
}

0 commit comments

Comments
 (0)