diff --git a/docs/rules/no-unsupported-features.md b/docs/rules/no-unsupported-features.md index 2e7d67501..393a30476 100644 --- a/docs/rules/no-unsupported-features.md +++ b/docs/rules/no-unsupported-features.md @@ -29,6 +29,8 @@ This rule reports unsupported Vue.js syntax on the specified version. - `version` ... The `version` option accepts [the valid version range of `node-semver`](https://github.com/npm/node-semver#range-grammar). Set the version of Vue.js you are using. This option is required. - `ignores` ... You can use this `ignores` option to ignore the given features. The `"ignores"` option accepts an array of the following strings. + - Vue.js 3.4.0+ + - `"v-bind-same-name-shorthand"` ... `v-bind` same-name shorthand. - Vue.js 3.3.0+ - `"define-slots"` ... `defineSlots()` macro. - `"define-options"` ... `defineOptions()` macro. diff --git a/lib/rules/html-quotes.js b/lib/rules/html-quotes.js index d1c2b6ffb..2be837c0d 100644 --- a/lib/rules/html-quotes.js +++ b/lib/rules/html-quotes.js @@ -51,6 +51,11 @@ module.exports = { return } + if (utils.isVBindSameNameShorthand(node)) { + // v-bind same-name shorthand (Vue 3.4+) + return + } + const text = sourceCode.getText(node.value) const firstChar = text[0] diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js index 07123818f..e40d55ebe 100644 --- a/lib/rules/no-unsupported-features.js +++ b/lib/rules/no-unsupported-features.js @@ -35,7 +35,9 @@ const FEATURES = { 'v-bind-attr-modifier': require('./syntaxes/v-bind-attr-modifier'), // Vue.js 3.3.0+ 'define-options': require('./syntaxes/define-options'), - 'define-slots': require('./syntaxes/define-slots') + 'define-slots': require('./syntaxes/define-slots'), + // Vue.js 3.4.0+ + 'v-bind-same-name-shorthand': require('./syntaxes/v-bind-same-name-shorthand') } const SYNTAX_NAMES = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES)) @@ -124,7 +126,10 @@ module.exports = { forbiddenDefineOptions: '`defineOptions()` macros are not supported until Vue.js "3.3.0".', forbiddenDefineSlots: - '`defineSlots()` macros are not supported until Vue.js "3.3.0".' + '`defineSlots()` macros are not supported until Vue.js "3.3.0".', + // Vue.js 3.4.0+ + forbiddenVBindSameNameShorthand: + '`v-bind` same-name shorthand is not supported until Vue.js "3.4.0".' } }, /** @param {RuleContext} context */ diff --git a/lib/rules/syntaxes/v-bind-same-name-shorthand.js b/lib/rules/syntaxes/v-bind-same-name-shorthand.js new file mode 100644 index 000000000..d9e7a388c --- /dev/null +++ b/lib/rules/syntaxes/v-bind-same-name-shorthand.js @@ -0,0 +1,34 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../../utils') + +module.exports = { + supported: '>=3.4.0', + /** @param {RuleContext} context @returns {TemplateListener} */ + createTemplateBodyVisitor(context) { + /** + * Verify the directive node + * @param {VDirective} node The directive node to check + * @returns {void} + */ + function checkDirective(node) { + if (utils.isVBindSameNameShorthand(node)) { + context.report({ + node, + messageId: 'forbiddenVBindSameNameShorthand', + // fix to use `:x="x"` (downgrade) + fix: (fixer) => + fixer.insertTextAfter(node, `="${node.value.expression.name}"`) + }) + } + } + + return { + "VAttribute[directive=true][key.name.name='bind']": checkDirective + } + } +} diff --git a/lib/utils/index.js b/lib/utils/index.js index 1928571c2..0cd842e13 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -828,6 +828,8 @@ module.exports = { */ hasDirective, + isVBindSameNameShorthand, + /** * Returns the list of all registered components * @param {ObjectExpression} componentObject @@ -3021,6 +3023,21 @@ function hasDirective(node, name, argument) { return Boolean(getDirective(node, name, argument)) } +/** + * Check whether the given directive node is v-bind same-name shorthand. + * @param {VAttribute | VDirective} node The directive node to check. + * @returns {node is VDirective & { value: VExpressionContainer & { expression: Identifier } }} `true` if the directive node is v-bind same-name shorthand. + */ +function isVBindSameNameShorthand(node) { + return ( + node.directive && + node.key.name.name === 'bind' && + node.value?.expression?.type === 'Identifier' && + node.key.range[0] <= node.value.range[0] && + node.value.range[1] <= node.key.range[1] + ) +} + /** * Checks whether given defineProps call node has withDefaults. * @param {CallExpression} node The node of defineProps diff --git a/package.json b/package.json index 17e6c0571..dca226eda 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.13", "semver": "^7.5.4", - "vue-eslint-parser": "^9.3.1", + "vue-eslint-parser": "^9.4.0", "xml-name-validator": "^4.0.0" }, "devDependencies": { diff --git a/tests/lib/rules/html-button-has-type.js b/tests/lib/rules/html-button-has-type.js index 0ba8dba19..3b5fcef6e 100644 --- a/tests/lib/rules/html-button-has-type.js +++ b/tests/lib/rules/html-button-has-type.js @@ -203,16 +203,6 @@ ruleTester.run('html-button-has-type', rule, { column: 25 } ] - }, - { - filename: 'test.vue', - code: ``, - errors: [ - { - message: 'A value must be set for button type attribute.', - column: 19 - } - ] } ] }) diff --git a/tests/lib/rules/html-quotes.js b/tests/lib/rules/html-quotes.js index 62b151509..b151245f0 100644 --- a/tests/lib/rules/html-quotes.js +++ b/tests/lib/rules/html-quotes.js @@ -58,6 +58,15 @@ tester.run('html-quotes', rule, { code: '', options: ['single', { avoidEscape: true }] }, + // v-bind same-name shorthand (Vue 3.4+) + { + code: '', + options: ['double'] + }, + { + code: '', + options: ['single'] + }, // Invalid EOF { diff --git a/tests/lib/rules/no-deprecated-slot-attribute.js b/tests/lib/rules/no-deprecated-slot-attribute.js index 7f3bcbf0d..be9b4241a 100644 --- a/tests/lib/rules/no-deprecated-slot-attribute.js +++ b/tests/lib/rules/no-deprecated-slot-attribute.js @@ -321,7 +321,7 @@ tester.run('no-deprecated-slot-attribute', rule, { output: ` `, errors: [ diff --git a/tests/lib/rules/no-unsupported-features/v-bind-same-name-shorthand.js b/tests/lib/rules/no-unsupported-features/v-bind-same-name-shorthand.js new file mode 100644 index 000000000..088ba7cbc --- /dev/null +++ b/tests/lib/rules/no-unsupported-features/v-bind-same-name-shorthand.js @@ -0,0 +1,95 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../../lib/rules/no-unsupported-features') +const utils = require('./utils') + +const buildOptions = utils.optionsBuilder( + 'v-bind-same-name-shorthand', + '^3.3.0' +) +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2019 + } +}) + +tester.run('no-unsupported-features/v-bind-same-name-shorthand', rule, { + valid: [ + { + code: ` + `, + options: buildOptions({ version: '3.4.0' }) + }, + { + code: ` + `, + options: buildOptions() + } + ], + invalid: [ + { + code: ` + `, + output: ` + `, + options: buildOptions(), + errors: [ + { + message: + '`v-bind` same-name shorthand is not supported until Vue.js "3.4.0".', + line: 3 + } + ] + }, + { + code: ` + `, + output: ` + `, + options: buildOptions({ version: '2.7.0' }), + errors: [ + { + message: + '`v-bind` same-name shorthand is not supported until Vue.js "3.4.0".', + line: 3 + } + ] + }, + { + code: ` + `, + output: ` + `, + options: buildOptions(), + errors: [ + { + message: + '`v-bind` same-name shorthand is not supported until Vue.js "3.4.0".', + line: 3 + } + ] + } + ] +}) diff --git a/tests/lib/rules/no-unused-components.js b/tests/lib/rules/no-unused-components.js index 7bd45a6b1..c87166cbd 100644 --- a/tests/lib/rules/no-unused-components.js +++ b/tests/lib/rules/no-unused-components.js @@ -413,13 +413,6 @@ tester.run('no-unused-components', rule, { ` }, - { - filename: 'test.vue', - code: ` - ` - }, // computed properties { @@ -629,26 +622,6 @@ tester.run('no-unused-components', rule, { } ] }, - { - filename: 'test.vue', - code: ` - - `, - errors: [ - { - message: 'The "Foo" component has been registered but not used.', - line: 8 - } - ] - }, // computed properties { diff --git a/tests/lib/rules/v-bind-style.js b/tests/lib/rules/v-bind-style.js index 0cb67f62f..5b4e88ab2 100644 --- a/tests/lib/rules/v-bind-style.js +++ b/tests/lib/rules/v-bind-style.js @@ -106,6 +106,21 @@ tester.run('v-bind-style', rule, { output: '', options: ['longform'], errors: ["Expected 'v-bind:' instead of '.'."] + }, + // v-bind same-name shorthand (Vue 3.4+) + { + filename: 'test.vue', + code: '', + output: '', + options: ['shorthand'], + errors: ["Unexpected 'v-bind' before ':'."] + }, + { + filename: 'test.vue', + code: '', + output: '', + options: ['longform'], + errors: ["Expected 'v-bind' before ':'."] } ] }) diff --git a/tests/lib/rules/valid-v-bind.js b/tests/lib/rules/valid-v-bind.js index 05c3d2b03..2259f9129 100644 --- a/tests/lib/rules/valid-v-bind.js +++ b/tests/lib/rules/valid-v-bind.js @@ -71,6 +71,15 @@ tester.run('valid-v-bind', rule, { filename: 'test.vue', code: "" }, + // v-bind same-name shorthand (Vue 3.4+) + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, // parsing error { filename: 'parsing-error.vue', @@ -88,11 +97,6 @@ tester.run('valid-v-bind', rule, { code: '', errors: ["'v-bind' directives require an attribute value."] }, - { - filename: 'test.vue', - code: '', - errors: ["'v-bind' directives require an attribute value."] - }, { filename: 'test.vue', code: "",