diff --git a/docs/rules/README.md b/docs/rules/README.md
index b74075445..b2f19b512 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -353,6 +353,7 @@ For example:
| [vue/no-v-text](./no-v-text.md) | disallow use of v-text | |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: |
+| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: |
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
| [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: |
| [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: |
diff --git a/docs/rules/prefer-true-attribute-shorthand.md b/docs/rules/prefer-true-attribute-shorthand.md
new file mode 100644
index 000000000..b2b521b6a
--- /dev/null
+++ b/docs/rules/prefer-true-attribute-shorthand.md
@@ -0,0 +1,110 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/prefer-true-attribute-shorthand
+description: require shorthand form attribute when `v-bind` value is `true`
+---
+# vue/prefer-true-attribute-shorthand
+
+> require shorthand form attribute when `v-bind` value is `true`
+
+- :exclamation: ***This rule has not been released yet.***
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+
+## :book: Rule Details
+
+`v-bind` attribute with `true` value usually can be written in shorthand form. This can reduce verbosity.
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+::: warning Warning
+The shorthand form is not always equivalent! If a prop accepts multiple types, but Boolean is not the first one, a shorthand prop won't pass `true`.
+:::
+
+```vue
+
+```
+
+**Shorthand form:**
+
+```vue
+
+```
+
+```
+bool: true (boolean)
+boolOrString: true (boolean)
+stringOrBool: "" (string)
+```
+
+**Longhand form:**
+
+```vue
+
+```
+
+```
+bool: true (boolean)
+boolOrString: true (boolean)
+stringOrBool: true (boolean)
+```
+
+Those two calls will introduce different render result. See [this demo](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCBNeUNvbXBvbmVudCBmcm9tICcuL015Q29tcG9uZW50LnZ1ZSdcbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIFNob3J0aGFuZCBmb3JtOlxuICA8TXlDb21wb25lbnQgYm9vbCBib29sLW9yLXN0cmluZyBzdHJpbmctb3ItYm9vbCAvPlxuICBcbiAgTG9uZ2hhbmQgZm9ybTpcbiAgPE15Q29tcG9uZW50IDpib29sPVwidHJ1ZVwiIDpib29sLW9yLXN0cmluZz1cInRydWVcIiA6c3RyaW5nLW9yLWJvb2w9XCJ0cnVlXCIgLz5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSIsIk15Q29tcG9uZW50LnZ1ZSI6IjxzY3JpcHQ+XG5leHBvcnQgZGVmYXVsdCB7XG4gIHByb3BzOiB7XG4gICAgYm9vbDogQm9vbGVhbixcbiAgICBib29sT3JTdHJpbmc6IFtCb29sZWFuLCBTdHJpbmddLFxuICAgIHN0cmluZ09yQm9vbDogW1N0cmluZywgQm9vbGVhbl0sXG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIDxwcmU+XG5ib29sOiB7e2Jvb2x9fSAoe3sgdHlwZW9mIGJvb2wgfX0pXG5ib29sT3JTdHJpbmc6IHt7Ym9vbE9yU3RyaW5nfX0gKHt7IHR5cGVvZiBib29sT3JTdHJpbmcgfX0pXG5zdHJpbmdPckJvb2w6IHt7c3RyaW5nT3JCb29sfX0gKHt7IHR5cGVvZiBzdHJpbmdPckJvb2wgfX0pXG4gIDwvcHJlPlxuPC90ZW1wbGF0ZT4ifQ==).
+
+## :wrench: Options
+
+Default options is `"always"`.
+
+```json
+{
+ "vue/prefer-true-attribute-shorthand": ["error", "always" | "never"]
+}
+```
+
+- `"always"` (default) ... requires shorthand form.
+- `"never"` ... requires long form.
+
+### `"never"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-true-attribute-shorthand.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-true-attribute-shorthand.js)
diff --git a/lib/index.js b/lib/index.js
index 5d770e8b2..941de0b56 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -159,6 +159,7 @@ module.exports = {
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
'prefer-template': require('./rules/prefer-template'),
+ 'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'),
'prop-name-casing': require('./rules/prop-name-casing'),
'quote-props': require('./rules/quote-props'),
'require-component-is': require('./rules/require-component-is'),
diff --git a/lib/rules/prefer-true-attribute-shorthand.js b/lib/rules/prefer-true-attribute-shorthand.js
new file mode 100644
index 000000000..00fe95489
--- /dev/null
+++ b/lib/rules/prefer-true-attribute-shorthand.js
@@ -0,0 +1,110 @@
+/**
+ * @author Pig Fang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description:
+ 'require shorthand form attribute when `v-bind` value is `true`',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/prefer-true-attribute-shorthand.html'
+ },
+ fixable: null,
+ hasSuggestions: true,
+ schema: [{ enum: ['always', 'never'] }],
+ messages: {
+ expectShort:
+ "Boolean prop with 'true' value should be written in shorthand form.",
+ expectLong:
+ "Boolean prop with 'true' value should be written in long form.",
+ rewriteIntoShort: 'Rewrite this prop into shorthand form.',
+ rewriteIntoLongVueProp:
+ 'Rewrite this prop into long-form Vue component prop.',
+ rewriteIntoLongHtmlAttr:
+ 'Rewrite this prop into long-form HTML attribute.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ /** @type {'always' | 'never'} */
+ const option = context.options[0] || 'always'
+
+ 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 (option !== 'always') {
+ return
+ }
+
+ if (
+ !node.directive ||
+ !node.value ||
+ !node.value.expression ||
+ node.value.expression.type !== 'Literal' ||
+ node.value.expression.value !== true
+ ) {
+ return
+ }
+
+ const { argument } = node.key
+ if (!argument) {
+ return
+ }
+
+ 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
new file mode 100644
index 000000000..e7d52ef7f
--- /dev/null
+++ b/tests/lib/rules/prefer-true-attribute-shorthand.js
@@ -0,0 +1,283 @@
+/**
+ * @author Pig Fang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/prefer-true-attribute-shorthand')
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+})
+
+tester.run('prefer-true-attribute-shorthand', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `,
+ options: ['always']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `,
+ options: ['never']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `,
+ options: ['never']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `,
+ options: ['never']
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ errors: [
+ {
+ messageId: 'expectShort',
+ line: 3,
+ column: 17,
+ suggestions: [
+ {
+ messageId: 'rewriteIntoShort',
+ output: `
+
+
+ `
+ }
+ ]
+ }
+ ],
+ output: null
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ errors: [
+ {
+ messageId: 'expectShort',
+ line: 3,
+ column: 17,
+ suggestions: [
+ {
+ messageId: 'rewriteIntoShort',
+ output: `
+
+
+ `
+ }
+ ]
+ }
+ ],
+ output: null
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'expectShort',
+ line: 3,
+ column: 17,
+ suggestions: [
+ {
+ messageId: 'rewriteIntoShort',
+ output: `
+
+
+ `
+ }
+ ]
+ }
+ ],
+ output: null
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ options: ['always'],
+ errors: [
+ {
+ messageId: 'expectShort',
+ line: 3,
+ column: 17,
+ suggestions: [
+ {
+ messageId: 'rewriteIntoShort',
+ output: `
+
+
+ `
+ }
+ ]
+ }
+ ],
+ output: null
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ options: ['never'],
+ errors: [
+ {
+ messageId: 'expectLong',
+ line: 3,
+ column: 17,
+ suggestions: [
+ {
+ output: `
+
+
+ `
+ },
+ {
+ output: `
+
+
+ `
+ }
+ ]
+ }
+ ],
+ output: null
+ }
+ ]
+})