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 server crash if inlay hint entry not in cache #67810

Merged
merged 1 commit into from
Apr 14, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ public InlayHintCache() : base(maxCacheSize: 3)
/// <summary>
/// Cached data need to resolve a specific inlay hint item.
/// </summary>
internal record InlayHintCacheEntry(ImmutableArray<InlineHint> InlayHintMembers, TextDocumentIdentifier TextDocumentIdentifier, VersionStamp SyntaxVersion);
internal record InlayHintCacheEntry(ImmutableArray<InlineHint> InlayHintMembers, VersionStamp SyntaxVersion);
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request)

// Store the members in the resolve cache so that when we get a resolve request for a particular
// member we can re-use the inline hint.
var resultId = inlayHintCache.UpdateCache(new InlayHintCache.InlayHintCacheEntry(hints, request.TextDocument, syntaxVersion));
var resultId = inlayHintCache.UpdateCache(new InlayHintCache.InlayHintCacheEntry(hints, syntaxVersion));

for (var i = 0; i < hints.Length; i++)
{
Expand Down Expand Up @@ -85,7 +85,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(InlayHintParams request)
ToolTip = null,
PaddingLeft = leftPadding,
PaddingRight = rightPadding,
Data = new InlayHintResolveData(resultId, i)
Data = new InlayHintResolveData(resultId, i, request.TextDocument)
};

inlayHints.Add(inlayHint);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint;

/// <summary>
/// Datatype storing the information needed to resolve a particular inlay hint item.
/// </summary>
/// <param name="ResultId">the resultId associated with the inlay hint created on original request.</param>
/// <param name="ListIndex">the index of the specific inlay hint item in the original list.</param>
internal sealed record InlayHintResolveData(long ResultId, int ListIndex);
/// /// <param name="TextDocument">the text document associated with the inlay hint to resolve.</param>
internal sealed record InlayHintResolveData(long ResultId, int ListIndex, TextDocumentIdentifier TextDocument);
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.InlineHints;
using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeLens;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Newtonsoft.Json.Linq;
using Roslyn.Utilities;
Expand All @@ -32,12 +33,13 @@ public InlayHintResolveHandler(InlayHintCache inlayHintCache)
public bool RequiresLSPSolution => true;

public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.InlayHint request)
=> GetCacheEntry(request).CacheEntry.TextDocumentIdentifier;
=> GetInlayHintResolveData(request).TextDocument;

public async Task<LSP.InlayHint> HandleRequestAsync(LSP.InlayHint request, RequestContext context, CancellationToken cancellationToken)
{
var document = context.GetRequiredDocument();
var (cacheEntry, inlineHintToResolve) = GetCacheEntry(request);
var resolveData = GetInlayHintResolveData(request);
var (cacheEntry, inlineHintToResolve) = GetCacheEntry(resolveData);

var currentSyntaxVersion = await document.GetSyntaxVersionAsync(cancellationToken).ConfigureAwait(false);
var cachedSyntaxVersion = cacheEntry.SyntaxVersion;
Expand All @@ -56,14 +58,18 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.InlayHint request)
return request;
}

private (InlayHintCache.InlayHintCacheEntry CacheEntry, InlineHint InlineHintToResolve) GetCacheEntry(LSP.InlayHint request)
private (InlayHintCache.InlayHintCacheEntry CacheEntry, InlineHint InlineHintToResolve) GetCacheEntry(InlayHintResolveData resolveData)
{
var resolveData = (request.Data as JToken)?.ToObject<InlayHintResolveData>();
Contract.ThrowIfNull(resolveData, "Missing data for inlay hint resolve request");

var cacheEntry = _inlayHintCache.GetCachedEntry(resolveData.ResultId);
Contract.ThrowIfNull(cacheEntry, "Missing cache entry for inlay hint resolve request");
return (cacheEntry, cacheEntry.InlayHintMembers[resolveData.ListIndex]);
}

private static InlayHintResolveData GetInlayHintResolveData(LSP.InlayHint inlayHint)
{
var resolveData = (inlayHint.Data as JToken)?.ToObject<InlayHintResolveData>();
Contract.ThrowIfNull(resolveData, "Missing data for inlay hint resolve request");
return resolveData;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.InlineHints;
using Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint;
using Microsoft.CodeAnalysis.Text;
using Newtonsoft.Json;
using Roslyn.Test.Utilities;
using StreamJsonRpc;
using Xunit;
using Xunit.Abstractions;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.InlayHint
{
Expand Down Expand Up @@ -99,6 +106,58 @@ void X((int, bool) d)
await RunVerifyInlayHintAsync(markup, mutatingLspWorkspace, hasTextEdits: false);
}

[Theory, CombinatorialData]
public async Task TestDoesNotShutdownServerIfCacheEntryMissing(bool mutatingLspWorkspace)
{
var markup =
@"class A
{
void M()
{
var {|int:|}x = 5;
}
}";
await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, CapabilitiesWithVSExtensions);
testLspServer.TestWorkspace.GlobalOptions.SetGlobalOption(InlineHintsOptionsStorage.EnabledForParameters, LanguageNames.CSharp, true);
testLspServer.TestWorkspace.GlobalOptions.SetGlobalOption(InlineHintsOptionsStorage.EnabledForTypes, LanguageNames.CSharp, true);
var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();
var textDocument = CreateTextDocumentIdentifier(document.GetURI());
var sourceText = await document.GetTextAsync();
var span = TextSpan.FromBounds(0, sourceText.Length);

var inlayHintParams = new LSP.InlayHintParams
{
TextDocument = textDocument,
Range = ProtocolConversions.TextSpanToRange(span, sourceText)
};

var actualInlayHints = await testLspServer.ExecuteRequestAsync<LSP.InlayHintParams, LSP.InlayHint[]?>(LSP.Methods.TextDocumentInlayHintName, inlayHintParams, CancellationToken.None);
var firstInlayHint = actualInlayHints.First();
var data = JsonConvert.DeserializeObject<InlayHintResolveData>(firstInlayHint.Data!.ToString());
AssertEx.NotNull(data);
var firstResultId = data.ResultId;

// Verify the inlay hint item is in the cache.
var cache = testLspServer.GetRequiredLspService<InlayHintCache>();
Assert.NotNull(cache.GetCachedEntry(firstResultId));

// Execute a few more requests to ensure the first request is removed from the cache.
await testLspServer.ExecuteRequestAsync<LSP.InlayHintParams, LSP.InlayHint[]?>(LSP.Methods.TextDocumentInlayHintName, inlayHintParams, CancellationToken.None);
await testLspServer.ExecuteRequestAsync<LSP.InlayHintParams, LSP.InlayHint[]?>(LSP.Methods.TextDocumentInlayHintName, inlayHintParams, CancellationToken.None);
var lastInlayHints = await testLspServer.ExecuteRequestAsync<LSP.InlayHintParams, LSP.InlayHint[]?>(LSP.Methods.TextDocumentInlayHintName, inlayHintParams, CancellationToken.None);
Assert.True(lastInlayHints.Any());

// Assert that the first result id is no longer in the cache.
Assert.Null(cache.GetCachedEntry(firstResultId));

// Assert that the request throws because the item no longer exists in the cache.
await Assert.ThrowsAsync<RemoteInvocationException>(async () => await testLspServer.ExecuteRequestAsync<LSP.InlayHint, LSP.InlayHint>(LSP.Methods.InlayHintResolveName, firstInlayHint, CancellationToken.None));

// Assert that the server did not shutdown and that we can resolve the latest inlay hint request we made.
var lastInlayHint = await testLspServer.ExecuteRequestAsync<LSP.InlayHint, LSP.InlayHint>(LSP.Methods.InlayHintResolveName, lastInlayHints.First(), CancellationToken.None);
Assert.NotNull(lastInlayHint?.ToolTip);
}

private async Task RunVerifyInlayHintAsync(string markup, bool mutatingLspWorkspace, bool hasTextEdits = true)
{
await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, CapabilitiesWithVSExtensions);
Expand Down