Skip to content

Commit aca4412

Browse files
committed
Reproduce recursive declaration naming failures
Recursive values should retain their structure across declaration emit and downstream consumption instead of failing emit or acquiring any. Cover functions, object members, containers, and generic aliases, with repeated builds and displays at different expansion levels. Broader annotations and inaccessible names must not silently weaken recursive types. Preserve coverage of declaration error reporting even when formerly unrepresentable structural types become nameable. Declaration and call-site displays should agree on recursive boundaries without losing the selected overload or generic specialization. Recursive generic owners must remain reusable with new type arguments. A captured specialization must not change a method's own generic contract or leak into declarations consumed by downstream projects. For 55832
1 parent 71956e9 commit aca4412

28 files changed

Lines changed: 11467 additions & 404 deletions

‎tsc/internal/checker/checker_test.go‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package checker_test
22

33
import (
4+
"fmt"
45
"path/filepath"
6+
"strconv"
7+
"strings"
58
"testing"
69

710
"github.com/microsoft/TypeScript/tsc/internal/ast"
@@ -10,6 +13,7 @@ import (
1013
"github.com/microsoft/TypeScript/tsc/internal/compiler"
1114
"github.com/microsoft/TypeScript/tsc/internal/core"
1215
"github.com/microsoft/TypeScript/tsc/internal/repo"
16+
"github.com/microsoft/TypeScript/tsc/internal/testutil/baseline"
1317
"github.com/microsoft/TypeScript/tsc/internal/tsoptions"
1418
"github.com/microsoft/TypeScript/tsc/internal/tspath"
1519
"github.com/microsoft/TypeScript/tsc/internal/vfs/osvfs"
@@ -115,6 +119,76 @@ export type E = D;`,
115119
assert.Equal(t, defaultClause, defaultReference)
116120
}
117121

122+
func TestTypeToStringMergedNamespace(t *testing.T) {
123+
t.Parallel()
124+
const content = `declare function f<T>(value: T): T;
125+
declare namespace f { const extra: number; }
126+
type Sample = typeof f;`
127+
fs := bundled.WrapFS(vfstest.FromMap(map[string]string{
128+
"/foo.ts": content,
129+
"/tsconfig.json": `{"files":["foo.ts"]}`,
130+
}, tspath.CaseInsensitive))
131+
host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil)
132+
parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, fs, nil)
133+
assert.Equal(t, len(errors), 0)
134+
p := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host})
135+
p.BindSourceFiles()
136+
c, done := p.GetTypeChecker(t.Context())
137+
defer done()
138+
node := p.GetSourceFile("/foo.ts").Statements.Nodes[2]
139+
typ := c.GetTypeAtLocation(node)
140+
var result strings.Builder
141+
result.WriteString(content + "\n\n")
142+
for _, useTypeOf := range []bool{false, true} {
143+
flags := checker.TypeFormatFlagsInTypeAlias
144+
if useTypeOf {
145+
flags |= checker.TypeFormatFlagsUseTypeOfFunction
146+
}
147+
for level := range 3 {
148+
vc := &checker.VerbosityContext{Level: level}
149+
fmt.Fprintf(&result, "useTypeOfFunction=%t, verbosity=%d: %s\n", useTypeOf, level, c.TypeToStringEx(typ, node, flags, vc))
150+
}
151+
}
152+
baseline.Run(t, "typeToStringMergedNamespace.baseline", result.String(), baseline.Options{Subfolder: "checker"})
153+
}
154+
155+
func TestTypeToStringRecursivePrototype(t *testing.T) {
156+
t.Parallel()
157+
for _, count := range []int{4, 8, 16, 32} {
158+
t.Run(strconv.Itoa(count), func(t *testing.T) {
159+
t.Parallel()
160+
var content strings.Builder
161+
content.WriteString("function Color(obj) { this.example = true; }\nColor.prototype = {\n")
162+
for i := range count {
163+
parameters := "ratio"
164+
if i == 0 {
165+
parameters = ""
166+
}
167+
fmt.Fprintf(&content, "m%03d: function(%s) { return this; },\n", i, parameters)
168+
}
169+
content.WriteString("toJSON: function() { return this.rgb(); }\n};")
170+
fs := bundled.WrapFS(vfstest.FromMap(map[string]string{
171+
"/foo.js": content.String(),
172+
"/tsconfig.json": `{"compilerOptions":{"target":"es2015","allowJs":true,"checkJs":true,"noImplicitAny":true},"files":["foo.js"]}`,
173+
}, tspath.CaseInsensitive))
174+
host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil)
175+
parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, fs, nil)
176+
assert.Equal(t, len(errors), 0)
177+
p := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host})
178+
p.BindSourceFiles()
179+
c, done := p.GetTypeChecker(t.Context())
180+
defer done()
181+
node := p.GetSourceFile("/foo.js").Statements.Nodes[1].Expression().AsBinaryExpression().Left
182+
typ := c.GetTypeAtLocation(node)
183+
display := c.TypeToStringEx(typ, node.Parent, checker.TypeFormatFlagsNoTruncation|checker.TypeFormatFlagsGenerateNamesForShadowedTypeParams, nil)
184+
t.Logf("methods=%d, bytes=%d, first method occurrences=%d", count, len(display), strings.Count(display, "m000:"))
185+
for i := range count {
186+
assert.Equal(t, strings.Count(display, fmt.Sprintf("m%03d:", i)), 2)
187+
}
188+
})
189+
}
190+
}
191+
118192
func BenchmarkNewChecker(b *testing.B) {
119193
fs := bundled.WrapFS(osvfs.FS())
120194
rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler"))

‎tsc/internal/execute/tsctests/tsc_test.go‎

Lines changed: 181 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,8 @@ func TestTscDeclarationEmit(t *testing.T) {
610610
"type": "module"
611611
}`),
612612
"/home/src/workspaces/project/node_modules/ky/distribution/index.d.ts": stringtestutil.Dedent(`
613-
type KyInstance = {
613+
declare class KyInstance {
614+
private brand;
614615
extend(options: Record<string,unknown>): KyInstance;
615616
}
616617
declare const ky: KyInstance;
@@ -2481,6 +2482,185 @@ func TestTscIncremental(t *testing.T) {
24812482
noChange,
24822483
},
24832484
},
2485+
{
2486+
subScenario: "recursive function declaration consumption",
2487+
files: FileMap{
2488+
"/home/src/workspaces/project/producer/tsconfig.json": `{
2489+
"compilerOptions": { "strict": true, "composite": true, "outDir": "dist" }
2490+
}`,
2491+
"/home/src/workspaces/project/producer/index.ts": stringtestutil.Dedent(`
2492+
export const arrow = () => arrow;
2493+
export const expression = function self() { return self; };
2494+
export const first = () => second;
2495+
export const second = () => first;
2496+
export const generic = <T>(value: T) => generic;
2497+
export const objectReturn = () => ({ call: objectReturn });
2498+
export const tupleReturn = () => [tupleReturn] as const;
2499+
export const shadowed = function self(shadowed: number) { return self; };
2500+
export const object = { value: 1, next: () => object };
2501+
export const method = { value: 1, next() { return method; } };
2502+
export const accessor = { value: 1, get next() { return accessor; } };
2503+
export const tuple = [() => tuple] as const;
2504+
export const array = [() => array];
2505+
export const nested = { inner: { next() { return nested.inner; } } };
2506+
export const memberTuple = [function self() { return self; }] as const;
2507+
export const memberArray = [function self() { return self; }];
2508+
export const quoted = { "a-b": function self() { return self; } };
2509+
export const numeric = { 0: function self() { return self; } };
2510+
export const key = Symbol();
2511+
export const computed = { [key]: function self() { return self; } };
2512+
export const union = true as boolean ? { next: () => union } : undefined;
2513+
function create<T>(value: T) {
2514+
const result = { value, next: () => result };
2515+
return result;
2516+
}
2517+
export const specialized = create("text");
2518+
function createIndexed() {
2519+
const value: { [key: string]: typeof value } = {};
2520+
return value;
2521+
}
2522+
export const indexed = createIndexed();
2523+
function createMapped<T>() {
2524+
const value: { [K in keyof T]: typeof value } = null!;
2525+
return value;
2526+
}
2527+
export const mapped = createMapped<{ next: unknown }>();
2528+
function createAnnotated<T>(value: T) {
2529+
const node: { value: T; next: () => typeof node } = { value, next: () => node };
2530+
return node;
2531+
}
2532+
export const viaAnnotation = createAnnotated("text");
2533+
export type Link<T> = { value: T; next: Link<T> };
2534+
export type Callable<T> = { (value: T): Callable<T>; link: Link<T> };
2535+
export declare const link: Link<string>;
2536+
export declare const callable: Callable<number>;
2537+
export const nextLink = link.next;
2538+
export const nextCallable = callable(1);
2539+
function createHidden<T>(value: T) {
2540+
type Hidden = { value: T; next: Hidden };
2541+
return null! as Hidden;
2542+
}
2543+
export const hiddenAlias = createHidden("text");
2544+
export const siblingHiddenAliases = { first: hiddenAlias, second: hiddenAlias };
2545+
export const parenthesized = (function self() { return self; });
2546+
export const asserted = (function self() { return self; }) satisfies () => unknown;
2547+
export class Methods {
2548+
static recur() { return Methods.recur; }
2549+
static "a-b"() { return Methods["a-b"]; }
2550+
static [key]() { return Methods[key]; }
2551+
recur() { return this.recur; }
2552+
}
2553+
export function overloaded(value: string): typeof overloaded;
2554+
export function overloaded(value: number): typeof overloaded;
2555+
export function overloaded(value: string | number) { return overloaded; }
2556+
export const nestedOwner = {
2557+
make<T>(outer: T) {
2558+
return {
2559+
nested<U>(inner: U) { return { outer, inner, owner: nestedOwner }; },
2560+
constrained<U extends T = T>(inner: U) { return { outer, inner, owner: nestedOwner }; },
2561+
copied<U>(inner: U): { outer: T; inner: U; owner: typeof nestedOwner } {
2562+
return { outer, inner, owner: nestedOwner };
2563+
},
2564+
rest<U>(...values: [U]) { return { outer, inner: values[0], owner: nestedOwner }; },
2565+
};
2566+
},
2567+
};
2568+
export const nestedNumber = nestedOwner.make(1);
2569+
`),
2570+
"/home/src/workspaces/project/consumer/tsconfig.json": `{
2571+
"compilerOptions": { "strict": true, "noEmit": true },
2572+
"references": [{ "path": "../producer" }]
2573+
}`,
2574+
"/home/src/workspaces/project/consumer/index.ts": stringtestutil.Dedent(`
2575+
import { arrow, expression, first, generic, objectReturn, tupleReturn, shadowed } from "../producer/dist/index.js";
2576+
import { object, method, accessor, tuple, array, nested } from "../producer/dist/index.js";
2577+
import { memberTuple, memberArray, quoted, numeric, key, computed, union, specialized } from "../producer/dist/index.js";
2578+
import { indexed, mapped, viaAnnotation } from "../producer/dist/index.js";
2579+
import { nextLink, nextCallable, parenthesized, asserted, Methods, overloaded } from "../producer/dist/index.js";
2580+
import { hiddenAlias, siblingHiddenAliases } from "../producer/dist/index.js";
2581+
import { nestedNumber } from "../producer/dist/index.js";
2582+
const a: typeof arrow = arrow()()();
2583+
const b: typeof expression = expression()()();
2584+
const c: typeof first = first()()();
2585+
const d: typeof generic = generic(1)("text")(true);
2586+
const objectCall: typeof objectReturn = objectReturn().call().call;
2587+
const tupleCall: typeof tupleReturn = tupleReturn()[0]()[0];
2588+
const shadowedCall: typeof shadowed = shadowed(1)(2);
2589+
const objectValue: number = object.next().next().value;
2590+
const methodValue: number = method.next().next().value;
2591+
const accessorValue: number = accessor.next.next.value;
2592+
const tupleValue: typeof tuple = tuple[0]()[0]();
2593+
const arrayValue: typeof array = array[0]()[0]();
2594+
const nestedValue: typeof nested.inner = nested.inner.next().next();
2595+
const memberTupleValue: typeof memberTuple[0] = memberTuple[0]()();
2596+
const memberArrayValue: typeof memberArray[0] = memberArray[0]()();
2597+
const quotedValue: typeof quoted["a-b"] = quoted["a-b"]()();
2598+
const numericValue: typeof numeric[0] = numeric[0]()();
2599+
const computedValue: typeof computed[typeof key] = computed[key]()();
2600+
const unionValue: typeof union = union?.next()?.next();
2601+
const specializedValue: string = specialized.next().next().value;
2602+
const indexedValue: typeof indexed = indexed["next"]["next"];
2603+
const mappedValue: typeof mapped = mapped.next.next;
2604+
const annotationValue: string = viaAnnotation.next().next().value;
2605+
const linkValue: string = nextLink.next.next.value;
2606+
const callableValue: number = nextCallable(1)(2).link.next.value;
2607+
const parenthesizedValue: typeof parenthesized = parenthesized()()();
2608+
const assertedValue: typeof asserted = asserted()()();
2609+
const methodReference: typeof Methods.recur = Methods.recur()()();
2610+
const quotedMethodReference: typeof Methods["a-b"] = Methods["a-b"]()()();
2611+
const computedMethodReference: typeof Methods[typeof key] = Methods[key]()()();
2612+
const instance = new Methods();
2613+
const instanceMethodReference: typeof instance.recur = instance.recur()()();
2614+
const overloadReference: typeof overloaded = overloaded(1)("text")(2);
2615+
const hiddenValue: string = hiddenAlias.next.next.value;
2616+
const siblingHiddenValue: string = siblingHiddenAliases.second.next.next.value;
2617+
const nestedString = nestedNumber.nested(true).owner.make("text").nested(2);
2618+
const nestedBoolean = nestedString.owner.make(true).nested("inner");
2619+
const nestedStringOuter: string = nestedString.outer;
2620+
const nestedStringInner: number = nestedString.inner;
2621+
const nestedBooleanOuter: boolean = nestedBoolean.outer;
2622+
const nestedBooleanInner: string = nestedBoolean.inner;
2623+
const constrainedOuter: number = nestedNumber.constrained(2).outer;
2624+
const constrainedInner: number = nestedNumber.constrained(2).inner;
2625+
const copiedOuter: number = nestedNumber.copied("inner").outer;
2626+
const copiedInner: string = nestedNumber.copied("inner").inner;
2627+
const restOuter: number = nestedNumber.rest(true).outer;
2628+
const restInner: boolean = nestedNumber.rest(true).inner;
2629+
type IsAny<T> = 0 extends (1 & T) ? true : false;
2630+
const result = arrow()()();
2631+
const notAny: false = null as unknown as IsAny<typeof result>;
2632+
const invalid: number = arrow()()();
2633+
const invalidExpression: number = expression()()();
2634+
const invalidObject: number = objectReturn().call;
2635+
const invalidTuple: number = tupleReturn()[0];
2636+
const invalidShadowed: number = shadowed(1);
2637+
const invalidObjectValue: string = object.next().value;
2638+
const invalidTupleValue: number = tuple[0]();
2639+
const invalidSpecialized: number = specialized.next().value;
2640+
const invalidMapped: number = mapped.next;
2641+
const invalidAnnotation: number = viaAnnotation.next().value;
2642+
const invalidLink: number = nextLink.next.value;
2643+
const invalidCallable: string = nextCallable(1).link.value;
2644+
const invalidHiddenValue: number = hiddenAlias.next.next.value;
2645+
const invalidMethod: number = Methods.recur()();
2646+
const invalidComputedMethod: number = Methods[key]()();
2647+
const invalidNestedOuter: boolean = nestedString.outer;
2648+
const invalidNestedInner: string = nestedString.inner;
2649+
nestedNumber.constrained("wrong");
2650+
`),
2651+
},
2652+
commandLineArgs: []string{"--build", "consumer"},
2653+
edits: []*tscEdit{
2654+
noChange,
2655+
{
2656+
caption: "add a comment to the producer",
2657+
edit: func(sys *TestSys) {
2658+
sys.appendFile("/home/src/workspaces/project/producer/index.ts", "\n// comment-only edit\n")
2659+
},
2660+
},
2661+
noChange,
2662+
},
2663+
},
24842664
{
24852665
subScenario: "json module diagnostics are cleared after fixing the json file",
24862666
files: FileMap{

‎tsc/internal/fourslash/tests/quickinfoVerbosityConstMergedWithNamespace_test.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ declare namespace create/*2*/ {
1717
var version: string;
1818
function reset(): void;
1919
}
20+
type Factory/*3*/ = typeof create;
21+
declare const factory/*4*/: typeof create;
2022
`
2123
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
2224
defer done()
2325
f.VerifyBaselineHoverWithVerbosity(t, map[string][]int{
2426
"1": {0, 1},
2527
"2": {0, 1},
28+
"3": {0, 1, 2},
29+
"4": {0, 1, 2},
2630
})
2731
}

‎tsc/internal/fourslash/tests/quickinfoVerbosityRecursiveType_test.go‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,29 @@ const m/*m*/: TreeNode<number> = {
3535
left: undefined,
3636
right: undefined,
3737
orange: { name: "orange" },
38+
}
39+
type Wrapped/*wrapped*/<T> = ({ value: T; next: Wrapped<T> });
40+
declare const wrappedValue/*wrappedValue*/: Wrapped<string>;
41+
type Callable/*callable*/<T> = { (value: T): Callable<T>; link: Wrapped<T> };
42+
declare const callableValue/*callableValue*/: Callable<number>;
43+
const recur/*recur*/ = () => recur;
44+
type RecursiveArray/*recursiveArray*/ = RecursiveArray[];
45+
type RecursiveTuple/*recursiveTuple*/ = [RecursiveTuple?];
46+
type RecursiveUnion/*recursiveUnion*/ = { next: RecursiveUnion } | undefined;
47+
declare const aliasOwner/*aliasOwner*/: Wrapped<string>;
48+
const siblingOwners/*siblingOwners*/ = { first: aliasOwner, second: aliasOwner };
49+
function local() {
50+
const object/*localObject*/ = { next: () => object };
51+
const broad: unknown = function self/*localSelf*/() { return self; };
3852
}`
3953
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
4054
defer done()
41-
f.VerifyBaselineHoverWithVerbosity(t, map[string][]int{"N": {0}, "n": {0, 1}, "t": {0, 1}, "m": {0, 1, 2}})
55+
f.VerifyBaselineHoverWithVerbosity(t, map[string][]int{
56+
"N": {0}, "n": {0, 1}, "t": {0, 1}, "m": {0, 1, 2},
57+
"wrapped": {0, 1, 2}, "wrappedValue": {0, 1, 2},
58+
"callable": {0, 1, 2}, "callableValue": {0, 1, 2},
59+
"recur": {0, 1, 2}, "localObject": {0, 1, 2}, "localSelf": {0, 1, 2},
60+
"recursiveArray": {0, 1, 2}, "recursiveTuple": {0, 1, 2}, "recursiveUnion": {0, 1, 2},
61+
"aliasOwner": {0, 1, 2}, "siblingOwners": {0, 1, 2},
62+
})
4263
}

‎tsc/internal/fourslash/tests/quickinfoVerbosityTruncation1_test.go‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package fourslash_test
22

33
import (
4+
"strconv"
45
"testing"
56

67
"github.com/microsoft/TypeScript/tsc/internal/fourslash"
8+
"github.com/microsoft/TypeScript/tsc/internal/ls/lsutil"
79
"github.com/microsoft/TypeScript/tsc/internal/testutil"
810
)
911

@@ -39,3 +41,19 @@ const obj1/*o1*/: LotsOfProps = undefined as any as LotsOfProps;`
3941
defer done()
4042
f.VerifyBaselineHoverWithVerbosity(t, map[string][]int{"o1": {0, 1}})
4143
}
44+
45+
func TestQuickinfoVerbosityInstantiationTruncation(t *testing.T) {
46+
t.Parallel()
47+
for _, length := range []int{20, 40, 80} {
48+
t.Run("quickinfoVerbosityInstantiationTruncation"+strconv.Itoa(length), func(t *testing.T) {
49+
t.Parallel()
50+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
51+
const content = `declare function f<T>(value: T): T;
52+
type Sample/*sample*/ = (aVeryLongParameterNameThatExceedsTheBudget: string) => typeof f<number>;`
53+
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
54+
defer done()
55+
f.Configure(t, lsutil.UserPreferences{MaximumHoverLength: length})
56+
f.VerifyBaselineHoverWithVerbosity(t, map[string][]int{"sample": {0, 1, 2}})
57+
})
58+
}
59+
}

0 commit comments

Comments
 (0)