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
  • Loading branch information
ota-meshi committed Sep 17, 2024
1 parent 8b877f7 commit 7476414
Show file tree
Hide file tree
Showing 3 changed files with 245 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
63 changes: 63 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,22 @@ module.exports = {
* @returns { { [key: string]: Property | undefined } }
*/
getWithDefaultsProps,
/**
* Gets the default definition nodes for defineProp
* using the props destructure with assignment pattern.
* @param {CallExpression} node The node of defineProps
* @returns { Record<string, {prop: AssignmentProperty , expression: Expression} | undefined> }
*/
getDefaultPropExpressionsForPropsDestructure,
/**
* Checks whether the given defineProps node is using Props Destructure.
* @param {CallExpression} node The node of defineProps
* @returns {boolean}
*/
isUsingPropsDestructure(node) {
const left = getLeftOfDefineProps(node)
return left?.type === 'ObjectPattern'
},

getVueObjectType,
/**
Expand Down Expand Up @@ -3144,6 +3160,53 @@ function getWithDefaultsProps(node) {
return result
}

/**
* Gets the default definition nodes for defineProp
* using the props destructure with assignment pattern.
* @param {CallExpression} node The node of defineProps
* @returns { Record<string, {prop: AssignmentProperty , expression: Expression} | undefined> }
*/
function getDefaultPropExpressionsForPropsDestructure(node) {
const left = getLeftOfDefineProps(node)
if (!left || left.type !== 'ObjectPattern') {
return {}
}
/** @type {ReturnType<typeof getDefaultPropExpressionsForPropsDestructure>} */
const result = Object.create(null)
for (const prop of left.properties) {
if (prop.type !== 'Property') continue
const value = prop.value
if (value.type !== 'AssignmentPattern') continue
const name = getStaticPropertyName(prop)
if (name != null) {
result[name] = { prop, expression: value.right }
}
}
return result
}

/**
* Gets the pattern of the left operand of defineProps.
* @param {CallExpression} node The node of defineProps
* @returns {Pattern | null} The pattern of the left operand of defineProps
*/
function getLeftOfDefineProps(node) {
let target = node
if (hasWithDefaults(target)) {
target = target.parent
}
if (!target.parent) {
return null
}
if (
target.parent.type === 'VariableDeclarator' &&
target.parent.init === target
) {
return target.parent.id
}
return null
}

/**
* Get all props from component options object.
* @param {ObjectExpression} componentObject Object with component definition
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 7476414

Please sign in to comment.