Skip to content

Commit

Permalink
Fix false negatives and false positives in `vue/require-valid-default…
Browse files Browse the repository at this point in the history
…-prop` rule (#2586)
  • Loading branch information
ota-meshi authored Oct 28, 2024
1 parent 86300c4 commit 9a56de8
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 27 deletions.
93 changes: 66 additions & 27 deletions lib/rules/require-valid-default-prop.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ module.exports = {
}

/**
* @param {*} node
* @param {Expression} node
* @param {ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp} prop
* @param {Iterable<string>} expectedTypeNames
*/
Expand All @@ -249,17 +249,22 @@ module.exports = {
})
}

/**
* @typedef {object} DefaultDefine
* @property {Expression} expression
* @property {'assignment'|'withDefaults'|'defaultProperty'} src
*/
/**
* @param {(ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp)[]} props
* @param {(propName: string) => Expression[]} otherDefaultProvider
* @param {(propName: string) => Iterable<DefaultDefine>} otherDefaultProvider
*/
function processPropDefs(props, otherDefaultProvider) {
/** @type {PropDefaultFunctionContext[]} */
const propContexts = []
for (const prop of props) {
let typeList
/** @type {Expression[]} */
const defExprList = []
/** @type {DefaultDefine[]} */
const defaultList = []
if (prop.type === 'object') {
if (prop.value.type === 'ObjectExpression') {
const type = getPropertyNode(prop.value, 'type')
Expand All @@ -268,36 +273,44 @@ module.exports = {
typeList = getTypes(type.value)

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

defExprList.push(def.value)
if (def) {
defaultList.push({
src: 'defaultProperty',
expression: def.value
})
}
} else {
typeList = getTypes(prop.value)
}
} else {
typeList = prop.types
}
if (prop.propName != null) {
defExprList.push(...otherDefaultProvider(prop.propName))
defaultList.push(...otherDefaultProvider(prop.propName))
}

if (defExprList.length === 0) continue
if (defaultList.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

for (const defExpr of defExprList) {
const defType = getValueType(defExpr)
for (const defaultDef of defaultList) {
const defType = getValueType(defaultDef.expression)

if (!defType) continue

if (defType.function) {
if (typeNames.has('Function')) {
continue
}
if (defaultDef.src === 'assignment') {
// Factory functions cannot be used in default definitions with initial value assignments.
report(defaultDef.expression, prop, typeNames)
continue
}
if (defType.expression) {
if (!defType.returnType || typeNames.has(defType.returnType)) {
continue
Expand All @@ -311,18 +324,23 @@ module.exports = {
})
}
} else {
if (
typeNames.has(defType.type) &&
!FUNCTION_VALUE_TYPES.has(defType.type)
) {
continue
if (typeNames.has(defType.type)) {
if (defaultDef.src === 'assignment') {
continue
}
if (!FUNCTION_VALUE_TYPES.has(defType.type)) {
// For Array and Object, defaults must be defined in the factory function.
continue
}
}
report(
defExpr,
defaultDef.expression,
prop,
[...typeNames].map((type) =>
FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
)
defaultDef.src === 'assignment'
? typeNames
: [...typeNames].map((type) =>
FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
)
)
}
}
Expand Down Expand Up @@ -425,12 +443,19 @@ module.exports = {
utils.getWithDefaultsPropExpressions(node)
const defaultsByAssignmentPatterns =
utils.getDefaultPropExpressionsForPropsDestructure(node)
const propContexts = processPropDefs(props, (propName) =>
[
defaultsByWithDefaults[propName],
defaultsByAssignmentPatterns[propName]?.expression
].filter(utils.isDef)
)
const propContexts = processPropDefs(props, function* (propName) {
const withDefaults = defaultsByWithDefaults[propName]
if (withDefaults) {
yield { src: 'withDefaults', expression: withDefaults }
}
const assignmentPattern = defaultsByAssignmentPatterns[propName]
if (assignmentPattern) {
yield {
src: 'assignment',
expression: assignmentPattern.expression
}
}
})
scriptSetupPropsContexts.push({ node, props: propContexts })
},
/**
Expand All @@ -450,7 +475,21 @@ module.exports = {
}
},
onDefinePropsExit() {
scriptSetupPropsContexts.pop()
const data = scriptSetupPropsContexts.pop()
if (!data) {
return
}
for (const {
prop,
types: typeNames,
default: defType
} of data.props) {
for (const returnType of defType.returnTypes) {
if (typeNames.has(returnType.type)) continue

report(returnType.node, prop, typeNames)
}
}
}
})
)
Expand Down
102 changes: 102 additions & 0 deletions tests/lib/rules/require-valid-default-prop.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,21 @@ ruleTester.run('require-valid-default-prop', rule, {
languageOptions: {
parser: require('vue-eslint-parser')
}
},
{
filename: 'test.vue',
code: `
<script setup lang="ts">
const { foo = [] } = defineProps({
foo: {
type: Array,
}
})
</script>
`,
languageOptions: {
parser: require('vue-eslint-parser')
}
}
],

Expand Down Expand Up @@ -1098,6 +1113,93 @@ ruleTester.run('require-valid-default-prop', rule, {
line: 6
}
]
},
{
filename: 'test.vue',
code: `
<script setup>
const { foo = [] } = defineProps({
foo: {
type: Number,
}
})
</script>
`,
languageOptions: {
parser: require('vue-eslint-parser')
},
errors: [
{
message: "Type of the default value for 'foo' prop must be a number.",
line: 3
}
]
},
{
filename: 'test.vue',
code: `
<script setup>
const { foo = 42 } = defineProps({
foo: {
type: Array,
}
})
</script>
`,
languageOptions: {
parser: require('vue-eslint-parser')
},
errors: [
{
message: "Type of the default value for 'foo' prop must be a array.",
line: 3
}
]
},
{
filename: 'test.vue',
code: `
<script setup>
const { foo = [] } = defineProps({
foo: {
type: Array,
default: () => {
return 42
}
}
})
</script>
`,
languageOptions: {
parser: require('vue-eslint-parser')
},
errors: [
{
message: "Type of the default value for 'foo' prop must be a array.",
line: 7
}
]
},
{
filename: 'test.vue',
code: `
<script setup>
const { foo = (()=>[]) } = defineProps({
foo: {
type: Array,
}
})
</script>
`,
languageOptions: {
parser: require('vue-eslint-parser')
},
errors: [
{
message: "Type of the default value for 'foo' prop must be a array.",
line: 3
}
]
}
]
})

0 comments on commit 9a56de8

Please sign in to comment.