Skip to content

Commit 7dee01d

Browse files
authored
Add vue/no-useless-mustaches rule (#1187)
* Add `vue/no-useless-mustaches` rule * Add testcases
1 parent e644855 commit 7dee01d

File tree

5 files changed

+473
-0
lines changed

5 files changed

+473
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ For example:
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 | |
295295
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
296+
| [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
296297
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
297298
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
298299
| [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` | |

docs/rules/no-useless-mustaches.md

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

lib/rules/no-useless-mustaches.js

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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+
/**
10+
* @typedef {import('eslint').Rule.RuleContext} RuleContext
11+
* @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer
12+
*/
13+
14+
/**
15+
* Strip quotes string
16+
* @param {string} text
17+
* @returns {string}
18+
*/
19+
function stripQuotesForHTML(text) {
20+
if (
21+
(text[0] === '"' || text[0] === "'" || text[0] === '`') &&
22+
text[0] === text[text.length - 1]
23+
) {
24+
return text.slice(1, -1)
25+
}
26+
27+
const re = /^(?:&(?:quot|apos|#\d+|#x[\da-f]+);|["'`])([\s\S]*)(?:&(?:quot|apos|#\d+|#x[\da-f]+);|["'`])$/u.exec(
28+
text
29+
)
30+
if (!re) {
31+
return null
32+
}
33+
return re[1]
34+
}
35+
36+
module.exports = {
37+
meta: {
38+
docs: {
39+
description: 'disallow unnecessary mustache interpolations',
40+
categories: undefined,
41+
url: 'https://eslint.vuejs.org/rules/no-useless-mustaches.html'
42+
},
43+
fixable: 'code',
44+
messages: {
45+
unexpected:
46+
'Unexpected mustache interpolation with a string literal value.'
47+
},
48+
schema: [
49+
{
50+
type: 'object',
51+
properties: {
52+
ignoreIncludesComment: {
53+
type: 'boolean'
54+
},
55+
ignoreStringEscape: {
56+
type: 'boolean'
57+
}
58+
}
59+
}
60+
],
61+
type: 'suggestion'
62+
},
63+
/** @param {RuleContext} context */
64+
create(context) {
65+
const opts = context.options[0] || {}
66+
const ignoreIncludesComment = opts.ignoreIncludesComment
67+
const ignoreStringEscape = opts.ignoreStringEscape
68+
const sourceCode = context.getSourceCode()
69+
70+
/**
71+
* Report if the value expression is string literals
72+
* @param {VExpressionContainer} node the node to check
73+
*/
74+
function verify(node) {
75+
const { expression } = node
76+
if (!expression) {
77+
return
78+
}
79+
let strValue, rawValue
80+
if (expression.type === 'Literal') {
81+
if (typeof expression.value !== 'string') {
82+
return
83+
}
84+
strValue = expression.value
85+
rawValue = expression.raw.slice(1, -1)
86+
} else if (expression.type === 'TemplateLiteral') {
87+
if (expression.expressions.length > 0) {
88+
return
89+
}
90+
strValue = expression.quasis[0].value.cooked
91+
rawValue = expression.quasis[0].value.raw
92+
} else {
93+
return
94+
}
95+
96+
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
97+
const hasComment = tokenStore
98+
.getTokens(node, { includeComments: true })
99+
.some((t) => t.type === 'Block' || t.type === 'Line')
100+
if (ignoreIncludesComment && hasComment) {
101+
return
102+
}
103+
104+
let hasEscape = false
105+
if (rawValue !== strValue) {
106+
// check escapes
107+
const chars = [...rawValue]
108+
let c = chars.shift()
109+
while (c) {
110+
if (c === '\\') {
111+
c = chars.shift()
112+
if (
113+
c == null ||
114+
// ignore "\\", '"', "'", "`" and "$"
115+
'nrvtbfux'.includes(c)
116+
) {
117+
// has useful escape.
118+
hasEscape = true
119+
break
120+
}
121+
}
122+
c = chars.shift()
123+
}
124+
}
125+
if (ignoreStringEscape && hasEscape) {
126+
return
127+
}
128+
129+
context.report({
130+
// @ts-ignore
131+
node,
132+
messageId: 'unexpected',
133+
fix(fixer) {
134+
if (hasComment || hasEscape) {
135+
// cannot fix
136+
return null
137+
}
138+
context.parserServices.getDocumentFragment()
139+
const text = stripQuotesForHTML(sourceCode.getText(expression))
140+
if (text == null) {
141+
// unknowns
142+
return null
143+
}
144+
if (text.includes('\n') || /^\s|\s$/u.test(text)) {
145+
// It doesn't autofix because another rule like indent or eol space might remove spaces.
146+
return null
147+
}
148+
149+
return [fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1'))]
150+
}
151+
})
152+
}
153+
154+
return utils.defineTemplateBodyVisitor(context, {
155+
'VElement > VExpressionContainer': verify
156+
})
157+
}
158+
}

0 commit comments

Comments
 (0)