diff --git a/docs/rules/define-props-destructuring.md b/docs/rules/define-props-destructuring.md
new file mode 100644
index 000000000..7f3bc7849
--- /dev/null
+++ b/docs/rules/define-props-destructuring.md
@@ -0,0 +1,95 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/define-props-destructuring
+description: enforce consistent style for props destructuring
+---
+
+# vue/define-props-destructuring
+
+> enforce consistent style for props destructuring
+
+- :exclamation: _**This rule has not been released yet.**_
+
+## :book: Rule Details
+
+This rule enforces a consistent style for handling Vue 3 Composition API props, allowing you to choose between requiring destructuring or prohibiting it.
+
+By default, the rule requires you to use destructuring syntax when using `defineProps` instead of storing props in a variable and warns against combining `withDefaults` with destructuring.
+
+
+
+```vue
+
+```
+
+
+
+The rule applies to both JavaScript and TypeScript props:
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+```js
+{
+ "vue/define-props-destructuring": ["error", {
+ "destructure": "always" | "never"
+ }]
+}
+```
+
+- `destructure` - Sets the destructuring preference for props
+ - `"always"` (default) - Requires destructuring when using `defineProps` and warns against using `withDefaults` with destructuring
+ - `"never"` - Requires using a variable to store props and prohibits destructuring
+
+### `"destructure": "never"`
+
+
+
+```vue
+
+```
+
+
+
+## :books: Further Reading
+
+- [Reactive Props Destructure](https://vuejs.org/guide/components/props.html#reactive-props-destructure)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-props-destructuring.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-props-destructuring.js)
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 55c5b96c9..7828b58bb 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -218,6 +218,7 @@ For example:
| [vue/define-emits-declaration] | enforce declaration style of `defineEmits` | | :hammer: |
| [vue/define-macros-order] | enforce order of compiler macros (`defineProps`, `defineEmits`, etc.) | :wrench::bulb: | :lipstick: |
| [vue/define-props-declaration] | enforce declaration style of `defineProps` | | :hammer: |
+| [vue/define-props-destructuring] | enforce consistent style for props destructuring | | :hammer: |
| [vue/enforce-style-attribute] | enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags | | :hammer: |
| [vue/html-button-has-type] | disallow usage of button without an explicit type attribute | | :hammer: |
| [vue/html-comment-content-newline] | enforce unified line break in HTML comments | :wrench: | :lipstick: |
@@ -398,6 +399,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
[vue/define-emits-declaration]: ./define-emits-declaration.md
[vue/define-macros-order]: ./define-macros-order.md
[vue/define-props-declaration]: ./define-props-declaration.md
+[vue/define-props-destructuring]: ./define-props-destructuring.md
[vue/dot-location]: ./dot-location.md
[vue/dot-notation]: ./dot-notation.md
[vue/enforce-style-attribute]: ./enforce-style-attribute.md
diff --git a/lib/index.js b/lib/index.js
index 834e5f28b..e511536fa 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -58,6 +58,7 @@ const plugin = {
'define-emits-declaration': require('./rules/define-emits-declaration'),
'define-macros-order': require('./rules/define-macros-order'),
'define-props-declaration': require('./rules/define-props-declaration'),
+ 'define-props-destructuring': require('./rules/define-props-destructuring'),
'dot-location': require('./rules/dot-location'),
'dot-notation': require('./rules/dot-notation'),
'enforce-style-attribute': require('./rules/enforce-style-attribute'),
diff --git a/lib/rules/define-props-destructuring.js b/lib/rules/define-props-destructuring.js
new file mode 100644
index 000000000..65ec1dcd7
--- /dev/null
+++ b/lib/rules/define-props-destructuring.js
@@ -0,0 +1,79 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'enforce consistent style for props destructuring',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/define-props-destructuring.html'
+ },
+ fixable: null,
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ destructure: {
+ enum: ['always', 'never']
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ preferDestructuring: 'Prefer destructuring from `defineProps` directly.',
+ avoidDestructuring: 'Avoid destructuring from `defineProps`.',
+ avoidWithDefaults: 'Avoid using `withDefaults` with destructuring.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const options = context.options[0] || {}
+ const destructurePreference = options.destructure || 'always'
+
+ return utils.compositingVisitors(
+ utils.defineScriptSetupVisitor(context, {
+ onDefinePropsEnter(node, props) {
+ const hasNoArgs = props.filter((prop) => prop.propName).length === 0
+ if (hasNoArgs) {
+ return
+ }
+
+ const hasDestructure = utils.isUsingPropsDestructure(node)
+ const hasWithDefaults = utils.hasWithDefaults(node)
+
+ if (destructurePreference === 'never') {
+ if (hasDestructure) {
+ context.report({
+ node,
+ messageId: 'avoidDestructuring'
+ })
+ }
+ return
+ }
+
+ if (!hasDestructure) {
+ context.report({
+ node,
+ messageId: 'preferDestructuring'
+ })
+ return
+ }
+
+ if (hasWithDefaults) {
+ context.report({
+ node: node.parent.callee,
+ messageId: 'avoidWithDefaults'
+ })
+ }
+ }
+ })
+ )
+ }
+}
diff --git a/tests/lib/rules/define-props-destructuring.js b/tests/lib/rules/define-props-destructuring.js
new file mode 100644
index 000000000..ec24b4328
--- /dev/null
+++ b/tests/lib/rules/define-props-destructuring.js
@@ -0,0 +1,212 @@
+/**
+ * @author Wayne Zhang
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/define-props-destructuring')
+
+const tester = new RuleTester({
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ecmaVersion: 2015,
+ sourceType: 'module'
+ }
+})
+
+tester.run('define-props-destructuring', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ destructure: 'never' }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ destructure: 'never' }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ destructure: 'never' }],
+ languageOptions: {
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ }
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'preferDestructuring',
+ line: 3,
+ column: 21
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'preferDestructuring',
+ line: 3,
+ column: 34
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'avoidWithDefaults',
+ line: 3,
+ column: 23
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ },
+ errors: [
+ {
+ messageId: 'preferDestructuring',
+ line: 3,
+ column: 34
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ },
+ errors: [
+ {
+ messageId: 'avoidWithDefaults',
+ line: 3,
+ column: 23
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ destructure: 'never' }],
+ errors: [
+ {
+ messageId: 'avoidDestructuring',
+ line: 3,
+ column: 23
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ destructure: 'never' }],
+ errors: [
+ {
+ messageId: 'avoidDestructuring',
+ line: 3,
+ column: 36
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ destructure: 'never' }],
+ languageOptions: {
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ },
+ errors: [
+ {
+ messageId: 'avoidDestructuring',
+ line: 3,
+ column: 23
+ }
+ ]
+ }
+ ]
+})