refactor(compiler-sfc): rework macro type resolution

This commit is contained in:
Evan You 2023-04-11 23:00:28 +08:00
parent ae5a9323b7
commit b2cdb2645f
7 changed files with 341 additions and 303 deletions

View File

@ -16,8 +16,7 @@ import {
Identifier,
ExportSpecifier,
Statement,
CallExpression,
TSEnumDeclaration
CallExpression
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map-js'
@ -47,7 +46,6 @@ import { DEFINE_OPTIONS, processDefineOptions } from './script/defineOptions'
import { processDefineSlots } from './script/defineSlots'
import { DEFINE_MODEL, processDefineModel } from './script/defineModel'
import { isLiteralNode, unwrapTSNode, isCallOf } from './script/utils'
import { inferRuntimeType } from './script/resolveType'
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
import { isImportUsed } from './script/importUsageCheck'
import { processAwait } from './script/topLevelAwait'
@ -169,7 +167,6 @@ export function compileScript(
// metadata that needs to be returned
// const ctx.bindingMetadata: BindingMetadata = {}
const userImports: Record<string, ImportBinding> = Object.create(null)
const scriptBindings: Record<string, BindingTypes> = Object.create(null)
const setupBindings: Record<string, BindingTypes> = Object.create(null)
@ -223,7 +220,7 @@ export function compileScript(
isUsedInTemplate = isImportUsed(local, sfc)
}
userImports[local] = {
ctx.userImports[local] = {
isType,
imported,
local,
@ -303,7 +300,7 @@ export function compileScript(
const local = specifier.local.name
const imported = getImportedName(specifier)
const source = node.source.value
const existing = userImports[local]
const existing = ctx.userImports[local]
if (
source === 'vue' &&
(imported === DEFINE_PROPS ||
@ -345,8 +342,8 @@ export function compileScript(
// 1.3 resolve possible user import alias of `ref` and `reactive`
const vueImportAliases: Record<string, string> = {}
for (const key in userImports) {
const { source, imported, local } = userImports[key]
for (const key in ctx.userImports) {
const { source, imported, local } = ctx.userImports[key]
if (source === 'vue') vueImportAliases[imported] = local
}
@ -658,7 +655,6 @@ export function compileScript(
node.exportKind === 'type') ||
(node.type === 'VariableDeclaration' && node.declare)
) {
recordType(node, ctx.declaredTypes)
if (node.type !== 'TSEnumDeclaration') {
hoistNode(node)
}
@ -723,7 +719,7 @@ export function compileScript(
Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body))
}
for (const [key, { isType, imported, source }] of Object.entries(
userImports
ctx.userImports
)) {
if (isType) continue
ctx.bindingMetadata[key] =
@ -823,8 +819,11 @@ export function compileScript(
...scriptBindings,
...setupBindings
}
for (const key in userImports) {
if (!userImports[key].isType && userImports[key].isUsedInTemplate) {
for (const key in ctx.userImports) {
if (
!ctx.userImports[key].isType &&
ctx.userImports[key].isUsedInTemplate
) {
allBindings[key] = true
}
}
@ -832,8 +831,8 @@ export function compileScript(
for (const key in allBindings) {
if (
allBindings[key] === true &&
userImports[key].source !== 'vue' &&
!userImports[key].source.endsWith('.vue')
ctx.userImports[key].source !== 'vue' &&
!ctx.userImports[key].source.endsWith('.vue')
) {
// generate getter for import bindings
// skip vue imports since we know they will never change
@ -1012,7 +1011,7 @@ export function compileScript(
return {
...scriptSetup,
bindings: ctx.bindingMetadata,
imports: userImports,
imports: ctx.userImports,
content: ctx.s.toString(),
map:
options.sourceMap !== false
@ -1201,38 +1200,6 @@ function walkPattern(
}
}
function recordType(node: Node, declaredTypes: Record<string, string[]>) {
if (node.type === 'TSInterfaceDeclaration') {
declaredTypes[node.id.name] = [`Object`]
} else if (node.type === 'TSTypeAliasDeclaration') {
declaredTypes[node.id.name] = inferRuntimeType(
node.typeAnnotation,
declaredTypes
)
} else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
recordType(node.declaration, declaredTypes)
} else if (node.type === 'TSEnumDeclaration') {
declaredTypes[node.id.name] = inferEnumType(node)
}
}
function inferEnumType(node: TSEnumDeclaration): string[] {
const types = new Set<string>()
for (const m of node.members) {
if (m.initializer) {
switch (m.initializer.type) {
case 'StringLiteral':
types.add('String')
break
case 'NumericLiteral':
types.add('Number')
break
}
}
}
return types.size ? [...types] : ['Number']
}
function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
if (isCallOf(node, userReactiveImport)) {
return true

View File

@ -2,12 +2,12 @@ import { Node, ObjectPattern, Program } from '@babel/types'
import { SFCDescriptor } from '../parse'
import { generateCodeFrame } from '@vue/shared'
import { parse as babelParse, ParserOptions, ParserPlugin } from '@babel/parser'
import { SFCScriptCompileOptions } from '../compileScript'
import { PropsDeclType, PropsDestructureBindings } from './defineProps'
import { ImportBinding, SFCScriptCompileOptions } from '../compileScript'
import { PropsDestructureBindings } from './defineProps'
import { ModelDecl } from './defineModel'
import { BindingMetadata } from '../../../compiler-core/src'
import MagicString from 'magic-string'
import { EmitsDeclType } from './defineEmits'
import { TypeScope } from './resolveType'
export class ScriptCompileContext {
isJS: boolean
@ -20,7 +20,9 @@ export class ScriptCompileContext {
startOffset = this.descriptor.scriptSetup?.loc.start.offset
endOffset = this.descriptor.scriptSetup?.loc.end.offset
declaredTypes: Record<string, string[]> = Object.create(null)
// import / type analysis
scope: TypeScope | undefined
userImports: Record<string, ImportBinding> = Object.create(null)
// macros presence check
hasDefinePropsCall = false
@ -35,7 +37,7 @@ export class ScriptCompileContext {
// defineProps
propsIdentifier: string | undefined
propsRuntimeDecl: Node | undefined
propsTypeDecl: PropsDeclType | undefined
propsTypeDecl: Node | undefined
propsDestructureDecl: ObjectPattern | undefined
propsDestructuredBindings: PropsDestructureBindings = Object.create(null)
propsDestructureRestId: string | undefined
@ -43,7 +45,7 @@ export class ScriptCompileContext {
// defineEmits
emitsRuntimeDecl: Node | undefined
emitsTypeDecl: EmitsDeclType | undefined
emitsTypeDecl: Node | undefined
emitIdentifier: string | undefined
// defineModel

View File

@ -1,22 +1,10 @@
import {
Identifier,
LVal,
Node,
RestElement,
TSFunctionType,
TSInterfaceBody,
TSTypeLiteral
} from '@babel/types'
import { FromNormalScript, isCallOf } from './utils'
import { Identifier, LVal, Node, RestElement } from '@babel/types'
import { isCallOf } from './utils'
import { ScriptCompileContext } from './context'
import { resolveQualifiedType } from './resolveType'
import { resolveTypeElements } from './resolveType'
export const DEFINE_EMITS = 'defineEmits'
export type EmitsDeclType = FromNormalScript<
TSFunctionType | TSTypeLiteral | TSInterfaceBody
>
export function processDefineEmits(
ctx: ScriptCompileContext,
node: Node,
@ -38,21 +26,7 @@ export function processDefineEmits(
node
)
}
const emitsTypeDeclRaw = node.typeParameters.params[0]
ctx.emitsTypeDecl = resolveQualifiedType(
ctx,
emitsTypeDeclRaw,
node => node.type === 'TSFunctionType' || node.type === 'TSTypeLiteral'
) as EmitsDeclType | undefined
if (!ctx.emitsTypeDecl) {
ctx.error(
`type argument passed to ${DEFINE_EMITS}() must be a function type, ` +
`a literal type with call signatures, or a reference to the above types.`,
emitsTypeDeclRaw
)
}
ctx.emitsTypeDecl = node.typeParameters.params[0]
}
if (declId) {
@ -89,36 +63,32 @@ export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> {
const emits = new Set<string>()
const node = ctx.emitsTypeDecl!
if (node.type === 'TSTypeLiteral' || node.type === 'TSInterfaceBody') {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
let hasCallSignature = false
let hasProperty = false
for (let t of members) {
if (t.type === 'TSCallSignatureDeclaration') {
extractEventNames(t.parameters[0], emits)
hasCallSignature = true
}
if (t.type === 'TSPropertySignature') {
if (t.key.type === 'Identifier' && !t.computed) {
emits.add(t.key.name)
hasProperty = true
} else if (t.key.type === 'StringLiteral' && !t.computed) {
emits.add(t.key.value)
hasProperty = true
} else {
ctx.error(`defineEmits() type cannot use computed keys.`, t.key)
}
}
}
if (hasCallSignature && hasProperty) {
if (node.type === 'TSFunctionType') {
extractEventNames(node.parameters[0], emits)
return emits
}
const elements = resolveTypeElements(ctx, node)
let hasProperty = false
for (const key in elements) {
emits.add(key)
hasProperty = true
}
if (elements.__callSignatures) {
if (hasProperty) {
ctx.error(
`defineEmits() type cannot mixed call signature and property syntax.`,
node
)
}
} else {
extractEventNames(node.parameters[0], emits)
for (const call of elements.__callSignatures) {
extractEventNames(call.parameters[0], emits)
}
}
return emits
}

View File

@ -99,7 +99,7 @@ export function genModelProps(ctx: ScriptCompileContext) {
for (const [name, { type, options }] of Object.entries(ctx.modelDecls)) {
let skipCheck = false
let runtimeTypes = type && inferRuntimeType(type, ctx.declaredTypes)
let runtimeTypes = type && inferRuntimeType(ctx, type)
if (runtimeTypes) {
const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE)

View File

@ -1,8 +1,6 @@
import {
Node,
LVal,
TSTypeLiteral,
TSInterfaceBody,
ObjectProperty,
ObjectMethod,
ObjectExpression,
@ -10,9 +8,8 @@ import {
} from '@babel/types'
import { BindingTypes, isFunctionType } from '@vue/compiler-dom'
import { ScriptCompileContext } from './context'
import { inferRuntimeType, resolveQualifiedType } from './resolveType'
import { inferRuntimeType, resolveTypeElements } from './resolveType'
import {
FromNormalScript,
resolveObjectKey,
UNKNOWN_TYPE,
concatStrings,
@ -28,8 +25,6 @@ import { processPropsDestructure } from './definePropsDestructure'
export const DEFINE_PROPS = 'defineProps'
export const WITH_DEFAULTS = 'withDefaults'
export type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
export interface PropTypeData {
key: string
type: string[]
@ -76,20 +71,7 @@ export function processDefineProps(
node
)
}
const rawDecl = node.typeParameters.params[0]
ctx.propsTypeDecl = resolveQualifiedType(
ctx,
rawDecl,
node => node.type === 'TSTypeLiteral'
) as PropsDeclType | undefined
if (!ctx.propsTypeDecl) {
ctx.error(
`type argument passed to ${DEFINE_PROPS}() must be a literal type, ` +
`or a reference to an interface or literal type.`,
rawDecl
)
}
ctx.propsTypeDecl = node.typeParameters.params[0]
}
if (declId) {
@ -176,56 +158,19 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
}
function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
// this is only called if propsTypeDecl exists
const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!)
if (!props.length) {
return
}
const propStrings: string[] = []
const hasStaticDefaults = hasStaticWithDefaults(ctx)
// this is only called if propsTypeDecl exists
const node = ctx.propsTypeDecl!
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'
) {
const key = m.key.name
let type: string[] | undefined
let skipCheck = false
if (m.type === 'TSMethodSignature') {
type = ['Function']
} else if (m.typeAnnotation) {
type = inferRuntimeType(
m.typeAnnotation.typeAnnotation,
ctx.declaredTypes
)
// skip check for result containing unknown types
if (type.includes(UNKNOWN_TYPE)) {
if (type.includes('Boolean') || type.includes('Function')) {
type = type.filter(t => t !== UNKNOWN_TYPE)
skipCheck = true
} else {
type = ['null']
}
}
}
propStrings.push(
genRuntimePropFromType(
ctx,
key,
!m.optional,
type || [`null`],
skipCheck,
hasStaticDefaults
)
)
// register bindings
ctx.bindingMetadata[key] = BindingTypes.PROPS
}
}
if (!propStrings.length) {
return
for (const prop of props) {
propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults))
// register bindings
ctx.bindingMetadata[prop.key] = BindingTypes.PROPS
}
let propsDecls = `{
@ -240,12 +185,43 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
return propsDecls
}
function resolveRuntimePropsFromType(
ctx: ScriptCompileContext,
node: Node
): PropTypeData[] {
const props: PropTypeData[] = []
const elements = resolveTypeElements(ctx, node)
for (const key in elements) {
const e = elements[key]
let type: string[] | undefined
let skipCheck = false
if (e.type === 'TSMethodSignature') {
type = ['Function']
} else if (e.typeAnnotation) {
type = inferRuntimeType(ctx, e.typeAnnotation.typeAnnotation)
// skip check for result containing unknown types
if (type.includes(UNKNOWN_TYPE)) {
if (type.includes('Boolean') || type.includes('Function')) {
type = type.filter(t => t !== UNKNOWN_TYPE)
skipCheck = true
} else {
type = ['null']
}
}
}
props.push({
key,
required: !e.optional,
type: type || [`null`],
skipCheck
})
}
return props
}
function genRuntimePropFromType(
ctx: ScriptCompileContext,
key: string,
required: boolean,
type: string[],
skipCheck: boolean,
{ key, required, type, skipCheck }: PropTypeData,
hasStaticDefaults: boolean
): string {
let defaultString: string | undefined

View File

@ -1,122 +1,227 @@
import {
Node,
Statement,
TSInterfaceBody,
TSCallSignatureDeclaration,
TSEnumDeclaration,
TSExpressionWithTypeArguments,
TSFunctionType,
TSMethodSignature,
TSPropertySignature,
TSType,
TSTypeElement
TSTypeAnnotation,
TSTypeElement,
TSTypeReference
} from '@babel/types'
import { FromNormalScript, UNKNOWN_TYPE } from './utils'
import { UNKNOWN_TYPE } from './utils'
import { ScriptCompileContext } from './context'
import { ImportBinding } from '../compileScript'
import { TSInterfaceDeclaration } from '@babel/types'
import { hasOwn } from '@vue/shared'
export function resolveQualifiedType(
export interface TypeScope {
filename: string
body: Statement[]
imports: Record<string, ImportBinding>
types: Record<string, Node>
}
type ResolvedElements = Record<
string,
TSPropertySignature | TSMethodSignature
> & {
__callSignatures?: (TSCallSignatureDeclaration | TSFunctionType)[]
}
/**
* Resolve arbitrary type node to a list of type elements that can be then
* mapped to runtime props or emits.
*/
export function resolveTypeElements(
ctx: ScriptCompileContext,
node: Node,
qualifier: (node: Node) => boolean
): Node | undefined {
if (qualifier(node)) {
return node
node: Node & { _resolvedElements?: ResolvedElements }
): ResolvedElements {
if (node._resolvedElements) {
return node._resolvedElements
}
if (node.type === 'TSTypeReference' && node.typeName.type === 'Identifier') {
const refName = node.typeName.name
const { scriptAst, scriptSetupAst } = ctx
const body = scriptAst
? [...scriptSetupAst!.body, ...scriptAst.body]
: scriptSetupAst!.body
for (let i = 0; i < body.length; i++) {
const node = body[i]
let qualified = isQualifiedType(
node,
qualifier,
refName
) as TSInterfaceBody
if (qualified) {
const extendsTypes = resolveExtendsType(body, node, qualifier)
if (extendsTypes.length) {
const bodies: TSTypeElement[] = [...qualified.body]
filterExtendsType(extendsTypes, bodies)
qualified.body = bodies
}
;(qualified as FromNormalScript<Node>).__fromNormalScript =
scriptAst && i >= scriptSetupAst!.body.length
return qualified
return (node._resolvedElements = innerResolveTypeElements(ctx, node))
}
function innerResolveTypeElements(
ctx: ScriptCompileContext,
node: Node
): ResolvedElements {
switch (node.type) {
case 'TSTypeLiteral':
return typeElementsToMap(ctx, node.members)
case 'TSInterfaceDeclaration':
return resolveInterfaceMembers(ctx, node)
case 'TSTypeAliasDeclaration':
case 'TSParenthesizedType':
return resolveTypeElements(ctx, node.typeAnnotation)
case 'TSFunctionType': {
const ret: ResolvedElements = {}
addCallSignature(ret, node)
return ret
}
case 'TSExpressionWithTypeArguments':
case 'TSTypeReference':
return resolveTypeElements(ctx, resolveTypeReference(ctx, node))
}
ctx.error(`Unsupported type in SFC macro: ${node.type}`, node)
}
function addCallSignature(
elements: ResolvedElements,
node: TSCallSignatureDeclaration | TSFunctionType
) {
if (!elements.__callSignatures) {
Object.defineProperty(elements, '__callSignatures', {
enumerable: false,
value: [node]
})
} else {
elements.__callSignatures.push(node)
}
}
function typeElementsToMap(
ctx: ScriptCompileContext,
elements: TSTypeElement[]
): ResolvedElements {
const ret: ResolvedElements = {}
for (const e of elements) {
if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
const name =
e.key.type === 'Identifier'
? e.key.name
: e.key.type === 'StringLiteral'
? e.key.value
: null
if (name && !e.computed) {
ret[name] = e
} else {
ctx.error(
`computed keys are not supported in types referenced by SFC macros.`,
e
)
}
} else if (e.type === 'TSCallSignatureDeclaration') {
addCallSignature(ret, e)
}
}
return ret
}
function resolveInterfaceMembers(
ctx: ScriptCompileContext,
node: TSInterfaceDeclaration
): ResolvedElements {
const base = typeElementsToMap(ctx, node.body.body)
if (node.extends) {
for (const ext of node.extends) {
const resolvedExt = resolveTypeElements(ctx, ext)
for (const key in resolvedExt) {
if (!hasOwn(base, key)) {
base[key] = resolvedExt[key]
}
}
}
}
return base
}
function resolveTypeReference(
ctx: ScriptCompileContext,
node: TSTypeReference | TSExpressionWithTypeArguments,
scope?: TypeScope
): Node
function resolveTypeReference(
ctx: ScriptCompileContext,
node: TSTypeReference | TSExpressionWithTypeArguments,
scope: TypeScope,
bail: false
): Node | undefined
function resolveTypeReference(
ctx: ScriptCompileContext,
node: TSTypeReference | TSExpressionWithTypeArguments,
scope = getRootScope(ctx),
bail = true
): Node | undefined {
const ref = node.type === 'TSTypeReference' ? node.typeName : node.expression
if (ref.type === 'Identifier') {
if (scope.imports[ref.name]) {
// TODO external import
} else if (scope.types[ref.name]) {
return scope.types[ref.name]
}
} else {
// TODO qualified name, e.g. Foo.Bar
// return resolveTypeReference()
}
if (bail) {
ctx.error('Failed to resolve type reference.', node)
}
}
function getRootScope(ctx: ScriptCompileContext): TypeScope {
if (ctx.scope) {
return ctx.scope
}
const body = ctx.scriptAst
? [...ctx.scriptAst.body, ...ctx.scriptSetupAst!.body]
: ctx.scriptSetupAst!.body
return (ctx.scope = {
filename: ctx.descriptor.filename,
imports: ctx.userImports,
types: recordTypes(body),
body
})
}
function recordTypes(body: Statement[]) {
const types: Record<string, Node> = Object.create(null)
for (const s of body) {
recordType(s, types)
}
return types
}
function recordType(node: Node, types: Record<string, Node>) {
switch (node.type) {
case 'TSInterfaceDeclaration':
case 'TSEnumDeclaration':
types[node.id.name] = node
break
case 'TSTypeAliasDeclaration':
types[node.id.name] = node.typeAnnotation
break
case 'ExportNamedDeclaration': {
if (node.exportKind === 'type') {
recordType(node.declaration!, types)
}
break
}
case 'VariableDeclaration': {
if (node.declare) {
for (const decl of node.declarations) {
if (decl.id.type === 'Identifier' && decl.id.typeAnnotation) {
types[decl.id.name] = (
decl.id.typeAnnotation as TSTypeAnnotation
).typeAnnotation
}
}
}
break
}
}
}
function isQualifiedType(
node: Node,
qualifier: (node: Node) => boolean,
refName: String
): Node | undefined {
if (node.type === 'TSInterfaceDeclaration' && node.id.name === refName) {
return node.body
} else if (
node.type === 'TSTypeAliasDeclaration' &&
node.id.name === refName &&
qualifier(node.typeAnnotation)
) {
return node.typeAnnotation
} else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
return isQualifiedType(node.declaration, qualifier, refName)
}
}
function resolveExtendsType(
body: Statement[],
node: Node,
qualifier: (node: Node) => boolean,
cache: Array<Node> = []
): Array<Node> {
if (node.type === 'TSInterfaceDeclaration' && node.extends) {
node.extends.forEach(extend => {
if (
extend.type === 'TSExpressionWithTypeArguments' &&
extend.expression.type === 'Identifier'
) {
for (const node of body) {
const qualified = isQualifiedType(
node,
qualifier,
extend.expression.name
)
if (qualified) {
cache.push(qualified)
resolveExtendsType(body, node, qualifier, cache)
return cache
}
}
}
})
}
return cache
}
// filter all extends types to keep the override declaration
function filterExtendsType(extendsTypes: Node[], bodies: TSTypeElement[]) {
extendsTypes.forEach(extend => {
const body = (extend as TSInterfaceBody).body
body.forEach(newBody => {
if (
newBody.type === 'TSPropertySignature' &&
newBody.key.type === 'Identifier'
) {
const name = newBody.key.name
const hasOverride = bodies.some(
seenBody =>
seenBody.type === 'TSPropertySignature' &&
seenBody.key.type === 'Identifier' &&
seenBody.key.name === name
)
if (!hasOverride) bodies.push(newBody)
}
})
})
}
export function inferRuntimeType(
node: TSType,
declaredTypes: Record<string, string[]>
ctx: ScriptCompileContext,
node: Node,
scope = getRootScope(ctx)
): string[] {
switch (node.type) {
case 'TSStringKeyword':
@ -129,10 +234,13 @@ export function inferRuntimeType(
return ['Object']
case 'TSNullKeyword':
return ['null']
case 'TSTypeLiteral': {
case 'TSTypeLiteral':
case 'TSInterfaceDeclaration': {
// TODO (nice to have) generate runtime property validation
const types = new Set<string>()
for (const m of node.members) {
const members =
node.type === 'TSTypeLiteral' ? node.members : node.body.body
for (const m of members) {
if (
m.type === 'TSCallSignatureDeclaration' ||
m.type === 'TSConstructSignatureDeclaration'
@ -166,8 +274,9 @@ export function inferRuntimeType(
case 'TSTypeReference':
if (node.typeName.type === 'Identifier') {
if (declaredTypes[node.typeName.name]) {
return declaredTypes[node.typeName.name]
const resolved = resolveTypeReference(ctx, node, scope, false)
if (resolved) {
return inferRuntimeType(ctx, resolved, scope)
}
switch (node.typeName.name) {
case 'Array':
@ -205,26 +314,21 @@ export function inferRuntimeType(
case 'NonNullable':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
ctx,
node.typeParameters.params[0],
declaredTypes
scope
).filter(t => t !== 'null')
}
break
case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType(
node.typeParameters.params[1],
declaredTypes
)
return inferRuntimeType(ctx, node.typeParameters.params[1], scope)
}
break
case 'Exclude':
case 'OmitThisParameter':
if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType(
node.typeParameters.params[0],
declaredTypes
)
return inferRuntimeType(ctx, node.typeParameters.params[0], scope)
}
break
}
@ -233,16 +337,19 @@ export function inferRuntimeType(
return [UNKNOWN_TYPE]
case 'TSParenthesizedType':
return inferRuntimeType(node.typeAnnotation, declaredTypes)
return inferRuntimeType(ctx, node.typeAnnotation, scope)
case 'TSUnionType':
return flattenTypes(node.types, declaredTypes)
return flattenTypes(ctx, node.types, scope)
case 'TSIntersectionType': {
return flattenTypes(node.types, declaredTypes).filter(
return flattenTypes(ctx, node.types, scope).filter(
t => t !== UNKNOWN_TYPE
)
}
case 'TSEnumDeclaration':
return inferEnumType(node)
case 'TSSymbolKeyword':
return ['Symbol']
@ -252,14 +359,32 @@ export function inferRuntimeType(
}
function flattenTypes(
ctx: ScriptCompileContext,
types: TSType[],
declaredTypes: Record<string, string[]>
scope: TypeScope
): string[] {
return [
...new Set(
([] as string[]).concat(
...types.map(t => inferRuntimeType(t, declaredTypes))
...types.map(t => inferRuntimeType(ctx, t, scope))
)
)
]
}
function inferEnumType(node: TSEnumDeclaration): string[] {
const types = new Set<string>()
for (const m of node.members) {
if (m.initializer) {
switch (m.initializer.type) {
case 'StringLiteral':
types.add('String')
break
case 'NumericLiteral':
types.add('Number')
break
}
}
}
return types.size ? [...types] : ['Number']
}

View File

@ -3,8 +3,6 @@ import { TS_NODE_TYPES } from '@vue/compiler-dom'
export const UNKNOWN_TYPE = 'Unknown'
export type FromNormalScript<T> = T & { __fromNormalScript?: boolean | null }
export function resolveObjectKey(node: Node, computed: boolean) {
switch (node.type) {
case 'StringLiteral':