From 1191301a378fa2bfa5c9c2fa9d2d046978c4d47b Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:22:44 -0700 Subject: [PATCH 1/3] Guard against workspace folders without a URI on initialize When a client sends `workspaceFolders` on `initialize` and one of the entries has no `uri`, the `OnInitialize` handler threw a `NullReferenceException` while resolving the initial working directory (`workspaceService.WorkspaceFolders.FirstOrDefault()?.Uri.GetFileSystemPath()`). The `?.` only guarded the folder being null, not its `Uri`, so calling `GetFileSystemPath()` on a null `Uri` blew up, the `initialize` response was never returned, and the server was unusable for that client. The same hazard exists in every other place we dereference `folder.Uri` (`WorkspacePaths`, `GetRelativePath`, `FindFileInWorkspace`). The issue (#2300) reports this as Linux-only with a repro that includes a valid `uri`; that exact payload doesn't actually throw (I reproduced both ways by driving a built PSES over stdio). The real trigger is a workspace folder lacking a URI, which is what the captured stack trace (`PsesLanguageServer.cs:line 150`) points at. - Add `WorkspaceService.AddWorkspaceFolders`, which owns the invariant that every folder in `WorkspaceFolders` has a non-null `Uri`. It skips null folders and folders without a URI (logging a warning) and treats a null collection as "no folders yet", falling back to the existing single-root and CWD behavior. - Call it from `OnInitialize` instead of the inline null-check plus `AddRange`. - Add a regression test covering null input and uriless/null folders. Fixes #2300. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Server/PsesLanguageServer.cs | 5 +--- .../Services/Workspace/WorkspaceService.cs | 29 +++++++++++++++++++ .../Session/WorkspaceTests.cs | 26 +++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 042e4e8fa..954651112 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -140,10 +140,7 @@ public async Task StartAsync() // Set the workspace path from the parameters. WorkspaceService workspaceService = languageServer.Services.GetService(); - if (initializeParams.WorkspaceFolders is not null) - { - workspaceService.WorkspaceFolders.AddRange(initializeParams.WorkspaceFolders); - } + workspaceService.AddWorkspaceFolders(initializeParams.WorkspaceFolders); // Parse initialization options. JObject initializationOptions = initializeParams.InitializationOptions as JObject; diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index 9b721387a..db17c3d7c 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -106,6 +106,35 @@ public WorkspaceService(ILoggerFactory factory) public IEnumerable WorkspacePaths => WorkspaceFolders.Select(i => i.Uri.GetFileSystemPath()); + /// + /// Adds the given workspace folders, ignoring any that are null or lack a URI. + /// + /// + /// Some LSP clients send workspace folders without a URI on initialize. Adding such a + /// folder would later throw a when its URI is + /// dereferenced (e.g. when resolving the initial working directory or a relative path), + /// breaking the handshake. See + /// https://github.com/PowerShell/PowerShellEditorServices/issues/2300. + /// + public void AddWorkspaceFolders(IEnumerable workspaceFolders) + { + if (workspaceFolders is null) + { + return; + } + + foreach (WorkspaceFolder workspaceFolder in workspaceFolders) + { + if (workspaceFolder?.Uri is null) + { + logger.LogWarning("Ignored workspace folder without a URI: " + workspaceFolder?.Name); + continue; + } + + WorkspaceFolders.Add(workspaceFolder); + } + } + /// /// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it. /// IMPORTANT: Not all documents have a backing file e.g. untitled: scheme documents. Consider using diff --git a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs index 4abd80f21..82b60efbd 100644 --- a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs @@ -86,6 +86,32 @@ internal static WorkspaceService FixturesWorkspace() }; } + // Regression test for https://github.com/PowerShell/PowerShellEditorServices/issues/2300: + // a client that sends a workspace folder without a URI on initialize used to crash the + // server with a NullReferenceException when the URI was later dereferenced. + [Fact] + public void AddWorkspaceFoldersIgnoresNullAndUrilessFolders() + { + WorkspaceService workspace = new(NullLoggerFactory.Instance); + + // Null collection is a no-op rather than a throw. + workspace.AddWorkspaceFolders(null); + Assert.Empty(workspace.WorkspaceFolders); + + workspace.AddWorkspaceFolders(new[] + { + new WorkspaceFolder { Uri = DocumentUri.FromFileSystemPath(s_workspacePath), Name = "valid" }, + new WorkspaceFolder { Name = "missing-uri" }, + null + }); + + WorkspaceFolder folder = Assert.Single(workspace.WorkspaceFolders); + Assert.Equal("valid", folder.Name); + + // The downstream dereferences that previously threw now succeed. + Assert.Equal(s_workspacePath, Assert.Single(workspace.WorkspacePaths)); + } + [Fact] public void HasDefaultForWorkspacePaths() { From 5a9a5cef5c14527c977a30ed331c1d267cf6557b Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:50:56 -0700 Subject: [PATCH 2/3] Refactor workspace folder guard into a pure, testable filter Extract the null/URI-less filtering from `AddWorkspaceFolders` into a pure `GetValidWorkspaceFolders` helper. `WorkspaceService` remains the single chokepoint where client-supplied folders are ingested, so every downstream `Uri` dereference stays protected, but the filtering logic is now trivially unit-testable without constructing a service or logger. Drop the per-folder warning so the filter is a simple, allocation-free expression, and add direct tests covering null, empty, and mixed (null folder / URI-less folder / valid folder) inputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Services/Workspace/WorkspaceService.cs | 24 +++++++++--------- .../Session/WorkspaceTests.cs | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index db17c3d7c..6cb36d524 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -118,23 +118,23 @@ public WorkspaceService(ILoggerFactory factory) /// public void AddWorkspaceFolders(IEnumerable workspaceFolders) { - if (workspaceFolders is null) + foreach (WorkspaceFolder workspaceFolder in GetValidWorkspaceFolders(workspaceFolders)) { - return; - } - - foreach (WorkspaceFolder workspaceFolder in workspaceFolders) - { - if (workspaceFolder?.Uri is null) - { - logger.LogWarning("Ignored workspace folder without a URI: " + workspaceFolder?.Name); - continue; - } - WorkspaceFolders.Add(workspaceFolder); } } + /// + /// Filters workspace folders down to those usable by the service: non-null folders with a + /// non-null URI. The workspaceFolders field is optional in LSP, so the collection + /// itself may also be null. + /// + /// The workspace folders from the initialize parameters. + /// The folders that have a non-null URI, or an empty sequence. + internal static IEnumerable GetValidWorkspaceFolders(IEnumerable workspaceFolders) + => workspaceFolders?.Where(static folder => folder?.Uri is not null) + ?? Enumerable.Empty(); + /// /// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it. /// IMPORTANT: Not all documents have a backing file e.g. untitled: scheme documents. Consider using diff --git a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs index 82b60efbd..52edda8cb 100644 --- a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs @@ -112,6 +112,31 @@ public void AddWorkspaceFoldersIgnoresNullAndUrilessFolders() Assert.Equal(s_workspacePath, Assert.Single(workspace.WorkspacePaths)); } + [Fact] + public void GetValidWorkspaceFoldersReturnsEmptyWhenNull() + => Assert.Empty(WorkspaceService.GetValidWorkspaceFolders(null)); + + [Fact] + public void GetValidWorkspaceFoldersReturnsEmptyWhenEmpty() + => Assert.Empty(WorkspaceService.GetValidWorkspaceFolders(new Container())); + + [Fact] + public void GetValidWorkspaceFoldersSkipsNullFoldersAndNullUris() + { + WorkspaceFolder valid = new() + { + Uri = DocumentUri.FromFileSystemPath(s_workspacePath), + Name = "valid" + }; + + Container folders = new( + null, + new WorkspaceFolder { Name = "missing-uri" }, + valid); + + Assert.Equal(valid, Assert.Single(WorkspaceService.GetValidWorkspaceFolders(folders))); + } + [Fact] public void HasDefaultForWorkspacePaths() { From 8958a72b95131a239f6c6c8bbbc674a328251ed8 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Mon, 15 Jun 2026 18:03:46 -0700 Subject: [PATCH 3/3] Log a warning when skipping workspace folders without a URI Restore the per-folder warning that the previous refactor dropped. Silently discarding malformed workspace folders makes it hard for server operators to diagnose why a client's folder didn't take effect, and it matches the behavior the PR description advertises. Logging each skipped folder requires the explicit loop, so revert the pure `GetValidWorkspaceFolders` helper (and its dedicated tests); the existing `AddWorkspaceFoldersIgnoresNullAndUrilessFolders` regression test still covers null input, URI-less/null folders, and the downstream dereference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Services/Workspace/WorkspaceService.cs | 26 ++++++++++--------- .../Session/WorkspaceTests.cs | 25 ------------------ 2 files changed, 14 insertions(+), 37 deletions(-) diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index 6cb36d524..db71ad786 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -118,23 +118,25 @@ public WorkspaceService(ILoggerFactory factory) /// public void AddWorkspaceFolders(IEnumerable workspaceFolders) { - foreach (WorkspaceFolder workspaceFolder in GetValidWorkspaceFolders(workspaceFolders)) + if (workspaceFolders is null) { + return; + } + + foreach (WorkspaceFolder workspaceFolder in workspaceFolders) + { + // Some LSP clients send folders without a URI; skip them (and warn) so we never + // store a folder whose URI would later be dereferenced and throw. + if (workspaceFolder?.Uri is null) + { + logger.LogWarning($"Ignoring workspace folder without a URI: {workspaceFolder?.Name}"); + continue; + } + WorkspaceFolders.Add(workspaceFolder); } } - /// - /// Filters workspace folders down to those usable by the service: non-null folders with a - /// non-null URI. The workspaceFolders field is optional in LSP, so the collection - /// itself may also be null. - /// - /// The workspace folders from the initialize parameters. - /// The folders that have a non-null URI, or an empty sequence. - internal static IEnumerable GetValidWorkspaceFolders(IEnumerable workspaceFolders) - => workspaceFolders?.Where(static folder => folder?.Uri is not null) - ?? Enumerable.Empty(); - /// /// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it. /// IMPORTANT: Not all documents have a backing file e.g. untitled: scheme documents. Consider using diff --git a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs index 52edda8cb..82b60efbd 100644 --- a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs @@ -112,31 +112,6 @@ public void AddWorkspaceFoldersIgnoresNullAndUrilessFolders() Assert.Equal(s_workspacePath, Assert.Single(workspace.WorkspacePaths)); } - [Fact] - public void GetValidWorkspaceFoldersReturnsEmptyWhenNull() - => Assert.Empty(WorkspaceService.GetValidWorkspaceFolders(null)); - - [Fact] - public void GetValidWorkspaceFoldersReturnsEmptyWhenEmpty() - => Assert.Empty(WorkspaceService.GetValidWorkspaceFolders(new Container())); - - [Fact] - public void GetValidWorkspaceFoldersSkipsNullFoldersAndNullUris() - { - WorkspaceFolder valid = new() - { - Uri = DocumentUri.FromFileSystemPath(s_workspacePath), - Name = "valid" - }; - - Container folders = new( - null, - new WorkspaceFolder { Name = "missing-uri" }, - valid); - - Assert.Equal(valid, Assert.Single(WorkspaceService.GetValidWorkspaceFolders(folders))); - } - [Fact] public void HasDefaultForWorkspacePaths() {