/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionsTests.cs
324 строки
12 KB
Jared Parsons
Port more tests to run on Linux (#83052)
10 апр 2026, 00:39
Не верифицирован
10 апр 2026, 00:39
82a89d0
Код
Авторство
О чём код?
// 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.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.AddImport; using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions; using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; using LSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.CodeActions; public sealed class CodeActionsTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) { [Theory, CombinatorialData] public async Task TestCodeActionHandlerAsync(bool mutatingLspWorkspace) { var markup = """ class A { void M() { {|caret:|}int i = 1; } } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, initializationOptions: new InitializationOptions() { ClientCapabilities = new VSInternalClientCapabilities { SupportsVisualStudioExtensions = true } }); var titlePath = new[] { CSharpAnalyzersResources.Use_implicit_type }; var caretLocation = testLspServer.GetLocations("caret").Single(); var expected = CreateCodeAction( title: CSharpAnalyzersResources.Use_implicit_type, kind: CodeActionKind.Refactor, children: [], data: CreateCodeActionResolveData( CSharpAnalyzersResources.Use_implicit_type, caretLocation, codeActionPath: titlePath, customTags: new[] { PredefinedCodeRefactoringProviderNames.UseImplicitType }), priority: VSInternalPriorityLevel.Low, groupName: "Roslyn2", applicableRange: new LSP.Range { Start = new Position { Line = 4, Character = 8 }, End = new Position { Line = 4, Character = 11 } }, diagnostics: null); var results = await RunGetCodeActionsAsync(testLspServer, CreateCodeActionParams(caretLocation)); var useImplicitType = results.FirstOrDefault(r => r.Title == CSharpAnalyzersResources.Use_implicit_type); AssertJsonEquals(expected, useImplicitType); } [Theory, CombinatorialData] public async Task TestCodeActionHandlerAsync_NestedAction(bool mutatingLspWorkspace) { var markup = """ class A { void M() { int {|caret:|}i = 1; } } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, CapabilitiesWithVSExtensions); var caretLocation = testLspServer.GetLocations("caret").Single(); var titlePath = new[] { FeaturesResources.Introduce_constant, string.Format(FeaturesResources.Introduce_constant_for_0, "1") }; var expected = CreateCodeAction( title: string.Format(FeaturesResources.Introduce_constant_for_0, "1"), kind: CodeActionKind.Refactor, children: [], data: CreateCodeActionResolveData( string.Format(FeaturesResources.Introduce_constant_for_0, "1"), caretLocation, codeActionPath: titlePath), priority: VSInternalPriorityLevel.Normal, groupName: "Roslyn3", applicableRange: new LSP.Range { Start = new Position { Line = 4, Character = 12 }, End = new Position { Line = 4, Character = 12 } }, diagnostics: null); var results = await RunGetCodeActionsAsync(testLspServer, CreateCodeActionParams(caretLocation)); var topLevelAction = Assert.Single(results, action => action.Title == titlePath[0]); var introduceConstant = topLevelAction.Children!.FirstOrDefault( r => JsonSerializer.Deserialize<CodeActionResolveData>((JsonElement)r.Data!, ProtocolConversions.LspJsonSerializerOptions)!.UniqueIdentifier == titlePath[1]); AssertJsonEquals(expected, introduceConstant); } [Theory, CombinatorialData] public async Task TestCodeActionHasCorrectDiagnostics(bool mutatingLspWorkspace) { var markup = """ class A { void M() { {|caret:|}Task.Delay(1); } } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var caret = testLspServer.GetLocations("caret").Single(); var codeActionParams = new CodeActionParams { TextDocument = CreateTextDocumentIdentifier(caret.DocumentUri), Range = caret.Range, Context = new CodeActionContext { Diagnostics = [ new LSP.Diagnostic { Code = AddImportDiagnosticIds.CS0103 }, new LSP.Diagnostic { Code = "SomeCode" } ] } }; var results = await RunGetCodeActionsAsync(testLspServer, codeActionParams); var addImport = results.FirstOrDefault(r => r.Title.Contains($"using System.Threading.Tasks")); Assert.Equal(1, addImport!.Diagnostics!.Length); Assert.Equal(AddImportDiagnosticIds.CS0103, addImport.Diagnostics.Single().Code!.Value); } [ConditionalTheory(typeof(IsEnglishLocal)), CombinatorialData] public async Task TestStandardLspNestedCodeAction(bool mutatingLspWorkspace) { var markup = """ class ABC { private void XYZ() { var a = {|caret:A()|}; } private int A() => 1; } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var caret = testLspServer.GetLocations("caret").Single(); var codeActionParams = new CodeActionParams { TextDocument = CreateTextDocumentIdentifier(caret.DocumentUri), Range = caret.Range, Context = new CodeActionContext { } }; var results = await RunGetCodeActionsAsync(testLspServer, codeActionParams); var inline = results.FirstOrDefault(r => r.Title.Contains($"Inline 'A()'")); var data = GetCodeActionResolveData(inline!); Assert.NotNull(data); // Asserts that there are NestedActions on Inline Assert.NotNull(data!.NestedCodeActions); Assert.NotEmpty(data!.NestedCodeActions); // Asserts that the second NestedAction's path is correct var nestedActionData = GetCodeActionResolveData(data!.NestedCodeActions!.Value[1]); Assert.NotNull(nestedActionData); Assert.Equal("Inline 'A()'", nestedActionData!.CodeActionPath[0]); Assert.Equal("Inline and keep 'A()'", nestedActionData!.CodeActionPath[1]); // Asserts that there is a Command present on an action with nested actions Assert.NotNull(inline?.Command); } [ConditionalTheory(typeof(IsEnglishLocal)), CombinatorialData] public async Task TestStandardLspNestedFixAllCodeAction(bool mutatingLspWorkspace) { var markup = """ class ABC { private static async void XYZ() { {|caret:System.Threading.Tasks.Task.Yield()|}; } } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var caret = testLspServer.GetLocations("caret").Single(); var codeActionParams = new CodeActionParams { TextDocument = CreateTextDocumentIdentifier(caret.DocumentUri), Range = caret.Range, Context = new CodeActionContext { Diagnostics = [ new LSP.Diagnostic { // Task-returning method not awaited Code = "CS4014" } ] } }; var results = await RunGetCodeActionsAsync(testLspServer, codeActionParams); Assert.Equal(10, results.Length); Assert.Equal("Suppress or configure issues", results[9].Title); var data = GetCodeActionResolveData(results[9]); Assert.NotNull(data); // Asserts that there are NestedActions present Assert.NotNull(data!.NestedCodeActions); //Asserts that a Nested Action could be a Fix All Action Assert.Equal("Fix All: in Source", data.NestedCodeActions!.Value[1].Title); } [ConditionalTheory(typeof(IsEnglishLocal)), CombinatorialData] public async Task TestStandardLspNestedResolveTopLevelCodeAction(bool mutatingLspWorkspace) { var markup = """ class ABC { private void XYZ() { var a = {|caret:A()|}; } private int A() => 1; } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var caret = testLspServer.GetLocations("caret").Single(); var codeActionParams = new CodeActionParams { TextDocument = CreateTextDocumentIdentifier(caret.DocumentUri), Range = caret.Range, Context = new CodeActionContext { } }; var results = await RunGetCodeActionsAsync(testLspServer, codeActionParams); // Assert that nested code actions aren't enumerated. var inline = results.FirstOrDefault(r => r.Title.Contains($"Inline 'A()'")); var resolvedAction = await RunGetCodeActionResolveAsync(testLspServer, inline!); Assert.Null(resolvedAction.Edit); } private static async Task<VSInternalCodeAction[]> RunGetCodeActionsAsync( TestLspServer testLspServer, CodeActionParams codeActionParams) { var result = await testLspServer.ExecuteRequestAsync<CodeActionParams, CodeAction[]>( LSP.Methods.TextDocumentCodeActionName, codeActionParams, CancellationToken.None); return [.. result!.Cast<VSInternalCodeAction>()]; } private static async Task<VSInternalCodeAction> RunGetCodeActionResolveAsync( TestLspServer testLspServer, CodeAction codeAction) { var result = await testLspServer.ExecuteRequestAsync<CodeAction, CodeAction>( LSP.Methods.CodeActionResolveName, codeAction, CancellationToken.None); Assert.NotNull(result); return (VSInternalCodeAction)result; } private static CodeActionResolveData? GetCodeActionResolveData(CodeAction codeAction) { return JsonSerializer.Deserialize<CodeActionResolveData>((JsonElement)codeAction.Data!, ProtocolConversions.LspJsonSerializerOptions); } internal static CodeActionParams CreateCodeActionParams(LSP.Location caret) => new() { TextDocument = CreateTextDocumentIdentifier(caret.DocumentUri), Range = caret.Range, Context = new CodeActionContext { // TODO - Code actions should respect context. } }; internal static VSInternalCodeAction CreateCodeAction( string title, CodeActionKind kind, VSInternalCodeAction[] children, CodeActionResolveData data, LSP.Diagnostic[]? diagnostics, VSInternalPriorityLevel? priority, string groupName, LSP.Range applicableRange, WorkspaceEdit? edit = null, Command? command = null) { var action = new VSInternalCodeAction { Title = title, Kind = kind, Children = children, Data = JsonSerializer.SerializeToElement(data, ProtocolConversions.LspJsonSerializerOptions), Diagnostics = diagnostics, Edit = edit, Group = groupName, Priority = priority, ApplicableRange = applicableRange, Command = command }; return action; } }