microsoft / microsoft/TypeScript
textDocument/diagnostic on the first-opened file in a session can silently omit real errors
- Ngôn ngữ chính
- Go
- Star
- 111k
- Fork
- 14.3k
- Merge trung bình
- 2 ngày 4 giờ
- Pull request đã merge (30 ngày)
- 132
Mô tả
## Summary
The first `textDocument/diagnostic` (pull) request against a file, in a fresh `tsgo --lsp` session, can return `{"kind":"full","items":[]}` — no diagnostics — even when the file has a real, unambiguous error, and even after the request completes normally and program construction has finished. It is not a timing issue: waiting 2s, 8s, and 10s+ before issuing the pull request all reproduce it identically, and retrying the pull request itself doesn't help either. Querying a *different* file first, then the original file, reliably makes the original file's diagnostics correct.
This reproduced with two unrelated diagnostic codes (a missing-module error, TS2307, and a plain type mismatch, TS2322), so it doesn't look specific to one kind of check.
## Minimal repro
Two files, no external dependencies:
`helper.ts`
```ts
export function greet(name: string): string {
return `Hello, ${name}`;
}
```
`broken.ts`
```ts
import { greet } from "./helper";
export const message = greet("world");
const bad: string = 123; // real error: TS2322
```
`tsconfig.json`
```json
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "noEmit": true }, "include": ["*.ts"] }
```
Standalone Node script (no dependencies beyond `node:child_process`/`node:fs`) — byte-accurate Content-Length framing, opens `broken.ts` as the *only* file in the session, waits, then pulls diagnostics:
```js
import { spawn } from 'node:child_process';
import fs from 'node:fs';
const TSGO = process.argv[2]; // path to tsgo binary
const DIR = process.argv[3]; // path to the fixture directory above
const FILE = `${DIR}/broken.ts`;
const child = spawn(TSGO, ['--lsp', '--stdio'], { stdio: ['pipe', 'pipe', 'pipe'] });
child.stderr.on('data', () => {});
let buf = Buffer.alloc(0);
const listeners = new Set();
child.stdout.on('data', (c) => {
buf = Buffer.concat([buf, c]);
while (true) {
const he = buf.indexOf('\r\n\r\n'); if (he === -1) break;
const m = /Content-Length: (\d+)/i.exec(buf.subarray(0, he).toString('ascii'));
if (!m) { buf = buf.subarray(he + 4); continue; }
const len = parseInt(m[1], 10), bs = he + 4;
if (buf.length < bs + len) break;
const body = buf.subarray(bs, bs + len); buf = buf.subarray(bs + len);
let msg; try { msg = JSON.parse(body.toString('utf8')); } catch { continue; }
if (msg.method && msg.id !== undefined && !msg.result && !msg.error) {
const rb = JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: null });
child.stdin.write(`Content-Length: ${Buffer.byteLength(rb, 'utf8')}\r\n\r\n${rb}`);
}
for (const l of [...listeners]) l(msg);
}
});
function send(method, params) { const b = JSON.stringify({ jsonrpc: '2.0', method, params }); child.stdin.write(`Content-Length: ${Buffer.byteLength(b, 'utf8')}\r\n\r\n${b}`); }
let seq = 0;
function sendReq(method, params) { const id = ++seq; const b = JSON.stringify({ jsonrpc: '2.0', id, method, params }); child.stdin.write(`Content-Length: ${Buffer.byteLength(b, 'utf8')}\r\n\r\n${b}`); return id; }
function waitFor(pred, ms = 15000) { return new Promise((res, rej) => { const t = setTimeout(() => rej(new Error('timeout')), ms); const h = (m) => { if (pred(m)) { clearTimeout(t); listeners.delete(h); res(m); } }; listeners.add(h); }); }
const toUri = (p) => `file://${p}`;
(async () => {
const initId = sendReq('initialize', { processId: process.pid, rootUri: toUri(DIR), capabilities: { textDocument: { publishDiagnostics: {}, diagnostic: {} } } });
await waitFor((m) => m.id === initId);
send('initialized', {});
send('textDocument/didOpen', { textDocument: { uri: toUri(FILE), languageId: 'typescript', version: 1, text: fs.readFileSync(FILE, 'utf8') } });
await new Promise((r) => setTimeout(r, 10000)); // 10s+ wait — does not help
const id = sendReq('textDocument/diagnostic', { textDocument: { uri: toUri(FILE) } });
const resp = await waitFor((m) => m.id === id);
console.log('broken.ts, first file in session:', JSON.stringify(resp.result));
// Expected: one item, TS2322. Actual: { kind: "full", items: [] }
child.kill('SIGKILL');
process.exit(0);
})();
```
## What's been ruled out
| Hypothesis | Result |
|---|---|
| Simple timing race, needs a short wait | No — reproduces identically at 2s, and does not resolve at 10s+ |
| Needs a retry of the pull request | No — 3 retries with backoff (0/500/1500ms) still returns empty |
| Needs *any* second file opened first | No — opening an unrelated file (same project) first does not resolve it, tested against a large real-world project |
| Needs a diagnostic pull for *any* other file first | No — pulling diagnostics for an unrelated file first, then pulling the target, still returns empty, tested against the same large real-world project |
| Needs a *related* file (same project) opened and queried first | **Yes in every case tested** — but see the open question below |
## Open question — project scale, or flakiness?
Against a large real-world project (~11,000 files in the reachable graph), an *unrelated* same-project file did **not** unblock the target file — only a specific working sequence did. Against the minimal two-file fixture above, an *unrelated* sibling file (importing the same `helper.ts` but with no direct relationship to `broken.ts`) reliably **did** unblock it. Same failure class, opposite behavior depending on project size. I haven't run enough repetitions to rule out flakiness as the actual explanation rather than a real scale-dependent rule — flagging this honestly rather than asserting a clean mechanism.
## Environment
- `microsoft/typescript-go` built from source, `main`, commit from 2026-08-12
- macOS, arm64
- `tsgo --lsp --stdio`
## Possibly related
microsoft/TypeScript#63859 describes root-file-*order* affecting genuine conditional-type inference results in the batch compiler, which a maintainer noted is an accepted/intentional class of behavior for TS7's type ordering. This looks like a different mechanism — the errors here are unambiguous (a missing module, a plain type mismatch), and the issue is specifically that the LSP pull-diagnostics response for the first-queried file is empty rather than the underlying type-checking result differing — but flagging it in case they're related at some lower level.
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- 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.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Đánh giá
Issue này chưa được đánh giá.