diff --git a/docs/rules/v-bind-style.md b/docs/rules/v-bind-style.md
index 23be4a8cc..271506c31 100644
--- a/docs/rules/v-bind-style.md
+++ b/docs/rules/v-bind-style.md
@@ -32,16 +32,20 @@ This rule enforces `v-bind` directive style which you should use shorthand or lo
## :wrench: Options
-Default is set to `shorthand`.
-
```json
{
- "vue/v-bind-style": ["error", "shorthand" | "longform"]
+ "vue/v-bind-style": ["error", "shorthand" | "longform", {
+ "sameNameShorthand": "ignore" | "always" | "never"
+ }]
}
```
- `"shorthand"` (default) ... requires using shorthand.
- `"longform"` ... requires using long form.
+- `sameNameShorthand` ... enforce the `v-bind` same-name shorthand style (Vue 3.4+).
+ - `"ignore"` (default) ... ignores the same-name shorthand style.
+ - `"always"` ... always enforces same-name shorthand where possible.
+ - `"never"` ... always disallow same-name shorthand where possible.
### `"longform"`
@@ -59,6 +63,38 @@ Default is set to `shorthand`.
+### `{ "sameNameShorthand": "always" }`
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
+### `{ "sameNameShorthand": "never" }`
+
+
+
+```vue
+
+
+
+
+
+
+
+```
+
+
+
## :books: Further Reading
- [Style guide - Directive shorthands](https://vuejs.org/style-guide/rules-strongly-recommended.html#directive-shorthands)
diff --git a/lib/rules/v-bind-style.js b/lib/rules/v-bind-style.js
index 18911cfc3..847db33a2 100644
--- a/lib/rules/v-bind-style.js
+++ b/lib/rules/v-bind-style.js
@@ -6,6 +6,41 @@
'use strict'
const utils = require('../utils')
+const casing = require('../utils/casing')
+
+/**
+ * @typedef { VDirectiveKey & { name: VIdentifier & { name: 'bind' }, argument: VExpressionContainer | VIdentifier } } VBindDirectiveKey
+ * @typedef { VDirective & { key: VBindDirectiveKey } } VBindDirective
+ */
+
+/**
+ * @param {VBindDirective} node
+ * @returns {boolean}
+ */
+function isSameName(node) {
+ const attrName =
+ node.key.argument.type === 'VIdentifier' ? node.key.argument.rawName : null
+ const valueName =
+ node.value?.expression?.type === 'Identifier'
+ ? node.value.expression.name
+ : null
+ return Boolean(
+ attrName &&
+ valueName &&
+ casing.camelCase(attrName) === casing.camelCase(valueName)
+ )
+}
+
+/**
+ * @param {VBindDirectiveKey} key
+ * @returns {number}
+ */
+function getCutStart(key) {
+ const modifiers = key.modifiers
+ return modifiers.length > 0
+ ? modifiers[modifiers.length - 1].range[1]
+ : key.argument.range[1]
+}
module.exports = {
meta: {
@@ -16,60 +51,113 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/v-bind-style.html'
},
fixable: 'code',
- schema: [{ enum: ['shorthand', 'longform'] }],
+ schema: [
+ { enum: ['shorthand', 'longform'] },
+ {
+ type: 'object',
+ properties: {
+ sameNameShorthand: { enum: ['always', 'never', 'ignore'] }
+ },
+ additionalProperties: false
+ }
+ ],
messages: {
expectedLonghand: "Expected 'v-bind' before ':'.",
unexpectedLonghand: "Unexpected 'v-bind' before ':'.",
- expectedLonghandForProp: "Expected 'v-bind:' instead of '.'."
+ expectedLonghandForProp: "Expected 'v-bind:' instead of '.'.",
+ expectedShorthand: 'Expected same-name shorthand.',
+ unexpectedShorthand: 'Unexpected same-name shorthand.'
}
},
/** @param {RuleContext} context */
create(context) {
const preferShorthand = context.options[0] !== 'longform'
+ /** @type {"always" | "never" | "ignore"} */
+ const sameNameShorthand = context.options[1]?.sameNameShorthand || 'ignore'
- return utils.defineTemplateBodyVisitor(context, {
- /** @param {VDirective} node */
- "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]"(
- node
- ) {
- const shorthandProp = node.key.name.rawName === '.'
- const shorthand = node.key.name.rawName === ':' || shorthandProp
- if (shorthand === preferShorthand) {
- return
- }
+ /** @param {VBindDirective} node */
+ function checkAttributeStyle(node) {
+ const shorthandProp = node.key.name.rawName === '.'
+ const shorthand = node.key.name.rawName === ':' || shorthandProp
+ if (shorthand === preferShorthand) {
+ return
+ }
- let messageId = 'expectedLonghand'
- if (preferShorthand) {
- messageId = 'unexpectedLonghand'
- } else if (shorthandProp) {
- messageId = 'expectedLonghandForProp'
- }
+ let messageId = 'expectedLonghand'
+ if (preferShorthand) {
+ messageId = 'unexpectedLonghand'
+ } else if (shorthandProp) {
+ messageId = 'expectedLonghandForProp'
+ }
+
+ context.report({
+ node,
+ loc: node.loc,
+ messageId,
+ *fix(fixer) {
+ if (preferShorthand) {
+ yield fixer.remove(node.key.name)
+ } else {
+ yield fixer.insertTextBefore(node, 'v-bind')
+
+ if (shorthandProp) {
+ // Replace `.` by `:`.
+ yield fixer.replaceText(node.key.name, ':')
- context.report({
- node,
- loc: node.loc,
- messageId,
- *fix(fixer) {
- if (preferShorthand) {
- yield fixer.remove(node.key.name)
- } else {
- yield fixer.insertTextBefore(node, 'v-bind')
-
- if (shorthandProp) {
- // Replace `.` by `:`.
- yield fixer.replaceText(node.key.name, ':')
-
- // Insert `.prop` modifier if it doesn't exist.
- const modifier = node.key.modifiers[0]
- const isAutoGeneratedPropModifier =
- modifier.name === 'prop' && modifier.rawName === ''
- if (isAutoGeneratedPropModifier) {
- yield fixer.insertTextBefore(modifier, '.prop')
- }
+ // Insert `.prop` modifier if it doesn't exist.
+ const modifier = node.key.modifiers[0]
+ const isAutoGeneratedPropModifier =
+ modifier.name === 'prop' && modifier.rawName === ''
+ if (isAutoGeneratedPropModifier) {
+ yield fixer.insertTextBefore(modifier, '.prop')
}
}
}
- })
+ }
+ })
+ }
+
+ /** @param {VBindDirective} node */
+ function checkAttributeSameName(node) {
+ if (sameNameShorthand === 'ignore' || !isSameName(node)) return
+
+ const preferShorthand = sameNameShorthand === 'always'
+ const isShorthand = utils.isVBindSameNameShorthand(node)
+ if (isShorthand === preferShorthand) {
+ return
+ }
+
+ const messageId = preferShorthand
+ ? 'expectedShorthand'
+ : 'unexpectedShorthand'
+
+ context.report({
+ node,
+ loc: node.loc,
+ messageId,
+ *fix(fixer) {
+ if (preferShorthand) {
+ /** @type {Range} */
+ const valueRange = [getCutStart(node.key), node.range[1]]
+
+ yield fixer.removeRange(valueRange)
+ } else if (node.key.argument.type === 'VIdentifier') {
+ yield fixer.insertTextAfter(
+ node,
+ `="${casing.camelCase(node.key.argument.rawName)}"`
+ )
+ }
+ }
+ })
+ }
+
+ return utils.defineTemplateBodyVisitor(context, {
+ /** @param {VBindDirective} node */
+ "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]"(
+ node
+ ) {
+ checkAttributeSameName(node)
+ checkAttributeStyle(node)
}
})
}
diff --git a/tests/lib/rules/v-bind-style.js b/tests/lib/rules/v-bind-style.js
index 26490790a..b678bf783 100644
--- a/tests/lib/rules/v-bind-style.js
+++ b/tests/lib/rules/v-bind-style.js
@@ -12,6 +12,9 @@ const tester = new RuleTester({
languageOptions: { parser: require('vue-eslint-parser'), ecmaVersion: 2015 }
})
+const expectedShorthand = 'Expected same-name shorthand.'
+const unexpectedShorthand = 'Unexpected same-name shorthand.'
+
tester.run('v-bind-style', rule, {
valid: [
{
@@ -34,7 +37,7 @@ tester.run('v-bind-style', rule, {
{
filename: 'test.vue',
code: '',
- options: ['longform']
+ options: ['longform', { sameNameShorthand: 'ignore' }]
},
// Don't enforce `.prop` shorthand because of experimental.
@@ -55,6 +58,72 @@ tester.run('v-bind-style', rule, {
filename: 'test.vue',
code: '',
options: ['shorthand']
+ },
+ // same-name shorthand: never
+ {
+ filename: 'test.vue',
+ code: '',
+ options: ['shorthand', { sameNameShorthand: 'never' }]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ options: ['longform', { sameNameShorthand: 'never' }]
+ },
+ {
+ // modifier
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: ['shorthand', { sameNameShorthand: 'never' }]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ options: ['longform', { sameNameShorthand: 'never' }]
+ },
+ {
+ // camel case
+ filename: 'test.vue',
+ code: '',
+ options: ['shorthand', { sameNameShorthand: 'never' }]
+ },
+ // same-name shorthand: always
+ {
+ filename: 'test.vue',
+ code: '',
+ options: ['shorthand', { sameNameShorthand: 'always' }]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ options: ['longform', { sameNameShorthand: 'always' }]
+ },
+ {
+ // modifier
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: ['shorthand', { sameNameShorthand: 'always' }]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ options: ['longform', { sameNameShorthand: 'always' }]
+ },
+ {
+ // camel case
+ filename: 'test.vue',
+ code: '',
+ options: ['shorthand', { sameNameShorthand: 'always' }]
}
],
invalid: [
@@ -120,6 +189,89 @@ tester.run('v-bind-style', rule, {
output: '',
options: ['longform'],
errors: ["Expected 'v-bind' before ':'."]
+ },
+ // same-name shorthand: never
+ {
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['shorthand', { sameNameShorthand: 'never' }],
+ errors: [unexpectedShorthand]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['longform', { sameNameShorthand: 'never' }],
+ errors: [unexpectedShorthand]
+ },
+ {
+ // modifier
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['shorthand', { sameNameShorthand: 'never' }],
+ errors: [unexpectedShorthand]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['shorthand', { sameNameShorthand: 'never' }],
+ errors: [unexpectedShorthand]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['longform', { sameNameShorthand: 'never' }],
+ errors: [unexpectedShorthand, "Expected 'v-bind:' instead of '.'."]
+ },
+ {
+ // camel case
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['shorthand', { sameNameShorthand: 'never' }],
+ errors: [unexpectedShorthand]
+ },
+ // same-name shorthand: always
+ {
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['shorthand', { sameNameShorthand: 'always' }],
+ errors: [expectedShorthand]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['longform', { sameNameShorthand: 'always' }],
+ errors: [expectedShorthand]
+ },
+ {
+ // modifier
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['shorthand', { sameNameShorthand: 'always' }],
+ errors: [expectedShorthand]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['shorthand', { sameNameShorthand: 'always' }],
+ errors: [expectedShorthand]
+ },
+ {
+ // camel case
+ filename: 'test.vue',
+ code: '',
+ output: '',
+ options: ['shorthand', { sameNameShorthand: 'always' }],
+ errors: [expectedShorthand]
}
]
})