Nimblesite / Nimblesite/SharpLsp

Closure dedup is case-insensitive on case-sensitive filesystems, silently dropping includes

Open
#190 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug cluster:csharp-single-file
Dominant language
TypeScript
Stars
132
Forks
5
Avg merge
6h 24m
Merged PRs (30d)
27

Description

DocumentClosure.ExpansionState.Visited is built with StringComparer.OrdinalIgnoreCase:

https://github.com/Nimblesite/SharpLsp/blob/main/sidecars/SharpLsp.Sidecar.CSharp/Workspace/DocumentClosure.cs#L192

That is correct on Windows and on case-insensitive macOS volumes, but wrong on Linux and case-sensitive APFS/HFS+ volumes, where Helpers/Foo.cs and Helpers/foo.cs are two distinct files.

Failure

A file-based app that includes both:

#:include Helpers/Foo.cs
#:include Helpers/foo.cs

visits the first, then Visited.Add(full) returns false for the second because the comparer folds case. VisitAsync returns early via RecordBound, which only records an issue when Files.Count >= MaxFiles or depth > MaxDepth — neither holds here. So the second file is dropped from the closure with no diagnostic and no logged issue. Every symbol it declares then reports CS0246/CS0103 in the editor with nothing explaining why.

The same fold applies to the cycle guard, so a genuine two-file include graph differing only in case is misreported as a cycle.

Fix

Match the comparer to the filesystem rather than the OS. OperatingSystem.IsWindows() is the wrong axis — macOS is case-insensitive by default but can be case-sensitive per volume. Probing the containing directory once per closure expansion and caching the result is the accurate approach; keying on OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() is the cheap approximation.

Whichever is chosen, RecordBound should also record an issue for the already-visited case so a dropped file is never silent.

Test

Coarse e2e in WorkspaceManagerSingleFileTests.cs, guarded so it asserts real behaviour on case-sensitive filesystems only: write Foo.cs and foo.cs declaring different types, include both, assert both types bind (no CS0246).

Found while reviewing #189. Not a regression from it — the comparer predates the closure rewrite in spirit and ships with it. Tracked separately to keep #189's scope honest.

Refs [SCRIPT-CLOSURE].

Contributor guide

No contributing guide indexed for this repository

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

Start in sidecars/SharpLsp.Sidecar.CSharp/Workspace/DocumentClosure.cs around ExpansionState.Visited and RecordBound, then inspect WorkspaceManagerSingleFileTests.cs for the coarse end-to-end coverage. Reproduce the case-sensitive filesystem scenario with Foo.cs and foo.cs, and consider the proposed filesystem probing or approximation; done means both types bind without CS0246 and a dropped already-visited file is not silent.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
operating-systems, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.