Skip to content

Semantic tokens: defaultLibrary modifier never set on case-insensitive filesystems (macOS/Windows) #4635

Description

@philiplindberg

Filed via gh, so the VS Code Editor Issue form wasn't applied — reproducing its fields below. Suggested label: Domain: Editor.

Extension Version

0.20260708.2 (darwin-arm64)

VS Code Version

1.128.0

Operating system Version

macOS 26.5.1 (arm64)

Steps to reproduce

  1. On macOS, enable the TypeScript Native Preview extension.
  2. Open a .ts file containing:
    export function clamp(val: number, lo: number, hi: number): number {
      return Math.max(lo, Math.min(hi, val))
    }
    
    export function parse(input: string): number | null {
      let num = parseInt(input)
      return isNaN(num) ? null : num
    }
    
    console.log(clamp(5, 0, 3))
  3. Run Developer: Inspect Editor Tokens and Scopes on console, Math, parseInt, or isNaN.

Issue

On a case-insensitive filesystem, tsgo never sets the defaultLibrary semantic token modifier on any symbol. Token types are correct; only the modifier bit is missing.

Visible effect: globals from lib.*.d.ts lose their themed color, because themes key off defaultLibrary to select the support.* scopes.

Expected (matches typescript@6.0):

  • console → type variable, modifiers defaultLibrarysupport.variable
  • parseInt → type function, modifiers defaultLibrarysupport.function

Actual (tsgo):

  • console → type variable, modifiers (none)variable.other.readwrite
  • parseInt → type function, modifiers (none)entity.name.function

I drove the extension's own binary (lib/tsc --lsp --stdio) directly as an LSP client, advertising the same semantic-token capabilities VS Code advertises, and decoded textDocument/semanticTokens/full against the server's own legend. Over one ~480-token file: 0 tokens carried defaultLibrary. Not one, for any symbol.

The server's legend is fine — it advertises defaultLibrary at index 9, matching the client list — so encoding and legend negotiation are not implicated.

Verified on macOS. The same defect is expected on Windows by the mechanism below (also case-insensitive, so useCaseSensitiveFileNames is false), but I have not tested it there.

Root cause

internal/ls/semantictokens.go:262 (and again at :268 in the symbol.Declarations fallback):

declSourceFile := ast.GetSourceFileOfNode(decl)
if declSourceFile != nil && program.IsSourceFileDefaultLibrary(tspath.Path(declSourceFile.FileName())) {
    tokenModifier |= tokenModifierDefaultLibrary
}

IsSourceFileDefaultLibrary is a map lookup — p.libFiles[path] — and libFiles is keyed by canonical tspath.Path values. Canonicalization runs GetCanonicalFileName, which lowercases the entire path when useCaseSensitiveFileNames is false.

This call site doesn't canonicalize. It string-casts FileName(), which preserves original casing. On macOS the lib files live under a path like /Users/<name>/.vscode/extensions/…/lib/lib.dom.d.ts, while the map key is /users/<name>/…. The capital U alone guarantees the lookup misses, so the modifier is never applied.

On a case-sensitive filesystem GetCanonicalFileName is the identity function, the lookup succeeds, and everything works — which is presumably why this survived CI.

Every other caller in the repo passes the canonical path correctly (program.go:694, rename.go:184, checker/services.go:1120 all use sourceFile.Path()). This is the one place that hand-rolls the cast.

Confirming the cause

Same binary, same source file, same client capabilities. The only variable changed was the casing of the directory holding the lib files:

lib directory tokens with defaultLibrary
/Users/<name>/.vscode/extensions/…/lib 0 / 483
copy of that same dir at an all-lowercase path 102 / 483

Relocating the libs to a lowercase path makes console, Math, parseInt, and isNaN all correctly report defaultLibrary.

Suggested fix

Use the source file's canonical path at both sites:

if declSourceFile != nil && program.IsSourceFileDefaultLibrary(declSourceFile.Path()) {

A regression test for this would need to run on a case-insensitive filesystem (or force useCaseSensitiveFileNames: false with a mixed-case lib path) to be meaningful — a Linux-only test would pass either way.

Behavior verified against main @ 8a749379d556.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions