]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): support inferring generic types (#8511)
authoredison <daiwei521@126.com>
Fri, 1 Dec 2023 13:21:12 +0000 (21:21 +0800)
committerGitHub <noreply@github.com>
Fri, 1 Dec 2023 13:21:12 +0000 (21:21 +0800)
close #8482

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

index 5f421708af012c9808433922632475355e1e5d4c..b67423e0a89d9de8db9c27cba4dd57105e86be0a 100644 (file)
@@ -455,6 +455,88 @@ describe('resolveType', () => {
     })
   })
 
+  describe('generics', () => {
+    test('generic with type literal', () => {
+      expect(
+        resolve(`
+        type Props<T> = T
+        defineProps<Props<{ foo: string }>>()
+      `).props
+      ).toStrictEqual({
+        foo: ['String']
+      })
+    })
+
+    test('generic used in intersection', () => {
+      expect(
+        resolve(`
+        type Foo = { foo: string; }
+        type Bar = { bar: number; }
+        type Props<T,U> = T & U & { baz: boolean }
+        defineProps<Props<Foo, Bar>>()
+      `).props
+      ).toStrictEqual({
+        foo: ['String'],
+        bar: ['Number'],
+        baz: ['Boolean']
+      })
+    })
+
+    test('generic type /w generic type alias', () => {
+      expect(
+        resolve(`
+        type Aliased<T> = Readonly<Partial<T>>
+        type Props<T> = Aliased<T>
+        type Foo = { foo: string; }
+        defineProps<Props<Foo>>()
+      `).props
+      ).toStrictEqual({
+        foo: ['String']
+      })
+    })
+
+    test('generic type /w aliased type literal', () => {
+      expect(
+        resolve(`
+        type Aliased<T> = { foo: T }
+        defineProps<Aliased<string>>()
+      `).props
+      ).toStrictEqual({
+        foo: ['String']
+      })
+    })
+
+    test('generic type /w interface', () => {
+      expect(
+        resolve(`
+        interface Props<T> {
+          foo: T
+        }
+        type Foo = string
+        defineProps<Props<Foo>>()
+      `).props
+      ).toStrictEqual({
+        foo: ['String']
+      })
+    })
+
+    test('generic from external-file', () => {
+      const files = {
+        '/foo.ts': 'export type P<T> = { foo: T }'
+      }
+      const { props } = resolve(
+        `
+        import { P } from './foo'
+        defineProps<P<string>>()
+      `,
+        files
+      )
+      expect(props).toStrictEqual({
+        foo: ['String']
+      })
+    })
+  })
+
   describe('external type imports', () => {
     test('relative ts', () => {
       const files = {
index d9b4dd1cb8c78dc500bc8fa7aef946a51a1ef881..c5f7681a6aa89d66c251958f041be92684555f0b 100644 (file)
@@ -118,7 +118,8 @@ interface ResolvedElements {
 export function resolveTypeElements(
   ctx: TypeResolveContext,
   node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements },
-  scope?: TypeScope
+  scope?: TypeScope,
+  typeParameters?: Record<string, Node>
 ): ResolvedElements {
   if (node._resolvedElements) {
     return node._resolvedElements
@@ -126,30 +127,37 @@ export function resolveTypeElements(
   return (node._resolvedElements = innerResolveTypeElements(
     ctx,
     node,
-    node._ownerScope || scope || ctxToScope(ctx)
+    node._ownerScope || scope || ctxToScope(ctx),
+    typeParameters
   ))
 }
 
 function innerResolveTypeElements(
   ctx: TypeResolveContext,
   node: Node,
-  scope: TypeScope
+  scope: TypeScope,
+  typeParameters?: Record<string, Node>
 ): ResolvedElements {
   switch (node.type) {
     case 'TSTypeLiteral':
-      return typeElementsToMap(ctx, node.members, scope)
+      return typeElementsToMap(ctx, node.members, scope, typeParameters)
     case 'TSInterfaceDeclaration':
-      return resolveInterfaceMembers(ctx, node, scope)
+      return resolveInterfaceMembers(ctx, node, scope, typeParameters)
     case 'TSTypeAliasDeclaration':
     case 'TSParenthesizedType':
-      return resolveTypeElements(ctx, node.typeAnnotation, scope)
+      return resolveTypeElements(
+        ctx,
+        node.typeAnnotation,
+        scope,
+        typeParameters
+      )
     case 'TSFunctionType': {
       return { props: {}, calls: [node] }
     }
     case 'TSUnionType':
     case 'TSIntersectionType':
       return mergeElements(
-        node.types.map(t => resolveTypeElements(ctx, t, scope)),
+        node.types.map(t => resolveTypeElements(ctx, t, scope, typeParameters)),
         node.type
       )
     case 'TSMappedType':
@@ -171,20 +179,57 @@ function innerResolveTypeElements(
         scope.imports[typeName]?.source === 'vue'
       ) {
         return resolveExtractPropTypes(
-          resolveTypeElements(ctx, node.typeParameters.params[0], scope),
+          resolveTypeElements(
+            ctx,
+            node.typeParameters.params[0],
+            scope,
+            typeParameters
+          ),
           scope
         )
       }
       const resolved = resolveTypeReference(ctx, node, scope)
       if (resolved) {
-        return resolveTypeElements(ctx, resolved, resolved._ownerScope)
+        const typeParams: Record<string, Node> = Object.create(null)
+        if (
+          (resolved.type === 'TSTypeAliasDeclaration' ||
+            resolved.type === 'TSInterfaceDeclaration') &&
+          resolved.typeParameters &&
+          node.typeParameters
+        ) {
+          resolved.typeParameters.params.forEach((p, i) => {
+            let param = typeParameters && typeParameters[p.name]
+            if (!param) param = node.typeParameters!.params[i]
+            typeParams[p.name] = param
+          })
+        }
+        return resolveTypeElements(
+          ctx,
+          resolved,
+          resolved._ownerScope,
+          typeParams
+        )
       } else {
         if (typeof typeName === 'string') {
+          if (typeParameters && typeParameters[typeName]) {
+            return resolveTypeElements(
+              ctx,
+              typeParameters[typeName],
+              scope,
+              typeParameters
+            )
+          }
           if (
             // @ts-ignore
             SupportedBuiltinsSet.has(typeName)
           ) {
-            return resolveBuiltin(ctx, node, typeName as any, scope)
+            return resolveBuiltin(
+              ctx,
+              node,
+              typeName as any,
+              scope,
+              typeParameters
+            )
           } else if (typeName === 'ReturnType' && node.typeParameters) {
             // limited support, only reference types
             const ret = resolveReturnType(
@@ -243,11 +288,17 @@ function innerResolveTypeElements(
 function typeElementsToMap(
   ctx: TypeResolveContext,
   elements: TSTypeElement[],
-  scope = ctxToScope(ctx)
+  scope = ctxToScope(ctx),
+  typeParameters?: Record<string, Node>
 ): ResolvedElements {
   const res: ResolvedElements = { props: {} }
   for (const e of elements) {
     if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
+      // capture generic parameters on node's scope
+      if (typeParameters) {
+        scope = createChildScope(scope)
+        Object.assign(scope.types, typeParameters)
+      }
       ;(e as MaybeWithScope)._ownerScope = scope
       const name = getId(e.key)
       if (name && !e.computed) {
@@ -323,9 +374,15 @@ function createProperty(
 function resolveInterfaceMembers(
   ctx: TypeResolveContext,
   node: TSInterfaceDeclaration & MaybeWithScope,
-  scope: TypeScope
+  scope: TypeScope,
+  typeParameters?: Record<string, Node>
 ): ResolvedElements {
-  const base = typeElementsToMap(ctx, node.body.body, node._ownerScope)
+  const base = typeElementsToMap(
+    ctx,
+    node.body.body,
+    node._ownerScope,
+    typeParameters
+  )
   if (node.extends) {
     for (const ext of node.extends) {
       if (
@@ -543,9 +600,15 @@ function resolveBuiltin(
   ctx: TypeResolveContext,
   node: TSTypeReference | TSExpressionWithTypeArguments,
   name: GetSetType<typeof SupportedBuiltinsSet>,
-  scope: TypeScope
+  scope: TypeScope,
+  typeParameters?: Record<string, Node>
 ): ResolvedElements {
-  const t = resolveTypeElements(ctx, node.typeParameters!.params[0], scope)
+  const t = resolveTypeElements(
+    ctx,
+    node.typeParameters!.params[0],
+    scope,
+    typeParameters
+  )
   switch (name) {
     case 'Partial': {
       const res: ResolvedElements = { props: {}, calls: t.calls }
@@ -1103,14 +1166,7 @@ function moduleDeclToScope(
     return node._resolvedChildScope
   }
 
-  const scope = new TypeScope(
-    parentScope.filename,
-    parentScope.source,
-    parentScope.offset,
-    Object.create(parentScope.imports),
-    Object.create(parentScope.types),
-    Object.create(parentScope.declares)
-  )
+  const scope = createChildScope(parentScope)
 
   if (node.body.type === 'TSModuleDeclaration') {
     const decl = node.body as TSModuleDeclaration & WithScope
@@ -1124,6 +1180,17 @@ function moduleDeclToScope(
   return (node._resolvedChildScope = scope)
 }
 
+function createChildScope(parentScope: TypeScope) {
+  return new TypeScope(
+    parentScope.filename,
+    parentScope.source,
+    parentScope.offset,
+    Object.create(parentScope.imports),
+    Object.create(parentScope.types),
+    Object.create(parentScope.declares)
+  )
+}
+
 const importExportRE = /^Import|^Export/
 
 function recordTypes(
@@ -1262,7 +1329,7 @@ function recordType(
       if (overwriteId || node.id) types[overwriteId || getId(node.id!)] = node
       break
     case 'TSTypeAliasDeclaration':
-      types[node.id.name] = node.typeAnnotation
+      types[node.id.name] = node.typeParameters ? node : node.typeAnnotation
       break
     case 'TSDeclareFunction':
       if (node.id) declares[node.id.name] = node