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

View File

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

View File

@ -1,22 +1,10 @@
import { import { Identifier, LVal, Node, RestElement } from '@babel/types'
Identifier, import { isCallOf } from './utils'
LVal,
Node,
RestElement,
TSFunctionType,
TSInterfaceBody,
TSTypeLiteral
} from '@babel/types'
import { FromNormalScript, isCallOf } from './utils'
import { ScriptCompileContext } from './context' import { ScriptCompileContext } from './context'
import { resolveQualifiedType } from './resolveType' import { resolveTypeElements } from './resolveType'
export const DEFINE_EMITS = 'defineEmits' export const DEFINE_EMITS = 'defineEmits'
export type EmitsDeclType = FromNormalScript<
TSFunctionType | TSTypeLiteral | TSInterfaceBody
>
export function processDefineEmits( export function processDefineEmits(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
node: Node, node: Node,
@ -38,21 +26,7 @@ export function processDefineEmits(
node node
) )
} }
ctx.emitsTypeDecl = node.typeParameters.params[0]
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
)
}
} }
if (declId) { if (declId) {
@ -89,36 +63,32 @@ export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> { function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> {
const emits = new Set<string>() const emits = new Set<string>()
const node = ctx.emitsTypeDecl! const node = ctx.emitsTypeDecl!
if (node.type === 'TSTypeLiteral' || node.type === 'TSInterfaceBody') {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body if (node.type === 'TSFunctionType') {
let hasCallSignature = false extractEventNames(node.parameters[0], emits)
return emits
}
const elements = resolveTypeElements(ctx, node)
let hasProperty = false let hasProperty = false
for (let t of members) { for (const key in elements) {
if (t.type === 'TSCallSignatureDeclaration') { emits.add(key)
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 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 (elements.__callSignatures) {
if (hasCallSignature && hasProperty) { if (hasProperty) {
ctx.error( ctx.error(
`defineEmits() type cannot mixed call signature and property syntax.`, `defineEmits() type cannot mixed call signature and property syntax.`,
node node
) )
} }
} else { for (const call of elements.__callSignatures) {
extractEventNames(node.parameters[0], emits) extractEventNames(call.parameters[0], emits)
} }
}
return emits return emits
} }

View File

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

View File

@ -1,8 +1,6 @@
import { import {
Node, Node,
LVal, LVal,
TSTypeLiteral,
TSInterfaceBody,
ObjectProperty, ObjectProperty,
ObjectMethod, ObjectMethod,
ObjectExpression, ObjectExpression,
@ -10,9 +8,8 @@ import {
} from '@babel/types' } from '@babel/types'
import { BindingTypes, isFunctionType } from '@vue/compiler-dom' import { BindingTypes, isFunctionType } from '@vue/compiler-dom'
import { ScriptCompileContext } from './context' import { ScriptCompileContext } from './context'
import { inferRuntimeType, resolveQualifiedType } from './resolveType' import { inferRuntimeType, resolveTypeElements } from './resolveType'
import { import {
FromNormalScript,
resolveObjectKey, resolveObjectKey,
UNKNOWN_TYPE, UNKNOWN_TYPE,
concatStrings, concatStrings,
@ -28,8 +25,6 @@ import { processPropsDestructure } from './definePropsDestructure'
export const DEFINE_PROPS = 'defineProps' export const DEFINE_PROPS = 'defineProps'
export const WITH_DEFAULTS = 'withDefaults' export const WITH_DEFAULTS = 'withDefaults'
export type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
export interface PropTypeData { export interface PropTypeData {
key: string key: string
type: string[] type: string[]
@ -76,20 +71,7 @@ export function processDefineProps(
node node
) )
} }
ctx.propsTypeDecl = node.typeParameters.params[0]
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
)
}
} }
if (declId) { if (declId) {
@ -176,56 +158,19 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
} }
function genRuntimePropsFromTypes(ctx: ScriptCompileContext) { 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 propStrings: string[] = []
const hasStaticDefaults = hasStaticWithDefaults(ctx) const hasStaticDefaults = hasStaticWithDefaults(ctx)
// this is only called if propsTypeDecl exists for (const prop of props) {
const node = ctx.propsTypeDecl! propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults))
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 // register bindings
ctx.bindingMetadata[key] = BindingTypes.PROPS ctx.bindingMetadata[prop.key] = BindingTypes.PROPS
}
}
if (!propStrings.length) {
return
} }
let propsDecls = `{ let propsDecls = `{
@ -240,12 +185,43 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
return propsDecls 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( function genRuntimePropFromType(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
key: string, { key, required, type, skipCheck }: PropTypeData,
required: boolean,
type: string[],
skipCheck: boolean,
hasStaticDefaults: boolean hasStaticDefaults: boolean
): string { ): string {
let defaultString: string | undefined let defaultString: string | undefined

View File

@ -1,122 +1,227 @@
import { import {
Node, Node,
Statement, Statement,
TSInterfaceBody, TSCallSignatureDeclaration,
TSEnumDeclaration,
TSExpressionWithTypeArguments,
TSFunctionType,
TSMethodSignature,
TSPropertySignature,
TSType, TSType,
TSTypeElement TSTypeAnnotation,
TSTypeElement,
TSTypeReference
} from '@babel/types' } from '@babel/types'
import { FromNormalScript, UNKNOWN_TYPE } from './utils' import { UNKNOWN_TYPE } from './utils'
import { ScriptCompileContext } from './context' 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, ctx: ScriptCompileContext,
node: Node, node: Node & { _resolvedElements?: ResolvedElements }
qualifier: (node: Node) => boolean ): ResolvedElements {
): Node | undefined { if (node._resolvedElements) {
if (qualifier(node)) { return node._resolvedElements
return node
} }
if (node.type === 'TSTypeReference' && node.typeName.type === 'Identifier') { return (node._resolvedElements = innerResolveTypeElements(ctx, node))
const refName = node.typeName.name }
const { scriptAst, scriptSetupAst } = ctx
const body = scriptAst function innerResolveTypeElements(
? [...scriptSetupAst!.body, ...scriptAst.body] ctx: ScriptCompileContext,
: scriptSetupAst!.body node: Node
for (let i = 0; i < body.length; i++) { ): ResolvedElements {
const node = body[i] switch (node.type) {
let qualified = isQualifiedType( case 'TSTypeLiteral':
node, return typeElementsToMap(ctx, node.members)
qualifier, case 'TSInterfaceDeclaration':
refName return resolveInterfaceMembers(ctx, node)
) as TSInterfaceBody case 'TSTypeAliasDeclaration':
if (qualified) { case 'TSParenthesizedType':
const extendsTypes = resolveExtendsType(body, node, qualifier) return resolveTypeElements(ctx, node.typeAnnotation)
if (extendsTypes.length) { case 'TSFunctionType': {
const bodies: TSTypeElement[] = [...qualified.body] const ret: ResolvedElements = {}
filterExtendsType(extendsTypes, bodies) addCallSignature(ret, node)
qualified.body = bodies return ret
}
;(qualified as FromNormalScript<Node>).__fromNormalScript =
scriptAst && i >= scriptSetupAst!.body.length
return qualified
} }
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 isQualifiedType( function typeElementsToMap(
node: Node, ctx: ScriptCompileContext,
qualifier: (node: Node) => boolean, elements: TSTypeElement[]
refName: String ): ResolvedElements {
): Node | undefined { const ret: ResolvedElements = {}
if (node.type === 'TSInterfaceDeclaration' && node.id.name === refName) { for (const e of elements) {
return node.body if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
} else if ( const name =
node.type === 'TSTypeAliasDeclaration' && e.key.type === 'Identifier'
node.id.name === refName && ? e.key.name
qualifier(node.typeAnnotation) : e.key.type === 'StringLiteral'
) { ? e.key.value
return node.typeAnnotation : null
} else if (node.type === 'ExportNamedDeclaration' && node.declaration) { if (name && !e.computed) {
return isQualifiedType(node.declaration, qualifier, refName) ret[name] = e
} } else {
} ctx.error(
`computed keys are not supported in types referenced by SFC macros.`,
function resolveExtendsType( e
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) } else if (e.type === 'TSCallSignatureDeclaration') {
resolveExtendsType(body, node, qualifier, cache) addCallSignature(ret, e)
return cache
} }
} }
} return ret
})
}
return cache
} }
// filter all extends types to keep the override declaration function resolveInterfaceMembers(
function filterExtendsType(extendsTypes: Node[], bodies: TSTypeElement[]) { ctx: ScriptCompileContext,
extendsTypes.forEach(extend => { node: TSInterfaceDeclaration
const body = (extend as TSInterfaceBody).body ): ResolvedElements {
body.forEach(newBody => { const base = typeElementsToMap(ctx, node.body.body)
if ( if (node.extends) {
newBody.type === 'TSPropertySignature' && for (const ext of node.extends) {
newBody.key.type === 'Identifier' const resolvedExt = resolveTypeElements(ctx, ext)
) { for (const key in resolvedExt) {
const name = newBody.key.name if (!hasOwn(base, key)) {
const hasOverride = bodies.some( base[key] = resolvedExt[key]
seenBody =>
seenBody.type === 'TSPropertySignature' &&
seenBody.key.type === 'Identifier' &&
seenBody.key.name === name
)
if (!hasOverride) bodies.push(newBody)
} }
}
}
}
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
}
}
} }
export function inferRuntimeType( export function inferRuntimeType(
node: TSType, ctx: ScriptCompileContext,
declaredTypes: Record<string, string[]> node: Node,
scope = getRootScope(ctx)
): string[] { ): string[] {
switch (node.type) { switch (node.type) {
case 'TSStringKeyword': case 'TSStringKeyword':
@ -129,10 +234,13 @@ export function inferRuntimeType(
return ['Object'] return ['Object']
case 'TSNullKeyword': case 'TSNullKeyword':
return ['null'] return ['null']
case 'TSTypeLiteral': { case 'TSTypeLiteral':
case 'TSInterfaceDeclaration': {
// TODO (nice to have) generate runtime property validation // TODO (nice to have) generate runtime property validation
const types = new Set<string>() 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 ( if (
m.type === 'TSCallSignatureDeclaration' || m.type === 'TSCallSignatureDeclaration' ||
m.type === 'TSConstructSignatureDeclaration' m.type === 'TSConstructSignatureDeclaration'
@ -166,8 +274,9 @@ export function inferRuntimeType(
case 'TSTypeReference': case 'TSTypeReference':
if (node.typeName.type === 'Identifier') { if (node.typeName.type === 'Identifier') {
if (declaredTypes[node.typeName.name]) { const resolved = resolveTypeReference(ctx, node, scope, false)
return declaredTypes[node.typeName.name] if (resolved) {
return inferRuntimeType(ctx, resolved, scope)
} }
switch (node.typeName.name) { switch (node.typeName.name) {
case 'Array': case 'Array':
@ -205,26 +314,21 @@ export function inferRuntimeType(
case 'NonNullable': case 'NonNullable':
if (node.typeParameters && node.typeParameters.params[0]) { if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType( return inferRuntimeType(
ctx,
node.typeParameters.params[0], node.typeParameters.params[0],
declaredTypes scope
).filter(t => t !== 'null') ).filter(t => t !== 'null')
} }
break break
case 'Extract': case 'Extract':
if (node.typeParameters && node.typeParameters.params[1]) { if (node.typeParameters && node.typeParameters.params[1]) {
return inferRuntimeType( return inferRuntimeType(ctx, node.typeParameters.params[1], scope)
node.typeParameters.params[1],
declaredTypes
)
} }
break break
case 'Exclude': case 'Exclude':
case 'OmitThisParameter': case 'OmitThisParameter':
if (node.typeParameters && node.typeParameters.params[0]) { if (node.typeParameters && node.typeParameters.params[0]) {
return inferRuntimeType( return inferRuntimeType(ctx, node.typeParameters.params[0], scope)
node.typeParameters.params[0],
declaredTypes
)
} }
break break
} }
@ -233,16 +337,19 @@ export function inferRuntimeType(
return [UNKNOWN_TYPE] return [UNKNOWN_TYPE]
case 'TSParenthesizedType': case 'TSParenthesizedType':
return inferRuntimeType(node.typeAnnotation, declaredTypes) return inferRuntimeType(ctx, node.typeAnnotation, scope)
case 'TSUnionType': case 'TSUnionType':
return flattenTypes(node.types, declaredTypes) return flattenTypes(ctx, node.types, scope)
case 'TSIntersectionType': { case 'TSIntersectionType': {
return flattenTypes(node.types, declaredTypes).filter( return flattenTypes(ctx, node.types, scope).filter(
t => t !== UNKNOWN_TYPE t => t !== UNKNOWN_TYPE
) )
} }
case 'TSEnumDeclaration':
return inferEnumType(node)
case 'TSSymbolKeyword': case 'TSSymbolKeyword':
return ['Symbol'] return ['Symbol']
@ -252,14 +359,32 @@ export function inferRuntimeType(
} }
function flattenTypes( function flattenTypes(
ctx: ScriptCompileContext,
types: TSType[], types: TSType[],
declaredTypes: Record<string, string[]> scope: TypeScope
): string[] { ): string[] {
return [ return [
...new Set( ...new Set(
([] as string[]).concat( ([] 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 const UNKNOWN_TYPE = 'Unknown'
export type FromNormalScript<T> = T & { __fromNormalScript?: boolean | null }
export function resolveObjectKey(node: Node, computed: boolean) { export function resolveObjectKey(node: Node, computed: boolean) {
switch (node.type) { switch (node.type) {
case 'StringLiteral': case 'StringLiteral':