diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 3dded52a4d450..29fe9d76fd835 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -19668,24 +19668,37 @@ func (c *Checker) getMemberLayoutKey(t *Type, baseTypes []*Type) CacheHashKey { } func (c *Checker) createMemberLayout(t *Type, baseTypes []*Type) *MemberLayout { - // Reuse c.layoutMemberInfos to avoid repeated allocations - layoutMemberInfos := c.layoutMemberInfos + // Reuse c.layoutMemberInfos to avoid repeated allocations. The map records names already + // placed so a later base cannot override an earlier declaration. + seen := c.layoutMemberInfos c.layoutMemberInfos = nil + memberInfos := make([]MemberInfo, 0) addMember := func(symbol *ast.Symbol, baseIndex int) { - if _, ok := layoutMemberInfos[symbol.Name()]; !ok { - if layoutMemberInfos == nil { - layoutMemberInfos = make(map[string]MemberInfo) - } - layoutMemberInfos[symbol.Name()] = MemberInfo{symbol: symbol, baseIndex: int32(baseIndex)} + if _, ok := seen[symbol.Name()]; ok { + return } + if seen == nil { + seen = make(map[string]MemberInfo) + } + info := MemberInfo{symbol: symbol, baseIndex: int32(baseIndex)} + seen[symbol.Name()] = info + memberInfos = append(memberInfos, info) } - // Add declared members + // Declared members come first. Variance measurement stops at the first failing member, + // and declared members are the ones most likely to carry the structural difference. for id, symbol := range t.AsInterfaceType().declaredMembers.All() { if c.isNamedMember(symbol, id) { addMember(symbol, -1) } } - // Add inherited members + declaredCount := len(memberInfos) + slices.SortFunc(memberInfos[:declaredCount], func(a, b MemberInfo) int { + return c.compareSymbols(a.symbol, b.symbol) + }) + // Inherited members follow in base-clause order, and within a base in that base's order. + // Sorting them by declaration position pulls a grandbase member ahead of the base's own + // members, so an empty subtype is measured differently from its base. + // See https://github.com/microsoft/TypeScript/issues/65052. for baseIndex, baseType := range baseTypes { if baseType.objectFlags&(ObjectFlagsReference|ObjectFlagsClassOrInterface) != 0 { c.resolveStructuredTypeMembers(baseType) @@ -19702,20 +19715,9 @@ func (c *Checker) createMemberLayout(t *Type, baseTypes []*Type) *MemberLayout { } } } - memberInfos := slices.AppendSeq(make([]MemberInfo, 0, len(layoutMemberInfos)), maps.Values(layoutMemberInfos)) - clear(layoutMemberInfos) - c.layoutMemberInfos = layoutMemberInfos - // Sort by symbols, but put declared members (baseIndex < 0) first - slices.SortFunc(memberInfos, func(a, b MemberInfo) int { - if a.baseIndex < 0 && b.baseIndex >= 0 { - return -1 - } - if a.baseIndex >= 0 && b.baseIndex < 0 { - return 1 - } - return c.CompareSymbols(a.symbol, b.symbol) - }) - // Store slot indices in the sorted memberInfos and build the memberInfoMap. Slot indices are used to + clear(seen) + c.layoutMemberInfos = seen + // Store slot indices in memberInfos and build the memberInfoMap. Slot indices are used to // identify slots in the instantiated type's properties list. memberInfoMap := make(map[string]*MemberInfo, len(memberInfos)) for i := range memberInfos { diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index acd75f06eb8e8..bb884705ada97 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -1313,7 +1313,7 @@ type LazyState struct { type MemberLayout struct { id uint32 // Unique identifier for the member layout memberInfoMap map[string]*MemberInfo // MemberInfos keyed by property name - memberInfos []MemberInfo // MemberInfos in sorted order + memberInfos []MemberInfo // Declared members, then inherited members in base order layoutProperties []*ast.Symbol // Lazily created list of layout properties (symbols corresponding to memberInfos) } diff --git a/tsc/internal/fourslash/tests/codeFixClassImplementDeepInheritance_test.go b/tsc/internal/fourslash/tests/codeFixClassImplementDeepInheritance_test.go index 83828aa4d18e0..32fcb76309257 100644 --- a/tsc/internal/fourslash/tests/codeFixClassImplementDeepInheritance_test.go +++ b/tsc/internal/fourslash/tests/codeFixClassImplementDeepInheritance_test.go @@ -38,6 +38,7 @@ interface I6 extends C4 {} class C5 implements I6 {}` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() + // Implemented members follow I6's base order: C4's fields, then C3's, then C2's. f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ Description: "Implement interface 'I6'", NewFileContent: `// Referenced throughout the inheritance chain. @@ -64,12 +65,12 @@ class C4 extends C3 implements I0, I4, I5 { interface I6 extends C4 {} class C5 implements I6 { - c: number; + e: number; + f: number; a: number; b: number; d: number; - e: number; - f: number; + c: number; }`, Index: 0, }) diff --git a/tsc/testdata/baselines/reference/compiler/emptySubtypeInheritedMemberOrder.errors.txt b/tsc/testdata/baselines/reference/compiler/emptySubtypeInheritedMemberOrder.errors.txt new file mode 100644 index 0000000000000..90e6a04673398 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/emptySubtypeInheritedMemberOrder.errors.txt @@ -0,0 +1,72 @@ +emptySubtypeInheritedMemberOrder.ts(5,14): error TS2739: Type '{}' is missing the following properties from type 'C': own, inherited +emptySubtypeInheritedMemberOrder.ts(8,14): error TS2322: Type 'B<{ a: string; }>' is not assignable to type 'B<{ a: string; b?: number | undefined; }>'. + The types returned by 'own(...)' are incompatible between these types. + Property 'b' is missing in type 'Required<{ a: string; }>' but required in type 'Required<{ a: string; b?: number | undefined; }>'. +emptySubtypeInheritedMemberOrder.ts(11,14): error TS2322: Type 'C<{ a: string; }>' is not assignable to type 'C<{ a: string; b?: number | undefined; }>'. + The types returned by 'own(...)' are incompatible between these types. + Property 'b' is missing in type 'Required<{ a: string; }>' but required in type 'Required<{ a: string; b?: number | undefined; }>'. +emptySubtypeInheritedMemberOrder.ts(14,14): error TS2322: Type 'D<{ a: string; }>' is not assignable to type 'D<{ a: string; b?: number | undefined; }>'. + The types returned by 'own(...)' are incompatible between these types. + Property 'b' is missing in type 'Required<{ a: string; }>' but required in type 'Required<{ a: string; b?: number | undefined; }>'. +emptySubtypeInheritedMemberOrder.ts(16,14): error TS2739: Type '{}' is missing the following properties from type 'Both': right, left + + +==== emptySubtypeInheritedMemberOrder.ts (5 errors) ==== + // Inherited members keep the base type's order. Sorting them by declaration position + // makes an empty subtype invariant and drops the assignability error. + // https://github.com/microsoft/TypeScript/issues/65052 + + export const empty: C = {}; + ~~~~~ +!!! error TS2739: Type '{}' is missing the following properties from type 'C': own, inherited + + declare const narrowB: B<{ a: string }>; + export const wideB: B<{ a: string; b?: number }> = narrowB; + ~~~~~ +!!! error TS2322: Type 'B<{ a: string; }>' is not assignable to type 'B<{ a: string; b?: number | undefined; }>'. +!!! error TS2322: The types returned by 'own(...)' are incompatible between these types. +!!! error TS2322: Property 'b' is missing in type 'Required<{ a: string; }>' but required in type 'Required<{ a: string; b?: number | undefined; }>'. +!!! related TS2728 emptySubtypeInheritedMemberOrder.ts:8:36: 'b' is declared here. + + declare const narrowC: C<{ a: string }>; + export const wideC: C<{ a: string; b?: number }> = narrowC; + ~~~~~ +!!! error TS2322: Type 'C<{ a: string; }>' is not assignable to type 'C<{ a: string; b?: number | undefined; }>'. +!!! error TS2322: The types returned by 'own(...)' are incompatible between these types. +!!! error TS2322: Property 'b' is missing in type 'Required<{ a: string; }>' but required in type 'Required<{ a: string; b?: number | undefined; }>'. +!!! related TS2728 emptySubtypeInheritedMemberOrder.ts:11:36: 'b' is declared here. + + declare const narrowD: D<{ a: string }>; + export const wideD: D<{ a: string; b?: number }> = narrowD; + ~~~~~ +!!! error TS2322: Type 'D<{ a: string; }>' is not assignable to type 'D<{ a: string; b?: number | undefined; }>'. +!!! error TS2322: The types returned by 'own(...)' are incompatible between these types. +!!! error TS2322: Property 'b' is missing in type 'Required<{ a: string; }>' but required in type 'Required<{ a: string; b?: number | undefined; }>'. +!!! related TS2728 emptySubtypeInheritedMemberOrder.ts:14:36: 'b' is declared here. + + export const both: Both = {}; + ~~~~ +!!! error TS2739: Type '{}' is missing the following properties from type 'Both': right, left + + interface A { + inherited: () => T; + } + + interface B extends A { + own: (x: T) => Required; + } + + interface C extends B {} + + interface D extends C {} + + interface Left { + left: string; + } + + interface Right { + right: string; + } + + interface Both extends Right, Left {} + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/emptySubtypeInheritedMemberOrder.symbols b/tsc/testdata/baselines/reference/compiler/emptySubtypeInheritedMemberOrder.symbols new file mode 100644 index 0000000000000..aade07b6b669f --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/emptySubtypeInheritedMemberOrder.symbols @@ -0,0 +1,105 @@ +//// [tests/cases/compiler/emptySubtypeInheritedMemberOrder.ts] //// + +=== emptySubtypeInheritedMemberOrder.ts === +// Inherited members keep the base type's order. Sorting them by declaration position +// makes an empty subtype invariant and drops the assignability error. +// https://github.com/microsoft/TypeScript/issues/65052 + +export const empty: C = {}; +>empty : Symbol(empty, Decl(emptySubtypeInheritedMemberOrder.ts, 4, 12)) +>C : Symbol(C, Decl(emptySubtypeInheritedMemberOrder.ts, 23, 1)) + +declare const narrowB: B<{ a: string }>; +>narrowB : Symbol(narrowB, Decl(emptySubtypeInheritedMemberOrder.ts, 6, 13)) +>B : Symbol(B, Decl(emptySubtypeInheritedMemberOrder.ts, 19, 1)) +>a : Symbol(a, Decl(emptySubtypeInheritedMemberOrder.ts, 6, 26)) + +export const wideB: B<{ a: string; b?: number }> = narrowB; +>wideB : Symbol(wideB, Decl(emptySubtypeInheritedMemberOrder.ts, 7, 12)) +>B : Symbol(B, Decl(emptySubtypeInheritedMemberOrder.ts, 19, 1)) +>a : Symbol(a, Decl(emptySubtypeInheritedMemberOrder.ts, 7, 23)) +>b : Symbol(b, Decl(emptySubtypeInheritedMemberOrder.ts, 7, 34)) +>narrowB : Symbol(narrowB, Decl(emptySubtypeInheritedMemberOrder.ts, 6, 13)) + +declare const narrowC: C<{ a: string }>; +>narrowC : Symbol(narrowC, Decl(emptySubtypeInheritedMemberOrder.ts, 9, 13)) +>C : Symbol(C, Decl(emptySubtypeInheritedMemberOrder.ts, 23, 1)) +>a : Symbol(a, Decl(emptySubtypeInheritedMemberOrder.ts, 9, 26)) + +export const wideC: C<{ a: string; b?: number }> = narrowC; +>wideC : Symbol(wideC, Decl(emptySubtypeInheritedMemberOrder.ts, 10, 12)) +>C : Symbol(C, Decl(emptySubtypeInheritedMemberOrder.ts, 23, 1)) +>a : Symbol(a, Decl(emptySubtypeInheritedMemberOrder.ts, 10, 23)) +>b : Symbol(b, Decl(emptySubtypeInheritedMemberOrder.ts, 10, 34)) +>narrowC : Symbol(narrowC, Decl(emptySubtypeInheritedMemberOrder.ts, 9, 13)) + +declare const narrowD: D<{ a: string }>; +>narrowD : Symbol(narrowD, Decl(emptySubtypeInheritedMemberOrder.ts, 12, 13)) +>D : Symbol(D, Decl(emptySubtypeInheritedMemberOrder.ts, 25, 30)) +>a : Symbol(a, Decl(emptySubtypeInheritedMemberOrder.ts, 12, 26)) + +export const wideD: D<{ a: string; b?: number }> = narrowD; +>wideD : Symbol(wideD, Decl(emptySubtypeInheritedMemberOrder.ts, 13, 12)) +>D : Symbol(D, Decl(emptySubtypeInheritedMemberOrder.ts, 25, 30)) +>a : Symbol(a, Decl(emptySubtypeInheritedMemberOrder.ts, 13, 23)) +>b : Symbol(b, Decl(emptySubtypeInheritedMemberOrder.ts, 13, 34)) +>narrowD : Symbol(narrowD, Decl(emptySubtypeInheritedMemberOrder.ts, 12, 13)) + +export const both: Both = {}; +>both : Symbol(both, Decl(emptySubtypeInheritedMemberOrder.ts, 15, 12)) +>Both : Symbol(Both, Decl(emptySubtypeInheritedMemberOrder.ts, 35, 1)) + +interface A { +>A : Symbol(A, Decl(emptySubtypeInheritedMemberOrder.ts, 15, 29)) +>T : Symbol(T, Decl(emptySubtypeInheritedMemberOrder.ts, 17, 12)) + + inherited: () => T; +>inherited : Symbol(A.inherited, Decl(emptySubtypeInheritedMemberOrder.ts, 17, 16)) +>T : Symbol(T, Decl(emptySubtypeInheritedMemberOrder.ts, 17, 12)) +} + +interface B extends A { +>B : Symbol(B, Decl(emptySubtypeInheritedMemberOrder.ts, 19, 1)) +>T : Symbol(T, Decl(emptySubtypeInheritedMemberOrder.ts, 21, 12)) +>A : Symbol(A, Decl(emptySubtypeInheritedMemberOrder.ts, 15, 29)) +>T : Symbol(T, Decl(emptySubtypeInheritedMemberOrder.ts, 21, 12)) + + own: (x: T) => Required; +>own : Symbol(B.own, Decl(emptySubtypeInheritedMemberOrder.ts, 21, 29)) +>x : Symbol(x, Decl(emptySubtypeInheritedMemberOrder.ts, 22, 10)) +>T : Symbol(T, Decl(emptySubtypeInheritedMemberOrder.ts, 21, 12)) +>Required : Symbol(Required, Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(emptySubtypeInheritedMemberOrder.ts, 21, 12)) +} + +interface C extends B {} +>C : Symbol(C, Decl(emptySubtypeInheritedMemberOrder.ts, 23, 1)) +>T : Symbol(T, Decl(emptySubtypeInheritedMemberOrder.ts, 25, 12)) +>B : Symbol(B, Decl(emptySubtypeInheritedMemberOrder.ts, 19, 1)) +>T : Symbol(T, Decl(emptySubtypeInheritedMemberOrder.ts, 25, 12)) + +interface D extends C {} +>D : Symbol(D, Decl(emptySubtypeInheritedMemberOrder.ts, 25, 30)) +>T : Symbol(T, Decl(emptySubtypeInheritedMemberOrder.ts, 27, 12)) +>C : Symbol(C, Decl(emptySubtypeInheritedMemberOrder.ts, 23, 1)) +>T : Symbol(T, Decl(emptySubtypeInheritedMemberOrder.ts, 27, 12)) + +interface Left { +>Left : Symbol(Left, Decl(emptySubtypeInheritedMemberOrder.ts, 27, 30)) + + left: string; +>left : Symbol(Left.left, Decl(emptySubtypeInheritedMemberOrder.ts, 29, 16)) +} + +interface Right { +>Right : Symbol(Right, Decl(emptySubtypeInheritedMemberOrder.ts, 31, 1)) + + right: string; +>right : Symbol(Right.right, Decl(emptySubtypeInheritedMemberOrder.ts, 33, 17)) +} + +interface Both extends Right, Left {} +>Both : Symbol(Both, Decl(emptySubtypeInheritedMemberOrder.ts, 35, 1)) +>Right : Symbol(Right, Decl(emptySubtypeInheritedMemberOrder.ts, 31, 1)) +>Left : Symbol(Left, Decl(emptySubtypeInheritedMemberOrder.ts, 27, 30)) + diff --git a/tsc/testdata/baselines/reference/compiler/emptySubtypeInheritedMemberOrder.types b/tsc/testdata/baselines/reference/compiler/emptySubtypeInheritedMemberOrder.types new file mode 100644 index 0000000000000..f4bf5699a3a1a --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/emptySubtypeInheritedMemberOrder.types @@ -0,0 +1,72 @@ +//// [tests/cases/compiler/emptySubtypeInheritedMemberOrder.ts] //// + +=== emptySubtypeInheritedMemberOrder.ts === +// Inherited members keep the base type's order. Sorting them by declaration position +// makes an empty subtype invariant and drops the assignability error. +// https://github.com/microsoft/TypeScript/issues/65052 + +export const empty: C = {}; +>empty : C +>{} : {} + +declare const narrowB: B<{ a: string }>; +>narrowB : B<{ a: string; }> +>a : string + +export const wideB: B<{ a: string; b?: number }> = narrowB; +>wideB : B<{ a: string; b?: number; }> +>a : string +>b : number | undefined +>narrowB : B<{ a: string; }> + +declare const narrowC: C<{ a: string }>; +>narrowC : C<{ a: string; }> +>a : string + +export const wideC: C<{ a: string; b?: number }> = narrowC; +>wideC : C<{ a: string; b?: number; }> +>a : string +>b : number | undefined +>narrowC : C<{ a: string; }> + +declare const narrowD: D<{ a: string }>; +>narrowD : D<{ a: string; }> +>a : string + +export const wideD: D<{ a: string; b?: number }> = narrowD; +>wideD : D<{ a: string; b?: number; }> +>a : string +>b : number | undefined +>narrowD : D<{ a: string; }> + +export const both: Both = {}; +>both : Both +>{} : {} + +interface A { + inherited: () => T; +>inherited : () => T +} + +interface B extends A { + own: (x: T) => Required; +>own : (x: T) => Required +>x : T +} + +interface C extends B {} + +interface D extends C {} + +interface Left { + left: string; +>left : string +} + +interface Right { + right: string; +>right : string +} + +interface Both extends Right, Left {} + diff --git a/tsc/testdata/tests/cases/compiler/emptySubtypeInheritedMemberOrder.ts b/tsc/testdata/tests/cases/compiler/emptySubtypeInheritedMemberOrder.ts new file mode 100644 index 0000000000000..74d8eed21696f --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/emptySubtypeInheritedMemberOrder.ts @@ -0,0 +1,41 @@ +// @strict: true +// @noEmit: true + +// Inherited members keep the base type's order. Sorting them by declaration position +// makes an empty subtype invariant and drops the assignability error. +// https://github.com/microsoft/TypeScript/issues/65052 + +export const empty: C = {}; + +declare const narrowB: B<{ a: string }>; +export const wideB: B<{ a: string; b?: number }> = narrowB; + +declare const narrowC: C<{ a: string }>; +export const wideC: C<{ a: string; b?: number }> = narrowC; + +declare const narrowD: D<{ a: string }>; +export const wideD: D<{ a: string; b?: number }> = narrowD; + +export const both: Both = {}; + +interface A { + inherited: () => T; +} + +interface B extends A { + own: (x: T) => Required; +} + +interface C extends B {} + +interface D extends C {} + +interface Left { + left: string; +} + +interface Right { + right: string; +} + +interface Both extends Right, Left {}