microsoft / microsoft/TypeScript

API returns a stale SourceFile after the latest snapshot is disposed

オープン
#63,890 コメント 0 件 リアクション 0 件 担当者 1 名 GitHub で見る

@andrewbranch がすでに取り組んでいます。

2026年8月19日 から。

Needs Investigation
主要言語
Go
スター
111k
フォーク
14.3k
平均マージ
2日 4時間
マージ済み PR(30日)
132

説明

## Summary

When an API client queries a source file, disposes the latest snapshot, edits the open file, and calls `updateSnapshot` again, the new snapshot can return the cached `SourceFile` from the disposed snapshot.

In VS Code this presents as follows: the first edit is reflected in the first API snapshot, but after that snapshot is disposed, a snapshot created for the second edit still contains the text from the first edit.

Observed with `@typescript/native` version `7.1.0-dev.20260818.1` using an API created through `API.fromLSPConnection`.

The caller follows this pattern:

```ts
const snapshot = await api.updateSnapshot({ openFiles: [{ uri }] });
try {
const project = await snapshot.getDefaultProjectForFile({ uri });
const sourceFile = await project?.program.getSourceFile({ uri });
// sourceFile.text can contain the previous edit here
} finally {
await snapshot.dispose();
}
```

## Steps to reproduce

1. Start the native preview language server and create an API with `API.fromLSPConnection`.
2. Edit an open TypeScript file.
3. Call `updateSnapshot`, fetch its `SourceFile` (populating the client-side source-file cache), and dispose the snapshot.
4. Edit the same file again and let the LSP `didChange` notification complete.
5. Call `updateSnapshot` again and fetch the same `SourceFile`.

A direct API regression test can use the equivalent lifecycle:

```ts
const firstSnapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" });
const firstSourceFile = await firstSnapshot
.getProject("/tsconfig.json")!
.program.getSourceFile("/src/foo.ts");
assert.ok(firstSourceFile);
await firstSnapshot.dispose();

fs.writeFile!("/src/foo.ts", `export const foo = "changed";`);
const secondSnapshot = await api.updateSnapshot({
fileChanges: { changed: ["/src/foo.ts"] },
});
const secondSourceFile = await secondSnapshot
.getProject("/tsconfig.json")!
.program.getSourceFile("/src/foo.ts");

assert.equal(secondSourceFile?.text, `export const foo = "changed";`);
```

## Expected behavior

The second snapshot returns a `SourceFile` containing the second edit.

## Actual behavior

The second snapshot can return the cached `SourceFile` containing the first edit.

## Likely cause

The server and JS client appear to have different lifetime assumptions for the latest disposed snapshot:

- `Session.releaseSnapshot` deletes the latest snapshot from `s.snapshots` when its API refcount reaches zero, while `s.latestSnapshot` still points to that handle.
- On the next `handleUpdateSnapshot`, `prevSD := s.snapshots[s.latestSnapshot]` is therefore `nil`, so the response has no `SnapshotChanges` diff.
- The JS API intentionally retains cache references for a disposed latest snapshot. On the next update it calls `SourceFileCache.retainForSnapshot` before releasing that snapshot's cache references.
- With `data.changes === undefined`, the cache treats every previously fetched source file as unchanged and retains it for the new snapshot. `getSourceFile` then returns it through `getRetained` without asking the server for the new content.

Relevant code paths:

- `internal/api/session.go`: `releaseSnapshot` and `handleUpdateSnapshot`
- `_packages/native-preview/src/api/async/api.ts`: `API.updateSnapshot`
- `_packages/native-preview/src/api/sourceFileCache.ts`: `retainForSnapshot` and `getRetained`

Either the server needs to retain a diff base independently of the client-visible snapshot lifetime, or the client must not carry cache entries forward when its previous latest snapshot was disposed and no reliable diff is available.

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。