Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix URI comparisons for different casing #74746

Merged
merged 2 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,16 @@ public int GetHashCode(Uri obj)
{
if (obj.IsAbsoluteUri)
{
return obj.AbsoluteUri.GetHashCode();
// Since the Uri type does not consider an encoded Uri equal to an unencoded Uri, we need to handle this ourselves.
// The AbsoluteUri property is always encoded, so we can use this to compare the URIs (see Equals above).
//
// However, depending on the kind of URI, case sensitivity in AbsoluteUri should be ignored.
// Uri.GetHashCode normally handles this internally, but the parameters it uses to determine which comparison to use are not exposed.
//
// Instead, we will always create the hash code ignoring case, and will rely on the Equals implementation
// to handle collisions (between two Uris with different casing). This should be very rare in practice.
// Collisions can happen for non UNC URIs (e.g. `git:/blah` vs `git:/Blah`).
return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.AbsoluteUri);
}
else
{
Expand Down
85 changes: 85 additions & 0 deletions src/LanguageServer/ProtocolUnitTests/UriTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,91 @@ public async Task TestFindsExistingDocumentWhenUriHasDifferentEncodingAsync(bool
Assert.Same(originalText, encodedText);
}

[Theory, CombinatorialData, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2208409")]
public async Task TestFindsExistingDocumentWhenUriHasDifferentCasingForCaseInsensitiveUriAsync(bool mutatingLspWorkspace)
{
await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });

#pragma warning disable RS0030 // Do not use banned APIs
var upperCaseUri = new Uri(@"file:///C:/Users/dabarbet/source/repos/XUnitApp1/UnitTest1.cs", UriKind.Absolute);
var lowerCaseUri = new Uri(@"file:///c:/Users/dabarbet/source/repos/XUnitApp1/UnitTest1.cs", UriKind.Absolute);
#pragma warning restore RS0030 // Do not use banned APIs

// Execute the request as JSON directly to avoid the test client serializing System.Uri.
var requestJson = $$$"""
{
"textDocument": {
"uri": "{{{upperCaseUri.OriginalString}}}",
"languageId": "csharp",
"text": "LSP text"
}
}
""";
var jsonDocument = JsonDocument.Parse(requestJson);
await testLspServer.ExecutePreSerializedRequestAsync(LSP.Methods.TextDocumentDidOpenName, jsonDocument);

// Access the document using the upper case to make sure we find it in the C# misc files.
var (workspace, _, lspDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { Uri = upperCaseUri }, CancellationToken.None).ConfigureAwait(false);
AssertEx.NotNull(lspDocument);
Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace?.Kind);
Assert.Equal(LanguageNames.CSharp, lspDocument.Project.Language);
var originalText = await lspDocument.GetTextAsync(CancellationToken.None);
Assert.Equal("LSP text", originalText.ToString());

// Now make a request using different case.
var info = await testLspServer.ExecuteRequestAsync<CustomResolveParams, ResolvedDocumentInfo>(CustomResolveHandler.MethodName,
new CustomResolveParams(new LSP.TextDocumentIdentifier { Uri = lowerCaseUri }), CancellationToken.None);
Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace?.Kind);
Assert.Equal(LanguageNames.CSharp, lspDocument.Project.Language);

var (lowerCaseWorkspace, _, lowerCaseDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { Uri = lowerCaseUri }, CancellationToken.None).ConfigureAwait(false);
Assert.Same(workspace, lowerCaseWorkspace);
AssertEx.NotNull(lowerCaseDocument);
Assert.Equal(LanguageNames.CSharp, lowerCaseDocument.Project.Language);
var lowerCaseText = await lowerCaseDocument.GetTextAsync(CancellationToken.None);
Assert.Equal("LSP text", lowerCaseText.ToString());

// The text we get back should be the exact same instance that was originally saved by the unencoded request.
Assert.Same(originalText, lowerCaseText);
}

[Theory, CombinatorialData, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2208409")]
public async Task TestUsesDifferentDocumentForDifferentCaseWithNonUncUriAsync(bool mutatingLspWorkspace)
{
await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });

#pragma warning disable RS0030 // Do not use banned APIs
var upperCaseUri = new Uri(@"git:/Blah", UriKind.Absolute);
var lowerCaseUri = new Uri(@"git:/blah", UriKind.Absolute);
#pragma warning restore RS0030 // Do not use banned APIs

// Execute the request as JSON directly to avoid the test client serializing System.Uri.
var requestJson = $$$"""
{
"textDocument": {
"uri": "{{{upperCaseUri.OriginalString}}}",
"languageId": "csharp",
"text": "LSP text"
}
}
""";
var jsonDocument = JsonDocument.Parse(requestJson);
await testLspServer.ExecutePreSerializedRequestAsync(LSP.Methods.TextDocumentDidOpenName, jsonDocument);

// Access the document using the upper case to make sure we find it in the C# misc files.
var (workspace, _, lspDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { Uri = upperCaseUri }, CancellationToken.None).ConfigureAwait(false);
AssertEx.NotNull(lspDocument);
Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace?.Kind);
Assert.Equal(LanguageNames.CSharp, lspDocument.Project.Language);
var originalText = await lspDocument.GetTextAsync(CancellationToken.None);
Assert.Equal("LSP text", originalText.ToString());

// Now make a request using different case. This should throw since we have not opened a document with the URI with different case (and not UNC).
await Assert.ThrowsAnyAsync<Exception>(async ()
=> await testLspServer.ExecuteRequestAsync<CustomResolveParams, ResolvedDocumentInfo>(CustomResolveHandler.MethodName,
new CustomResolveParams(new LSP.TextDocumentIdentifier { Uri = lowerCaseUri }), CancellationToken.None));
}

private record class ResolvedDocumentInfo(string WorkspaceKind, string ProjectLanguage);
private record class CustomResolveParams([property: JsonPropertyName("textDocument")] LSP.TextDocumentIdentifier TextDocument);

Expand Down
Loading