Skip to content

Commit 5fb406e

Browse files
committed
fix(compiler-sfc): properly handle unknown types in runtime prop inference
fix #7511
1 parent 6f5698c commit 5fb406e

File tree

3 files changed

+57
-21
lines changed

3 files changed

+57
-21
lines changed

packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1712,7 +1712,10 @@ export default /*#__PURE__*/_defineComponent({
17121712
literalUnionMixed: { type: [String, Number, Boolean], required: true },
17131713
intersection: { type: Object, required: true },
17141714
intersection2: { type: String, required: true },
1715-
foo: { type: [Function, null], required: true }
1715+
foo: { type: [Function, null], required: true },
1716+
unknown: { type: null, required: true },
1717+
unknownUnion: { type: null, required: true },
1718+
unknownIntersection: { type: Object, required: true }
17161719
},
17171720
setup(__props: any, { expose }) {
17181721
expose();

packages/compiler-sfc/__tests__/compileScript.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,10 @@ const emit = defineEmits(['a', 'b'])
10361036
intersection: Test & {}
10371037
intersection2: 'foo' & ('foo' | 'bar')
10381038
foo: ((item: any) => boolean) | null
1039+
1040+
unknown: UnknownType
1041+
unknownUnion: UnknownType | string
1042+
unknownIntersection: UnknownType & Object
10391043
}>()
10401044
</script>`)
10411045
assertCode(content)
@@ -1082,6 +1086,13 @@ const emit = defineEmits(['a', 'b'])
10821086
expect(content).toMatch(`intersection: { type: Object, required: true }`)
10831087
expect(content).toMatch(`intersection2: { type: String, required: true }`)
10841088
expect(content).toMatch(`foo: { type: [Function, null], required: true }`)
1089+
expect(content).toMatch(`unknown: { type: null, required: true }`)
1090+
// uninon containing unknown type: skip check
1091+
expect(content).toMatch(`unknownUnion: { type: null, required: true }`)
1092+
// intersection containing unknown type: narrow to the known types
1093+
expect(content).toMatch(
1094+
`unknownIntersection: { type: Object, required: true }`
1095+
)
10851096
expect(bindings).toStrictEqual({
10861097
string: BindingTypes.PROPS,
10871098
number: BindingTypes.PROPS,
@@ -1115,7 +1126,10 @@ const emit = defineEmits(['a', 'b'])
11151126
foo: BindingTypes.PROPS,
11161127
uppercase: BindingTypes.PROPS,
11171128
params: BindingTypes.PROPS,
1118-
nonNull: BindingTypes.PROPS
1129+
nonNull: BindingTypes.PROPS,
1130+
unknown: BindingTypes.PROPS,
1131+
unknownUnion: BindingTypes.PROPS,
1132+
unknownIntersection: BindingTypes.PROPS
11191133
})
11201134
})
11211135

packages/compiler-sfc/src/compileScript.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ export function compileScript(
386386
isFromSetup: boolean,
387387
needTemplateUsageCheck: boolean
388388
) {
389-
// template usage check is only needed in non-inline mode, so we can skip
389+
// template usage check is only needed in non-inline mode, so we can UNKNOWN
390390
// the work if inlineTemplate is true.
391391
let isUsedInTemplate = needTemplateUsageCheck
392392
if (
@@ -1100,7 +1100,7 @@ export function compileScript(
11001100

11011101
// check if user has manually specified `name` or 'render` option in
11021102
// export default
1103-
// if has name, skip name inference
1103+
// if has name, UNKNOWN name inference
11041104
// if has render and no template, generate return object instead of
11051105
// empty render function (#4980)
11061106
let optionProperties
@@ -1403,7 +1403,7 @@ export function compileScript(
14031403

14041404
// 4. extract runtime props/emits code from setup context type
14051405
if (propsTypeDecl) {
1406-
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes, isProd)
1406+
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
14071407
}
14081408
if (emitsTypeDecl) {
14091409
extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
@@ -1576,7 +1576,7 @@ export function compileScript(
15761576
!userImports[key].source.endsWith('.vue')
15771577
) {
15781578
// generate getter for import bindings
1579-
// skip vue imports since we know they will never change
1579+
// UNKNOWN vue imports since we know they will never change
15801580
returned += `get ${key}() { return ${key} }, `
15811581
} else if (bindingMetadata[key] === BindingTypes.SETUP_LET) {
15821582
// local let binding, also add setter
@@ -1972,20 +1972,23 @@ function recordType(node: Node, declaredTypes: Record<string, string[]>) {
19721972
function extractRuntimeProps(
19731973
node: TSTypeLiteral | TSInterfaceBody,
19741974
props: Record<string, PropTypeData>,
1975-
declaredTypes: Record<string, string[]>,
1976-
isProd: boolean
1975+
declaredTypes: Record<string, string[]>
19771976
) {
19781977
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
19791978
for (const m of members) {
19801979
if (
19811980
(m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature') &&
19821981
m.key.type === 'Identifier'
19831982
) {
1984-
let type
1983+
let type: string[] | undefined
19851984
if (m.type === 'TSMethodSignature') {
19861985
type = ['Function']
19871986
} else if (m.typeAnnotation) {
19881987
type = inferRuntimeType(m.typeAnnotation.typeAnnotation, declaredTypes)
1988+
// skip check for result containing unknown types
1989+
if (type.includes(UNKNOWN_TYPE)) {
1990+
type = [`null`]
1991+
}
19891992
}
19901993
props[m.key.name] = {
19911994
key: m.key.name,
@@ -1996,6 +1999,8 @@ function extractRuntimeProps(
19961999
}
19972000
}
19982001

2002+
const UNKNOWN_TYPE = 'Unknown'
2003+
19992004
function inferRuntimeType(
20002005
node: TSType,
20012006
declaredTypes: Record<string, string[]>
@@ -2009,6 +2014,8 @@ function inferRuntimeType(
20092014
return ['Boolean']
20102015
case 'TSObjectKeyword':
20112016
return ['Object']
2017+
case 'TSNullKeyword':
2018+
return ['null']
20122019
case 'TSTypeLiteral': {
20132020
// TODO (nice to have) generate runtime property validation
20142021
const types = new Set<string>()
@@ -2041,7 +2048,7 @@ function inferRuntimeType(
20412048
case 'BigIntLiteral':
20422049
return ['Number']
20432050
default:
2044-
return [`null`]
2051+
return [`UNKNOWN`]
20452052
}
20462053

20472054
case 'TSTypeReference':
@@ -2104,31 +2111,43 @@ function inferRuntimeType(
21042111
declaredTypes
21052112
)
21062113
}
2107-
// cannot infer, fallback to null: ThisParameterType
2114+
// cannot infer, fallback to UNKNOWN: ThisParameterType
21082115
}
21092116
}
2110-
return [`null`]
2117+
return [UNKNOWN_TYPE]
21112118

21122119
case 'TSParenthesizedType':
21132120
return inferRuntimeType(node.typeAnnotation, declaredTypes)
2121+
21142122
case 'TSUnionType':
2115-
case 'TSIntersectionType':
2116-
return [
2117-
...new Set(
2118-
[].concat(
2119-
...(node.types.map(t => inferRuntimeType(t, declaredTypes)) as any)
2120-
)
2121-
)
2122-
]
2123+
return flattenTypes(node.types, declaredTypes)
2124+
case 'TSIntersectionType': {
2125+
return flattenTypes(node.types, declaredTypes).filter(
2126+
t => t !== UNKNOWN_TYPE
2127+
)
2128+
}
21232129

21242130
case 'TSSymbolKeyword':
21252131
return ['Symbol']
21262132

21272133
default:
2128-
return [`null`] // no runtime check
2134+
return [UNKNOWN_TYPE] // no runtime check
21292135
}
21302136
}
21312137

2138+
function flattenTypes(
2139+
types: TSType[],
2140+
declaredTypes: Record<string, string[]>
2141+
): string[] {
2142+
return [
2143+
...new Set(
2144+
([] as string[]).concat(
2145+
...types.map(t => inferRuntimeType(t, declaredTypes))
2146+
)
2147+
)
2148+
]
2149+
}
2150+
21322151
function toRuntimeTypeString(types: string[]) {
21332152
return types.length > 1 ? `[${types.join(', ')}]` : types[0]
21342153
}

0 commit comments

Comments
 (0)