microsoft / microsoft/TypeScript

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

Open
#64,300 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
111k
Forks
14.3k
Avg merge
2d 4h
Merged PRs (30d)
132

Description

### 🔎 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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Run the provided tsgo-repro.py against node_modules/.bin/tsc to reproduce the crash. Start with tsc/internal/lsp/lsproto/lsp.go and the LSP request/notification handlers, then trace the mentioned ProjectCollectionBuilder and configfileregistrybuilder.go entry points. Done means a file:// document is rejected or ignored without reaching the VFS panic and the LSP process remains running.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
devtools
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.