Skip to content

Commit

Permalink
fix(compiler-sfc): properly handle unknown types in runtime prop infe…
Browse files Browse the repository at this point in the history
…rence

fix vuejs#7511
  • Loading branch information
yyx990803 authored and IAmSSH committed Apr 29, 2023
1 parent 4654be3 commit 3ad7f5b
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1712,7 +1712,10 @@ export default /*#__PURE__*/_defineComponent({
literalUnionMixed: { type: [String, Number, Boolean], required: true },
intersection: { type: Object, required: true },
intersection2: { type: String, required: true },
foo: { type: [Function, null], required: true }
foo: { type: [Function, null], required: true },
unknown: { type: null, required: true },
unknownUnion: { type: null, required: true },
unknownIntersection: { type: Object, required: true }
},
setup(__props: any, { expose }) {
expose();
Expand Down
16 changes: 15 additions & 1 deletion packages/compiler-sfc/__tests__/compileScript.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,10 @@ const emit = defineEmits(['a', 'b'])
intersection: Test & {}
intersection2: 'foo' & ('foo' | 'bar')
foo: ((item: any) => boolean) | null
unknown: UnknownType
unknownUnion: UnknownType | string
unknownIntersection: UnknownType & Object
}>()
</script>`)
assertCode(content)
Expand Down Expand Up @@ -1082,6 +1086,13 @@ const emit = defineEmits(['a', 'b'])
expect(content).toMatch(`intersection: { type: Object, required: true }`)
expect(content).toMatch(`intersection2: { type: String, required: true }`)
expect(content).toMatch(`foo: { type: [Function, null], required: true }`)
expect(content).toMatch(`unknown: { type: null, required: true }`)
// uninon containing unknown type: skip check
expect(content).toMatch(`unknownUnion: { type: null, required: true }`)
// intersection containing unknown type: narrow to the known types
expect(content).toMatch(
`unknownIntersection: { type: Object, required: true }`
)
expect(bindings).toStrictEqual({
string: BindingTypes.PROPS,
number: BindingTypes.PROPS,
Expand Down Expand Up @@ -1115,7 +1126,10 @@ const emit = defineEmits(['a', 'b'])
foo: BindingTypes.PROPS,
uppercase: BindingTypes.PROPS,
params: BindingTypes.PROPS,
nonNull: BindingTypes.PROPS
nonNull: BindingTypes.PROPS,
unknown: BindingTypes.PROPS,
unknownUnion: BindingTypes.PROPS,
unknownIntersection: BindingTypes.PROPS
})
})

Expand Down
57 changes: 38 additions & 19 deletions packages/compiler-sfc/src/compileScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ export function compileScript(
isFromSetup: boolean,
needTemplateUsageCheck: boolean
) {
// template usage check is only needed in non-inline mode, so we can skip
// template usage check is only needed in non-inline mode, so we can UNKNOWN
// the work if inlineTemplate is true.
let isUsedInTemplate = needTemplateUsageCheck
if (
Expand Down Expand Up @@ -1100,7 +1100,7 @@ export function compileScript(

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

// 4. extract runtime props/emits code from setup context type
if (propsTypeDecl) {
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes, isProd)
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
}
if (emitsTypeDecl) {
extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
Expand Down Expand Up @@ -1576,7 +1576,7 @@ export function compileScript(
!userImports[key].source.endsWith('.vue')
) {
// generate getter for import bindings
// skip vue imports since we know they will never change
// UNKNOWN vue imports since we know they will never change
returned += `get ${key}() { return ${key} }, `
} else if (bindingMetadata[key] === BindingTypes.SETUP_LET) {
// local let binding, also add setter
Expand Down Expand Up @@ -1972,20 +1972,23 @@ function recordType(node: Node, declaredTypes: Record<string, string[]>) {
function extractRuntimeProps(
node: TSTypeLiteral | TSInterfaceBody,
props: Record<string, PropTypeData>,
declaredTypes: Record<string, string[]>,
isProd: boolean
declaredTypes: Record<string, string[]>
) {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
for (const m of members) {
if (
(m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature') &&
m.key.type === 'Identifier'
) {
let type
let type: string[] | undefined
if (m.type === 'TSMethodSignature') {
type = ['Function']
} else if (m.typeAnnotation) {
type = inferRuntimeType(m.typeAnnotation.typeAnnotation, declaredTypes)
// skip check for result containing unknown types
if (type.includes(UNKNOWN_TYPE)) {
type = [`null`]
}
}
props[m.key.name] = {
key: m.key.name,
Expand All @@ -1996,6 +1999,8 @@ function extractRuntimeProps(
}
}

const UNKNOWN_TYPE = 'Unknown'

function inferRuntimeType(
node: TSType,
declaredTypes: Record<string, string[]>
Expand All @@ -2009,6 +2014,8 @@ function inferRuntimeType(
return ['Boolean']
case 'TSObjectKeyword':
return ['Object']
case 'TSNullKeyword':
return ['null']
case 'TSTypeLiteral': {
// TODO (nice to have) generate runtime property validation
const types = new Set<string>()
Expand Down Expand Up @@ -2041,7 +2048,7 @@ function inferRuntimeType(
case 'BigIntLiteral':
return ['Number']
default:
return [`null`]
return [`UNKNOWN`]
}

case 'TSTypeReference':
Expand Down Expand Up @@ -2104,31 +2111,43 @@ function inferRuntimeType(
declaredTypes
)
}
// cannot infer, fallback to null: ThisParameterType
// cannot infer, fallback to UNKNOWN: ThisParameterType
}
}
return [`null`]
return [UNKNOWN_TYPE]

case 'TSParenthesizedType':
return inferRuntimeType(node.typeAnnotation, declaredTypes)

case 'TSUnionType':
case 'TSIntersectionType':
return [
...new Set(
[].concat(
...(node.types.map(t => inferRuntimeType(t, declaredTypes)) as any)
)
)
]
return flattenTypes(node.types, declaredTypes)
case 'TSIntersectionType': {
return flattenTypes(node.types, declaredTypes).filter(
t => t !== UNKNOWN_TYPE
)
}

case 'TSSymbolKeyword':
return ['Symbol']

default:
return [`null`] // no runtime check
return [UNKNOWN_TYPE] // no runtime check
}
}

function flattenTypes(
types: TSType[],
declaredTypes: Record<string, string[]>
): string[] {
return [
...new Set(
([] as string[]).concat(
...types.map(t => inferRuntimeType(t, declaredTypes))
)
)
]
}

function toRuntimeTypeString(types: string[]) {
return types.length > 1 ? `[${types.join(', ')}]` : types[0]
}
Expand Down

0 comments on commit 3ad7f5b

Please sign in to comment.