typescript-go: Infinite recursion in inferFromTypes via getReturnTypeOfSignature (tsc 5.9 OK)

Đang mở
#64,701 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức phù hợp với người mới
12/100
Loại issue
Lỗi
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
go, typescript
Lĩnh vực
compilers

Hướng nghiên cứu

Start from the stack in the issue: getReturnTypeOfSignature in the checker, then inferFromTypes and inferFromTypeArguments in inference.go (around lines 89-98 and 311), and fillMissingTypeArguments in checker.go. Reproduce with the linked repo (npx oxlint --type-aware src) and api-probe.cjs. Done means the recursion terminates, ideally by hitting an instantiation or depth limit, with tsc and tsgo agreeing on the result.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Bug
🔎 Search Terms

Infinite Recursion, getReturnTypeOfSignature, IsEqual

🕗 Version & Regression Information

Versions

⏯ Playground Link

https://github.com/jandolezal71/typescript-go-issue-64701-repro

💻 Code

Repro

https://github.com/jandolezal71/typescript-go-issue-64701-repro

npm i
npx tsc --noEmit               # ok, ~1 s
npx oxlint --type-aware src    # never finishes

package.json devDependencies: [email protected], [email protected], [email protected], [email protected], [email protected]

.oxlintrc.json

{"plugins":["typescript"],"categories":{"correctness":"off"},"rules":{"typescript/no-misused-promises":"error"}}

tsconfig.json

{"compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"Bundler","strict":true,"noEmit":true,"skipLibCheck":true},"include":["src"]}

src/repro.spec.ts

import type { Asyncify, IsEqual, PartialDeep } from 'type-fest';
import { expect, vi } from 'vitest';

type Fn = (...args: any) => any;
type FirstOverload<TFn extends Fn> = (...args: Parameters<TFn>) => ReturnType<TFn>;
type NextOverload<TFn extends Fn, TStack extends Fn> = (...args: Parameters<TStack & TFn>) => ReturnType<TStack & TFn>;
type SafeAsyncify<TFn extends Fn> = ReturnType<TFn> extends Promise<unknown> ? TFn : Asyncify<TFn>;

// Collects all overloads of TFn; recursion stops once the next overload equals the previous one.
type PromisifyInner<TFn extends Fn, TAcc extends Fn, TStack extends Fn, TPrevious extends Fn, TCurrent extends Fn = NextOverload<TFn, TStack>> =
	IsEqual<TCurrent, TPrevious> extends true ? TAcc : PromisifyInner<TFn, TAcc & SafeAsyncify<TCurrent>, TCurrent & TStack, TCurrent>;
type Promisify<TFn extends Fn, TFirst extends Fn = FirstOverload<TFn>> = PromisifyInner<TFn, SafeAsyncify<TFirst>, TFirst, TFirst>;

type IfThenElse<C, T, F> = false extends C ? F : T;
type OwnProperties<T> = { [K in keyof T]: T[K] };
type AddOptionalRequestOptions<TFn> = TFn extends (...args: [...infer TArgs]) => infer TReturn
	? ((...args: [...TArgs, requestOptions?: { timeout?: number }]) => TReturn) & TFn
	: TFn;

type PromisifyProcedureTree<T> = T extends Fn
	? ReturnType<T> extends Promise<unknown>
		? AddOptionalRequestOptions<T>
		: IfThenElse<
				keyof OwnProperties<T> extends never ? true : false,
				AddOptionalRequestOptions<Promisify<T>>,
				AddOptionalRequestOptions<Promisify<T>> & PromisifyProcedureTree<OwnProperties<T>>
			>
	: T extends Record<string, unknown>
		? { readonly [K in keyof T]: PromisifyProcedureTree<T[K]> }
		: T;

type ProtocolValue = null | string | number | boolean | Date | readonly ProtocolValue[] | { readonly [key: string]: ProtocolValue };

type ProcedureTree<TProtocolValue> = {
	readonly [key: string | number]:
		| ProcedureTree<TProtocolValue>
		| ((...args: never[]) => void | PartialDeep<TProtocolValue> | Promise<void | PartialDeep<TProtocolValue>>);
};

declare class Client<TTree> {
	get call(): PromisifyProcedureTree<TTree>;
}

declare function createHandler<TTree extends ProcedureTree<ProtocolValue>>(): (client: Client<TTree>) => void;

expect(vi.mocked(createHandler)).toHaveBeenCalledTimes(1);

tsc API comparison (node api-probe.cjs, finishes in ~80 ms)

const ts = require('typescript');
const cfg = ts.getParsedCommandLineOfConfigFile('./tsconfig.json', {}, { ...ts.sys, onUnRecoverableConfigFileDiagnostic() {} });
const program = ts.createProgram(cfg.fileNames, cfg.options);
const checker = program.getTypeChecker();
const sf = program.getSourceFile('./src/repro.spec.ts');
let n = 0;
const t0 = Date.now();
(function visit(node) {
	if (ts.isExpression(node)) {
		const type = checker.getTypeAtLocation(node);
		for (const part of type.isUnion() ? type.types : [type]) {
			for (const sig of part.getCallSignatures()) { checker.getReturnTypeOfSignature(sig); n++; }
			for (const prop of part.getProperties()) {
				const pt = checker.getTypeOfSymbolAtLocation(prop, node);
				for (const sig of pt.getCallSignatures()) { checker.getReturnTypeOfSignature(sig); n++; }
			}
		}
	}
	ts.forEachChild(node, visit);
})(sf);
console.log(`tsc ${ts.version} API: ${n} return types resolved in ${Date.now() - t0} ms`);
🙁 Actual behavior
Checker never terminates in getReturnTypeOfSignature (inference recursion) on overload-walking type; tsc 5.9 resolves it in milliseconds

What happens

  • checker.getReturnTypeOfSignature never returns: 100 % CPU indefinitely (observed >20 min).
  • It is reached through the type-aware linter tsgolint, rule no-misused-promises (also strict-void-return). The rule only asks for the return type of a call signature.
  • tsgo --noEmit on the same project finishes in ~1 s with no errors, because the plain check never requests this return type.

tsc 5.9.3 does not hang on the same requests

Through the TS API, I requested the return type of every call signature of every expression and its properties in the file. That is a superset of what the lint rule requests. All 1709 resolved in 79 ms (script below).

The type itself terminates

Forcing Promisify on concrete overloaded functions (1, 2 and 3 overloads, generic overloads) type-checks correctly in both tsc and tsgo. tsc reports 4174 instantiations.

Trigger

A recursive type walks function overloads and stops when IsEqual<Current, Previous> is true.

  • With the type-fest 4.31+ IsEqual, <G>() => G extends A & G | G ? 1 : 2, it hangs.
  • With the older form, <G>() => G extends A ? 1 : 2, it finishes in ~1 s.

All of these are needed to reproduce. Removing any one makes it finish:

  • the recursive PromisifyInner,
  • the Client class with a call getter,
  • the generic createHandler constrained to ProcedureTree,
  • expect(vi.mocked(createHandler)) (vitest's MockedFunction type).

Stack (SIGQUIT on the hung process, trimmed; full dump attached as stack.txt):

tsgolint/internal/rules/no_misused_promises ...
typescript-go/internal/checker.(*Checker).getReturnTypeOfSignature
  .instantiateType → .instantiateTypeWithAlias → .instantiateTypeWorker
  .getConditionalTypeInstantiation → .getConditionalType
  ...458 frames elided...
  .inferFromTypes            (inference.go:89 / :98)
  .inferFromTypeArguments    (inference.go:311)
  .inferFromTypes
  .inferFromTypeArguments
  ... (repeats, same source/target pointers on each inferFromTypes call)
  .fillMissingTypeArguments  (checker.go:21859)

🙂 Expected behavior

Expected

The checker terminates like tsc does. If the type is too deep, it should report or bail out at the instantiation and depth limits, not loop.

stack.txt

Additional information about the issue

attaching stacktrace

Ngôn ngữ chính
Go
Star
112k
Fork
15.4k
Merge trung bình
2 ngày 21 giờ
Pull request đã merge (30 ngày)
126

Chuẩn bị môi trường

Mở trong Codespaces

Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của microsoft/TypeScript

Tất cả issue của microsoft/TypeScript

Issue tương tự

Thêm issue về Go

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.