Skip to content

Commit 3137e50

Browse files
authored
Add support for props destructure to vue/no-required-prop-with-default rule (#2560)
1 parent 3d32c1b commit 3137e50

File tree

2 files changed

+182
-50
lines changed

2 files changed

+182
-50
lines changed

Diff for: lib/rules/no-required-prop-with-default.js

+51-50
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,16 @@ module.exports = {
4747
}
4848

4949
/**
50-
* @param {ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp | ComponentProp} prop
51-
* */
52-
const handleObjectProp = (prop) => {
50+
* @param {ComponentProp} prop
51+
* @param {Set<string>} [defaultProps]
52+
**/
53+
const handleObjectProp = (prop, defaultProps) => {
5354
if (
5455
prop.type === 'object' &&
5556
prop.propName &&
5657
prop.value.type === 'ObjectExpression' &&
57-
utils.findProperty(prop.value, 'default')
58+
(utils.findProperty(prop.value, 'default') ||
59+
defaultProps?.has(prop.propName))
5860
) {
5961
const requiredProperty = utils.findProperty(prop.value, 'required')
6062
if (!requiredProperty) return
@@ -84,62 +86,61 @@ module.exports = {
8486
]
8587
})
8688
}
89+
} else if (
90+
prop.type === 'type' &&
91+
defaultProps?.has(prop.propName) &&
92+
prop.required
93+
) {
94+
// skip setter & getter case
95+
if (
96+
prop.node.type === 'TSMethodSignature' &&
97+
(prop.node.kind === 'get' || prop.node.kind === 'set')
98+
) {
99+
return
100+
}
101+
// skip computed
102+
if (prop.node.computed) {
103+
return
104+
}
105+
context.report({
106+
node: prop.node,
107+
loc: prop.node.loc,
108+
data: {
109+
key: prop.propName
110+
},
111+
messageId: 'requireOptional',
112+
fix: canAutoFix
113+
? (fixer) => fixer.insertTextAfter(prop.key, '?')
114+
: null,
115+
suggest: canAutoFix
116+
? null
117+
: [
118+
{
119+
messageId: 'fixRequiredProp',
120+
fix: (fixer) => fixer.insertTextAfter(prop.key, '?')
121+
}
122+
]
123+
})
87124
}
88125
}
89126

90127
return utils.compositingVisitors(
91128
utils.defineVueVisitor(context, {
92129
onVueObjectEnter(node) {
93-
utils.getComponentPropsFromOptions(node).map(handleObjectProp)
130+
utils
131+
.getComponentPropsFromOptions(node)
132+
.map((prop) => handleObjectProp(prop))
94133
}
95134
}),
96135
utils.defineScriptSetupVisitor(context, {
97136
onDefinePropsEnter(node, props) {
98-
if (!utils.hasWithDefaults(node)) {
99-
props.map(handleObjectProp)
100-
return
101-
}
102-
const withDefaultsProps = Object.keys(
103-
utils.getWithDefaultsPropExpressions(node)
104-
)
105-
const requiredProps = props.flatMap((item) =>
106-
item.type === 'type' && item.required ? [item] : []
107-
)
108-
109-
for (const prop of requiredProps) {
110-
if (withDefaultsProps.includes(prop.propName)) {
111-
// skip setter & getter case
112-
if (
113-
prop.node.type === 'TSMethodSignature' &&
114-
(prop.node.kind === 'get' || prop.node.kind === 'set')
115-
) {
116-
return
117-
}
118-
// skip computed
119-
if (prop.node.computed) {
120-
return
121-
}
122-
context.report({
123-
node: prop.node,
124-
loc: prop.node.loc,
125-
data: {
126-
key: prop.propName
127-
},
128-
messageId: 'requireOptional',
129-
fix: canAutoFix
130-
? (fixer) => fixer.insertTextAfter(prop.key, '?')
131-
: null,
132-
suggest: canAutoFix
133-
? null
134-
: [
135-
{
136-
messageId: 'fixRequiredProp',
137-
fix: (fixer) => fixer.insertTextAfter(prop.key, '?')
138-
}
139-
]
140-
})
141-
}
142-
}
137+
const defaultProps = new Set([
138+
...Object.keys(utils.getWithDefaultsPropExpressions(node)),
139+
...Object.keys(
140+
utils.getDefaultPropExpressionsForPropsDestructure(node)
141+
)
142+
])
143+
props.map((prop) => handleObjectProp(prop, defaultProps))
143144
}
144145
})
145146
)

Diff for: tests/lib/rules/no-required-prop-with-default.js

+131
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,49 @@ tester.run('no-required-prop-with-default', rule, {
189189
})
190190
</script>
191191
`
192+
},
193+
{
194+
filename: 'test.vue',
195+
code: `
196+
<script setup lang="ts">
197+
interface TestPropType {
198+
name?: string
199+
}
200+
const {name="World"} = defineProps<TestPropType>();
201+
</script>
202+
`,
203+
languageOptions: {
204+
parserOptions: {
205+
parser: require.resolve('@typescript-eslint/parser')
206+
}
207+
}
208+
},
209+
{
210+
filename: 'test.vue',
211+
code: `
212+
<script setup lang="ts">
213+
const {name="World"} = defineProps<{
214+
name?: string
215+
}>();
216+
</script>
217+
`,
218+
languageOptions: {
219+
parserOptions: {
220+
parser: require.resolve('@typescript-eslint/parser')
221+
}
222+
}
223+
},
224+
{
225+
filename: 'test.vue',
226+
code: `
227+
<script setup>
228+
const {name='Hello'} = defineProps({
229+
name: {
230+
required: false
231+
}
232+
})
233+
</script>
234+
`
192235
}
193236
],
194237
invalid: [
@@ -918,6 +961,94 @@ tester.run('no-required-prop-with-default', rule, {
918961
line: 4
919962
}
920963
]
964+
},
965+
{
966+
filename: 'test.vue',
967+
code: `
968+
<script setup lang="ts">
969+
interface TestPropType {
970+
name: string
971+
}
972+
const {name="World"} = defineProps<TestPropType>();
973+
</script>
974+
`,
975+
output: `
976+
<script setup lang="ts">
977+
interface TestPropType {
978+
name?: string
979+
}
980+
const {name="World"} = defineProps<TestPropType>();
981+
</script>
982+
`,
983+
options: [{ autofix: true }],
984+
languageOptions: {
985+
parserOptions: {
986+
parser: require.resolve('@typescript-eslint/parser')
987+
}
988+
},
989+
errors: [
990+
{
991+
message: 'Prop "name" should be optional.',
992+
line: 4
993+
}
994+
]
995+
},
996+
{
997+
filename: 'test.vue',
998+
code: `
999+
<script setup lang="ts">
1000+
const {name="World"} = defineProps<{
1001+
name: string
1002+
}>();
1003+
</script>
1004+
`,
1005+
output: `
1006+
<script setup lang="ts">
1007+
const {name="World"} = defineProps<{
1008+
name?: string
1009+
}>();
1010+
</script>
1011+
`,
1012+
options: [{ autofix: true }],
1013+
languageOptions: {
1014+
parserOptions: {
1015+
parser: require.resolve('@typescript-eslint/parser')
1016+
}
1017+
},
1018+
errors: [
1019+
{
1020+
message: 'Prop "name" should be optional.',
1021+
line: 4
1022+
}
1023+
]
1024+
},
1025+
{
1026+
filename: 'test.vue',
1027+
code: `
1028+
<script setup lang="ts">
1029+
const {name="World"} = defineProps({
1030+
name: {
1031+
required: true,
1032+
}
1033+
});
1034+
</script>
1035+
`,
1036+
output: `
1037+
<script setup lang="ts">
1038+
const {name="World"} = defineProps({
1039+
name: {
1040+
required: false,
1041+
}
1042+
});
1043+
</script>
1044+
`,
1045+
options: [{ autofix: true }],
1046+
errors: [
1047+
{
1048+
message: 'Prop "name" should be optional.',
1049+
line: 4
1050+
}
1051+
]
9211052
}
9221053
]
9231054
})

0 commit comments

Comments
 (0)