microsoft / microsoft/TypeScript

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

Đang mở
#64,300 2 bình luận 0 reaction 0 người được giao Xem trên GitHub

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

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. 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.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. 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

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.