diff --git a/docs/rules/prefer-true-attribute-shorthand.md b/docs/rules/prefer-true-attribute-shorthand.md
index 2a70b92eb..700921ce1 100644
--- a/docs/rules/prefer-true-attribute-shorthand.md
+++ b/docs/rules/prefer-true-attribute-shorthand.md
@@ -81,12 +81,18 @@ Default options is `"always"`.
```json
{
- "vue/prefer-true-attribute-shorthand": ["error", "always" | "never"]
+ "vue/prefer-true-attribute-shorthand": ["error",
+ "always" | "never",
+ {
+ except: []
+ }
+ ]
}
```
- `"always"` (default) ... requires shorthand form.
- `"never"` ... requires long form.
+- `except` (`string[]`) ... specifies a list of attribute names that should be treated differently.
### `"never"`
@@ -105,6 +111,26 @@ Default options is `"always"`.
+### `"never", { 'except': ['value', '/^foo-/'] }`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
## :couple: Related Rules
- [vue/no-boolean-default](./no-boolean-default.md)
diff --git a/lib/rules/prefer-true-attribute-shorthand.js b/lib/rules/prefer-true-attribute-shorthand.js
index 42d3326a9..817525d1d 100644
--- a/lib/rules/prefer-true-attribute-shorthand.js
+++ b/lib/rules/prefer-true-attribute-shorthand.js
@@ -4,8 +4,60 @@
*/
'use strict'
+const { toRegExp } = require('../utils/regexp')
const utils = require('../utils')
+/**
+ * @typedef { 'always' | 'never' } PreferOption
+ */
+
+/**
+ * @param {VDirective | VAttribute} node
+ * @returns {string | null}
+ */
+function getAttributeName(node) {
+ if (!node.directive) {
+ return node.key.rawName
+ }
+
+ if (
+ (node.key.name.name === 'bind' || node.key.name.name === 'model') &&
+ node.key.argument &&
+ node.key.argument.type === 'VIdentifier'
+ ) {
+ return node.key.argument.rawName
+ }
+
+ return null
+}
+/**
+ * @param {VAttribute | VDirective} node
+ * @param {boolean} isExcepted
+ * @param {PreferOption} option
+ */
+function shouldConvertToLongForm(node, isExcepted, option) {
+ return (
+ !node.directive &&
+ !node.value &&
+ (option === 'always' ? isExcepted : !isExcepted)
+ )
+}
+
+/**
+ * @param {VAttribute | VDirective} node
+ * @param {boolean} isExcepted
+ * @param {PreferOption} option
+ */
+function shouldConvertToShortForm(node, isExcepted, option) {
+ const isLiteralTrue =
+ node.directive &&
+ node.value?.expression?.type === 'Literal' &&
+ node.value.expression.value === true &&
+ Boolean(node.key.argument)
+
+ return isLiteralTrue && (option === 'always' ? !isExcepted : isExcepted)
+}
+
module.exports = {
meta: {
type: 'suggestion',
@@ -17,7 +69,20 @@ module.exports = {
},
fixable: null,
hasSuggestions: true,
- schema: [{ enum: ['always', 'never'] }],
+ schema: [
+ { enum: ['always', 'never'] },
+ {
+ type: 'object',
+ properties: {
+ except: {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true
+ }
+ },
+ additionalProperties: false
+ }
+ ],
messages: {
expectShort:
"Boolean prop with 'true' value should be written in shorthand form.",
@@ -34,68 +99,81 @@ module.exports = {
create(context) {
/** @type {'always' | 'never'} */
const option = context.options[0] || 'always'
+ /** @type {RegExp[]} */
+ const exceptReg = (context.options[1]?.except || []).map(toRegExp)
+
+ /**
+ * @param {VAttribute | VDirective} node
+ * @param {string} messageId
+ * @param {string} longVuePropText
+ * @param {string} longHtmlAttrText
+ */
+ function reportLongForm(
+ node,
+ messageId,
+ longVuePropText,
+ longHtmlAttrText
+ ) {
+ context.report({
+ node,
+ messageId,
+ suggest: [
+ {
+ messageId: 'rewriteIntoLongVueProp',
+ fix: (fixer) => fixer.replaceText(node, longVuePropText)
+ },
+ {
+ messageId: 'rewriteIntoLongHtmlAttr',
+ fix: (fixer) => fixer.replaceText(node, longHtmlAttrText)
+ }
+ ]
+ })
+ }
+
+ /**
+ * @param {VAttribute | VDirective} node
+ * @param {string} messageId
+ * @param {string} shortFormText
+ */
+ function reportShortForm(node, messageId, shortFormText) {
+ context.report({
+ node,
+ messageId,
+ suggest: [
+ {
+ messageId: 'rewriteIntoShort',
+ fix: (fixer) => fixer.replaceText(node, shortFormText)
+ }
+ ]
+ })
+ }
return utils.defineTemplateBodyVisitor(context, {
VAttribute(node) {
- if (!utils.isCustomComponent(node.parent.parent)) {
- return
- }
-
- if (option === 'never' && !node.directive && !node.value) {
- context.report({
- node,
- messageId: 'expectLong',
- suggest: [
- {
- messageId: 'rewriteIntoLongVueProp',
- fix: (fixer) =>
- fixer.replaceText(node, `:${node.key.rawName}="true"`)
- },
- {
- messageId: 'rewriteIntoLongHtmlAttr',
- fix: (fixer) =>
- fixer.replaceText(
- node,
- `${node.key.rawName}="${node.key.rawName}"`
- )
- }
- ]
- })
- return
- }
+ if (!utils.isCustomComponent(node.parent.parent)) return
- if (option !== 'always') {
- return
- }
+ const name = getAttributeName(node)
+ if (name === null) return
- if (
- !node.directive ||
- !node.value ||
- !node.value.expression ||
- node.value.expression.type !== 'Literal' ||
- node.value.expression.value !== true
- ) {
- return
- }
+ const isExcepted = exceptReg.some((re) => re.test(name))
- const { argument } = node.key
- if (!argument) {
- return
+ if (shouldConvertToLongForm(node, isExcepted, option)) {
+ const key = /** @type {VIdentifier} */ (node.key)
+ reportLongForm(
+ node,
+ 'expectLong',
+ `:${key.rawName}="true"`,
+ `${key.rawName}="${key.rawName}"`
+ )
+ } else if (shouldConvertToShortForm(node, isExcepted, option)) {
+ const directiveKey = /** @type {VDirectiveKey} */ (node.key)
+ if (
+ directiveKey.argument &&
+ directiveKey.argument.type === 'VIdentifier'
+ ) {
+ reportShortForm(node, 'expectShort', directiveKey.argument.rawName)
+ }
}
-
- context.report({
- node,
- messageId: 'expectShort',
- suggest: [
- {
- messageId: 'rewriteIntoShort',
- fix: (fixer) => {
- const sourceCode = context.getSourceCode()
- return fixer.replaceText(node, sourceCode.getText(argument))
- }
- }
- ]
- })
}
})
}
diff --git a/tests/lib/rules/prefer-true-attribute-shorthand.js b/tests/lib/rules/prefer-true-attribute-shorthand.js
index 43253fd69..80965bac6 100644
--- a/tests/lib/rules/prefer-true-attribute-shorthand.js
+++ b/tests/lib/rules/prefer-true-attribute-shorthand.js
@@ -148,6 +148,24 @@ tester.run('prefer-true-attribute-shorthand', rule, {
`,
options: ['never']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `,
+ options: ['always', { except: ['value', '/^foo-/'] }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `,
+ options: ['never', { except: ['value', '/^foo-/'] }]
}
],
invalid: [
@@ -280,6 +298,98 @@ tester.run('prefer-true-attribute-shorthand', rule, {
]
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ output: null,
+ options: ['always', { except: ['value', '/^foo-/'] }],
+ errors: [
+ {
+ messageId: 'expectLong',
+ line: 3,
+ column: 17,
+ suggestions: [
+ {
+ messageId: 'rewriteIntoLongVueProp',
+ output: `
+
+
+ `
+ },
+ {
+ messageId: 'rewriteIntoLongHtmlAttr',
+ output: `
+
+
+ `
+ }
+ ]
+ },
+ {
+ messageId: 'expectLong',
+ line: 3,
+ column: 23,
+ suggestions: [
+ {
+ messageId: 'rewriteIntoLongVueProp',
+ output: `
+
+
+ `
+ },
+ {
+ messageId: 'rewriteIntoLongHtmlAttr',
+ output: `
+
+
+ `
+ }
+ ]
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ output: null,
+ options: ['never', { except: ['value', '/^foo-/'] }],
+ errors: [
+ {
+ messageId: 'expectShort',
+ line: 3,
+ column: 17,
+ suggestions: [
+ {
+ messageId: 'rewriteIntoShort',
+ output: `
+
+
+ `
+ }
+ ]
+ },
+ {
+ messageId: 'expectShort',
+ line: 3,
+ column: 31,
+ suggestions: [
+ {
+ messageId: 'rewriteIntoShort',
+ output: `
+
+
+ `
+ }
+ ]
+ }
+ ]
}
]
})