Skip to content

Commit ca0ec69

Browse files
committed
feat: add define-props-destructuring rule
1 parent 7dec48d commit ca0ec69

File tree

5 files changed

+375
-0
lines changed

5 files changed

+375
-0
lines changed
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/define-props-destructuring
5+
description: enforce consistent style for prop destructuring
6+
---
7+
8+
# vue/define-props-destructuring
9+
10+
> enforce consistent style for prop destructuring
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
13+
14+
## :book: Rule Details
15+
16+
This rule enforces a consistent style for handling Vue 3 Composition API props, allowing you to choose between requiring destructuring or prohibiting it.
17+
18+
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.
19+
20+
<eslint-code-block :rules="{'vue/define-props-destructuring': ['error']}">
21+
22+
```vue
23+
<script setup>
24+
// ✓ GOOD
25+
const { foo } = defineProps(['foo'])
26+
const { bar = 'default' } = defineProps(['bar'])
27+
28+
// ✗ BAD
29+
const props = defineProps(['foo'])
30+
const propsWithDefaults = withDefaults(defineProps(['foo']), { foo: 'default' })
31+
32+
// ✗ BAD
33+
const { baz } = withDefaults(defineProps(['baz']), { baz: 'default' })
34+
</script>
35+
```
36+
37+
</eslint-code-block>
38+
39+
The rule applies to both JavaScript and TypeScript props:
40+
41+
<eslint-code-block :rules="{'vue/define-props-destructuring': ['error']}">
42+
43+
```vue
44+
<script setup lang="ts">
45+
// ✓ GOOD
46+
const { foo } = defineProps<{ foo?: string }>()
47+
const { bar = 'default' } = defineProps<{ bar?: string }>()
48+
49+
// ✗ BAD
50+
const props = defineProps<{ foo?: string }>()
51+
const propsWithDefaults = withDefaults(defineProps<{ foo?: string }>(), { foo: 'default' })
52+
</script>
53+
```
54+
55+
</eslint-code-block>
56+
57+
## :wrench: Options
58+
59+
```js
60+
{
61+
"vue/define-props-destructuring": ["error", {
62+
"destructure": "always" | "never"
63+
}]
64+
}
65+
```
66+
67+
- `destructure` - Sets the destructuring preference for props
68+
- `"always"` (default) - Requires destructuring when using `defineProps` and warns against using `withDefaults` with destructuring
69+
- `"never"` - Requires using a variable to store props and prohibits destructuring
70+
71+
### `"destructure": "never"`
72+
73+
<eslint-code-block :rules="{'vue/define-props-destructuring': ['error', { destructure: 'never' }]}">
74+
75+
```vue
76+
<script setup>
77+
// ✓ GOOD
78+
const props = defineProps(['foo'])
79+
const propsWithDefaults = withDefaults(defineProps(['foo']), { foo: 'default' })
80+
81+
// ✗ BAD
82+
const { foo } = defineProps(['foo'])
83+
</script>
84+
```
85+
86+
</eslint-code-block>
87+
88+
## :books: Further Reading
89+
90+
- [Reactive Props Destructure](https://vuejs.org/guide/components/props.html#reactive-props-destructure)
91+
92+
## :mag: Implementation
93+
94+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-props-destructuring.js)
95+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-props-destructuring.js)

docs/rules/index.md

+2
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ For example:
218218
| [vue/define-emits-declaration] | enforce declaration style of `defineEmits` | | :hammer: |
219219
| [vue/define-macros-order] | enforce order of compiler macros (`defineProps`, `defineEmits`, etc.) | :wrench::bulb: | :lipstick: |
220220
| [vue/define-props-declaration] | enforce declaration style of `defineProps` | | :hammer: |
221+
| [vue/define-props-destructuring] | enforce consistent style for prop destructuring | | :hammer: |
221222
| [vue/enforce-style-attribute] | enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags | | :hammer: |
222223
| [vue/html-button-has-type] | disallow usage of button without an explicit type attribute | | :hammer: |
223224
| [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
398399
[vue/define-emits-declaration]: ./define-emits-declaration.md
399400
[vue/define-macros-order]: ./define-macros-order.md
400401
[vue/define-props-declaration]: ./define-props-declaration.md
402+
[vue/define-props-destructuring]: ./define-props-destructuring.md
401403
[vue/dot-location]: ./dot-location.md
402404
[vue/dot-notation]: ./dot-notation.md
403405
[vue/enforce-style-attribute]: ./enforce-style-attribute.md

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const plugin = {
5858
'define-emits-declaration': require('./rules/define-emits-declaration'),
5959
'define-macros-order': require('./rules/define-macros-order'),
6060
'define-props-declaration': require('./rules/define-props-declaration'),
61+
'define-props-destructuring': require('./rules/define-props-destructuring'),
6162
'dot-location': require('./rules/dot-location'),
6263
'dot-notation': require('./rules/dot-notation'),
6364
'enforce-style-attribute': require('./rules/enforce-style-attribute'),
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* @author Wayne Zhang
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
module.exports = {
10+
meta: {
11+
type: 'suggestion',
12+
docs: {
13+
description: 'enforce consistent style for prop destructuring',
14+
categories: undefined,
15+
url: 'https://eslint.vuejs.org/rules/define-props-destructuring.html'
16+
},
17+
fixable: undefined,
18+
schema: [
19+
{
20+
type: 'object',
21+
properties: {
22+
destructure: {
23+
enum: ['always', 'never']
24+
}
25+
},
26+
additionalProperties: false
27+
}
28+
],
29+
messages: {
30+
preferDestructuring: 'Prefer destructuring from `defineProps` directly.',
31+
avoidDestructuring: 'Avoid destructuring from `defineProps`.',
32+
avoidWithDefaults: 'Avoid using `withDefaults` with destructuring.'
33+
}
34+
},
35+
/** @param {RuleContext} context */
36+
create(context) {
37+
const options = context.options[0] || {}
38+
const destructurePreference = options.destructure || 'always'
39+
40+
return utils.compositingVisitors(
41+
utils.defineScriptSetupVisitor(context, {
42+
onDefinePropsEnter(node) {
43+
const hasDestructure = utils.isUsingPropsDestructure(node)
44+
const hasWithDefaults = utils.hasWithDefaults(node)
45+
46+
if (destructurePreference === 'always') {
47+
if (!hasDestructure) {
48+
context.report({
49+
node,
50+
messageId: 'preferDestructuring'
51+
})
52+
return
53+
}
54+
55+
if (hasWithDefaults) {
56+
context.report({
57+
node: node.parent.callee,
58+
messageId: 'avoidWithDefaults'
59+
})
60+
}
61+
} else {
62+
if (hasDestructure) {
63+
context.report({
64+
node,
65+
messageId: 'avoidDestructuring'
66+
})
67+
}
68+
}
69+
}
70+
})
71+
)
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* @author Wayne Zhang
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const RuleTester = require('../../eslint-compat').RuleTester
8+
const rule = require('../../../lib/rules/define-props-destructuring')
9+
10+
const tester = new RuleTester({
11+
languageOptions: {
12+
parser: require('vue-eslint-parser'),
13+
ecmaVersion: 2015,
14+
sourceType: 'module'
15+
}
16+
})
17+
18+
tester.run('define-props-destructuring', rule, {
19+
valid: [
20+
{
21+
filename: 'test.vue',
22+
code: `
23+
<script setup>
24+
const { foo = 'default' } = defineProps(['foo'])
25+
</script>
26+
`
27+
},
28+
{
29+
filename: 'test.vue',
30+
code: `
31+
<script setup lang="ts">
32+
const { foo = 'default' } = defineProps<{ foo?: string }>()
33+
</script>
34+
`,
35+
languageOptions: {
36+
parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
37+
}
38+
},
39+
{
40+
filename: 'test.vue',
41+
code: `
42+
<script setup>
43+
const props = defineProps(['foo'])
44+
</script>
45+
`,
46+
options: [{ destructure: 'never' }]
47+
},
48+
{
49+
filename: 'test.vue',
50+
code: `
51+
<script setup>
52+
const props = withDefaults(defineProps(['foo']), { foo: 'default' })
53+
</script>
54+
`,
55+
options: [{ destructure: 'never' }]
56+
},
57+
{
58+
filename: 'test.vue',
59+
code: `
60+
<script setup lang="ts">
61+
const props = defineProps<{ foo?: string }>()
62+
</script>
63+
`,
64+
options: [{ destructure: 'never' }],
65+
languageOptions: {
66+
parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
67+
}
68+
}
69+
],
70+
invalid: [
71+
{
72+
filename: 'test.vue',
73+
code: `
74+
<script setup>
75+
const props = defineProps(['foo'])
76+
</script>
77+
`,
78+
errors: [
79+
{
80+
messageId: 'preferDestructuring',
81+
line: 3,
82+
column: 21
83+
}
84+
]
85+
},
86+
{
87+
filename: 'test.vue',
88+
code: `
89+
<script setup>
90+
const props = withDefaults(defineProps(['foo']), { foo: 'default' })
91+
</script>
92+
`,
93+
errors: [
94+
{
95+
messageId: 'preferDestructuring',
96+
line: 3,
97+
column: 34
98+
}
99+
]
100+
},
101+
{
102+
filename: 'test.vue',
103+
code: `
104+
<script setup>
105+
const { foo } = withDefaults(defineProps(['foo']), { foo: 'default' })
106+
</script>
107+
`,
108+
errors: [
109+
{
110+
messageId: 'avoidWithDefaults',
111+
line: 3,
112+
column: 23
113+
}
114+
]
115+
},
116+
{
117+
filename: 'test.vue',
118+
code: `
119+
<script setup lang="ts">
120+
const props = withDefaults(defineProps<{ foo?: string }>(), { foo: 'default' })
121+
</script>
122+
`,
123+
languageOptions: {
124+
parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
125+
},
126+
errors: [
127+
{
128+
messageId: 'preferDestructuring',
129+
line: 3,
130+
column: 34
131+
}
132+
]
133+
},
134+
{
135+
filename: 'test.vue',
136+
code: `
137+
<script setup lang="ts">
138+
const { foo } = withDefaults(defineProps<{ foo?: string }>(), { foo: 'default' })
139+
</script>
140+
`,
141+
languageOptions: {
142+
parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
143+
},
144+
errors: [
145+
{
146+
messageId: 'avoidWithDefaults',
147+
line: 3,
148+
column: 23
149+
}
150+
]
151+
},
152+
{
153+
filename: 'test.vue',
154+
code: `
155+
<script setup>
156+
const { foo } = defineProps(['foo'])
157+
</script>
158+
`,
159+
options: [{ destructure: 'never' }],
160+
errors: [
161+
{
162+
messageId: 'avoidDestructuring',
163+
line: 3,
164+
column: 23
165+
}
166+
]
167+
},
168+
{
169+
filename: 'test.vue',
170+
code: `
171+
<script setup>
172+
const { foo } = withDefaults(defineProps(['foo']), { foo: 'default' })
173+
</script>
174+
`,
175+
options: [{ destructure: 'never' }],
176+
errors: [
177+
{
178+
messageId: 'avoidDestructuring',
179+
line: 3,
180+
column: 36
181+
}
182+
]
183+
},
184+
{
185+
filename: 'test.vue',
186+
code: `
187+
<script setup lang="ts">
188+
const { foo } = defineProps<{ foo?: string }>()
189+
</script>
190+
`,
191+
options: [{ destructure: 'never' }],
192+
languageOptions: {
193+
parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
194+
},
195+
errors: [
196+
{
197+
messageId: 'avoidDestructuring',
198+
line: 3,
199+
column: 23
200+
}
201+
]
202+
}
203+
]
204+
})

0 commit comments

Comments
 (0)