Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 2fb1f30

Browse files
authored
feat(prefer-true-attribute-shorthand): add except option (#2694)
1 parent 82f7e2b commit 2fb1f30

File tree

3 files changed

+271
-57
lines changed

3 files changed

+271
-57
lines changed

‎docs/rules/prefer-true-attribute-shorthand.md‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,18 @@ Default options is `"always"`.
8181

8282
```json
8383
{
84-
"vue/prefer-true-attribute-shorthand": ["error", "always" | "never"]
84+
"vue/prefer-true-attribute-shorthand": ["error",
85+
"always" | "never",
86+
{
87+
except: []
88+
}
89+
]
8590
}
8691
```
8792

8893
- `"always"` (default) ... requires shorthand form.
8994
- `"never"` ... requires long form.
95+
- `except` (`string[]`) ... specifies a list of attribute names that should be treated differently.
9096

9197
### `"never"`
9298

@@ -105,6 +111,26 @@ Default options is `"always"`.
105111

106112
</eslint-code-block>
107113

114+
### `"never", { 'except': ['value', '/^foo-/'] }`
115+
116+
<eslint-code-block :rules="{'vue/prefer-true-attribute-shorthand': ['error', 'never', { 'except': ['value', '/^foo-/'] }]}">
117+
118+
```vue
119+
<template>
120+
<!-- ✗ BAD -->
121+
<MyComponent show />
122+
<MyComponent :value="true" />
123+
<MyComponent :foo-bar="true" />
124+
125+
<!-- ✓ GOOD -->
126+
<MyComponent :show="true" />
127+
<MyComponent value />
128+
<MyComponent foo-bar />
129+
</template>
130+
```
131+
132+
</eslint-code-block>
133+
108134
## :couple: Related Rules
109135

110136
- [vue/no-boolean-default](./no-boolean-default.md)

‎lib/rules/prefer-true-attribute-shorthand.js‎

Lines changed: 134 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,60 @@
44
*/
55
'use strict'
66

7+
const { toRegExp } = require('../utils/regexp')
78
const utils = require('../utils')
89

10+
/**
11+
* @typedef { 'always' | 'never' } PreferOption
12+
*/
13+
14+
/**
15+
* @param {VDirective | VAttribute} node
16+
* @returns {string | null}
17+
*/
18+
function getAttributeName(node) {
19+
if (!node.directive) {
20+
return node.key.rawName
21+
}
22+
23+
if (
24+
(node.key.name.name === 'bind' || node.key.name.name === 'model') &&
25+
node.key.argument &&
26+
node.key.argument.type === 'VIdentifier'
27+
) {
28+
return node.key.argument.rawName
29+
}
30+
31+
return null
32+
}
33+
/**
34+
* @param {VAttribute | VDirective} node
35+
* @param {boolean} isExcepted
36+
* @param {PreferOption} option
37+
*/
38+
function shouldConvertToLongForm(node, isExcepted, option) {
39+
return (
40+
!node.directive &&
41+
!node.value &&
42+
(option === 'always' ? isExcepted : !isExcepted)
43+
)
44+
}
45+
46+
/**
47+
* @param {VAttribute | VDirective} node
48+
* @param {boolean} isExcepted
49+
* @param {PreferOption} option
50+
*/
51+
function shouldConvertToShortForm(node, isExcepted, option) {
52+
const isLiteralTrue =
53+
node.directive &&
54+
node.value?.expression?.type === 'Literal' &&
55+
node.value.expression.value === true &&
56+
Boolean(node.key.argument)
57+
58+
return isLiteralTrue && (option === 'always' ? !isExcepted : isExcepted)
59+
}
60+
961
module.exports = {
1062
meta: {
1163
type: 'suggestion',
@@ -17,7 +69,20 @@ module.exports = {
1769
},
1870
fixable: null,
1971
hasSuggestions: true,
20-
schema: [{ enum: ['always', 'never'] }],
72+
schema: [
73+
{ enum: ['always', 'never'] },
74+
{
75+
type: 'object',
76+
properties: {
77+
except: {
78+
type: 'array',
79+
items: { type: 'string' },
80+
uniqueItems: true
81+
}
82+
},
83+
additionalProperties: false
84+
}
85+
],
2186
messages: {
2287
expectShort:
2388
"Boolean prop with 'true' value should be written in shorthand form.",
@@ -34,68 +99,81 @@ module.exports = {
3499
create(context) {
35100
/** @type {'always' | 'never'} */
36101
const option = context.options[0] || 'always'
102+
/** @type {RegExp[]} */
103+
const exceptReg = (context.options[1]?.except || []).map(toRegExp)
104+
105+
/**
106+
* @param {VAttribute | VDirective} node
107+
* @param {string} messageId
108+
* @param {string} longVuePropText
109+
* @param {string} longHtmlAttrText
110+
*/
111+
function reportLongForm(
112+
node,
113+
messageId,
114+
longVuePropText,
115+
longHtmlAttrText
116+
) {
117+
context.report({
118+
node,
119+
messageId,
120+
suggest: [
121+
{
122+
messageId: 'rewriteIntoLongVueProp',
123+
fix: (fixer) => fixer.replaceText(node, longVuePropText)
124+
},
125+
{
126+
messageId: 'rewriteIntoLongHtmlAttr',
127+
fix: (fixer) => fixer.replaceText(node, longHtmlAttrText)
128+
}
129+
]
130+
})
131+
}
132+
133+
/**
134+
* @param {VAttribute | VDirective} node
135+
* @param {string} messageId
136+
* @param {string} shortFormText
137+
*/
138+
function reportShortForm(node, messageId, shortFormText) {
139+
context.report({
140+
node,
141+
messageId,
142+
suggest: [
143+
{
144+
messageId: 'rewriteIntoShort',
145+
fix: (fixer) => fixer.replaceText(node, shortFormText)
146+
}
147+
]
148+
})
149+
}
37150

38151
return utils.defineTemplateBodyVisitor(context, {
39152
VAttribute(node) {
40-
if (!utils.isCustomComponent(node.parent.parent)) {
41-
return
42-
}
43-
44-
if (option === 'never' && !node.directive && !node.value) {
45-
context.report({
46-
node,
47-
messageId: 'expectLong',
48-
suggest: [
49-
{
50-
messageId: 'rewriteIntoLongVueProp',
51-
fix: (fixer) =>
52-
fixer.replaceText(node, `:${node.key.rawName}="true"`)
53-
},
54-
{
55-
messageId: 'rewriteIntoLongHtmlAttr',
56-
fix: (fixer) =>
57-
fixer.replaceText(
58-
node,
59-
`${node.key.rawName}="${node.key.rawName}"`
60-
)
61-
}
62-
]
63-
})
64-
return
65-
}
153+
if (!utils.isCustomComponent(node.parent.parent)) return
66154

67-
if (option !== 'always') {
68-
return
69-
}
155+
const name = getAttributeName(node)
156+
if (name === null) return
70157

71-
if (
72-
!node.directive ||
73-
!node.value ||
74-
!node.value.expression ||
75-
node.value.expression.type !== 'Literal' ||
76-
node.value.expression.value !== true
77-
) {
78-
return
79-
}
158+
const isExcepted = exceptReg.some((re) => re.test(name))
80159

81-
const { argument } = node.key
82-
if (!argument) {
83-
return
160+
if (shouldConvertToLongForm(node, isExcepted, option)) {
161+
const key = /** @type {VIdentifier} */ (node.key)
162+
reportLongForm(
163+
node,
164+
'expectLong',
165+
`:${key.rawName}="true"`,
166+
`${key.rawName}="${key.rawName}"`
167+
)
168+
} else if (shouldConvertToShortForm(node, isExcepted, option)) {
169+
const directiveKey = /** @type {VDirectiveKey} */ (node.key)
170+
if (
171+
directiveKey.argument &&
172+
directiveKey.argument.type === 'VIdentifier'
173+
) {
174+
reportShortForm(node, 'expectShort', directiveKey.argument.rawName)
175+
}
84176
}
85-
86-
context.report({
87-
node,
88-
messageId: 'expectShort',
89-
suggest: [
90-
{
91-
messageId: 'rewriteIntoShort',
92-
fix: (fixer) => {
93-
const sourceCode = context.getSourceCode()
94-
return fixer.replaceText(node, sourceCode.getText(argument))
95-
}
96-
}
97-
]
98-
})
99177
}
100178
})
101179
}

‎tests/lib/rules/prefer-true-attribute-shorthand.js‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,24 @@ tester.run('prefer-true-attribute-shorthand', rule, {
148148
</template>
149149
`,
150150
options: ['never']
151+
},
152+
{
153+
filename: 'test.vue',
154+
code: `
155+
<template>
156+
<input :value="true" :foo-bar="true" />
157+
</template>
158+
`,
159+
options: ['always', { except: ['value', '/^foo-/'] }]
160+
},
161+
{
162+
filename: 'test.vue',
163+
code: `
164+
<template>
165+
<input value foo-bar />
166+
</template>
167+
`,
168+
options: ['never', { except: ['value', '/^foo-/'] }]
151169
}
152170
],
153171
invalid: [
@@ -280,6 +298,98 @@ tester.run('prefer-true-attribute-shorthand', rule, {
280298
]
281299
}
282300
]
301+
},
302+
{
303+
filename: 'test.vue',
304+
code: `
305+
<template>
306+
<MyComp value foo-bar />
307+
</template>`,
308+
output: null,
309+
options: ['always', { except: ['value', '/^foo-/'] }],
310+
errors: [
311+
{
312+
messageId: 'expectLong',
313+
line: 3,
314+
column: 17,
315+
suggestions: [
316+
{
317+
messageId: 'rewriteIntoLongVueProp',
318+
output: `
319+
<template>
320+
<MyComp :value="true" foo-bar />
321+
</template>`
322+
},
323+
{
324+
messageId: 'rewriteIntoLongHtmlAttr',
325+
output: `
326+
<template>
327+
<MyComp value="value" foo-bar />
328+
</template>`
329+
}
330+
]
331+
},
332+
{
333+
messageId: 'expectLong',
334+
line: 3,
335+
column: 23,
336+
suggestions: [
337+
{
338+
messageId: 'rewriteIntoLongVueProp',
339+
output: `
340+
<template>
341+
<MyComp value :foo-bar="true" />
342+
</template>`
343+
},
344+
{
345+
messageId: 'rewriteIntoLongHtmlAttr',
346+
output: `
347+
<template>
348+
<MyComp value foo-bar="foo-bar" />
349+
</template>`
350+
}
351+
]
352+
}
353+
]
354+
},
355+
{
356+
filename: 'test.vue',
357+
code: `
358+
<template>
359+
<MyComp :value="true" :foo-bar="true" />
360+
</template>`,
361+
output: null,
362+
options: ['never', { except: ['value', '/^foo-/'] }],
363+
errors: [
364+
{
365+
messageId: 'expectShort',
366+
line: 3,
367+
column: 17,
368+
suggestions: [
369+
{
370+
messageId: 'rewriteIntoShort',
371+
output: `
372+
<template>
373+
<MyComp value :foo-bar="true" />
374+
</template>`
375+
}
376+
]
377+
},
378+
{
379+
messageId: 'expectShort',
380+
line: 3,
381+
column: 31,
382+
suggestions: [
383+
{
384+
messageId: 'rewriteIntoShort',
385+
output: `
386+
<template>
387+
<MyComp :value="true" foo-bar />
388+
</template>`
389+
}
390+
]
391+
}
392+
]
283393
}
284394
]
285395
})

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /