Repository: vuejs/eslint-plugin-vue (js)

Issue:
Rule suggestion: `vue/prefer-prop-type-boolean-first`
**Please describe what the rule should do:**
In a prop definition with multiple allowed types, the rule suggests to put `Boolean` first to allow shorthand boolean props (see #775, #1781, #1796).

**What category should the rule belong to?**
[ ] Enforces code style (layout)
[x] Warns about a potential error (problem)
[ ] Suggests an alternate way of doing something (suggestion)
[ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**

```js
export default {
  props: {
    // PASS
    foo: Boolean,
    foo: String,
    foo: [Boolean],
    foo: [Boolean, String],
    foo: { type: Boolean },
    foo: { type: String },
    foo: { type: [Boolean] },
    foo: { type: [Boolean, String] },

    // FAIL
    foo: [String, Boolean],
    foo: { type: [String, Boolean] },
  },
}
```

Pull request description:
Add `prefer-prop-type-boolean-first` rule
Close #1797 .

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 6e879b2f..4a46ba66 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -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 |  |
diff --git a/docs/rules/prefer-prop-type-boolean-first.md b/docs/rules/prefer-prop-type-boolean-first.md
new file mode 100644
index 00000000..3bf2edb4
--- /dev/null
+++ b/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)
diff --git a/docs/rules/prefer-true-attribute-shorthand.md b/docs/rules/prefer-true-attribute-shorthand.md
index f2704f4c..c64f9a8c 100644
--- a/docs/rules/prefer-true-attribute-shorthand.md
+++ b/docs/rules/prefer-true-attribute-shorthand.md
@@ -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
 
diff --git a/lib/index.js b/lib/index.js
index 97ff805d..4e29c8ed 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -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'),
diff --git a/lib/rules/prefer-prop-type-boolean-first.js b/lib/rules/prefer-prop-type-boolean-first.js
new file mode 100644
index 00000000..215262d3
--- /dev/null
+++ b/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)
+      })
+    )
+  }
+}


Test patch:
diff --git a/tests/lib/rules/prefer-prop-type-boolean-first.js b/tests/lib/rules/prefer-prop-type-boolean-first.js
new file mode 100644
index 00000000..9f67e336
--- /dev/null
+++ b/tests/lib/rules/prefer-prop-type-boolean-first.js
@@ -0,0 +1,317 @@
+/**
+ * @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-prop-type-boolean-first')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('prefer-prop-type-boolean-first', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: {
+              a: Boolean,
+              b: String,
+              c: [Boolean],
+              d: [String],
+              e: [Boolean, String],
+              f: { type: Boolean },
+              g: { type: String },
+              h: { type: [Boolean] },
+              i: { type: [String] },
+              j: { type: [Boolean, String] },
+              k: {},
+            }
+          }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default Vue.extend({
+            props: {
+              a: [Boolean, String],
+              b: { type: [Boolean, String] },
+            }
+          })
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          defineProps({
+            a: [Boolean, String],
+            b: { type: [Boolean, String] },
+          })
+        </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: {
+              foo: [String, Boolean]
+            }
+          }
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'shouldBeFirst',
+          line: 5,
+          column: 29,
+          suggestions: [
+            {
+              messageId: 'moveToFirst',
+              output: `
+        <script>
+          export default {
+            props: {
+              foo: [Boolean, String]
+            }
+          }
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: {
+              foo: { type: [String, Boolean] }
+            }
+          }
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'shouldBeFirst',
+          line: 5,
+          column: 37,
+          suggestions: [
+            {
+              messageId: 'moveToFirst',
+              output: `
+        <script>
+          export default {
+            props: {
+              foo: { type: [Boolean, String] }
+            }
+          }
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default Vue.extend({
+            props: {
+              foo: [String, Boolean]
+            }
+          })
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'shouldBeFirst',
+          line: 5,
+          column: 29,
+          suggestions: [
+            {
+              messageId: 'moveToFirst',
+              output: `
+        <script>
+          export default Vue.extend({
+            props: {
+              foo: [Boolean, String]
+            }
+          })
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default Vue.extend({
+            props: {
+              foo: { type: [String, Boolean] }
+            }
+          })
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'shouldBeFirst',
+          line: 5,
+          column: 37,
+          suggestions: [
+            {
+              messageId: 'moveToFirst',
+              output: `
+        <script>
+          export default Vue.extend({
+            props: {
+              foo: { type: [Boolean, String] }
+            }
+          })
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          defineProps({
+            foo: [String, Boolean]
+          })
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'shouldBeFirst',
+          line: 4,
+          column: 27,
+          suggestions: [
+            {
+              messageId: 'moveToFirst',
+              output: `
+        <script setup>
+          defineProps({
+            foo: [Boolean, String]
+          })
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          defineProps({
+            foo: { type: [String, Boolean] }
+          })
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'shouldBeFirst',
+          line: 4,
+          column: 35,
+          suggestions: [
+            {
+              messageId: 'moveToFirst',
+              output: `
+        <script setup>
+          defineProps({
+            foo: { type: [Boolean, String] }
+          })
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          defineProps({
+            foo: { type: [String, Boolean, Number] }
+          })
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'shouldBeFirst',
+          line: 4,
+          column: 35,
+          suggestions: [
+            {
+              messageId: 'moveToFirst',
+              output: `
+        <script setup>
+          defineProps({
+            foo: { type: [Boolean, String, Number] }
+          })
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          defineProps({
+            foo: { type: [String, Number, Boolean] }
+          })
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'shouldBeFirst',
+          line: 4,
+          column: 43,
+          suggestions: [
+            {
+              messageId: 'moveToFirst',
+              output: `
+        <script setup>
+          defineProps({
+            foo: { type: [Boolean, String, Number] }
+          })
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    }
+  ]
+})