]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(compiler-sfc): rework macro type resolution
authorEvan You <yyx990803@gmail.com>
Tue, 11 Apr 2023 15:00:28 +0000 (23:00 +0800)
committerEvan You <yyx990803@gmail.com>
Tue, 11 Apr 2023 15:00:28 +0000 (23:00 +0800)
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/script/context.ts
packages/compiler-sfc/src/script/defineEmits.ts
packages/compiler-sfc/src/script/defineModel.ts
packages/compiler-sfc/src/script/defineProps.ts
packages/compiler-sfc/src/script/resolveType.ts
packages/compiler-sfc/src/script/utils.ts

index 40deb6723c601afbc88a4a8b711a4ff9f08a19b8..c828d77f6d5414efafdd67a036540c03165d75b6 100644 (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
index c5e325c743407a499311cab1b4c34772d2518a99..718f23da5cad35b8a294de7b5b6504b3a0ebd804 100644 (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
index 0a52620168acf2d1eb939db5a3b672b8e50cfea8..0e080b4fed44c19c1bd1c7952c5c0bc0bdd7f322 100644 (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
 }
 
index e2d2317b1a77dcb9efed272dd6392f39e542c92d..0584196707aa986afff18f4449f89f0aa1e505db 100644 (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)
 
index 903d038dbe0d032337ffe812e4e95455754cb9a7..bd462a2a8ea86105fb1ea27b1176f051a166181c 100644 (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) {
-  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
-    }
+  const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!)
+  if (!props.length) {
+    return
   }
 
-  if (!propStrings.length) {
-    return
+  const propStrings: string[] = []
+  const hasStaticDefaults = hasStaticWithDefaults(ctx)
+
+  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
index 73e76d13791beea838210017ec1ecaca5baad9d2..ba41757069e82eff82168d020b0874358a877e89 100644 (file)
 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
+  }
+  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)
   }
-  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
+}
+
+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]
         }
-        ;(qualified as FromNormalScript<Node>).__fromNormalScript =
-          scriptAst && i >= scriptSetupAst!.body.length
-        return qualified
       }
     }
   }
+  return base
 }
 
-function isQualifiedType(
-  node: Node,
-  qualifier: (node: Node) => boolean,
-  refName: String
+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 {
-  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)
+  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 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
-          }
-        }
-      }
-    })
+function getRootScope(ctx: ScriptCompileContext): TypeScope {
+  if (ctx.scope) {
+    return ctx.scope
   }
-  return cache
+
+  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
+  })
 }
 
-// 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)
+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(
-  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']
+}
index 1f4b139463baafdd35fedef995757ffafdb6f725..11bc011820e5e44de756dd3be4438f9ca6a72a19 100644 (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':