]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): handle type merging + fix namespace access when inferring type
authorEvan You <yyx990803@gmail.com>
Thu, 20 Apr 2023 06:13:08 +0000 (14:13 +0800)
committerEvan You <yyx990803@gmail.com>
Thu, 20 Apr 2023 06:13:08 +0000 (14:13 +0800)
close #8102

packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
packages/compiler-sfc/src/script/resolveType.ts

index 0f2a47d6b99e4f116d4525f50ca57f7983d09266..960ef592ca78a83ad91cda433b293ce1ff2ea7ca 100644 (file)
@@ -294,6 +294,84 @@ describe('resolveType', () => {
     })
   })
 
+  test('interface merging', () => {
+    expect(
+      resolve(`
+      interface Foo {
+        a: string
+      }
+      interface Foo {
+        b: number
+      }
+      defineProps<{
+        foo: Foo['a'],
+        bar: Foo['b']
+      }>()
+    `).props
+    ).toStrictEqual({
+      foo: ['String'],
+      bar: ['Number']
+    })
+  })
+
+  test('namespace merging', () => {
+    expect(
+      resolve(`
+      namespace Foo {
+        export type A = string
+      }
+      namespace Foo {
+        export type B = number
+      }
+      defineProps<{
+        foo: Foo.A,
+        bar: Foo.B
+      }>()
+    `).props
+    ).toStrictEqual({
+      foo: ['String'],
+      bar: ['Number']
+    })
+  })
+
+  test('namespace merging with other types', () => {
+    expect(
+      resolve(`
+      namespace Foo {
+        export type A = string
+      }
+      interface Foo {
+        b: number
+      }
+      defineProps<{
+        foo: Foo.A,
+        bar: Foo['b']
+      }>()
+    `).props
+    ).toStrictEqual({
+      foo: ['String'],
+      bar: ['Number']
+    })
+  })
+
+  test('enum merging', () => {
+    expect(
+      resolve(`
+      enum Foo {
+        A = 1
+      }
+      enum Foo {
+        B = 'hi'
+      }
+      defineProps<{
+        foo: Foo
+      }>()
+    `).props
+    ).toStrictEqual({
+      foo: ['Number', 'String']
+    })
+  })
+
   describe('external type imports', () => {
     const files = {
       '/foo.ts': 'export type P = { foo: number }',
@@ -436,6 +514,34 @@ describe('resolveType', () => {
       })
       expect(deps && [...deps]).toStrictEqual(Object.keys(files))
     })
+
+    test('global types with ambient references', () => {
+      const files = {
+        // with references
+        '/backend.d.ts': `
+          declare namespace App.Data {
+            export type AircraftData = {
+              id: string
+              manufacturer: App.Data.Listings.ManufacturerData
+            }
+          }
+          declare namespace App.Data.Listings {
+            export type ManufacturerData = {
+              id: string
+            }
+          }
+        `
+      }
+
+      const { props } = resolve(`defineProps<App.Data.AircraftData>()`, files, {
+        globalTypeFiles: Object.keys(files)
+      })
+
+      expect(props).toStrictEqual({
+        id: ['String'],
+        manufacturer: ['Object']
+      })
+    })
   })
 
   describe('errors', () => {
index 022c259f79e63352cf48c8c2ca6a6242445c9c07..d34c80469709cf204b3364b5a3d6cb1c6ed48cdf 100644 (file)
@@ -65,11 +65,14 @@ export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext
 
 type Import = Pick<ImportBinding, 'source' | 'imported'>
 
-type ScopeTypeNode = Node & {
-  // scope types always has ownerScope attached
+interface WithScope {
   _ownerScope: TypeScope
 }
 
+// scope types always has ownerScope attached
+type ScopeTypeNode = Node &
+  WithScope & { _ns?: TSModuleDeclaration & WithScope }
+
 export interface TypeScope {
   filename: string
   source: string
@@ -79,7 +82,7 @@ export interface TypeScope {
   exportedTypes: Record<string, ScopeTypeNode>
 }
 
-export interface WithScope {
+export interface MaybeWithScope {
   _ownerScope?: TypeScope
 }
 
@@ -100,7 +103,7 @@ interface ResolvedElements {
  */
 export function resolveTypeElements(
   ctx: TypeResolveContext,
-  node: Node & WithScope & { _resolvedElements?: ResolvedElements },
+  node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements },
   scope?: TypeScope
 ): ResolvedElements {
   if (node._resolvedElements) {
@@ -177,7 +180,7 @@ function typeElementsToMap(
   const res: ResolvedElements = { props: {} }
   for (const e of elements) {
     if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
-      ;(e as WithScope)._ownerScope = scope
+      ;(e as MaybeWithScope)._ownerScope = scope
       const name = getId(e.key)
       if (name && !e.computed) {
         res.props[name] = e as ResolvedElements['props'][string]
@@ -248,7 +251,7 @@ function createProperty(
 
 function resolveInterfaceMembers(
   ctx: TypeResolveContext,
-  node: TSInterfaceDeclaration & WithScope,
+  node: TSInterfaceDeclaration & MaybeWithScope,
   scope: TypeScope
 ): ResolvedElements {
   const base = typeElementsToMap(ctx, node.body.body, node._ownerScope)
@@ -289,7 +292,7 @@ function resolveIndexType(
   ctx: TypeResolveContext,
   node: TSIndexedAccessType,
   scope: TypeScope
-): (TSType & WithScope)[] {
+): (TSType & MaybeWithScope)[] {
   if (node.indexType.type === 'TSNumberKeyword') {
     return resolveArrayElementType(ctx, node.objectType, scope)
   }
@@ -308,7 +311,7 @@ function resolveIndexType(
   for (const key of keys) {
     const targetType = resolved.props[key]?.typeAnnotation?.typeAnnotation
     if (targetType) {
-      ;(targetType as TSType & WithScope)._ownerScope =
+      ;(targetType as TSType & MaybeWithScope)._ownerScope =
         resolved.props[key]._ownerScope
       types.push(targetType)
     }
@@ -532,22 +535,22 @@ function innerResolveTypeReference(
       }
     }
   } else {
-    const ns = innerResolveTypeReference(
-      ctx,
-      scope,
-      name[0],
-      node,
-      onlyExported
-    )
-    if (ns && ns.type === 'TSModuleDeclaration') {
-      const childScope = moduleDeclToScope(ns, scope)
-      return innerResolveTypeReference(
-        ctx,
-        childScope,
-        name.length > 2 ? name.slice(1) : name[name.length - 1],
-        node,
-        !ns.declare
-      )
+    let ns = innerResolveTypeReference(ctx, scope, name[0], node, onlyExported)
+    if (ns) {
+      if (ns.type !== 'TSModuleDeclaration') {
+        // namespace merged with other types, attached as _ns
+        ns = ns._ns
+      }
+      if (ns) {
+        const childScope = moduleDeclToScope(ns, ns._ownerScope || scope)
+        return innerResolveTypeReference(
+          ctx,
+          childScope,
+          name.length > 2 ? name.slice(1) : name[name.length - 1],
+          node,
+          !ns.declare
+        )
+      }
     }
   }
 }
@@ -771,7 +774,6 @@ export function fileToScope(
     exportedTypes: Object.create(null)
   }
   recordTypes(body, scope, asGlobal)
-
   fileToScopeCache.set(filename, scope)
   return scope
 }
@@ -858,10 +860,21 @@ function moduleDeclToScope(
   }
   const scope: TypeScope = {
     ...parentScope,
+    imports: Object.create(parentScope.imports),
+    // TODO this seems wrong
     types: Object.create(parentScope.types),
-    imports: Object.create(parentScope.imports)
+    exportedTypes: Object.create(null)
+  }
+
+  if (node.body.type === 'TSModuleDeclaration') {
+    const decl = node.body as TSModuleDeclaration & WithScope
+    decl._ownerScope = scope
+    const id = getId(decl.id)
+    scope.types[id] = scope.exportedTypes[id] = decl
+  } else {
+    recordTypes(node.body.body, scope)
   }
-  recordTypes((node.body as TSModuleBlock).body, scope)
+
   return (node._resolvedChildScope = scope)
 }
 
@@ -923,7 +936,9 @@ function recordTypes(body: Statement[], scope: TypeScope, asGlobal = false) {
     }
   }
   for (const key of Object.keys(types)) {
-    types[key]._ownerScope = scope
+    const node = types[key]
+    node._ownerScope = scope
+    if (node._ns) node._ns._ownerScope = scope
   }
 }
 
@@ -931,12 +946,42 @@ function recordType(node: Node, types: Record<string, Node>) {
   switch (node.type) {
     case 'TSInterfaceDeclaration':
     case 'TSEnumDeclaration':
-    case 'TSModuleDeclaration':
-    case 'ClassDeclaration': {
-      const id = node.id.type === 'Identifier' ? node.id.name : node.id.value
-      types[id] = node
+    case 'TSModuleDeclaration': {
+      const id = getId(node.id)
+      let existing = types[id]
+      if (existing) {
+        if (node.type === 'TSModuleDeclaration') {
+          if (existing.type === 'TSModuleDeclaration') {
+            mergeNamespaces(existing as typeof node, node)
+          } else {
+            attachNamespace(existing, node)
+          }
+          break
+        }
+        if (existing.type === 'TSModuleDeclaration') {
+          // replace and attach namespace
+          types[id] = node
+          attachNamespace(node, existing)
+          break
+        }
+
+        if (existing.type !== node.type) {
+          // type-level error
+          break
+        }
+        if (node.type === 'TSInterfaceDeclaration') {
+          ;(existing as typeof node).body.body.push(...node.body.body)
+        } else {
+          ;(existing as typeof node).members.push(...node.members)
+        }
+      } else {
+        types[id] = node
+      }
       break
     }
+    case 'ClassDeclaration':
+      types[getId(node.id)] = node
+      break
     case 'TSTypeAliasDeclaration':
       types[node.id.name] = node.typeAnnotation
       break
@@ -955,6 +1000,47 @@ function recordType(node: Node, types: Record<string, Node>) {
   }
 }
 
+function mergeNamespaces(to: TSModuleDeclaration, from: TSModuleDeclaration) {
+  const toBody = to.body
+  const fromBody = from.body
+  if (toBody.type === 'TSModuleDeclaration') {
+    if (fromBody.type === 'TSModuleDeclaration') {
+      // both decl
+      mergeNamespaces(toBody, fromBody)
+    } else {
+      // to: decl -> from: block
+      fromBody.body.push({
+        type: 'ExportNamedDeclaration',
+        declaration: toBody,
+        exportKind: 'type',
+        specifiers: []
+      })
+    }
+  } else if (fromBody.type === 'TSModuleDeclaration') {
+    // to: block <- from: decl
+    toBody.body.push({
+      type: 'ExportNamedDeclaration',
+      declaration: fromBody,
+      exportKind: 'type',
+      specifiers: []
+    })
+  } else {
+    // both block
+    toBody.body.push(...fromBody.body)
+  }
+}
+
+function attachNamespace(
+  to: Node & { _ns?: TSModuleDeclaration },
+  ns: TSModuleDeclaration
+) {
+  if (!to._ns) {
+    to._ns = ns
+  } else {
+    mergeNamespaces(to._ns, ns)
+  }
+}
+
 export function recordImports(body: Statement[]) {
   const imports: TypeScope['imports'] = Object.create(null)
   for (const s of body) {
@@ -977,7 +1063,7 @@ function recordImport(node: Node, imports: TypeScope['imports']) {
 
 export function inferRuntimeType(
   ctx: TypeResolveContext,
-  node: Node & WithScope,
+  node: Node & MaybeWithScope,
   scope = node._ownerScope || ctxToScope(ctx)
 ): string[] {
   switch (node.type) {
@@ -1035,11 +1121,11 @@ export function inferRuntimeType(
       }
 
     case 'TSTypeReference':
+      const resolved = resolveTypeReference(ctx, node, scope)
+      if (resolved) {
+        return inferRuntimeType(ctx, resolved, resolved._ownerScope)
+      }
       if (node.typeName.type === 'Identifier') {
-        const resolved = resolveTypeReference(ctx, node, scope)
-        if (resolved) {
-          return inferRuntimeType(ctx, resolved, resolved._ownerScope)
-        }
         switch (node.typeName.name) {
           case 'Array':
           case 'Function':