microsoft / microsoft/TypeScript

LSP: file contents are not re-read when `didChangeWatchedFiles` reports `Created` (type 1) for a file already in the program

未关闭
#64,195 0 条评论 0 个 reaction 已指派 1 人 在 GitHub 查看

@johnfav03 已经在做这个了。

开始于 2026年9月14日。

Needs Investigation
主要语言
Go
星标
111k
派生
14.3k
平均合并
2 天 4 小时
30 天内合并 PR
132

描述

🔎 Search Terms
didChangeWatchedFiles
FileChangeType.Created
type 1 Created event stale contents
language server stale file contents
LSP requires restart types out of sync
atomic save rename language server
has no exported member after the file was fixed
watched files not reloaded
tsc --lsp
🕗 Version & Regression Information
  • I was unable to test this on prior versions because the LSP server (tsc --lsp)
    is new in TypeScript 7; TypeScript 6.x and earlier have no equivalent
    workspace/didChangeWatchedFiles handling to compare against, so this is not a
    regression from a previous release.

Reproduces on both:

  • typescript@7.0.2 (latest)
  • typescript@7.1.0-dev.20260907.1 (next)

every-ts bisection is not applicable for the same reason — the code path does not
exist in earlier versions.

⏯ Playground Link

Not applicable. The bug is in how the language server reacts to file-system change notifications, so it cannot be reproduced in the Playground. A standalone LSP client script that reproduces it in isolation is attached instead (see 💻 Code).

💻 Code

Three files:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler"
  },
  "include": ["src/**/*"]
}
// src/main.ts
import { thing } from './helper';

export const used = thing;
// src/helper.ts  — starts out without `thing`, so main.ts has an error
export const other = 1;

Then, over LSP with main.ts open and helper.ts not open:

  1. initialize (advertising workspace.didChangeWatchedFiles.dynamicRegistration),
    initialized, then textDocument/didOpen for src/main.ts.

  2. textDocument/diagnostic for main.ts
    Module '"./helper"' has no exported member 'thing'. (expected at this point)

  3. Fix src/helper.ts on disk to export const thing = 1;, written the way an
    atomic save does: write helper.ts.tmp, then rename("helper.ts.tmp", "helper.ts").

  4. Send the change notification with the events a client actually sends for that
    rename (VS Code's watcher reports a rename-over as a create):

    { "changes": [
        { "uri": ".../src/helper.ts.tmp", "type": 1 },
        { "uri": ".../src/helper.ts",     "type": 1 },
        { "uri": ".../src/helper.ts.tmp", "type": 3 } ] }
    
  5. textDocument/diagnostic for main.ts again.

Attached: repro-created-event.js — performs exactly these steps against
tsc --lsp --stdio. Node only, no dependencies, no editor involved:

$ node repro-created-event.js <path-to-tsc>
PASS  Changed (type 2) only  — control
FAIL  Created (type 1) only
        still reported: Module '"./helper"' has no exported member 'thing'.
FAIL  what VS Code sends for an atomic save
        still reported: Module '"./helper"' has no exported member 'thing'.

repro-created-event.js

🙁 Actual behavior

After helper.ts is fixed on disk, textDocument/diagnostic for main.ts keeps
returning Module '"./helper"' has no exported member 'thing'. The server never
re-reads helper.ts, so the program holds the old contents indefinitely — the stale
diagnostic survives further edits to other files and only goes away on a server
restart, or on textDocument/didOpen of helper.ts (which supplies the contents
explicitly).

The trigger is the change type. For the same on-disk change:

notification for the changed file result
type: 2 (Changed) only ✓ error clears (correct)
type: 1 (Created) only ✗ error persists
tmp create → target create → tmp delete (what VS Code sends) ✗ error persists
🙂 Expected behavior

All three should clear the diagnostic, since the file's contents on disk changed.

A Created event for a path that is already in the program should invalidate that
file's cached contents, the same way Changed does. A client cannot reliably tell
the two apart: on Linux, rename() over an existing path produces IN_MOVED_TO,
which is a create from the watcher's point of view — VS Code therefore sends
type: 1 for what is semantically a modification.

This matters in practice because saving atomically (write a temp file, then rename
over the target) is a common pattern — formatters, code generators, editors with
files.atomicSave, and AI coding tools all do it. Every such save takes this path,
so the user-visible result is a permanent, wrong has no exported member on an
import line, "fixed" only by restarting the language server.

Additional information about the issue
Environment
  • TypeScript 7.0.2 and 7.1.0-dev.20260907.1, Linux (arm64), Node 24.14.0
  • Language server started as tsc --lsp --stdio
  • Also observed through VS Code 1.127.0 with js/ts.experimental.useTsgo: true
    and the "TypeScript 7" extension TypeScriptTeam.native-preview@0.20260708.2
    (whose bundled binary also reports 7.0.2)
What is not the cause
  • The watcher globs the server registers are **/* (the extension-suffixed globs
    from microsoft/typescript-go#2506 are gone), so the event does reach the server —
    VS Code's own logs show handled method 'workspace/didChangeWatchedFiles' for
    these changes.
  • The server is robust in every adjacent scenario tested: didChange,
    didClose + didOpen, type: 2 on an unopened file, 20 rapid successive writes,
    delete + create of a previously unknown file, and create-then-modify before
    notifying. Only "type: 1 for a path already in the program" fails.
  • Not a client-side file-watching problem: reproduced with a plain Node LSP client,
    with no editor in the picture.
Possibly related
  • microsoft/typescript-go#2506 — same area (watched-file handling), different cause
    (extension-suffixed watch globs and failedLookupLocations for unresolved
    modules). Already fixed: a Created event for a path that is not yet in the
    program does work.
  • JetBrains WEB-76338 "Typescript Go LSP often requires restart as types get out of
    sync" — same user-visible symptom from a different client, no root cause
    identified. Possibly the same bug, since JetBrains also saves atomically.
Workarounds
  • Open the changed file in the editor (didOpen re-reads from disk).
  • Restart the language server.
  • Follow the atomic write with an in-place rewrite of identical bytes (produces
    IN_MODIFYtype: 2). Note the client may coalesce the two events if they are
    close together: with VS Code's watcher, a gap of ~600ms or more was needed before
    the second event was reported as an update rather than another create.

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。