microsoft / microsoft/TypeScript

[TS7 LSP] Panic on document URI "file://" (empty path): vfs: path "tsconfig.json" is not absolute

Aperta
#64,300 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Lingua principale
Go
Stelle
111k
Fork
14.3k
Merge medio
2g 4h
PR unite (30g)
132

Descrizione

### 🔎 Search Terms

tsgo lsp panic, vfs: path "tsconfig.json" is not absolute, file:// empty path, computeConfigFileName, RootLength, DidChangeFiles, DidRequestFile

### 🕗 Version & Regression Information

- typescript `7.0.2` (`node_modules/.bin/tsc --lsp --stdio`, `serverInfo.name: typescript-go`), macOS arm64
- Code is unchanged on `main`: [`tsc/internal/lsp/lsproto/lsp.go` `DocumentUri.FileName`](https://github.com/microsoft/TypeScript/blob/main/tsc/internal/lsp/lsproto/lsp.go), [`tsc/internal/project/configfileregistrybuilder.go` `computeConfigFileName`](https://github.com/microsoft/TypeScript/blob/main/tsc/internal/project/configfileregistrybuilder.go)
- Native (Go) LSP only; not applicable to `tsserver`

### ⏯ Playground Link

_No response_ (LSP server crash, not a type-checking issue)

### 💻 Code

The server exits when a client sends a document with URI `file://` (a `file` URI with an empty path). Client-agnostic repro, no editor involved — run from any directory containing `node_modules/.bin/tsc`:

```python
#!/usr/bin/env python3
"""Repro: TS 7 LSP panics on document URI "file://" (empty path).
Usage: python3 tsgo-repro.py node_modules/.bin/tsc
"""
import json, os, subprocess, sys, threading

tsc = sys.argv[1] if len(sys.argv) > 1 else "node_modules/.bin/tsc"
p = subprocess.Popen([tsc, "--lsp", "--stdio"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)

def send(m):
b = json.dumps(m).encode()
p.stdin.write(b"Content-Length: %d\r\n\r\n" % len(b) + b); p.stdin.flush()

def read():
hdr = b""
while not hdr.endswith(b"\r\n\r\n"):
c = p.stdout.read(1)
if not c: return None
hdr += c
n = int(hdr.split(b":")[1]); return json.loads(p.stdout.read(n))

def pump(): # answer server->client requests so it does not cancel on us
while (m := read()) is not None:
if "id" in m and "method" in m:
send({"jsonrpc": "2.0", "id": m["id"], "result": [None] if m["method"] == "workspace/configuration" else None})
elif m.get("id") == 2:
print("hover response:", json.dumps(m)[:200])

threading.Thread(target=pump, daemon=True).start()
send({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"processId": None, "rootUri": "file://" + os.getcwd(), "capabilities": {}}})
send({"jsonrpc": "2.0", "method": "initialized", "params": {}})
send({"jsonrpc": "2.0", "method": "textDocument/didOpen", "params": {"textDocument": {"uri": "file://", "languageId": "typescript", "version": 1, "text": "const x = 1;\n"}}})
send({"jsonrpc": "2.0", "id": 2, "method": "textDocument/hover", "params": {"textDocument": {"uri": "file://"}, "position": {"line": 0, "character": 6}}})
try:
print("server exit code:", p.wait(timeout=5))
except subprocess.TimeoutExpired:
print("server still alive (no crash)"); p.kill()
```

```
$ python3 tsgo-repro.py node_modules/.bin/tsc
panic: vfs: path "tsconfig.json" is not absolute [recovered, repanicked]
...
server exit code: 2
```

How this happens in practice: Neovim's `vim.lsp` attaches to any buffer whose filetype matches, including unnamed buffers (`nvim_buf_get_name` is `""`), and `vim.uri_from_bufnr` serializes those as `file://`. Some plugins create such buffers, so the whole server dies mid-session and every open TypeScript file loses the language server (`method "textDocument/definition" is not supported by any server activated for this buffer`).

### 🙁 Actual behavior

Cause chain:

1. `lsproto.DocumentUri.FileName()` — `url.Parse("file://").Path` is `""`, so the file name becomes `""`.
2. `ProjectCollectionBuilder.DidChangeFiles` / `DidRequestFile` → `toPath("")` resolves to the current directory, `fileName` stays `""`.
3. `configFileRegistryBuilder.computeConfigFileName("")` → `tspath.GetDirectoryPath("")` is `""` → `tspath.ForEachAncestorDirectory("", ...)` → `CombinePaths("", "tsconfig.json")` = `"tsconfig.json"` → `FS().FileExists("tsconfig.json")` → `vfs/internal.RootLength` panics on a relative path.
4. The panic is recovered and re-panicked in `Snapshot.Clone`, the process exits with code 2, and all documents lose the server.

Full stack trace (7.0.2)

```
panic: vfs: path "tsconfig.json" is not absolute [recovered, repanicked]

goroutine 6 [running]:
github.com/microsoft/typescript-go/internal/project.(*Snapshot).Clone.func1()
github.com/microsoft/typescript-go/internal/project/snapshot.go:243 +0xb8
panic({0x1061a7d60?, 0x5b2c9e122100?})
runtime/panic.go:860 +0x12c
github.com/microsoft/typescript-go/internal/vfs/internal.RootLength({0x5b2c9e1041c0, 0xd})
github.com/microsoft/typescript-go/internal/vfs/internal/internal.go:23 +0x94
github.com/microsoft/typescript-go/internal/vfs/internal.SplitPath({0x5b2c9e1041c0?, 0x104fe0fe0?})
github.com/microsoft/typescript-go/internal/vfs/internal/internal.go:32 +0x2c
github.com/microsoft/typescript-go/internal/vfs/internal.(*Common).RootAndPath(0x1064e4c40, {0x5b2c9e1041c0?, 0x5b2c9e134048?})
github.com/microsoft/typescript-go/internal/vfs/internal/internal.go:39 +0x2c
github.com/microsoft/typescript-go/internal/vfs/internal.(*Common).Stat(0x5b2c9df120e0?, {0x5b2c9e1041c0?, 0x5b2c9e13c601?})
github.com/microsoft/typescript-go/internal/vfs/internal/internal.go:47 +0x20
github.com/microsoft/typescript-go/internal/vfs/internal.(*Common).FileExists(0x5b2c9e134048?, {0x5b2c9e1041c0?, 0x1061a7d60?})
github.com/microsoft/typescript-go/internal/vfs/internal/internal.go:59 +0x20
github.com/microsoft/typescript-go/internal/vfs/osvfs.(*osFS).FileExists(0x1064e4c40, {0x5b2c9e1041c0, 0xd})
github.com/microsoft/typescript-go/internal/vfs/osvfs/os.go:105 +0x74
github.com/microsoft/typescript-go/internal/vfs/cachedvfs.(*FS).FileExists(0x5b2c9e134000, {0x5b2c9e1041c0, 0xd})
github.com/microsoft/typescript-go/internal/vfs/cachedvfs/cachedvfs.go:71 +0x6c
github.com/microsoft/typescript-go/internal/project.(*snapshotFSBuilder).FileExists(0x5b2c9e120080, {0x5b2c9e1041c0, 0xd}, {0x5b2c9e13e060, 0x56})
github.com/microsoft/typescript-go/internal/project/snapshotfs.go:313 +0xc4
github.com/microsoft/typescript-go/internal/project.(*sourceFS).FileExists(0x5b2c9e11e5d0, {0x5b2c9e1041c0, 0xd})
github.com/microsoft/typescript-go/internal/project/snapshotfs.go:738 +0x70
github.com/microsoft/typescript-go/internal/project.(*configFileRegistryBuilder).computeConfigFileName.func2({0x0, 0x0})
github.com/microsoft/typescript-go/internal/project/configfileregistrybuilder.go:613 +0x7c
github.com/microsoft/typescript-go/internal/tspath.ForEachAncestorDirectory[...]({0x0?, 0x5b2c9e13c898?}, 0x5b2c9e13c8c0?)
github.com/microsoft/typescript-go/internal/tspath/path.go:1068 +0x4c
github.com/microsoft/typescript-go/internal/project.(*configFileRegistryBuilder).computeConfigFileName(0x5b2c9e1300c0, {0x0, 0x0}, 0x0, 0x5b2c9e10a0f0)
github.com/microsoft/typescript-go/internal/project/configfileregistrybuilder.go:610 +0x124
github.com/microsoft/typescript-go/internal/project.(*configFileRegistryBuilder).getConfigFileNameForFile(0x5b2c9e1300c0, {0x0, 0x0}, {0x5b2c9e11c1e0, 0x48}, 0x5b2c9e10a0f0)
github.com/microsoft/typescript-go/internal/project/configfileregistrybuilder.go:643 +0x170
github.com/microsoft/typescript-go/internal/project.(*ProjectCollectionBuilder).findOrCreateDefaultConfiguredProjectForFile(0x5b2c9e12e1e0, {0x0, 0x0}, {0x5b2c9e11c1e0, 0x48}, 0x1, 0x5b2c9e10a0f0)
github.com/microsoft/typescript-go/internal/project/projectcollectionbuilder.go:999 +0xe4
github.com/microsoft/typescript-go/internal/project.(*ProjectCollectionBuilder).ensureConfiguredProjectAndAncestorsForFile(0x5b2c9e12e1e0, {0x0, 0x0}, {0x5b2c9e11c1e0, 0x48}, 0x5b2c9e10a0f0)
github.com/microsoft/typescript-go/internal/project/projectcollectionbuilder.go:765 +0x3c
github.com/microsoft/typescript-go/internal/project.(*ProjectCollectionBuilder).DidChangeFiles(0x5b2c9e12e1e0, {{0x5b2c9e104150, 0x7}, {0x0, 0x0}, {0x0}, {0x0}, {0x5b2c9e11e5a0}, {0x0}, 0x0, ...}, ...)
github.com/microsoft/typescript-go/internal/project/projectcollectionbuilder.go:327 +0x324
github.com/microsoft/typescript-go/internal/project.(*Snapshot).Clone(0x5b2c9e0de000, {0x106451ef8, 0x5b2c9e11e150}, {{{0x5b2c9e122080, 0x1, 0x1}, {0x0, 0x0, 0x0}, {0x0, ...}, ...}, ...}, ...)
github.com/microsoft/typescript-go/internal/project/snapshot.go:348 +0x98c
github.com/microsoft/typescript-go/internal/project.(*Session).updateSnapshot(0x5b2c9e0e0008, {0x106451ef8, 0x5b2c9e11e150}, _, {{{0x5b2c9e122080, 0x1, 0x1}, {0x0, 0x0, 0x0}, ...}, ...}, ...)
github.com/microsoft/typescript-go/internal/project/session.go:1205 +0xfc
github.com/microsoft/typescript-go/internal/project.(*Session).UpdateSnapshot(...)
github.com/microsoft/typescript-go/internal/project/session.go:1191
github.com/microsoft/typescript-go/internal/project.(*Session).DidOpenFile(0x5b2c9e0e0008, {0x106451ef8, 0x5b2c9e11e150}, {0x5b2c9e104150, 0x7}, 0x1, {0x5b2c9e1041a0, 0xd}, {0x5b2c9e104180, 0xa})
github.com/microsoft/typescript-go/internal/project/session.go:311 +0x3e4
github.com/microsoft/typescript-go/internal/lsp.(*Server).handleDidOpen(0x10644d700?, {0x106451ef8?, 0x5b2c9e11e150?}, 0x104ff95cc?)
github.com/microsoft/typescript-go/internal/lsp/server.go:1325 +0x3c
github.com/microsoft/typescript-go/internal/lsp.init.func1.registerNotificationHandler[...].6({0x106451ef8, 0x5b2c9e11e150}, 0x14)
github.com/microsoft/typescript-go/internal/lsp/server.go:827 +0xc4
github.com/microsoft/typescript-go/internal/lsp.(*Server).handleRequestOrNotification(0x5b2c9de0e708, {0x106451ef8?, 0x5b2c9e11e120?}, 0x5b2c9e0b6060)
github.com/microsoft/typescript-go/internal/lsp/server.go:703 +0xc8
github.com/microsoft/typescript-go/internal/lsp.(*Server).dispatchLoop(0x5b2c9de0e708, {0x106451f30?, 0x5b2c9deba000?})
github.com/microsoft/typescript-go/internal/lsp/server.go:577 +0x3b8
github.com/microsoft/typescript-go/internal/lsp.(*Server).Run.func1()
github.com/microsoft/typescript-go/internal/lsp/server.go:426 +0x24
golang.org/x/sync/errgroup.(*Group).Go.func1()
golang.org/x/sync@v0.21.0/errgroup/errgroup.go:93 +0x4c
created by golang.org/x/sync/errgroup.(*Group).Go in goroutine 1
golang.org/x/sync@v0.21.0/errgroup/errgroup.go:78 +0x90
server exit code: 2
```

### 🙂 Expected behavior

The server should reject the document (e.g. respond with `InvalidParams` for requests targeting it, or ignore the `didOpen`) and keep running. Other servers handle the same URI gracefully — e.g. tailwindcss-language-server responds `InvalidParams: "workspace URI is not a valid file path: file://."` and stays alive.

Possible fix: validate the result of `DocumentUri.FileName()` (empty or non-absolute path) at the request/notification boundary before it reaches `ProjectCollectionBuilder`, rather than letting it hit the VFS assertion.

### Additional information about the issue

Possibly related: #63834 (`Fatal error during resolution through RootLength`) hits the same VFS assertion via module resolution with no repro; this one has a different entry point (`DidChangeFiles` / `DidRequestFile` → `computeConfigFileName`) and a deterministic repro.

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Esegui il tsgo-repro.py fornito su node_modules/.bin/tsc per riprodurre il crash. Inizia da tsc/internal/lsp/lsproto/lsp.go e dai gestori delle richieste/notifiche LSP, quindi segui gli entry point menzionati di ProjectCollectionBuilder e configfileregistrybuilder.go. Il lavoro è completato quando un documento file:// viene rifiutato o ignorato senza raggiungere il VFS panic e il processo LSP rimane in esecuzione.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
go
Ambito
devtools
Tipo di issue
Bug
Difficoltà
3/5
Tempo stimato
1-2 giorni
Stato di attività
Attiva
Chiarezza
Specificata chiaramente
Idoneità per principianti
76/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.