Skip to content

Commit

Permalink
fix(compiler-sfc): avoid all hard errors when inferring runtime type
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Apr 21, 2023
1 parent 1447596 commit 2d9f6f9
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 145 deletions.
Expand Up @@ -690,6 +690,12 @@ describe('resolveType', () => {
test('should not error on unresolved type when inferring runtime type', () => {
expect(() => resolve(`defineProps<{ foo: T }>()`)).not.toThrow()
expect(() => resolve(`defineProps<{ foo: T['bar'] }>()`)).not.toThrow()
expect(() =>
resolve(`
import type P from 'unknown'
defineProps<{ foo: P }>()
`)
).not.toThrow()
})
})
})
Expand Down
300 changes: 155 additions & 145 deletions packages/compiler-sfc/src/script/resolveType.ts
Expand Up @@ -1180,156 +1180,164 @@ export function inferRuntimeType(
node: Node & MaybeWithScope,
scope = node._ownerScope || ctxToScope(ctx)
): string[] {
switch (node.type) {
case 'TSStringKeyword':
return ['String']
case 'TSNumberKeyword':
return ['Number']
case 'TSBooleanKeyword':
return ['Boolean']
case 'TSObjectKeyword':
return ['Object']
case 'TSNullKeyword':
return ['null']
case 'TSTypeLiteral':
case 'TSInterfaceDeclaration': {
// TODO (nice to have) generate runtime property validation
const types = new Set<string>()
const members =
node.type === 'TSTypeLiteral' ? node.members : node.body.body
for (const m of members) {
if (
m.type === 'TSCallSignatureDeclaration' ||
m.type === 'TSConstructSignatureDeclaration'
) {
types.add('Function')
} else {
types.add('Object')
try {
switch (node.type) {
case 'TSStringKeyword':
return ['String']
case 'TSNumberKeyword':
return ['Number']
case 'TSBooleanKeyword':
return ['Boolean']
case 'TSObjectKeyword':
return ['Object']
case 'TSNullKeyword':
return ['null']
case 'TSTypeLiteral':
case 'TSInterfaceDeclaration': {
// TODO (nice to have) generate runtime property validation
const types = new Set<string>()
const members =
node.type === 'TSTypeLiteral' ? node.members : node.body.body
for (const m of members) {
if (
m.type === 'TSCallSignatureDeclaration' ||
m.type === 'TSConstructSignatureDeclaration'
) {
types.add('Function')
} else {
types.add('Object')
}
}
return types.size ? Array.from(types) : ['Object']
}
return types.size ? Array.from(types) : ['Object']
}
case 'TSPropertySignature':
if (node.typeAnnotation) {
return inferRuntimeType(ctx, node.typeAnnotation.typeAnnotation, scope)
}
case 'TSMethodSignature':
case 'TSFunctionType':
return ['Function']
case 'TSArrayType':
case 'TSTupleType':
// TODO (nice to have) generate runtime element type/length checks
return ['Array']

case 'TSLiteralType':
switch (node.literal.type) {
case 'StringLiteral':
return ['String']
case 'BooleanLiteral':
return ['Boolean']
case 'NumericLiteral':
case 'BigIntLiteral':
return ['Number']
default:
return [UNKNOWN_TYPE]
}

case 'TSTypeReference': {
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
return inferRuntimeType(ctx, resolved, resolved._ownerScope)
}
if (node.typeName.type === 'Identifier') {
switch (node.typeName.name) {
case 'Array':
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
case 'Date':
case 'Promise':
return [node.typeName.name]

// TS built-in utility types
// https://www.typescriptlang.org/docs/handbook/utility-types.html
case 'Partial':
case 'Required':
case 'Readonly':
case 'Record':
case 'Pick':
case 'Omit':
case 'InstanceType':
return ['Object']

case 'Uppercase':
case 'Lowercase':
case 'Capitalize':
case 'Uncapitalize':
case 'TSPropertySignature':
if (node.typeAnnotation) {
return inferRuntimeType(
ctx,
node.typeAnnotation.typeAnnotation,
scope
)
}
case 'TSMethodSignature':
case 'TSFunctionType':
return ['Function']
case 'TSArrayType':
case 'TSTupleType':
// TODO (nice to have) generate runtime element type/length checks
return ['Array']

case 'TSLiteralType':
switch (node.literal.type) {
case 'StringLiteral':
return ['String']
case 'BooleanLiteral':
return ['Boolean']
case 'NumericLiteral':
case 'BigIntLiteral':
return ['Number']
default:
return [UNKNOWN_TYPE]
}

case 'Parameters':
case 'ConstructorParameters':
return ['Array']

case 'NonNullable':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope
).filter(t => t !== 'null')
}
break
case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType(ctx, node.typeParameters.params[1], scope)
}
break
case 'Exclude':
case 'OmitThisParameter':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(ctx, node.typeParameters.params[0], scope)
}
break
case 'TSTypeReference': {
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
return inferRuntimeType(ctx, resolved, resolved._ownerScope)
}
if (node.typeName.type === 'Identifier') {
switch (node.typeName.name) {
case 'Array':
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
case 'Date':
case 'Promise':
return [node.typeName.name]

// TS built-in utility types
// https://www.typescriptlang.org/docs/handbook/utility-types.html
case 'Partial':
case 'Required':
case 'Readonly':
case 'Record':
case 'Pick':
case 'Omit':
case 'InstanceType':
return ['Object']

case 'Uppercase':
case 'Lowercase':
case 'Capitalize':
case 'Uncapitalize':
return ['String']

case 'Parameters':
case 'ConstructorParameters':
return ['Array']

case 'NonNullable':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope
).filter(t => t !== 'null')
}
break
case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[1],
scope
)
}
break
case 'Exclude':
case 'OmitThisParameter':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
scope
)
}
break
}
}
// cannot infer, fallback to UNKNOWN: ThisParameterType
break
}
// cannot infer, fallback to UNKNOWN: ThisParameterType
break
}

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

case 'TSUnionType':
return flattenTypes(ctx, node.types, scope)
case 'TSIntersectionType': {
return flattenTypes(ctx, node.types, scope).filter(
t => t !== UNKNOWN_TYPE
)
}
case 'TSUnionType':
return flattenTypes(ctx, node.types, scope)
case 'TSIntersectionType': {
return flattenTypes(ctx, node.types, scope).filter(
t => t !== UNKNOWN_TYPE
)
}

case 'TSEnumDeclaration':
return inferEnumType(node)
case 'TSEnumDeclaration':
return inferEnumType(node)

case 'TSSymbolKeyword':
return ['Symbol']
case 'TSSymbolKeyword':
return ['Symbol']

case 'TSIndexedAccessType': {
try {
case 'TSIndexedAccessType': {
const types = resolveIndexType(ctx, node, scope)
return flattenTypes(ctx, types, scope)
} catch (e) {
break
}
}

case 'ClassDeclaration':
return ['Object']
case 'ClassDeclaration':
return ['Object']

case 'TSImportType': {
try {
case 'TSImportType': {
const sourceScope = importSourceToScope(
ctx,
node.argument,
Expand All @@ -1340,21 +1348,23 @@ export function inferRuntimeType(
if (resolved) {
return inferRuntimeType(ctx, resolved, resolved._ownerScope)
}
} catch (e) {}
break
}
break
}

case 'TSTypeQuery': {
const id = node.exprName
if (id.type === 'Identifier') {
// typeof only support identifier in local scope
const matched = scope.declares[id.name]
if (matched) {
return inferRuntimeType(ctx, matched, matched._ownerScope)
case 'TSTypeQuery': {
const id = node.exprName
if (id.type === 'Identifier') {
// typeof only support identifier in local scope
const matched = scope.declares[id.name]
if (matched) {
return inferRuntimeType(ctx, matched, matched._ownerScope)
}
}
break
}
break
}
} catch (e) {
// always soft fail on failed runtime type inference
}
return [UNKNOWN_TYPE] // no runtime check
}
Expand Down

0 comments on commit 2d9f6f9

Please sign in to comment.