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 + +``` + + + +## :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 + +``` + + + +### `"ignoreStringEscape": true` + + + +```vue + +``` + + + +## :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: [ + ` + `, + { + code: ` + + `, + options: [{ ignoreIncludesComment: true }] + }, + { + code: ` + `, + options: [{ ignoreStringEscape: true }] + } + ], + invalid: [ + { + code: ` + `, + output: ` + `, + errors: [ + { + message: + 'Unexpected mustache interpolation with a string literal value.', + line: 3, + column: 9, + endLine: 3 + } + ] + }, + { + code: ` + + `, + output: null, + errors: [ + 'Unexpected mustache interpolation with a string literal value.', + 'Unexpected mustache interpolation with a string literal value.' + ] + }, + { + code: ` + `, + output: null, + errors: [ + 'Unexpected mustache interpolation with a string literal value.', + 'Unexpected mustache interpolation with a string literal value.' + ] + }, + { + code: ` + `, + output: ` + `, + 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: ` + `, + output: ` + `, + 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: ` + `, + output: ` + `, + 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: ` + + `, + output: null, + errors: ['Unexpected mustache interpolation with a string literal value.'] + }, + { + code: ` + + `, + 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.' + ] + } + ] +})