Skip to content

Commit d53e157

Browse files
committed
fix(compiler-sfc): handle type merging + fix namespace access when inferring type
close #8102
1 parent 5510ce3 commit d53e157

File tree

2 files changed

+229
-37
lines changed

2 files changed

+229
-37
lines changed

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

+106
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,84 @@ describe('resolveType', () => {
294294
})
295295
})
296296

297+
test('interface merging', () => {
298+
expect(
299+
resolve(`
300+
interface Foo {
301+
a: string
302+
}
303+
interface Foo {
304+
b: number
305+
}
306+
defineProps<{
307+
foo: Foo['a'],
308+
bar: Foo['b']
309+
}>()
310+
`).props
311+
).toStrictEqual({
312+
foo: ['String'],
313+
bar: ['Number']
314+
})
315+
})
316+
317+
test('namespace merging', () => {
318+
expect(
319+
resolve(`
320+
namespace Foo {
321+
export type A = string
322+
}
323+
namespace Foo {
324+
export type B = number
325+
}
326+
defineProps<{
327+
foo: Foo.A,
328+
bar: Foo.B
329+
}>()
330+
`).props
331+
).toStrictEqual({
332+
foo: ['String'],
333+
bar: ['Number']
334+
})
335+
})
336+
337+
test('namespace merging with other types', () => {
338+
expect(
339+
resolve(`
340+
namespace Foo {
341+
export type A = string
342+
}
343+
interface Foo {
344+
b: number
345+
}
346+
defineProps<{
347+
foo: Foo.A,
348+
bar: Foo['b']
349+
}>()
350+
`).props
351+
).toStrictEqual({
352+
foo: ['String'],
353+
bar: ['Number']
354+
})
355+
})
356+
357+
test('enum merging', () => {
358+
expect(
359+
resolve(`
360+
enum Foo {
361+
A = 1
362+
}
363+
enum Foo {
364+
B = 'hi'
365+
}
366+
defineProps<{
367+
foo: Foo
368+
}>()
369+
`).props
370+
).toStrictEqual({
371+
foo: ['Number', 'String']
372+
})
373+
})
374+
297375
describe('external type imports', () => {
298376
const files = {
299377
'/foo.ts': 'export type P = { foo: number }',
@@ -436,6 +514,34 @@ describe('resolveType', () => {
436514
})
437515
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
438516
})
517+
518+
test('global types with ambient references', () => {
519+
const files = {
520+
// with references
521+
'/backend.d.ts': `
522+
declare namespace App.Data {
523+
export type AircraftData = {
524+
id: string
525+
manufacturer: App.Data.Listings.ManufacturerData
526+
}
527+
}
528+
declare namespace App.Data.Listings {
529+
export type ManufacturerData = {
530+
id: string
531+
}
532+
}
533+
`
534+
}
535+
536+
const { props } = resolve(`defineProps<App.Data.AircraftData>()`, files, {
537+
globalTypeFiles: Object.keys(files)
538+
})
539+
540+
expect(props).toStrictEqual({
541+
id: ['String'],
542+
manufacturer: ['Object']
543+
})
544+
})
439545
})
440546

441547
describe('errors', () => {

packages/compiler-sfc/src/script/resolveType.ts

+123-37
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,14 @@ export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext
6565

6666
type Import = Pick<ImportBinding, 'source' | 'imported'>
6767

68-
type ScopeTypeNode = Node & {
69-
// scope types always has ownerScope attached
68+
interface WithScope {
7069
_ownerScope: TypeScope
7170
}
7271

72+
// scope types always has ownerScope attached
73+
type ScopeTypeNode = Node &
74+
WithScope & { _ns?: TSModuleDeclaration & WithScope }
75+
7376
export interface TypeScope {
7477
filename: string
7578
source: string
@@ -79,7 +82,7 @@ export interface TypeScope {
7982
exportedTypes: Record<string, ScopeTypeNode>
8083
}
8184

82-
export interface WithScope {
85+
export interface MaybeWithScope {
8386
_ownerScope?: TypeScope
8487
}
8588

@@ -100,7 +103,7 @@ interface ResolvedElements {
100103
*/
101104
export function resolveTypeElements(
102105
ctx: TypeResolveContext,
103-
node: Node & WithScope & { _resolvedElements?: ResolvedElements },
106+
node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements },
104107
scope?: TypeScope
105108
): ResolvedElements {
106109
if (node._resolvedElements) {
@@ -177,7 +180,7 @@ function typeElementsToMap(
177180
const res: ResolvedElements = { props: {} }
178181
for (const e of elements) {
179182
if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
180-
;(e as WithScope)._ownerScope = scope
183+
;(e as MaybeWithScope)._ownerScope = scope
181184
const name = getId(e.key)
182185
if (name && !e.computed) {
183186
res.props[name] = e as ResolvedElements['props'][string]
@@ -248,7 +251,7 @@ function createProperty(
248251

249252
function resolveInterfaceMembers(
250253
ctx: TypeResolveContext,
251-
node: TSInterfaceDeclaration & WithScope,
254+
node: TSInterfaceDeclaration & MaybeWithScope,
252255
scope: TypeScope
253256
): ResolvedElements {
254257
const base = typeElementsToMap(ctx, node.body.body, node._ownerScope)
@@ -289,7 +292,7 @@ function resolveIndexType(
289292
ctx: TypeResolveContext,
290293
node: TSIndexedAccessType,
291294
scope: TypeScope
292-
): (TSType & WithScope)[] {
295+
): (TSType & MaybeWithScope)[] {
293296
if (node.indexType.type === 'TSNumberKeyword') {
294297
return resolveArrayElementType(ctx, node.objectType, scope)
295298
}
@@ -308,7 +311,7 @@ function resolveIndexType(
308311
for (const key of keys) {
309312
const targetType = resolved.props[key]?.typeAnnotation?.typeAnnotation
310313
if (targetType) {
311-
;(targetType as TSType & WithScope)._ownerScope =
314+
;(targetType as TSType & MaybeWithScope)._ownerScope =
312315
resolved.props[key]._ownerScope
313316
types.push(targetType)
314317
}
@@ -532,22 +535,22 @@ function innerResolveTypeReference(
532535
}
533536
}
534537
} else {
535-
const ns = innerResolveTypeReference(
536-
ctx,
537-
scope,
538-
name[0],
539-
node,
540-
onlyExported
541-
)
542-
if (ns && ns.type === 'TSModuleDeclaration') {
543-
const childScope = moduleDeclToScope(ns, scope)
544-
return innerResolveTypeReference(
545-
ctx,
546-
childScope,
547-
name.length > 2 ? name.slice(1) : name[name.length - 1],
548-
node,
549-
!ns.declare
550-
)
538+
let ns = innerResolveTypeReference(ctx, scope, name[0], node, onlyExported)
539+
if (ns) {
540+
if (ns.type !== 'TSModuleDeclaration') {
541+
// namespace merged with other types, attached as _ns
542+
ns = ns._ns
543+
}
544+
if (ns) {
545+
const childScope = moduleDeclToScope(ns, ns._ownerScope || scope)
546+
return innerResolveTypeReference(
547+
ctx,
548+
childScope,
549+
name.length > 2 ? name.slice(1) : name[name.length - 1],
550+
node,
551+
!ns.declare
552+
)
553+
}
551554
}
552555
}
553556
}
@@ -771,7 +774,6 @@ export function fileToScope(
771774
exportedTypes: Object.create(null)
772775
}
773776
recordTypes(body, scope, asGlobal)
774-
775777
fileToScopeCache.set(filename, scope)
776778
return scope
777779
}
@@ -858,10 +860,21 @@ function moduleDeclToScope(
858860
}
859861
const scope: TypeScope = {
860862
...parentScope,
863+
imports: Object.create(parentScope.imports),
864+
// TODO this seems wrong
861865
types: Object.create(parentScope.types),
862-
imports: Object.create(parentScope.imports)
866+
exportedTypes: Object.create(null)
867+
}
868+
869+
if (node.body.type === 'TSModuleDeclaration') {
870+
const decl = node.body as TSModuleDeclaration & WithScope
871+
decl._ownerScope = scope
872+
const id = getId(decl.id)
873+
scope.types[id] = scope.exportedTypes[id] = decl
874+
} else {
875+
recordTypes(node.body.body, scope)
863876
}
864-
recordTypes((node.body as TSModuleBlock).body, scope)
877+
865878
return (node._resolvedChildScope = scope)
866879
}
867880

@@ -923,20 +936,52 @@ function recordTypes(body: Statement[], scope: TypeScope, asGlobal = false) {
923936
}
924937
}
925938
for (const key of Object.keys(types)) {
926-
types[key]._ownerScope = scope
939+
const node = types[key]
940+
node._ownerScope = scope
941+
if (node._ns) node._ns._ownerScope = scope
927942
}
928943
}
929944

930945
function recordType(node: Node, types: Record<string, Node>) {
931946
switch (node.type) {
932947
case 'TSInterfaceDeclaration':
933948
case 'TSEnumDeclaration':
934-
case 'TSModuleDeclaration':
935-
case 'ClassDeclaration': {
936-
const id = node.id.type === 'Identifier' ? node.id.name : node.id.value
937-
types[id] = node
949+
case 'TSModuleDeclaration': {
950+
const id = getId(node.id)
951+
let existing = types[id]
952+
if (existing) {
953+
if (node.type === 'TSModuleDeclaration') {
954+
if (existing.type === 'TSModuleDeclaration') {
955+
mergeNamespaces(existing as typeof node, node)
956+
} else {
957+
attachNamespace(existing, node)
958+
}
959+
break
960+
}
961+
if (existing.type === 'TSModuleDeclaration') {
962+
// replace and attach namespace
963+
types[id] = node
964+
attachNamespace(node, existing)
965+
break
966+
}
967+
968+
if (existing.type !== node.type) {
969+
// type-level error
970+
break
971+
}
972+
if (node.type === 'TSInterfaceDeclaration') {
973+
;(existing as typeof node).body.body.push(...node.body.body)
974+
} else {
975+
;(existing as typeof node).members.push(...node.members)
976+
}
977+
} else {
978+
types[id] = node
979+
}
938980
break
939981
}
982+
case 'ClassDeclaration':
983+
types[getId(node.id)] = node
984+
break
940985
case 'TSTypeAliasDeclaration':
941986
types[node.id.name] = node.typeAnnotation
942987
break
@@ -955,6 +1000,47 @@ function recordType(node: Node, types: Record<string, Node>) {
9551000
}
9561001
}
9571002

1003+
function mergeNamespaces(to: TSModuleDeclaration, from: TSModuleDeclaration) {
1004+
const toBody = to.body
1005+
const fromBody = from.body
1006+
if (toBody.type === 'TSModuleDeclaration') {
1007+
if (fromBody.type === 'TSModuleDeclaration') {
1008+
// both decl
1009+
mergeNamespaces(toBody, fromBody)
1010+
} else {
1011+
// to: decl -> from: block
1012+
fromBody.body.push({
1013+
type: 'ExportNamedDeclaration',
1014+
declaration: toBody,
1015+
exportKind: 'type',
1016+
specifiers: []
1017+
})
1018+
}
1019+
} else if (fromBody.type === 'TSModuleDeclaration') {
1020+
// to: block <- from: decl
1021+
toBody.body.push({
1022+
type: 'ExportNamedDeclaration',
1023+
declaration: fromBody,
1024+
exportKind: 'type',
1025+
specifiers: []
1026+
})
1027+
} else {
1028+
// both block
1029+
toBody.body.push(...fromBody.body)
1030+
}
1031+
}
1032+
1033+
function attachNamespace(
1034+
to: Node & { _ns?: TSModuleDeclaration },
1035+
ns: TSModuleDeclaration
1036+
) {
1037+
if (!to._ns) {
1038+
to._ns = ns
1039+
} else {
1040+
mergeNamespaces(to._ns, ns)
1041+
}
1042+
}
1043+
9581044
export function recordImports(body: Statement[]) {
9591045
const imports: TypeScope['imports'] = Object.create(null)
9601046
for (const s of body) {
@@ -977,7 +1063,7 @@ function recordImport(node: Node, imports: TypeScope['imports']) {
9771063

9781064
export function inferRuntimeType(
9791065
ctx: TypeResolveContext,
980-
node: Node & WithScope,
1066+
node: Node & MaybeWithScope,
9811067
scope = node._ownerScope || ctxToScope(ctx)
9821068
): string[] {
9831069
switch (node.type) {
@@ -1035,11 +1121,11 @@ export function inferRuntimeType(
10351121
}
10361122

10371123
case 'TSTypeReference':
1124+
const resolved = resolveTypeReference(ctx, node, scope)
1125+
if (resolved) {
1126+
return inferRuntimeType(ctx, resolved, resolved._ownerScope)
1127+
}
10381128
if (node.typeName.type === 'Identifier') {
1039-
const resolved = resolveTypeReference(ctx, node, scope)
1040-
if (resolved) {
1041-
return inferRuntimeType(ctx, resolved, resolved._ownerScope)
1042-
}
10431129
switch (node.typeName.name) {
10441130
case 'Array':
10451131
case 'Function':

0 commit comments

Comments
 (0)