Skip to content

Commit

Permalink
Add prefer-prop-type-boolean-first rule (#1822)
Browse files Browse the repository at this point in the history
* 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 <florian-edelmann@online.de>
  • Loading branch information
g-plane and FloEdelmann committed Mar 23, 2022
1 parent 0924d62 commit f5f4f97
Show file tree
Hide file tree
Showing 6 changed files with 495 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -352,6 +352,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-import-from-vue](./prefer-import-from-vue.md) | enforce import from 'vue' instead of import from '@vue/*' | :wrench: |
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: |
| [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 | |
Expand Down
60 changes: 60 additions & 0 deletions docs/rules/prefer-prop-type-boolean-first.md
@@ -0,0 +1,60 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/prefer-prop-type-boolean-first
description: enforce `Boolean` comes first in component prop types
---
# vue/prefer-prop-type-boolean-first

> enforce `Boolean` comes first in component prop types
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :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

When declaring types of a property in component, we can use array style to accept multiple types.

When using components in template,
we can use shorthand-style property if its value is `true`.

However, if a property allows `Boolean` or `String` and we use it with shorthand form in somewhere else,
different types order can introduce different behaviors:
If `Boolean` comes first, it will be `true`; if `String` comes first, it will be `""` (empty string).

See [this demo](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCBNeUNvbXBvbmVudCBmcm9tICcuL015Q29tcG9uZW50LnZ1ZSdcbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIFNob3J0aGFuZCBmb3JtOlxuICA8TXlDb21wb25lbnQgYm9vbCBib29sLW9yLXN0cmluZyBzdHJpbmctb3ItYm9vbCAvPlxuICBcbiAgTG9uZ2hhbmQgZm9ybTpcbiAgPE15Q29tcG9uZW50IDpib29sPVwidHJ1ZVwiIDpib29sLW9yLXN0cmluZz1cInRydWVcIiA6c3RyaW5nLW9yLWJvb2w9XCJ0cnVlXCIgLz5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSIsIk15Q29tcG9uZW50LnZ1ZSI6IjxzY3JpcHQ+XG5leHBvcnQgZGVmYXVsdCB7XG4gIHByb3BzOiB7XG4gICAgYm9vbDogQm9vbGVhbixcbiAgICBib29sT3JTdHJpbmc6IFtCb29sZWFuLCBTdHJpbmddLFxuICAgIHN0cmluZ09yQm9vbDogW1N0cmluZywgQm9vbGVhbl0sXG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIDxwcmU+XG5ib29sOiB7e2Jvb2x9fSAoe3sgdHlwZW9mIGJvb2wgfX0pXG5ib29sT3JTdHJpbmc6IHt7Ym9vbE9yU3RyaW5nfX0gKHt7IHR5cGVvZiBib29sT3JTdHJpbmcgfX0pXG5zdHJpbmdPckJvb2w6IHt7c3RyaW5nT3JCb29sfX0gKHt7IHR5cGVvZiBzdHJpbmdPckJvb2wgfX0pXG4gIDwvcHJlPlxuPC90ZW1wbGF0ZT4ifQ==).

<eslint-code-block :rules="{'vue/prefer-prop-type-boolean-first': ['error']}">

```vue
<script>
export default {
props: {
// ✓ GOOD
a: Boolean,
b: String,
c: [Boolean, String],
d: {
type: [Boolean, String]
},
// ✗ BAD
e: [String, Boolean],
f: {
type: [String, Boolean]
}
}
}
</script>
```

</eslint-code-block>

## :couple: Related Rules

- [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md)

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-prop-type-boolean-first.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-prop-type-boolean-first.js)
1 change: 1 addition & 0 deletions docs/rules/prefer-true-attribute-shorthand.md
Expand Up @@ -107,6 +107,7 @@ Default options is `"always"`.
## :couple: Related Rules

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

## :rocket: Version

Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -158,6 +158,7 @@ module.exports = {
'order-in-components': require('./rules/order-in-components'),
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'),
'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'),
Expand Down
115 changes: 115 additions & 0 deletions lib/rules/prefer-prop-type-boolean-first.js
@@ -0,0 +1,115 @@
/**
* @author Pig Fang
* See LICENSE file in root directory for full license.
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

/**
* @param {ArrayExpression} node
* @param {RuleContext} context
*/
function checkArrayExpression(node, context) {
const booleanType = node.elements.find(
(element) =>
element && element.type === 'Identifier' && element.name === 'Boolean'
)
if (!booleanType) {
return
}
const booleanTypeIndex = node.elements.indexOf(booleanType)
if (booleanTypeIndex > 0) {
context.report({
node: booleanType,
messageId: 'shouldBeFirst',
suggest: [
{
messageId: 'moveToFirst',
fix: (fixer) => {
const sourceCode = context.getSourceCode()

const elements = node.elements.slice()
elements.splice(booleanTypeIndex, 1)
const code = elements
.filter(utils.isDef)
.map((element) => sourceCode.getText(element))
code.unshift('Boolean')

return fixer.replaceText(node, `[${code.join(', ')}]`)
}
}
]
})
}
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce `Boolean` comes first in component prop types',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/prefer-prop-type-boolean-first.html'
},
fixable: null,
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- `context.report` with suggestion is not recognized in `checkArrayExpression`
hasSuggestions: true,
schema: [],
messages: {
shouldBeFirst: 'Type `Boolean` should be at first in prop types.',
moveToFirst: 'Move `Boolean` to be first in prop types.'
}
},
/** @param {RuleContext} context */
create(context) {
/**
* @param {import('../utils').ComponentProp} prop
*/
function checkProperty(prop) {
const { value } = prop
if (!value) {
return
}

if (value.type === 'ArrayExpression') {
checkArrayExpression(value, context)
} else if (value.type === 'ObjectExpression') {
const type = value.properties.find(
/** @return {property is Property} */
(property) =>
property.type === 'Property' &&
utils.getStaticPropertyName(property) === 'type'
)
if (!type || type.value.type !== 'ArrayExpression') {
return
}
checkArrayExpression(type.value, context)
}
}

return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
onDefinePropsEnter(_, props) {
props.forEach(checkProperty)
}
}),
utils.executeOnVue(context, (obj) => {
const props = utils.getComponentPropsFromOptions(obj)
props.forEach(checkProperty)
})
)
}
}

0 comments on commit f5f4f97

Please sign in to comment.