Skip to content

Commit

Permalink
Add vue/no-useless-mustaches rule (#1187)
Browse files Browse the repository at this point in the history
* Add `vue/no-useless-mustaches` rule

* Add testcases
  • Loading branch information
ota-meshi committed Jun 5, 2020
1 parent e644855 commit 7dee01d
Show file tree
Hide file tree
Showing 5 changed files with 473 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -293,6 +293,7 @@ For example:
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
| [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties | |
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
| [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
| [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` | |
Expand Down
88 changes: 88 additions & 0 deletions docs/rules/no-useless-mustaches.md
@@ -0,0 +1,88 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-useless-mustaches
description: disallow unnecessary mustache interpolations
---
# vue/no-useless-mustaches
> disallow unnecessary mustache interpolations
- :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 reports mustache interpolation with a string literal value.
The mustache interpolation with a string literal value can be changed to a static contents.

<eslint-code-block fix :rules="{'vue/no-useless-mustaches': ['error']}">

```vue
<template>
<!-- ✓ GOOD -->
Lorem ipsum
{{ foo }}
<!-- ✗ BAD -->
{{ 'Lorem ipsum' }}
{{ "Lorem ipsum" }}
{{ `Lorem ipsum` }}
</template>
```

</eslint-code-block>

## :wrench: Options

```js
{
"vue/no-useless-mustaches": ["error", {
"ignoreIncludesComment": false,
"ignoreStringEscape": false
}]
}
```

- `ignoreIncludesComment` ... If `true`, do not report expressions containing comments. default `false`.
- `ignoreStringEscape` ... If `true`, do not report string literals with useful escapes. default `false`.

### `"ignoreIncludesComment": true`

<eslint-code-block fix :rules="{'vue/no-useless-mustaches': ['error', {ignoreIncludesComment: true}]}">

```vue
<template>
<!-- ✓ GOOD -->
{{ 'Lorem ipsum'/* comment */ }}
<!-- ✗ BAD -->
{{ 'Lorem ipsum' }}
</template>
```

</eslint-code-block>

### `"ignoreStringEscape": true`

<eslint-code-block fix :rules="{'vue/no-useless-mustaches': ['error', {ignoreStringEscape: true}]}">

```vue
<template>
<!-- ✓ GOOD -->
{{ 'Lorem \n ipsum' }}
</template>
```

</eslint-code-block>

## :couple: Related rules

- [vue/no-useless-v-bind]
- [vue/no-useless-concat]

[vue/no-useless-v-bind]: ./no-useless-v-bind.md
[vue/no-useless-concat]: ./no-useless-concat.md

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-useless-mustaches.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-useless-mustaches.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -97,6 +97,7 @@ module.exports = {
'no-unused-vars': require('./rules/no-unused-vars'),
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
'no-useless-concat': require('./rules/no-useless-concat'),
'no-useless-mustaches': require('./rules/no-useless-mustaches'),
'no-useless-v-bind': require('./rules/no-useless-v-bind'),
'no-v-html': require('./rules/no-v-html'),
'no-v-model-argument': require('./rules/no-v-model-argument'),
Expand Down
158 changes: 158 additions & 0 deletions lib/rules/no-useless-mustaches.js
@@ -0,0 +1,158 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

const utils = require('../utils')

/**
* @typedef {import('eslint').Rule.RuleContext} RuleContext
* @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer
*/

/**
* Strip quotes string
* @param {string} text
* @returns {string}
*/
function stripQuotesForHTML(text) {
if (
(text[0] === '"' || text[0] === "'" || text[0] === '`') &&
text[0] === text[text.length - 1]
) {
return text.slice(1, -1)
}

const re = /^(?:&(?:quot|apos|#\d+|#x[\da-f]+);|["'`])([\s\S]*)(?:&(?:quot|apos|#\d+|#x[\da-f]+);|["'`])$/u.exec(
text
)
if (!re) {
return null
}
return re[1]
}

module.exports = {
meta: {
docs: {
description: 'disallow unnecessary mustache interpolations',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-useless-mustaches.html'
},
fixable: 'code',
messages: {
unexpected:
'Unexpected mustache interpolation with a string literal value.'
},
schema: [
{
type: 'object',
properties: {
ignoreIncludesComment: {
type: 'boolean'
},
ignoreStringEscape: {
type: 'boolean'
}
}
}
],
type: 'suggestion'
},
/** @param {RuleContext} context */
create(context) {
const opts = context.options[0] || {}
const ignoreIncludesComment = opts.ignoreIncludesComment
const ignoreStringEscape = opts.ignoreStringEscape
const sourceCode = context.getSourceCode()

/**
* Report if the value expression is string literals
* @param {VExpressionContainer} node the node to check
*/
function verify(node) {
const { expression } = node
if (!expression) {
return
}
let strValue, rawValue
if (expression.type === 'Literal') {
if (typeof expression.value !== 'string') {
return
}
strValue = expression.value
rawValue = expression.raw.slice(1, -1)
} else if (expression.type === 'TemplateLiteral') {
if (expression.expressions.length > 0) {
return
}
strValue = expression.quasis[0].value.cooked
rawValue = expression.quasis[0].value.raw
} else {
return
}

const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const hasComment = tokenStore
.getTokens(node, { includeComments: true })
.some((t) => t.type === 'Block' || t.type === 'Line')
if (ignoreIncludesComment && hasComment) {
return
}

let hasEscape = false
if (rawValue !== strValue) {
// check escapes
const chars = [...rawValue]
let c = chars.shift()
while (c) {
if (c === '\\') {
c = chars.shift()
if (
c == null ||
// ignore "\\", '"', "'", "`" and "$"
'nrvtbfux'.includes(c)
) {
// has useful escape.
hasEscape = true
break
}
}
c = chars.shift()
}
}
if (ignoreStringEscape && hasEscape) {
return
}

context.report({
// @ts-ignore
node,
messageId: 'unexpected',
fix(fixer) {
if (hasComment || hasEscape) {
// cannot fix
return null
}
context.parserServices.getDocumentFragment()
const text = stripQuotesForHTML(sourceCode.getText(expression))
if (text == null) {
// unknowns
return null
}
if (text.includes('\n') || /^\s|\s$/u.test(text)) {
// It doesn't autofix because another rule like indent or eol space might remove spaces.
return null
}

return [fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1'))]
}
})
}

return utils.defineTemplateBodyVisitor(context, {
'VElement > VExpressionContainer': verify
})
}
}

0 comments on commit 7dee01d

Please sign in to comment.