/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/LanguageServer/ProtocolUnitTests/SpellCheck/SpellCheckTests.cs
699 строк
29 KB
David Barbet
Compare DocumentUri instances in LSP tests instead of file path strings
08 май 2026, 02:04
08 май 2026, 02:04
d0a087a
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.UnitTests.Diagnostics; using Microsoft.CodeAnalysis.Text; using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; using Xunit.Abstractions; namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.SpellCheck { public sealed class SpellCheckTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) { #region Document [Theory, CombinatorialData] public async Task TestNoDocumentResultsForClosedFiles(bool mutatingLspWorkspace) { var markup = """ class A { } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI()); Assert.Empty(results); } [Theory, CombinatorialData] public async Task TestDocumentResultsForOpenFiles(bool mutatingLspWorkspace) { var markup = """ class {|Identifier:A|} { } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var testDocument = testLspServer.TestWorkspace.Documents.Single(); var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); await OpenDocumentAsync(testLspServer, document); var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI()); var sourceText = await document.GetTextAsync(); Assert.Single(results); AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport { ResultId = "DocumentSpellCheckHandler:1", Ranges = GetRanges(testDocument.AnnotatedSpans), }); } [Theory, CombinatorialData] public async Task TestLotsOfResults(bool mutatingLspWorkspace) { // Produce an 'interesting' large string, with varying length identifiers, and varying distances between the spans. var random = new Random(Seed: 0); var markup = string.Join(Environment.NewLine, Enumerable.Range(0, 5500).Select(v => $$""" class {|Identifier:A{{v}}|} { } {{string.Join(Environment.NewLine, Enumerable.Repeat("", random.Next() % 5))}} """)); await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var testDocument = testLspServer.TestWorkspace.Documents.Single(); var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); await OpenDocumentAsync(testLspServer, document); var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI()); var sourceText = await document.GetTextAsync(); Assert.True(results.Length == 6); var allRanges = GetRanges(testDocument.AnnotatedSpans); for (var i = 0; i < results.Length; i++) { AssertJsonEquals(results[i], new VSInternalSpellCheckableRangeReport { ResultId = "DocumentSpellCheckHandler:1", Ranges = [.. allRanges.Skip(3 * i * 1000).Take(3 * 1000)], }); } } [Theory, CombinatorialData] public async Task TestDocumentResultsForRemovedDocument(bool mutatingLspWorkspace) { var markup = """ class {|Identifier:A|} { } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var workspace = testLspServer.TestWorkspace; var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); // Get the diagnostics for the solution containing the doc. var solution = document.Project.Solution; await OpenDocumentAsync(testLspServer, document); var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI()).ConfigureAwait(false); var sourceText = await document.GetTextAsync(); Assert.Single(results); AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport { ResultId = "DocumentSpellCheckHandler:1", Ranges = GetRanges(workspace.Documents.Single().AnnotatedSpans), }); // Now remove the doc. workspace.OnDocumentRemoved(workspace.Documents.Single().Id); await CloseDocumentAsync(testLspServer, document); results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI(), results.Single().ResultId).ConfigureAwait(false); Assert.Null(results.Single().Ranges); Assert.Null(results.Single().ResultId); } [Theory, CombinatorialData] public async Task TestNoChangeIfDocumentResultsCalledTwice(bool mutatingLspWorkspace) { var markup = """ class {|Identifier:A|} { } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); await OpenDocumentAsync(testLspServer, document); var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI()); var sourceText = await document.GetTextAsync(); Assert.Single(results); AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport { ResultId = "DocumentSpellCheckHandler:1", Ranges = GetRanges(testLspServer.TestWorkspace.Documents.Single().AnnotatedSpans), }); var resultId = results.Single().ResultId; results = await RunGetDocumentSpellCheckSpansAsync( testLspServer, document.GetURI(), previousResultId: resultId); Assert.Null(results.Single().Ranges); Assert.Equal(resultId, results.Single().ResultId); } [Theory, CombinatorialData] public async Task TestDocumentResultChangedAfterEntityAdded(bool mutatingLspWorkspace) { var markup = """ class {|Identifier:A|} { } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); await OpenDocumentAsync(testLspServer, document); var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI()); var sourceText = await document.GetTextAsync(); Assert.Single(results); AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport { ResultId = "DocumentSpellCheckHandler:1", Ranges = GetRanges(testLspServer.TestWorkspace.Documents.Single().AnnotatedSpans), }); await InsertTextAsync(testLspServer, document, sourceText.Length, "// comment"); var (_, lspSolution) = await testLspServer.GetManager().GetLspSolutionInfoAsync(CancellationToken.None).ConfigureAwait(false); document = lspSolution!.Projects.Single().Documents.Single(); results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI(), results.Single().ResultId); MarkupTestFile.GetSpans( """ class {|Identifier:A|} { } {|Comment:// comment|} """, out _, out IDictionary<string, ImmutableArray<TextSpan>> annotatedSpans); sourceText = await document.GetTextAsync(); Assert.Single(results); AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport { ResultId = "DocumentSpellCheckHandler:2", Ranges = GetRanges(annotatedSpans), }); } [Theory, CombinatorialData] public async Task TestDocumentResultIdSameAfterIrrelevantEdit(bool mutatingLspWorkspace) { var markup = """ class {|Identifier:A|} { } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); await OpenDocumentAsync(testLspServer, document); var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI()); var sourceText = await document.GetTextAsync(); Assert.Single(results); AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport { ResultId = "DocumentSpellCheckHandler:1", Ranges = GetRanges(testLspServer.TestWorkspace.Documents.Single().AnnotatedSpans), }); await InsertTextAsync(testLspServer, document, sourceText.Length, text: " "); results = await RunGetDocumentSpellCheckSpansAsync( testLspServer, document.GetURI(), previousResultId: results[0].ResultId); sourceText = await document.GetTextAsync(); Assert.Single(results); AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport { ResultId = "DocumentSpellCheckHandler:1", }); } [Theory, CombinatorialData] public async Task TestDocumentResultsAreNotMapped(bool mutatingLspWorkspace) { var markup = """ #line 4 "test.txt" class {|Identifier:A|} { } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); await OpenDocumentAsync(testLspServer, document); var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI()); var sourceText = await document.GetTextAsync(); Assert.Single(results); AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport { ResultId = "DocumentSpellCheckHandler:1", Ranges = GetRanges(testLspServer.TestWorkspace.Documents.Single().AnnotatedSpans), }); } [Theory, CombinatorialData] public async Task TestStreamingDocumentDiagnostics(bool mutatingLspWorkspace) { var markup = """ class {|Identifier:A|} { } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); await OpenDocumentAsync(testLspServer, document); var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI(), useProgress: true); var sourceText = await document.GetTextAsync(); Assert.Single(results); AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport { ResultId = "DocumentSpellCheckHandler:1", Ranges = GetRanges(testLspServer.TestWorkspace.Documents.Single().AnnotatedSpans), }); } #endregion #region Workspace [Theory, CombinatorialData] public async Task TestWorkspaceResultsForClosedFiles(bool mutatingLspWorkspace) { var markup1 = """ class {|Identifier:A|} { } """; var markup2 = ""; await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace); var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer); Assert.Equal(2, results.Length); var document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.First(); var sourceText = await document.GetTextAsync(); AssertJsonEquals(results[0], new VSInternalWorkspaceSpellCheckableReport { TextDocument = CreateTextDocumentIdentifier(document.GetURI()), ResultId = "WorkspaceSpellCheckHandler:1", Ranges = GetRanges(testLspServer.TestWorkspace.Documents.First().AnnotatedSpans), }); AssertEx.Empty(results[1].Ranges); } [Theory, CombinatorialData] public async Task TestNoWorkspaceDiagnosticsForClosedFilesInProjectsWithIncorrectLanguage(bool mutatingLspWorkspace) { var csharpMarkup = @"class A {"; var typeScriptMarkup = "???"; var csharpFilePath = TestHelpers.GetRootedPath("C.cs"); var typeScriptFilePath = TestHelpers.GetRootedPath("T.ts"); var workspaceXml = $""" <Workspace> <Project Language="C#" CommonReferences="true" AssemblyName="CSProj1"> <Document FilePath="{csharpFilePath}">{csharpMarkup}</Document> </Project> <Project Language="TypeScript" CommonReferences="true" AssemblyName="TypeScriptProj"> <Document FilePath="{typeScriptFilePath}">{typeScriptMarkup}</Document> </Project> </Workspace> """; await using var testLspServer = await CreateXmlTestLspServerAsync(workspaceXml, mutatingLspWorkspace); var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer); Assert.True(results.All(r => r.TextDocument!.DocumentUri == ProtocolConversions.CreateAbsoluteDocumentUri(csharpFilePath))); } // [Fact] // public async Task TestWorkspaceDiagnosticsForSourceGeneratedFiles() // { // var markup1 = //@"class A {"; // var markup2 = ""; // await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync( // markups: Array.Empty<string>(), // sourceGeneratedMarkups: new[] { markup1, markup2 }, // BackgroundAnalysisScope.FullSolution, // useVSDiagnostics); // var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer); // // Project.GetSourceGeneratedDocumentsAsync may not return documents in a deterministic order, so we sort // // the results here to ensure subsequent assertions are not dependent on the order of items provided by the // // project. // results = results.Sort((x, y) => x.Uri.ToString().CompareTo(y.Uri.ToString())); // Assert.Equal(2, results.Length); // Assert.Equal("CS1513", results[0].Diagnostics.Single().Code); // Assert.Empty(results[1].Diagnostics); // } [Theory, CombinatorialData] public async Task TestWorkspaceResultsForRemovedDocument(bool mutatingLspWorkspace) { var markup1 = """ class {|Identifier:A|} { } """; var markup2 = ""; await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace); var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer); Assert.Equal(2, results.Length); var document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.First(); var sourceText = await document.GetTextAsync(); AssertJsonEquals(results[0], new VSInternalWorkspaceSpellCheckableReport { TextDocument = CreateTextDocumentIdentifier(document.GetURI()), ResultId = "WorkspaceSpellCheckHandler:1", Ranges = GetRanges(testLspServer.TestWorkspace.Documents.First().AnnotatedSpans), }); AssertEx.Empty(results[1].Ranges); testLspServer.TestWorkspace.OnDocumentRemoved(testLspServer.TestWorkspace.Documents.First().Id); var results2 = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer, previousResults: CreateParamsFromPreviousReports(results)); // First doc should show up as removed. Assert.Equal(2, results2.Length); Assert.Null(results2[0].Ranges); Assert.Null(results2[0].ResultId); // Second doc should be unchanged AssertEx.Empty(results[1].Ranges); Assert.Equal(results[1].ResultId, results2[1].ResultId); } [Theory, CombinatorialData] public async Task TestNoChangeIfWorkspaceResultsCalledTwice(bool mutatingLspWorkspace) { var markup1 = """ class {|Identifier:A|} { } """; var markup2 = ""; await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace); var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer); Assert.Equal(2, results.Length); var document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.First(); var sourceText = await document.GetTextAsync(); AssertJsonEquals(results[0], new VSInternalWorkspaceSpellCheckableReport { TextDocument = CreateTextDocumentIdentifier(document.GetURI()), ResultId = "WorkspaceSpellCheckHandler:1", Ranges = GetRanges(testLspServer.TestWorkspace.Documents.First().AnnotatedSpans), }); AssertEx.Empty(results[1].Ranges); var results2 = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer, previousResults: CreateParamsFromPreviousReports(results)); Assert.Equal(2, results2.Length); Assert.Null(results2[0].Ranges); Assert.Null(results2[1].Ranges); Assert.Equal(results[0].ResultId, results2[0].ResultId); Assert.Equal(results[1].ResultId, results2[1].ResultId); } [Theory, CombinatorialData] public async Task TestWorkspaceResultUpdatedAfterEdit(bool mutatingLspWorkspace) { var markup1 = """ class {|Identifier:A|} { } """; var markup2 = ""; await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace); var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer); Assert.Equal(2, results.Length); var document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.First(); var sourceText = await document.GetTextAsync(); AssertJsonEquals(results[0], new VSInternalWorkspaceSpellCheckableReport { TextDocument = CreateTextDocumentIdentifier(document.GetURI()), ResultId = "WorkspaceSpellCheckHandler:1", Ranges = GetRanges(testLspServer.TestWorkspace.Documents.First().AnnotatedSpans), }); AssertEx.Empty(results[1].Ranges); await PullDiagnosticTests.InsertInClosedDocumentAsync(testLspServer, document.Id, "// comment"); var results2 = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer, previousResults: CreateParamsFromPreviousReports(results)); Assert.Equal(2, results2.Length); var (_, lspSolution) = await testLspServer.GetManager().GetLspSolutionInfoAsync(CancellationToken.None).ConfigureAwait(false); document = lspSolution!.Projects.Single().Documents.First(); sourceText = await document.GetTextAsync(); MarkupTestFile.GetSpans( """ class {|Identifier:A|} { } {|Comment:// comment|} """, out _, out IDictionary<string, ImmutableArray<TextSpan>> annotatedSpans); AssertJsonEquals(results2[0], new VSInternalWorkspaceSpellCheckableReport { TextDocument = CreateTextDocumentIdentifier(document.GetURI()), ResultId = "WorkspaceSpellCheckHandler:3", Ranges = GetRanges(annotatedSpans), }); Assert.Null(results2[1].Ranges); Assert.NotEqual(results[0].ResultId, results2[0].ResultId); Assert.Equal(results[1].ResultId, results2[1].ResultId); } [Theory, CombinatorialData] public async Task TestStreamingWorkspaceResults(bool mutatingLspWorkspace) { var markup1 = """ class {|Identifier:A|} { } """; var markup2 = ""; await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace); var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer); Assert.Equal(2, results.Length); var document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.First(); var sourceText = await document.GetTextAsync(); AssertJsonEquals(results[0], new VSInternalWorkspaceSpellCheckableReport { TextDocument = CreateTextDocumentIdentifier(document.GetURI()), ResultId = "WorkspaceSpellCheckHandler:1", Ranges = GetRanges(testLspServer.TestWorkspace.Documents.First().AnnotatedSpans), }); AssertEx.Empty(results[1].Ranges); results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer, CreateParamsFromPreviousReports(results), useProgress: true); Assert.Equal(2, results.Length); Assert.Null(results[0].Ranges); Assert.Null(results[1].Ranges); } #endregion private static int[] GetRanges(IDictionary<string, ImmutableArray<TextSpan>> annotatedSpans) { var allSpans = annotatedSpans .SelectMany(kvp => kvp.Value.Select(textSpan => (kind: kvp.Key, textSpan)) .OrderBy(t => t.textSpan.Start)) .ToImmutableArray(); var ranges = new int[allSpans.Length * 3]; var index = 0; var lastSpanEnd = 0; foreach (var (kind, span) in allSpans) { ranges[index++] = (int)Convert(kind); ranges[index++] = span.Start - lastSpanEnd; ranges[index++] = span.Length; lastSpanEnd = span.End; } return ranges; } private static VSInternalSpellCheckableRangeKind Convert(string kind) => kind switch { "String" => VSInternalSpellCheckableRangeKind.String, "Comment" => VSInternalSpellCheckableRangeKind.Comment, "Identifier" => VSInternalSpellCheckableRangeKind.Identifier, _ => throw ExceptionUtilities.UnexpectedValue(kind), }; private static Task OpenDocumentAsync(TestLspServer testLspServer, Document document) => testLspServer.OpenDocumentAsync(document.GetURI()); private static Task CloseDocumentAsync(TestLspServer testLspServer, Document document) => testLspServer.CloseDocumentAsync(document.GetURI()); private static async Task<VSInternalSpellCheckableRangeReport[]> RunGetDocumentSpellCheckSpansAsync( TestLspServer testLspServer, DocumentUri uri, string? previousResultId = null, bool useProgress = false) { BufferedProgress<VSInternalSpellCheckableRangeReport[]>? progress = useProgress ? BufferedProgress.Create<VSInternalSpellCheckableRangeReport[]>(null) : null; var spans = await testLspServer.ExecuteRequestAsync<VSInternalDocumentSpellCheckableParams, VSInternalSpellCheckableRangeReport[]>( VSInternalMethods.TextDocumentSpellCheckableRangesName, CreateDocumentParams(uri, previousResultId, progress), CancellationToken.None).ConfigureAwait(false); if (useProgress) { Assert.Null(spans); spans = progress!.Value.GetFlattenedValues(); } Assert.NotNull(spans); return spans; } private static async Task<VSInternalWorkspaceSpellCheckableReport[]> RunGetWorkspaceSpellCheckSpansAsync( TestLspServer testLspServer, ImmutableArray<(string resultId, DocumentUri uri)>? previousResults = null, bool useProgress = false) { BufferedProgress<VSInternalWorkspaceSpellCheckableReport[]>? progress = useProgress ? BufferedProgress.Create<VSInternalWorkspaceSpellCheckableReport[]>(null) : null; var spans = await testLspServer.ExecuteRequestAsync<VSInternalWorkspaceSpellCheckableParams, VSInternalWorkspaceSpellCheckableReport[]>( VSInternalMethods.WorkspaceSpellCheckableRangesName, CreateWorkspaceParams(previousResults, progress), CancellationToken.None).ConfigureAwait(false); if (useProgress) { Assert.Null(spans); spans = progress!.Value.GetFlattenedValues(); } Assert.NotNull(spans); return spans; } private static async Task InsertTextAsync( TestLspServer testLspServer, Document document, int position, string text) { var sourceText = await document.GetTextAsync(); var lineInfo = sourceText.Lines.GetLinePositionSpan(new TextSpan(position, 0)); await testLspServer.InsertTextAsync(document.GetURI(), (lineInfo.Start.Line, lineInfo.Start.Character, text)); } private static VSInternalDocumentSpellCheckableParams CreateDocumentParams( DocumentUri uri, string? previousResultId = null, IProgress<VSInternalSpellCheckableRangeReport[]>? progress = null) { return new VSInternalDocumentSpellCheckableParams { TextDocument = new TextDocumentIdentifier { DocumentUri = uri }, PreviousResultId = previousResultId, PartialResultToken = progress, }; } private static VSInternalWorkspaceSpellCheckableParams CreateWorkspaceParams( ImmutableArray<(string resultId, DocumentUri uri)>? previousResults = null, IProgress<VSInternalWorkspaceSpellCheckableReport[]>? progress = null) { return new VSInternalWorkspaceSpellCheckableParams { PreviousResults = previousResults?.Select(r => new VSInternalStreamingParams { PreviousResultId = r.resultId, TextDocument = new TextDocumentIdentifier { DocumentUri = r.uri } }).ToArray(), PartialResultToken = progress, }; } private static ImmutableArray<(string resultId, DocumentUri uri)> CreateParamsFromPreviousReports(VSInternalWorkspaceSpellCheckableReport[] results) { return [.. results.Select(r => (r.ResultId!, Uri: r.TextDocument.DocumentUri))]; } } }