From 7e0dbfb3838b638609f194679627fbda5aa2580f Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 7 Jun 2023 11:00:10 +0800 Subject: [PATCH 1/6] fix(compiler-sfc): support infer generic type --- .../compileScript/resolveType.spec.ts | 15 ++++++ .../compiler-sfc/src/script/resolveType.ts | 49 ++++++++++++++++--- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 5f421708af0..a87354b7eba 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -347,6 +347,21 @@ describe('resolveType', () => { }) }) + test('generic type', () => { + expect( + resolve(` + type Foo = { foo: string; } + type Bar = { bar: number; } + type Props = T & U & { baz: boolean } + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'], + bar: ['Number'], + baz: ['Boolean'] + }) + }) + test('namespace merging', () => { expect( resolve(` diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index d9b4dd1cb8c..38216d71cfd 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -118,7 +118,8 @@ interface ResolvedElements { export function resolveTypeElements( ctx: TypeResolveContext, node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements }, - scope?: TypeScope + scope?: TypeScope, + typeParameters?: Record ): ResolvedElements { if (node._resolvedElements) { return node._resolvedElements @@ -126,14 +127,16 @@ 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 ): ResolvedElements { switch (node.type) { case 'TSTypeLiteral': @@ -142,14 +145,31 @@ function innerResolveTypeElements( return resolveInterfaceMembers(ctx, node, scope) 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': + const types = typeParameters + ? node.types.map(t => { + if ( + t.type === 'TSTypeReference' && + t.typeName.type === 'Identifier' && + typeParameters[t.typeName.name] + ) { + return typeParameters[t.typeName.name] + } + return t + }) + : node.types return mergeElements( - node.types.map(t => resolveTypeElements(ctx, t, scope)), + types.map(t => resolveTypeElements(ctx, t, scope)), node.type ) case 'TSMappedType': @@ -177,7 +197,22 @@ function innerResolveTypeElements( } const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { - return resolveTypeElements(ctx, resolved, resolved._ownerScope) + const typeParameters: Record = Object.create(null) + if ( + resolved.type === 'TSTypeAliasDeclaration' && + resolved.typeParameters && + node.typeParameters + ) { + resolved.typeParameters.params.forEach((p, i) => { + typeParameters[p.name] = node.typeParameters!.params[i] + }) + } + return resolveTypeElements( + ctx, + resolved, + resolved._ownerScope, + typeParameters + ) } else { if (typeof typeName === 'string') { if ( @@ -1262,7 +1297,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 From b6f991df10a17970b7f3658c4ca3d1ce07d285ec Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 14 Aug 2023 16:50:10 +0800 Subject: [PATCH 2/6] chore: more case --- .../compileScript/resolveType.spec.ts | 13 ++++++++ .../compiler-sfc/src/script/resolveType.ts | 31 +++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index a87354b7eba..e02857f3edb 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -362,6 +362,19 @@ describe('resolveType', () => { }) }) + test('generic type /w type alias', () => { + expect( + resolve(` + type Aliased = Readonly> + type Props = Aliased + type Foo = { foo: string; } + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + test('namespace merging', () => { expect( resolve(` diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 38216d71cfd..14f3be89c2b 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -197,21 +197,23 @@ function innerResolveTypeElements( } const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { - const typeParameters: Record = Object.create(null) + const typeParams: Record = Object.create(null) if ( resolved.type === 'TSTypeAliasDeclaration' && resolved.typeParameters && node.typeParameters ) { resolved.typeParameters.params.forEach((p, i) => { - typeParameters[p.name] = node.typeParameters!.params[i] + let param = typeParameters && typeParameters[p.name] + if (!param) param = node.typeParameters!.params[i] + typeParams[p.name] = param }) } return resolveTypeElements( ctx, resolved, resolved._ownerScope, - typeParameters + typeParams ) } else { if (typeof typeName === 'string') { @@ -219,7 +221,13 @@ function innerResolveTypeElements( // @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( @@ -578,9 +586,20 @@ function resolveBuiltin( ctx: TypeResolveContext, node: TSTypeReference | TSExpressionWithTypeArguments, name: GetSetType, - scope: TypeScope + scope: TypeScope, + typeParameters?: Record ): ResolvedElements { - const t = resolveTypeElements(ctx, node.typeParameters!.params[0], scope) + let param: Node = node.typeParameters!.params[0] + if ( + param.type === 'TSTypeReference' && + param.typeName.type === 'Identifier' && + typeParameters && + typeParameters[param.typeName.name] + ) { + param = typeParameters[param.typeName.name] + } + + const t = resolveTypeElements(ctx, param, scope, typeParameters) switch (name) { case 'Partial': { const res: ResolvedElements = { props: {}, calls: t.calls } From 1d9618cc11f6b602492e93f48752cfde982b1877 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 17 Aug 2023 16:34:11 +0800 Subject: [PATCH 3/6] chore: more case --- .../__tests__/compileScript/resolveType.spec.ts | 14 +++++++++++++- packages/compiler-sfc/src/script/resolveType.ts | 8 ++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index e02857f3edb..bebaa7a65e8 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -362,7 +362,7 @@ describe('resolveType', () => { }) }) - test('generic type /w type alias', () => { + test('generic type /w generic type alias', () => { expect( resolve(` type Aliased = Readonly> @@ -375,6 +375,18 @@ describe('resolveType', () => { }) }) + test('generic type /w simple type alias', () => { + expect( + resolve(` + type Aliased = T + type Foo = { foo: string; } + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + test('namespace merging', () => { expect( resolve(` diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 14f3be89c2b..c26533df937 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -217,6 +217,14 @@ function innerResolveTypeElements( ) } else { if (typeof typeName === 'string') { + if (typeParameters && typeParameters[typeName]) { + return resolveTypeElements( + ctx, + typeParameters[typeName], + scope, + typeParameters + ) + } if ( // @ts-ignore SupportedBuiltinsSet.has(typeName) From acc6ebb84bc891c829f5dc9f5aab9307fbc56ad7 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 17 Aug 2023 16:43:47 +0800 Subject: [PATCH 4/6] chore: more case --- packages/compiler-sfc/src/script/resolveType.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index c26533df937..557ecc125dd 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -597,17 +597,7 @@ function resolveBuiltin( scope: TypeScope, typeParameters?: Record ): ResolvedElements { - let param: Node = node.typeParameters!.params[0] - if ( - param.type === 'TSTypeReference' && - param.typeName.type === 'Identifier' && - typeParameters && - typeParameters[param.typeName.name] - ) { - param = typeParameters[param.typeName.name] - } - - const t = resolveTypeElements(ctx, param, scope, typeParameters) + const t = resolveTypeElements(ctx, node.typeParameters!.params[0], scope, typeParameters) switch (name) { case 'Partial': { const res: ResolvedElements = { props: {}, calls: t.calls } From 8a8a0b6f650083995e50b9ab04f5a8cf9ee20c12 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 17 Aug 2023 16:51:08 +0800 Subject: [PATCH 5/6] chore: format --- packages/compiler-sfc/src/script/resolveType.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 557ecc125dd..f3f6992c83f 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -597,7 +597,12 @@ function resolveBuiltin( scope: TypeScope, typeParameters?: Record ): ResolvedElements { - const t = resolveTypeElements(ctx, node.typeParameters!.params[0], scope, typeParameters) + const t = resolveTypeElements( + ctx, + node.typeParameters!.params[0], + scope, + typeParameters + ) switch (name) { case 'Partial': { const res: ResolvedElements = { props: {}, calls: t.calls } From 3522d6f3ebbb91bceaabd7cd4ad27576478b35ea Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 1 Dec 2023 21:16:39 +0800 Subject: [PATCH 6/6] fix: support more cases --- .../compileScript/resolveType.spec.ts | 122 ++++++++++++------ .../compiler-sfc/src/script/resolveType.ts | 66 ++++++---- 2 files changed, 120 insertions(+), 68 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index bebaa7a65e8..b67423e0a89 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -347,46 +347,6 @@ describe('resolveType', () => { }) }) - test('generic type', () => { - expect( - resolve(` - type Foo = { foo: string; } - type Bar = { bar: number; } - type Props = T & U & { baz: boolean } - defineProps>() - `).props - ).toStrictEqual({ - foo: ['String'], - bar: ['Number'], - baz: ['Boolean'] - }) - }) - - test('generic type /w generic type alias', () => { - expect( - resolve(` - type Aliased = Readonly> - type Props = Aliased - type Foo = { foo: string; } - defineProps>() - `).props - ).toStrictEqual({ - foo: ['String'] - }) - }) - - test('generic type /w simple type alias', () => { - expect( - resolve(` - type Aliased = T - type Foo = { foo: string; } - defineProps>() - `).props - ).toStrictEqual({ - foo: ['String'] - }) - }) - test('namespace merging', () => { expect( resolve(` @@ -495,6 +455,88 @@ describe('resolveType', () => { }) }) + describe('generics', () => { + test('generic with type literal', () => { + expect( + resolve(` + type Props = T + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + + test('generic used in intersection', () => { + expect( + resolve(` + type Foo = { foo: string; } + type Bar = { bar: number; } + type Props = T & U & { baz: boolean } + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'], + bar: ['Number'], + baz: ['Boolean'] + }) + }) + + test('generic type /w generic type alias', () => { + expect( + resolve(` + type Aliased = Readonly> + type Props = Aliased + type Foo = { foo: string; } + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + + test('generic type /w aliased type literal', () => { + expect( + resolve(` + type Aliased = { foo: T } + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + + test('generic type /w interface', () => { + expect( + resolve(` + interface Props { + foo: T + } + type Foo = string + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + + test('generic from external-file', () => { + const files = { + '/foo.ts': 'export type P = { foo: T }' + } + const { props } = resolve( + ` + import { P } from './foo' + defineProps>() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['String'] + }) + }) + }) + describe('external type imports', () => { test('relative ts', () => { const files = { diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index f3f6992c83f..c5f7681a6aa 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -140,9 +140,9 @@ function innerResolveTypeElements( ): 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( @@ -156,20 +156,8 @@ function innerResolveTypeElements( } case 'TSUnionType': case 'TSIntersectionType': - const types = typeParameters - ? node.types.map(t => { - if ( - t.type === 'TSTypeReference' && - t.typeName.type === 'Identifier' && - typeParameters[t.typeName.name] - ) { - return typeParameters[t.typeName.name] - } - return t - }) - : node.types return mergeElements( - types.map(t => resolveTypeElements(ctx, t, scope)), + node.types.map(t => resolveTypeElements(ctx, t, scope, typeParameters)), node.type ) case 'TSMappedType': @@ -191,7 +179,12 @@ 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 ) } @@ -199,7 +192,8 @@ function innerResolveTypeElements( if (resolved) { const typeParams: Record = Object.create(null) if ( - resolved.type === 'TSTypeAliasDeclaration' && + (resolved.type === 'TSTypeAliasDeclaration' || + resolved.type === 'TSInterfaceDeclaration') && resolved.typeParameters && node.typeParameters ) { @@ -294,11 +288,17 @@ function innerResolveTypeElements( function typeElementsToMap( ctx: TypeResolveContext, elements: TSTypeElement[], - scope = ctxToScope(ctx) + scope = ctxToScope(ctx), + typeParameters?: Record ): 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) { @@ -374,9 +374,15 @@ function createProperty( function resolveInterfaceMembers( ctx: TypeResolveContext, node: TSInterfaceDeclaration & MaybeWithScope, - scope: TypeScope + scope: TypeScope, + typeParameters?: Record ): 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 ( @@ -1160,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 @@ -1181,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(