Skip to content

Commit

Permalink
Add support for props destructure to vue/require-valid-default-prop
Browse files Browse the repository at this point in the history
… rule (#2551)
  • Loading branch information
ota-meshi authored Sep 18, 2024
1 parent 8b877f7 commit 05b7559
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 51 deletions.
107 changes: 62 additions & 45 deletions lib/rules/require-valid-default-prop.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,71 +250,81 @@ module.exports = {
}

/**
* @param {(ComponentObjectDefineProp | ComponentTypeProp | ComponentInferTypeProp)[]} props
* @param { { [key: string]: Expression | undefined } } withDefaults
* @param {(ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp)[]} props
* @param {(propName: string) => Expression[]} otherDefaultProvider
*/
function processPropDefs(props, withDefaults) {
function processPropDefs(props, otherDefaultProvider) {
/** @type {PropDefaultFunctionContext[]} */
const propContexts = []
for (const prop of props) {
let typeList
let defExpr
/** @type {Expression[]} */
const defExprList = []
if (prop.type === 'object') {
const type = getPropertyNode(prop.value, 'type')
if (!type) continue
if (prop.value.type === 'ObjectExpression') {
const type = getPropertyNode(prop.value, 'type')
if (!type) continue

typeList = getTypes(type.value)
typeList = getTypes(type.value)

const def = getPropertyNode(prop.value, 'default')
if (!def) continue
const def = getPropertyNode(prop.value, 'default')
if (!def) continue

defExpr = def.value
defExprList.push(def.value)
} else {
typeList = getTypes(prop.value)
}
} else {
typeList = prop.types
defExpr = withDefaults[prop.propName]
}
if (!defExpr) continue
if (prop.propName != null) {
defExprList.push(...otherDefaultProvider(prop.propName))
}

if (defExprList.length === 0) continue

const typeNames = new Set(
typeList.filter((item) => NATIVE_TYPES.has(item))
)
// There is no native types detected
if (typeNames.size === 0) continue

const defType = getValueType(defExpr)
for (const defExpr of defExprList) {
const defType = getValueType(defExpr)

if (!defType) continue
if (!defType) continue

if (defType.function) {
if (typeNames.has('Function')) {
continue
}
if (defType.expression) {
if (!defType.returnType || typeNames.has(defType.returnType)) {
if (defType.function) {
if (typeNames.has('Function')) {
continue
}
report(defType.functionBody, prop, typeNames)
if (defType.expression) {
if (!defType.returnType || typeNames.has(defType.returnType)) {
continue
}
report(defType.functionBody, prop, typeNames)
} else {
propContexts.push({
prop,
types: typeNames,
default: defType
})
}
} else {
propContexts.push({
if (
typeNames.has(defType.type) &&
!FUNCTION_VALUE_TYPES.has(defType.type)
) {
continue
}
report(
defExpr,
prop,
types: typeNames,
default: defType
})
}
} else {
if (
typeNames.has(defType.type) &&
!FUNCTION_VALUE_TYPES.has(defType.type)
) {
continue
}
report(
defExpr,
prop,
[...typeNames].map((type) =>
FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
[...typeNames].map((type) =>
FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
)
)
)
}
}
}
return propContexts
Expand Down Expand Up @@ -364,7 +374,7 @@ module.exports = {
prop.type === 'object' && prop.value.type === 'ObjectExpression'
)
)
const propContexts = processPropDefs(props, {})
const propContexts = processPropDefs(props, () => [])
vueObjectPropsContexts.set(obj, propContexts)
},
/**
Expand Down Expand Up @@ -402,18 +412,25 @@ module.exports = {
const props = baseProps.filter(
/**
* @param {ComponentProp} prop
* @returns {prop is ComponentObjectDefineProp | ComponentInferTypeProp | ComponentTypeProp}
* @returns {prop is ComponentObjectProp | ComponentInferTypeProp | ComponentTypeProp}
*/
(prop) =>
Boolean(
prop.type === 'type' ||
prop.type === 'infer-type' ||
(prop.type === 'object' &&
prop.value.type === 'ObjectExpression')
prop.type === 'object'
)
)
const defaults = utils.getWithDefaultsPropExpressions(node)
const propContexts = processPropDefs(props, defaults)
const defaultsByWithDefaults =
utils.getWithDefaultsPropExpressions(node)
const defaultsByAssignmentPatterns =
utils.getDefaultPropExpressionsForPropsDestructure(node)
const propContexts = processPropDefs(props, (propName) =>
[
defaultsByWithDefaults[propName],
defaultsByAssignmentPatterns[propName]?.expression
].filter(utils.isDef)
)
scriptSetupPropsContexts.push({ node, props: propContexts })
},
/**
Expand Down
84 changes: 84 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,28 @@ 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'
},
/**
* Gets the props destructure property nodes for defineProp.
* @param {CallExpression} node The node of defineProps
* @returns { Record<string, AssignmentProperty | undefined> }
*/
getPropsDestructure,

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

/**
* Gets the props destructure property nodes for defineProp.
* @param {CallExpression} node The node of defineProps
* @returns { Record<string, AssignmentProperty | undefined> }
*/
function getPropsDestructure(node) {
/** @type {ReturnType<typeof getPropsDestructure>} */
const result = Object.create(null)
const left = getLeftOfDefineProps(node)
if (!left || left.type !== 'ObjectPattern') {
return result
}
for (const prop of left.properties) {
if (prop.type !== 'Property') continue
const name = getStaticPropertyName(prop)
if (name != null) {
result[name] = prop
}
}
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) {
/** @type {ReturnType<typeof getDefaultPropExpressionsForPropsDestructure>} */
const result = Object.create(null)
for (const [name, prop] of Object.entries(getPropsDestructure(node))) {
if (!prop) continue
const value = prop.value
if (value.type !== 'AssignmentPattern') continue
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
69 changes: 63 additions & 6 deletions tests/lib/rules/require-valid-default-prop.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,7 @@ ruleTester.run('require-valid-default-prop', rule, {
parser: require('@typescript-eslint/parser'),
ecmaVersion: 6,
sourceType: 'module'
},
errors: errorMessage('function')
}
},
{
filename: 'test.vue',
Expand All @@ -241,8 +240,7 @@ ruleTester.run('require-valid-default-prop', rule, {
parser: require('@typescript-eslint/parser'),
ecmaVersion: 6,
sourceType: 'module'
},
errors: errorMessage('function')
}
},
{
filename: 'test.vue',
Expand All @@ -259,8 +257,7 @@ ruleTester.run('require-valid-default-prop', rule, {
parser: require('@typescript-eslint/parser'),
ecmaVersion: 6,
sourceType: 'module'
},
errors: errorMessage('function')
}
},
{
// https://github.com/vuejs/eslint-plugin-vue/issues/1853
Expand Down Expand Up @@ -304,6 +301,21 @@ ruleTester.run('require-valid-default-prop', rule, {
})
</script>`,
...getTypeScriptFixtureTestOptions()
},
{
filename: 'test.vue',
code: `
<script setup>
const { foo = 'abc' } = defineProps({
foo: {
type: String,
}
})
</script>
`,
languageOptions: {
parser: require('vue-eslint-parser')
}
}
],

Expand Down Expand Up @@ -1041,6 +1053,51 @@ ruleTester.run('require-valid-default-prop', rule, {
}
],
...getTypeScriptFixtureTestOptions()
},
{
filename: 'test.vue',
code: `
<script setup>
const { foo = 123 } = defineProps({
foo: String
})
</script>
`,
languageOptions: {
parser: require('vue-eslint-parser')
},
errors: [
{
message: "Type of the default value for 'foo' prop must be a string.",
line: 3
}
]
},
{
filename: 'test.vue',
code: `
<script setup>
const { foo = 123 } = defineProps({
foo: {
type: String,
default: 123
}
})
</script>
`,
languageOptions: {
parser: require('vue-eslint-parser')
},
errors: [
{
message: "Type of the default value for 'foo' prop must be a string.",
line: 3
},
{
message: "Type of the default value for 'foo' prop must be a string.",
line: 6
}
]
}
]
})

0 comments on commit 05b7559

Please sign in to comment.