diff --git a/docs/rules/README.md b/docs/rules/README.md index 55b3c4e0d..1e1a92a0d 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -292,6 +292,7 @@ For example: | [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | | | [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/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-v-bind.md b/docs/rules/no-useless-v-bind.md new file mode 100644 index 000000000..d202104e2 --- /dev/null +++ b/docs/rules/no-useless-v-bind.md @@ -0,0 +1,87 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-useless-v-bind +description: disallow unnecessary `v-bind` directives +--- +# vue/no-useless-v-bind +> disallow unnecessary `v-bind` directives + +- :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 `v-bind` with a string literal value. +The `v-bind` with a string literal value can be changed to a static attribute definition. + + + +```vue + +``` + + + +## :wrench: Options + +```js +{ + "vue/no-useless-v-bind": ["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-mustaches] +- [vue/no-useless-concat] + +[vue/no-useless-mustaches]: ./no-useless-mustaches.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-v-bind.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-useless-v-bind.js) diff --git a/lib/index.js b/lib/index.js index 942e564f0..6504e05d0 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-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'), 'no-watch-after-await': require('./rules/no-watch-after-await'), diff --git a/lib/rules/no-useless-v-bind.js b/lib/rules/no-useless-v-bind.js new file mode 100644 index 000000000..2ffa71400 --- /dev/null +++ b/lib/rules/no-useless-v-bind.js @@ -0,0 +1,153 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') + +const DOUBLE_QUOTES_RE = /"/gu +const SINGLE_QUOTES_RE = /'/gu + +/** + * @typedef {import('eslint').Rule.RuleContext} RuleContext + * @typedef {import('vue-eslint-parser').AST.VDirective} VDirective + */ + +module.exports = { + meta: { + docs: { + description: 'disallow unnecessary `v-bind` directives', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/no-useless-v-bind.html' + }, + fixable: 'code', + messages: { + unexpected: 'Unexpected `v-bind` 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 {VDirective} node the node to check + */ + function verify(node) { + if (!node.value || node.key.modifiers.length) { + return + } + const { expression } = node.value + 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.value, { 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 + } + const text = sourceCode.getText(node.value) + const quoteChar = text[0] + + const shorthand = node.key.name.rawName === ':' + /** @type { [number, number] } */ + const keyDirectiveRange = [ + node.key.name.range[0], + node.key.name.range[1] + (shorthand ? 0 : 1) + ] + + let attrValue + if (quoteChar === '"') { + attrValue = strValue.replace(DOUBLE_QUOTES_RE, '"') + } else if (quoteChar === "'") { + attrValue = strValue.replace(SINGLE_QUOTES_RE, ''') + } else { + attrValue = strValue + .replace(DOUBLE_QUOTES_RE, '"') + .replace(SINGLE_QUOTES_RE, ''') + } + return [ + fixer.removeRange(keyDirectiveRange), + fixer.replaceText(expression, attrValue) + ] + } + }) + } + + return utils.defineTemplateBodyVisitor(context, { + "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]": verify + }) + } +} diff --git a/tests/lib/rules/no-useless-v-bind.js b/tests/lib/rules/no-useless-v-bind.js new file mode 100644 index 000000000..8362aa0b9 --- /dev/null +++ b/tests/lib/rules/no-useless-v-bind.js @@ -0,0 +1,149 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-useless-v-bind.js') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('no-useless-v-bind', rule, { + valid: [ + ` + `, + { + code: ` + + `, + options: [{ ignoreIncludesComment: true }] + }, + { + code: ` + `, + options: [{ ignoreStringEscape: true }] + } + ], + invalid: [ + { + code: ` + `, + output: ` + `, + errors: [ + { + message: 'Unexpected `v-bind` with a string literal value.', + line: 3, + column: 14, + endLine: 3, + endColumn: 25 + }, + { + message: 'Unexpected `v-bind` with a string literal value.', + line: 4, + column: 14, + endLine: 4, + endColumn: 31 + } + ] + }, + { + code: ` + + `, + output: null, + errors: [ + 'Unexpected `v-bind` with a string literal value.', + 'Unexpected `v-bind` with a string literal value.' + ] + }, + { + code: ` + `, + output: null, + errors: [ + 'Unexpected `v-bind` with a string literal value.', + 'Unexpected `v-bind` with a string literal value.' + ] + }, + { + code: ` + `, + output: ` + `, + errors: [ + 'Unexpected `v-bind` with a string literal value.', + 'Unexpected `v-bind` with a string literal value.', + 'Unexpected `v-bind` with a string literal value.', + 'Unexpected `v-bind` with a string literal value.', + 'Unexpected `v-bind` with a string literal value.', + 'Unexpected `v-bind` with a string literal value.', + 'Unexpected `v-bind` with a string literal value.' + ] + } + ] +})