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: "",