Skip to content

Commit f5f4f97

Browse files
g-planeFloEdelmann
andauthored
Add prefer-prop-type-boolean-first rule (#1822)
* Add `prefer-prop-type-boolean-first` rule * fix docs * update docs * merge valid test cases * add more tests * Add explanation to eslint-disable comment Co-authored-by: Flo Edelmann <[email protected]>
1 parent 0924d62 commit f5f4f97

6 files changed

+495
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ For example:
352352
| [vue/no-v-text](./no-v-text.md) | disallow use of v-text | |
353353
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
354354
| [vue/prefer-import-from-vue](./prefer-import-from-vue.md) | enforce import from 'vue' instead of import from '@vue/*' | :wrench: |
355+
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: |
355356
| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: |
356357
| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: |
357358
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/prefer-prop-type-boolean-first
5+
description: enforce `Boolean` comes first in component prop types
6+
---
7+
# vue/prefer-prop-type-boolean-first
8+
9+
> enforce `Boolean` comes first in component prop types
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+
When declaring types of a property in component, we can use array style to accept multiple types.
17+
18+
When using components in template,
19+
we can use shorthand-style property if its value is `true`.
20+
21+
However, if a property allows `Boolean` or `String` and we use it with shorthand form in somewhere else,
22+
different types order can introduce different behaviors:
23+
If `Boolean` comes first, it will be `true`; if `String` comes first, it will be `""` (empty string).
24+
25+
See [this demo](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCBNeUNvbXBvbmVudCBmcm9tICcuL015Q29tcG9uZW50LnZ1ZSdcbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIFNob3J0aGFuZCBmb3JtOlxuICA8TXlDb21wb25lbnQgYm9vbCBib29sLW9yLXN0cmluZyBzdHJpbmctb3ItYm9vbCAvPlxuICBcbiAgTG9uZ2hhbmQgZm9ybTpcbiAgPE15Q29tcG9uZW50IDpib29sPVwidHJ1ZVwiIDpib29sLW9yLXN0cmluZz1cInRydWVcIiA6c3RyaW5nLW9yLWJvb2w9XCJ0cnVlXCIgLz5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSIsIk15Q29tcG9uZW50LnZ1ZSI6IjxzY3JpcHQ+XG5leHBvcnQgZGVmYXVsdCB7XG4gIHByb3BzOiB7XG4gICAgYm9vbDogQm9vbGVhbixcbiAgICBib29sT3JTdHJpbmc6IFtCb29sZWFuLCBTdHJpbmddLFxuICAgIHN0cmluZ09yQm9vbDogW1N0cmluZywgQm9vbGVhbl0sXG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIDxwcmU+XG5ib29sOiB7e2Jvb2x9fSAoe3sgdHlwZW9mIGJvb2wgfX0pXG5ib29sT3JTdHJpbmc6IHt7Ym9vbE9yU3RyaW5nfX0gKHt7IHR5cGVvZiBib29sT3JTdHJpbmcgfX0pXG5zdHJpbmdPckJvb2w6IHt7c3RyaW5nT3JCb29sfX0gKHt7IHR5cGVvZiBzdHJpbmdPckJvb2wgfX0pXG4gIDwvcHJlPlxuPC90ZW1wbGF0ZT4ifQ==).
26+
27+
<eslint-code-block :rules="{'vue/prefer-prop-type-boolean-first': ['error']}">
28+
29+
```vue
30+
<script>
31+
export default {
32+
props: {
33+
// ✓ GOOD
34+
a: Boolean,
35+
b: String,
36+
c: [Boolean, String],
37+
d: {
38+
type: [Boolean, String]
39+
},
40+
41+
// ✗ BAD
42+
e: [String, Boolean],
43+
f: {
44+
type: [String, Boolean]
45+
}
46+
}
47+
}
48+
</script>
49+
```
50+
51+
</eslint-code-block>
52+
53+
## :couple: Related Rules
54+
55+
- [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md)
56+
57+
## :mag: Implementation
58+
59+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-prop-type-boolean-first.js)
60+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-prop-type-boolean-first.js)

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

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ Default options is `"always"`.
107107
## :couple: Related Rules
108108

109109
- [vue/no-boolean-default](./no-boolean-default.md)
110+
- [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md)
110111

111112
## :rocket: Version
112113

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ module.exports = {
158158
'order-in-components': require('./rules/order-in-components'),
159159
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
160160
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
161+
'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'),
161162
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
162163
'prefer-template': require('./rules/prefer-template'),
163164
'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'),
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
// Helpers
15+
// ------------------------------------------------------------------------------
16+
17+
/**
18+
* @param {ArrayExpression} node
19+
* @param {RuleContext} context
20+
*/
21+
function checkArrayExpression(node, context) {
22+
const booleanType = node.elements.find(
23+
(element) =>
24+
element && element.type === 'Identifier' && element.name === 'Boolean'
25+
)
26+
if (!booleanType) {
27+
return
28+
}
29+
const booleanTypeIndex = node.elements.indexOf(booleanType)
30+
if (booleanTypeIndex > 0) {
31+
context.report({
32+
node: booleanType,
33+
messageId: 'shouldBeFirst',
34+
suggest: [
35+
{
36+
messageId: 'moveToFirst',
37+
fix: (fixer) => {
38+
const sourceCode = context.getSourceCode()
39+
40+
const elements = node.elements.slice()
41+
elements.splice(booleanTypeIndex, 1)
42+
const code = elements
43+
.filter(utils.isDef)
44+
.map((element) => sourceCode.getText(element))
45+
code.unshift('Boolean')
46+
47+
return fixer.replaceText(node, `[${code.join(', ')}]`)
48+
}
49+
}
50+
]
51+
})
52+
}
53+
}
54+
55+
// ------------------------------------------------------------------------------
56+
// Rule Definition
57+
// ------------------------------------------------------------------------------
58+
59+
module.exports = {
60+
meta: {
61+
type: 'problem',
62+
docs: {
63+
description: 'enforce `Boolean` comes first in component prop types',
64+
categories: undefined,
65+
url: 'https://eslint.vuejs.org/rules/prefer-prop-type-boolean-first.html'
66+
},
67+
fixable: null,
68+
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- `context.report` with suggestion is not recognized in `checkArrayExpression`
69+
hasSuggestions: true,
70+
schema: [],
71+
messages: {
72+
shouldBeFirst: 'Type `Boolean` should be at first in prop types.',
73+
moveToFirst: 'Move `Boolean` to be first in prop types.'
74+
}
75+
},
76+
/** @param {RuleContext} context */
77+
create(context) {
78+
/**
79+
* @param {import('../utils').ComponentProp} prop
80+
*/
81+
function checkProperty(prop) {
82+
const { value } = prop
83+
if (!value) {
84+
return
85+
}
86+
87+
if (value.type === 'ArrayExpression') {
88+
checkArrayExpression(value, context)
89+
} else if (value.type === 'ObjectExpression') {
90+
const type = value.properties.find(
91+
/** @return {property is Property} */
92+
(property) =>
93+
property.type === 'Property' &&
94+
utils.getStaticPropertyName(property) === 'type'
95+
)
96+
if (!type || type.value.type !== 'ArrayExpression') {
97+
return
98+
}
99+
checkArrayExpression(type.value, context)
100+
}
101+
}
102+
103+
return utils.compositingVisitors(
104+
utils.defineScriptSetupVisitor(context, {
105+
onDefinePropsEnter(_, props) {
106+
props.forEach(checkProperty)
107+
}
108+
}),
109+
utils.executeOnVue(context, (obj) => {
110+
const props = utils.getComponentPropsFromOptions(obj)
111+
props.forEach(checkProperty)
112+
})
113+
)
114+
}
115+
}

0 commit comments

Comments
 (0)