Skip to content

Commit 40cf5e1

Browse files
mgreenegitCopilot
andauthored
Enhance RenameHandler to handle null capabilities gracefully and add tests for registration options (#2296)
* Enhance RenameHandler to handle null capabilities gracefully and add tests for registration options * Refactor RenameHandler to support nullable capabilities and enhance tests for registration options * Address review: keep interface-compatible signature; cover explicit false capability - Revert GetRegistrationOptions parameter to the non-nullable RenameCapability declared by IPrepareRenameHandler/IRenameHandler, and rely on the null-conditional check (capability?.PrepareSupport == true) for the runtime case where the framework passes a null capability. This matches the OmniSharp interface signature exactly, avoiding any nullable-annotation mismatch (e.g. CS8765) while remaining null-safe during initialize. - Add explicit { false, false } theory cases for both handler kinds so the three distinct PrepareSupport inputs (omitted/null, true, false) are all covered and a regression that treats false as enabled would be caught. Verified: src and test projects build with 0 warnings under #nullable enable; all 6 GetRegistrationOptionsReflectsPrepareSupport cases pass on net462 and net8.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d2112c2 commit 40cf5e1

2 files changed

Lines changed: 56 additions & 13 deletions

File tree

src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal class PrepareRenameHandler
2020
RenameService renameService
2121
) : IPrepareRenameHandler
2222
{
23-
public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new();
23+
public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability?.PrepareSupport == true ? new() { PrepareProvider = true } : new();
2424

2525
public async Task<RangeOrPlaceholderRange?> Handle(PrepareRenameParams request, CancellationToken cancellationToken)
2626
=> await renameService.PrepareRenameSymbol(request, cancellationToken).ConfigureAwait(false);
@@ -34,7 +34,9 @@ RenameService renameService
3434
) : IRenameHandler
3535
{
3636
// RenameOptions may only be specified if the client states that it supports prepareSupport in its initial initialize request.
37-
public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new();
37+
// The framework passes a null capability when the client omits textDocument.rename from its advertised capabilities (e.g. a completion-only client).
38+
// The parameter keeps the interface's non-nullable signature; the null-conditional operator avoids a NullReferenceException during initialize.
39+
public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability?.PrepareSupport == true ? new() { PrepareProvider = true } : new();
3840

3941
public async Task<WorkspaceEdit?> Handle(RenameParams request, CancellationToken cancellationToken)
4042
=> await renameService.RenameSymbol(request, cancellationToken).ConfigureAwait(false);

test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System;
45
using Microsoft.Extensions.Logging.Abstractions;
56
using Microsoft.PowerShell.EditorServices.Handlers;
67
using Microsoft.PowerShell.EditorServices.Services;
78
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
89
using Microsoft.PowerShell.EditorServices.Test.Shared;
910
using OmniSharp.Extensions.LanguageServer.Protocol;
11+
using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
1012
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
1113
using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities;
1214
using System.Linq;
@@ -24,25 +26,25 @@ public class RenameHandlerTests
2426
private readonly WorkspaceService workspace = new(NullLoggerFactory.Instance);
2527

2628
private readonly RenameHandler testHandler;
29+
private readonly PrepareRenameHandler testPrepareHandler;
2730
public RenameHandlerTests()
2831
{
2932
workspace.WorkspaceFolders.Add(new WorkspaceFolder
3033
{
3134
Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath("Refactoring"))
3235
});
3336

34-
testHandler = new
37+
RenameService renameService = new
3538
(
36-
new RenameService
37-
(
38-
workspace,
39-
new FakeLspSendMessageRequestFacade("I Accept"),
40-
new EmptyConfiguration()
41-
)
42-
{
43-
DisclaimerAcceptedForSession = true //Disables UI prompts
44-
}
45-
);
39+
workspace,
40+
new FakeLspSendMessageRequestFacade("I Accept"),
41+
new EmptyConfiguration()
42+
)
43+
{
44+
DisclaimerAcceptedForSession = true //Disables UI prompts
45+
};
46+
testHandler = new(renameService);
47+
testPrepareHandler = new(renameService);
4648
}
4749

4850
// Decided to keep this DAMP instead of DRY due to memberdata boundaries, duplicates with PrepareRenameHandler
@@ -125,4 +127,43 @@ public async Task RenamedVariable(RenameTestTarget s)
125127

126128
Assert.Equal(expected, actual);
127129
}
130+
131+
public enum RegistrationHandlerKind
132+
{
133+
Rename,
134+
PrepareRename
135+
}
136+
137+
// prepareSupport has three distinct inputs: null = client omitted the capability entirely (framework hands us null),
138+
// true = client supports prepareRename, false = client explicitly does not. Only true should enable PrepareProvider.
139+
public static TheoryData<RegistrationHandlerKind, bool?, bool> RegistrationOptionsTestCases() => new()
140+
{
141+
{ RegistrationHandlerKind.Rename, null, false },
142+
{ RegistrationHandlerKind.Rename, false, false },
143+
{ RegistrationHandlerKind.Rename, true, true },
144+
{ RegistrationHandlerKind.PrepareRename, null, false },
145+
{ RegistrationHandlerKind.PrepareRename, false, false },
146+
{ RegistrationHandlerKind.PrepareRename, true, true }
147+
};
148+
149+
[Theory]
150+
[MemberData(nameof(RegistrationOptionsTestCases))]
151+
public void GetRegistrationOptionsReflectsPrepareSupport(RegistrationHandlerKind handlerKind, bool? prepareSupport, bool expectedPrepareProvider)
152+
{
153+
RenameCapability capability = prepareSupport is bool ps
154+
? new RenameCapability { PrepareSupport = ps }
155+
: null;
156+
157+
Func<RenameCapability, ClientCapabilities, RenameRegistrationOptions> getRegistrationOptions = handlerKind switch
158+
{
159+
RegistrationHandlerKind.Rename => testHandler.GetRegistrationOptions,
160+
RegistrationHandlerKind.PrepareRename => testPrepareHandler.GetRegistrationOptions,
161+
_ => throw new ArgumentOutOfRangeException(nameof(handlerKind))
162+
};
163+
164+
RenameRegistrationOptions opts = getRegistrationOptions(capability, new ClientCapabilities());
165+
166+
Assert.NotNull(opts);
167+
Assert.Equal(expectedPrepareProvider, opts.PrepareProvider);
168+
}
128169
}

0 commit comments

Comments
 (0)