microsoft / microsoft/TypeScript
[TS7 LSP] Panic on document URI "file://" (empty path): vfs: path "tsconfig.json" is not absolute
Chưa có ai nhận issue này.
- 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ả
### 🔎 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.
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.
Hướng nghiên cứu
Chạy tsgo-repro.py được cung cấp với node_modules/.bin/tsc để tái hiện crash. Bắt đầu với tsc/internal/lsp/lsproto/lsp.go và các trình xử lý request/notification của LSP, sau đó lần theo các entry point ProjectCollectionBuilder và configfileregistrybuilder.go đã được đề cập. Hoàn thành nghĩa là một tài liệu file:// bị từ chối hoặc bị bỏ qua mà không đi tới VFS panic, và tiến trình LSP vẫn đang chạy.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- go
- Lĩnh vực
- devtools
- Loại issue
- Lỗi
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức độ hoạt động
- Sôi nổi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 76/100