Skip to content

Commit 1d1be85

Browse files
authored
Add prefer-true-attribute-shorthand rule (#1796)
* Add `prefer-true-attribute-shorthand` rule * fix typo * accept suggestions * accept option `"always"` or `"never"` * ignore native HTML elements * provide suggestions instead of auto fix * meta update
1 parent 7ebbd85 commit 1d1be85

File tree

5 files changed

+505
-0
lines changed

5 files changed

+505
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ For example:
353353
| [vue/no-v-text](./no-v-text.md) | disallow use of v-text | |
354354
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
355355
| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: |
356+
| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: |
356357
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
357358
| [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: |
358359
| [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/prefer-true-attribute-shorthand
5+
description: require shorthand form attribute when `v-bind` value is `true`
6+
---
7+
# vue/prefer-true-attribute-shorthand
8+
9+
> require shorthand form attribute when `v-bind` value is `true`
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
13+
14+
## :book: Rule Details
15+
16+
`v-bind` attribute with `true` value usually can be written in shorthand form. This can reduce verbosity.
17+
18+
<eslint-code-block :rules="{'vue/prefer-true-attribute-shorthand': ['error']}">
19+
20+
```vue
21+
<template>
22+
<!-- ✗ BAD -->
23+
<MyComponent v-bind:show="true" />
24+
<MyComponent :show="true" />
25+
26+
<!-- ✓ GOOD -->
27+
<MyComponent show />
28+
<MyComponent another-prop="true" />
29+
</template>
30+
```
31+
32+
</eslint-code-block>
33+
34+
::: warning Warning
35+
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`.
36+
:::
37+
38+
```vue
39+
<script>
40+
export default {
41+
name: 'MyComponent',
42+
props: {
43+
bool: Boolean,
44+
boolOrString: [Boolean, String],
45+
stringOrBool: [String, Boolean],
46+
}
47+
}
48+
</script>
49+
```
50+
51+
**Shorthand form:**
52+
53+
```vue
54+
<MyComponent bool bool-or-string string-or-bool />
55+
```
56+
57+
```
58+
bool: true (boolean)
59+
boolOrString: true (boolean)
60+
stringOrBool: "" (string)
61+
```
62+
63+
**Longhand form:**
64+
65+
```vue
66+
<MyComponent :bool="true" :bool-or-string="true" :string-or-bool="true" />
67+
```
68+
69+
```
70+
bool: true (boolean)
71+
boolOrString: true (boolean)
72+
stringOrBool: true (boolean)
73+
```
74+
75+
Those two calls will introduce different render result. See [this demo](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCBNeUNvbXBvbmVudCBmcm9tICcuL015Q29tcG9uZW50LnZ1ZSdcbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIFNob3J0aGFuZCBmb3JtOlxuICA8TXlDb21wb25lbnQgYm9vbCBib29sLW9yLXN0cmluZyBzdHJpbmctb3ItYm9vbCAvPlxuICBcbiAgTG9uZ2hhbmQgZm9ybTpcbiAgPE15Q29tcG9uZW50IDpib29sPVwidHJ1ZVwiIDpib29sLW9yLXN0cmluZz1cInRydWVcIiA6c3RyaW5nLW9yLWJvb2w9XCJ0cnVlXCIgLz5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSIsIk15Q29tcG9uZW50LnZ1ZSI6IjxzY3JpcHQ+XG5leHBvcnQgZGVmYXVsdCB7XG4gIHByb3BzOiB7XG4gICAgYm9vbDogQm9vbGVhbixcbiAgICBib29sT3JTdHJpbmc6IFtCb29sZWFuLCBTdHJpbmddLFxuICAgIHN0cmluZ09yQm9vbDogW1N0cmluZywgQm9vbGVhbl0sXG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIDxwcmU+XG5ib29sOiB7e2Jvb2x9fSAoe3sgdHlwZW9mIGJvb2wgfX0pXG5ib29sT3JTdHJpbmc6IHt7Ym9vbE9yU3RyaW5nfX0gKHt7IHR5cGVvZiBib29sT3JTdHJpbmcgfX0pXG5zdHJpbmdPckJvb2w6IHt7c3RyaW5nT3JCb29sfX0gKHt7IHR5cGVvZiBzdHJpbmdPckJvb2wgfX0pXG4gIDwvcHJlPlxuPC90ZW1wbGF0ZT4ifQ==).
76+
77+
## :wrench: Options
78+
79+
Default options is `"always"`.
80+
81+
```json
82+
{
83+
"vue/prefer-true-attribute-shorthand": ["error", "always" | "never"]
84+
}
85+
```
86+
87+
- `"always"` (default) ... requires shorthand form.
88+
- `"never"` ... requires long form.
89+
90+
### `"never"`
91+
92+
<eslint-code-block :rules="{'vue/prefer-true-attribute-shorthand': ['error', 'never']}">
93+
94+
```vue
95+
<template>
96+
<!-- ✗ BAD -->
97+
<MyComponent show />
98+
99+
<!-- ✓ GOOD -->
100+
<MyComponent :show="true" />
101+
<MyComponent v-bind:show="true" />
102+
</template>
103+
```
104+
105+
</eslint-code-block>
106+
107+
## :mag: Implementation
108+
109+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-true-attribute-shorthand.js)
110+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-true-attribute-shorthand.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ module.exports = {
159159
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
160160
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
161161
'prefer-template': require('./rules/prefer-template'),
162+
'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'),
162163
'prop-name-casing': require('./rules/prop-name-casing'),
163164
'quote-props': require('./rules/quote-props'),
164165
'require-component-is': require('./rules/require-component-is'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* @author Pig Fang
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// ------------------------------------------------------------------------------
14+
// Rule Definition
15+
// ------------------------------------------------------------------------------
16+
17+
module.exports = {
18+
meta: {
19+
type: 'suggestion',
20+
docs: {
21+
description:
22+
'require shorthand form attribute when `v-bind` value is `true`',
23+
categories: undefined,
24+
url: 'https://eslint.vuejs.org/rules/prefer-true-attribute-shorthand.html'
25+
},
26+
fixable: null,
27+
hasSuggestions: true,
28+
schema: [{ enum: ['always', 'never'] }],
29+
messages: {
30+
expectShort:
31+
"Boolean prop with 'true' value should be written in shorthand form.",
32+
expectLong:
33+
"Boolean prop with 'true' value should be written in long form.",
34+
rewriteIntoShort: 'Rewrite this prop into shorthand form.',
35+
rewriteIntoLongVueProp:
36+
'Rewrite this prop into long-form Vue component prop.',
37+
rewriteIntoLongHtmlAttr:
38+
'Rewrite this prop into long-form HTML attribute.'
39+
}
40+
},
41+
/** @param {RuleContext} context */
42+
create(context) {
43+
/** @type {'always' | 'never'} */
44+
const option = context.options[0] || 'always'
45+
46+
return utils.defineTemplateBodyVisitor(context, {
47+
VAttribute(node) {
48+
if (!utils.isCustomComponent(node.parent.parent)) {
49+
return
50+
}
51+
52+
if (option === 'never' && !node.directive && !node.value) {
53+
context.report({
54+
node,
55+
messageId: 'expectLong',
56+
suggest: [
57+
{
58+
messageId: 'rewriteIntoLongVueProp',
59+
fix: (fixer) =>
60+
fixer.replaceText(node, `:${node.key.rawName}="true"`)
61+
},
62+
{
63+
messageId: 'rewriteIntoLongHtmlAttr',
64+
fix: (fixer) =>
65+
fixer.replaceText(
66+
node,
67+
`${node.key.rawName}="${node.key.rawName}"`
68+
)
69+
}
70+
]
71+
})
72+
return
73+
}
74+
75+
if (option !== 'always') {
76+
return
77+
}
78+
79+
if (
80+
!node.directive ||
81+
!node.value ||
82+
!node.value.expression ||
83+
node.value.expression.type !== 'Literal' ||
84+
node.value.expression.value !== true
85+
) {
86+
return
87+
}
88+
89+
const { argument } = node.key
90+
if (!argument) {
91+
return
92+
}
93+
94+
context.report({
95+
node,
96+
messageId: 'expectShort',
97+
suggest: [
98+
{
99+
messageId: 'rewriteIntoShort',
100+
fix: (fixer) => {
101+
const sourceCode = context.getSourceCode()
102+
return fixer.replaceText(node, sourceCode.getText(argument))
103+
}
104+
}
105+
]
106+
})
107+
}
108+
})
109+
}
110+
}

0 commit comments

Comments
 (0)