Skip to content

Commit

Permalink
Add support for props destructure to `vue/no-required-prop-with-defau…
Browse files Browse the repository at this point in the history
…lt` rule (#2560)
  • Loading branch information
ota-meshi committed Sep 18, 2024
1 parent 3d32c1b commit 3137e50
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 50 deletions.
101 changes: 51 additions & 50 deletions lib/rules/no-required-prop-with-default.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,16 @@ module.exports = {
}

/**
* @param {ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp | ComponentProp} prop
* */
const handleObjectProp = (prop) => {
* @param {ComponentProp} prop
* @param {Set<string>} [defaultProps]
**/
const handleObjectProp = (prop, defaultProps) => {
if (
prop.type === 'object' &&
prop.propName &&
prop.value.type === 'ObjectExpression' &&
utils.findProperty(prop.value, 'default')
(utils.findProperty(prop.value, 'default') ||
defaultProps?.has(prop.propName))
) {
const requiredProperty = utils.findProperty(prop.value, 'required')
if (!requiredProperty) return
Expand Down Expand Up @@ -84,62 +86,61 @@ module.exports = {
]
})
}
} else if (
prop.type === 'type' &&
defaultProps?.has(prop.propName) &&
prop.required
) {
// skip setter & getter case
if (
prop.node.type === 'TSMethodSignature' &&
(prop.node.kind === 'get' || prop.node.kind === 'set')
) {
return
}
// skip computed
if (prop.node.computed) {
return
}
context.report({
node: prop.node,
loc: prop.node.loc,
data: {
key: prop.propName
},
messageId: 'requireOptional',
fix: canAutoFix
? (fixer) => fixer.insertTextAfter(prop.key, '?')
: null,
suggest: canAutoFix
? null
: [
{
messageId: 'fixRequiredProp',
fix: (fixer) => fixer.insertTextAfter(prop.key, '?')
}
]
})
}
}

return utils.compositingVisitors(
utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
utils.getComponentPropsFromOptions(node).map(handleObjectProp)
utils
.getComponentPropsFromOptions(node)
.map((prop) => handleObjectProp(prop))
}
}),
utils.defineScriptSetupVisitor(context, {
onDefinePropsEnter(node, props) {
if (!utils.hasWithDefaults(node)) {
props.map(handleObjectProp)
return
}
const withDefaultsProps = Object.keys(
utils.getWithDefaultsPropExpressions(node)
)
const requiredProps = props.flatMap((item) =>
item.type === 'type' && item.required ? [item] : []
)

for (const prop of requiredProps) {
if (withDefaultsProps.includes(prop.propName)) {
// skip setter & getter case
if (
prop.node.type === 'TSMethodSignature' &&
(prop.node.kind === 'get' || prop.node.kind === 'set')
) {
return
}
// skip computed
if (prop.node.computed) {
return
}
context.report({
node: prop.node,
loc: prop.node.loc,
data: {
key: prop.propName
},
messageId: 'requireOptional',
fix: canAutoFix
? (fixer) => fixer.insertTextAfter(prop.key, '?')
: null,
suggest: canAutoFix
? null
: [
{
messageId: 'fixRequiredProp',
fix: (fixer) => fixer.insertTextAfter(prop.key, '?')
}
]
})
}
}
const defaultProps = new Set([
...Object.keys(utils.getWithDefaultsPropExpressions(node)),
...Object.keys(
utils.getDefaultPropExpressionsForPropsDestructure(node)
)
])
props.map((prop) => handleObjectProp(prop, defaultProps))
}
})
)
Expand Down
131 changes: 131 additions & 0 deletions tests/lib/rules/no-required-prop-with-default.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,49 @@ tester.run('no-required-prop-with-default', rule, {
})
</script>
`
},
{
filename: 'test.vue',
code: `
<script setup lang="ts">
interface TestPropType {
name?: string
}
const {name="World"} = defineProps<TestPropType>();
</script>
`,
languageOptions: {
parserOptions: {
parser: require.resolve('@typescript-eslint/parser')
}
}
},
{
filename: 'test.vue',
code: `
<script setup lang="ts">
const {name="World"} = defineProps<{
name?: string
}>();
</script>
`,
languageOptions: {
parserOptions: {
parser: require.resolve('@typescript-eslint/parser')
}
}
},
{
filename: 'test.vue',
code: `
<script setup>
const {name='Hello'} = defineProps({
name: {
required: false
}
})
</script>
`
}
],
invalid: [
Expand Down Expand Up @@ -918,6 +961,94 @@ tester.run('no-required-prop-with-default', rule, {
line: 4
}
]
},
{
filename: 'test.vue',
code: `
<script setup lang="ts">
interface TestPropType {
name: string
}
const {name="World"} = defineProps<TestPropType>();
</script>
`,
output: `
<script setup lang="ts">
interface TestPropType {
name?: string
}
const {name="World"} = defineProps<TestPropType>();
</script>
`,
options: [{ autofix: true }],
languageOptions: {
parserOptions: {
parser: require.resolve('@typescript-eslint/parser')
}
},
errors: [
{
message: 'Prop "name" should be optional.',
line: 4
}
]
},
{
filename: 'test.vue',
code: `
<script setup lang="ts">
const {name="World"} = defineProps<{
name: string
}>();
</script>
`,
output: `
<script setup lang="ts">
const {name="World"} = defineProps<{
name?: string
}>();
</script>
`,
options: [{ autofix: true }],
languageOptions: {
parserOptions: {
parser: require.resolve('@typescript-eslint/parser')
}
},
errors: [
{
message: 'Prop "name" should be optional.',
line: 4
}
]
},
{
filename: 'test.vue',
code: `
<script setup lang="ts">
const {name="World"} = defineProps({
name: {
required: true,
}
});
</script>
`,
output: `
<script setup lang="ts">
const {name="World"} = defineProps({
name: {
required: false,
}
});
</script>
`,
options: [{ autofix: true }],
errors: [
{
message: 'Prop "name" should be optional.',
line: 4
}
]
}
]
})

0 comments on commit 3137e50

Please sign in to comment.