diff --git a/docs/rules/README.md b/docs/rules/README.md
index 1e1a92a0d..e44b2855b 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -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()` | |
diff --git a/docs/rules/no-useless-mustaches.md b/docs/rules/no-useless-mustaches.md
new file mode 100644
index 000000000..6c9d2689e
--- /dev/null
+++ b/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.
+
+
+
+```vue
+
+
+ Lorem ipsum
+ {{ foo }}
+
+
+ {{ 'Lorem ipsum' }}
+ {{ "Lorem ipsum" }}
+ {{ `Lorem ipsum` }}
+
+```
+
+
+
+## :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`
+
+
+
+```vue
+
+
+ {{ 'Lorem ipsum'/* comment */ }}
+
+
+ {{ 'Lorem ipsum' }}
+
+```
+
+
+
+### `"ignoreStringEscape": true`
+
+
+
+```vue
+
+
+ {{ 'Lorem \n ipsum' }}
+
+```
+
+
+
+## :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)
diff --git a/lib/index.js b/lib/index.js
index 6504e05d0..04b4af116 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -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'),
diff --git a/lib/rules/no-useless-mustaches.js b/lib/rules/no-useless-mustaches.js
new file mode 100644
index 000000000..c10889a23
--- /dev/null
+++ b/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
+ })
+ }
+}
diff --git a/tests/lib/rules/no-useless-mustaches.js b/tests/lib/rules/no-useless-mustaches.js
new file mode 100644
index 000000000..e12d33c1e
--- /dev/null
+++ b/tests/lib/rules/no-useless-mustaches.js
@@ -0,0 +1,225 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-useless-mustaches.js')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+})
+
+tester.run('no-useless-mustaches', rule, {
+ valid: [
+ `
+
+ foo
+ 'foo'
+ {{ foo }}
+ {{ 'foo'||'bar' }}
+ {{ 1 }}
+ {{ }}
+ {{ . }}
+ {{ null }}
+ `,
+ {
+ code: `
+
+ {{ 'comment'/*comment*/ }}
+ {{ 'comment'//comment
+ " }}
+
+ `,
+ options: [{ ignoreIncludesComment: true }]
+ },
+ {
+ code: `
+
+ {{ '\\n' }}
+ {{ '\\r' }}
+ `,
+ options: [{ ignoreStringEscape: true }]
+ }
+ ],
+ invalid: [
+ {
+ code: `
+
+ {{ 'foo' }}
+ `,
+ output: `
+
+ foo
+ `,
+ errors: [
+ {
+ message:
+ 'Unexpected mustache interpolation with a string literal value.',
+ line: 3,
+ column: 9,
+ endLine: 3
+ }
+ ]
+ },
+ {
+ code: `
+
+ {{ 'comment'/*comment*/ }}
+ {{ 'comment'//comment
+ }}
+
+ `,
+ output: null,
+ errors: [
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.'
+ ]
+ },
+ {
+ code: `
+
+ {{ '\\n' }}
+ {{ '\\r' }}
+ `,
+ output: null,
+ errors: [
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.'
+ ]
+ },
+ {
+ code: `
+
+ {{ '"' }}
+ {{ \`"'\` }}
+ {{ '\\\\' }}
+ {{ '\\\\r' }}
+ {{ '\\' }}
+ {{ \`foo\` }}
+ {{ \`foo\${bar}\` }}
+ {{ "'" }}
+ {{ \`foo\` }}
+ `,
+ output: `
+
+ "
+ "'
+ \\
+ \\r
+ {{ '\\' }}
+ foo
+ {{ \`foo\${bar}\` }}
+ '
+ foo
+ `,
+ errors: [
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.'
+ ]
+ },
+ {
+ code: `
+
+ {{ 'msg' }}
+ {{ "msg" }}
+ {{ 'msg' }}
+ {{ "msg" }}
+ {{ 'msg' }}
+ {{ "msg" }}
+ {{ '<msg>' }}
+ {{ "I'm" }}
+ {{ "no semi" }}
+ `,
+ output: `
+
+ msg
+ msg
+ msg
+ msg
+ msg
+ msg
+ <msg>
+ I'm
+ {{ "no semi" }}
+ `,
+ errors: [
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.'
+ ]
+ },
+ {
+ code: `
+
+ {{ 'I\\'m' }}
+ {{ "\\"Happy\\"" }}
+ {{ \`backtick \\\` and dollar \\$\` }}
+ {{ "\\\\" }}
+ `,
+ output: `
+
+ I'm
+ "Happy"
+ backtick \` and dollar $
+ \\
+ `,
+ errors: [
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.'
+ ]
+ },
+ {
+ code: `
+
+ {{ \`foo
+bar\` }}
+
+ `,
+ output: null,
+ errors: ['Unexpected mustache interpolation with a string literal value.']
+ },
+ {
+ code: `
+
+ {{ 'space ' }}
+ {{ ' space' }}
+ {{ ' space ' }}
+ {{ ' ' }}
+
+ `,
+ output: null,
+ errors: [
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.',
+ 'Unexpected mustache interpolation with a string literal value.'
+ ]
+ }
+ ]
+})