File: TypeHierarchy\TypeHierarchyTests.cs
Web Access
Project: src\src\LanguageServer\ProtocolUnitTests\Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests)
// 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.Threading;
using System.Threading.Tasks;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
using LSP = Roslyn.LanguageServer.Protocol;
 
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.TypeHierarchy;
 
public sealed class TypeHierarchyTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper)
{
    [Theory, CombinatorialData]
    public async Task TestPrepareTypeHierarchyReturnsTypeItem(bool mutatingLspWorkspace)
    {
        var markup = """
            class {|definition:C|}
            {
                void {|caret:|}M()
                {
                }
            }
            """;
 
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var preparedItem = Assert.Single(await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()));
        var definition = testLspServer.GetLocations("definition").Single();
 
        AssertEqualsItem(preparedItem, "C", definition);
        Assert.NotNull(preparedItem.Data);
    }
 
    [Theory, CombinatorialData]
    public async Task TestPrepareTypeHierarchyReturnsTypeItemForCrossDocumentReference(bool mutatingLspWorkspace)
    {
        var markups = new[]
        {
            """
            class User
            {
                {|caret:|}C field;
            }
            """,
            """
            class {|definition:C|}
            {
            }
            """,
        };
 
        await using var testLspServer = await CreateTestLspServerAsync(markups, mutatingLspWorkspace);
 
        var preparedItem = Assert.Single(await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()));
        var definition = testLspServer.GetLocations("definition").Single();
 
        AssertEqualsItem(preparedItem, "C", definition);
        Assert.NotNull(preparedItem.Data);
    }
 
    [Theory, CombinatorialData]
    public async Task TestPrepareTypeHierarchyReturnsContainingTypeFromConstructor(bool mutatingLspWorkspace)
    {
        var markup = """
            class {|definition:C|}
            {
                {|caret:|}C()
                {
                }
            }
            """;
 
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var preparedItem = Assert.Single(await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()));
        var definition = testLspServer.GetLocations("definition").Single();
 
        AssertEqualsItem(preparedItem, "C", definition);
        Assert.NotNull(preparedItem.Data);
    }
 
    [Theory, CombinatorialData]
    public async Task TestPrepareTypeHierarchyReturnsContainingTypeFromStaticConstructor(bool mutatingLspWorkspace)
    {
        var markup = """
            class {|definition:C|}
            {
                static {|caret:|}C()
                {
                }
            }
            """;
 
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var preparedItem = Assert.Single(await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()));
        var definition = testLspServer.GetLocations("definition").Single();
 
        AssertEqualsItem(preparedItem, "C", definition);
        Assert.NotNull(preparedItem.Data);
    }
 
    [Theory, CombinatorialData]
    public async Task TestPrepareTypeHierarchyPrefersRequestDocumentForPartialType(bool mutatingLspWorkspace)
    {
        var markups = new[]
        {
            """
            partial class {|definitionInFirst:C|}
            {
            }
            """,
            """
            partial class {|definitionInSecond:C|}
            {
                void M()
                {
                    {|caret:|}C c;
                }
            }
            """,
        };
 
        await using var testLspServer = await CreateTestLspServerAsync(markups, mutatingLspWorkspace);
 
        var preparedItem = Assert.Single(await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()));
        var preferredDefinition = testLspServer.GetLocations("definitionInSecond").Single();
 
        AssertEqualsItem(preparedItem, "C", preferredDefinition);
        Assert.NotNull(preparedItem.Data);
    }
 
    [Theory, CombinatorialData]
    public async Task TestPrepareTypeHierarchyReturnsContainingTypeForLocalVariableSymbol(bool mutatingLspWorkspace)
    {
        var markup = """
            class {|definition:C|}
            {
                void M()
                {
                    int {|caret:local|} = 0;
                }
            }
            """;
 
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var preparedItem = Assert.Single(await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()));
        var definition = testLspServer.GetLocations("definition").Single();
 
        AssertEqualsItem(preparedItem, "C", definition);
        Assert.NotNull(preparedItem.Data);
    }
 
    [Theory, CombinatorialData]
    public async Task TestPrepareTypeHierarchyReturnsNullForNamespaceSymbol(bool mutatingLspWorkspace)
    {
        var markup = """
            namespace {|caret:N|}
            {
                class C
                {
                }
            }
            """;
 
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var preparedItems = await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single());
        Assert.Empty(preparedItems);
    }
 
    [Theory, CombinatorialData]
    public async Task TestPrepareTypeHierarchyReturnsNullForKeywordPosition(bool mutatingLspWorkspace)
    {
        var markup = """
            class C
            {
                void M()
                {
                    {|caret:if|} (true)
                    {
                    }
                }
            }
            """;
 
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var preparedItems = await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single());
        Assert.Empty(preparedItems);
    }
 
    [Theory, CombinatorialData]
    public async Task TestTypeHierarchySupertypesReturnsBaseAndInterface(bool mutatingLspWorkspace)
    {
        var markups = new[]
        {
            """
            interface {|interfaceDef:IRoot|}
            {
            }
            """,
            """
            class {|baseDef:Base|}
            {
            }
            """,
            """
            class {|derivedDef:Derived|} : Base, IRoot
            {
                void {|caret:|}M()
                {
                }
            }
            """,
        };
 
        await using var testLspServer = await CreateTestLspServerAsync(markups, mutatingLspWorkspace);
 
        var preparedItem = Assert.Single(await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()));
        var supertypes = await RunSupertypesAsync(testLspServer, preparedItem);
 
        var baseDefinition = testLspServer.GetLocations("baseDef").Single();
        var interfaceDefinition = testLspServer.GetLocations("interfaceDef").Single();
 
        Assert.Equal(2, supertypes.Length);
        AssertContainsItem(supertypes, "Base", baseDefinition);
        AssertContainsItem(supertypes, "IRoot", interfaceDefinition);
    }
 
    [Theory, CombinatorialData]
    public async Task TestTypeHierarchySubtypesReturnsDerivedInterfaceAndImplementation(bool mutatingLspWorkspace)
    {
        var markups = new[]
        {
            """
            interface {|rootDef:IRoot|}
            {
            }
            """,
            """
            interface {|childDef:IChild|} : {|caret:|}IRoot
            {
            }
            """,
            """
            class {|directImplDef:DirectImplementation|} : IRoot
            {
            }
            """,
            """
            class {|implDef:Implementation|} : IChild
            {
            }
            """,
        };
 
        await using var testLspServer = await CreateTestLspServerAsync(markups, mutatingLspWorkspace);
 
        var preparedItem = Assert.Single(await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()));
        var subtypes = await RunSubtypesAsync(testLspServer, preparedItem);
 
        var childDefinition = testLspServer.GetLocations("childDef").Single();
        var directImplementationDefinition = testLspServer.GetLocations("directImplDef").Single();
 
        Assert.Equal(2, subtypes.Length);
        AssertContainsItem(subtypes, "IChild", childDefinition);
        AssertContainsItem(subtypes, "DirectImplementation", directImplementationDefinition);
    }
 
    [Theory, CombinatorialData]
    public async Task TestTypeHierarchySupertypesReturnsOnlyImmediateSupertypes(bool mutatingLspWorkspace)
    {
        var markups = new[]
        {
            """
            interface {|topInterfaceDef:ITop|}
            {
            }
            """,
            """
            interface {|midInterfaceDef:IMid|} : ITop
            {
            }
            """,
            """
            class {|baseDef:Base|} : IMid
            {
            }
            """,
            """
            class {|midDef:Mid|} : Base
            {
            }
            """,
            """
            class {|derivedDef:Derived|} : Mid
            {
                void {|caret:|}M()
                {
                }
            }
            """,
        };
 
        await using var testLspServer = await CreateTestLspServerAsync(markups, mutatingLspWorkspace);
 
        var preparedItem = Assert.Single(await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()));
        var supertypes = await RunSupertypesAsync(testLspServer, preparedItem);
 
        var midDefinition = testLspServer.GetLocations("midDef").Single();
        var baseDefinition = testLspServer.GetLocations("baseDef").Single();
        var midInterfaceDefinition = testLspServer.GetLocations("midInterfaceDef").Single();
        var topInterfaceDefinition = testLspServer.GetLocations("topInterfaceDef").Single();
 
        var midItem = Assert.Single(supertypes);
        AssertEqualsItem(midItem, "Mid", midDefinition);
 
        var midSupertypes = await RunSupertypesAsync(testLspServer, midItem);
        var baseItem = Assert.Single(midSupertypes);
        AssertEqualsItem(baseItem, "Base", baseDefinition);
 
        var baseSupertypes = await RunSupertypesAsync(testLspServer, baseItem);
        var midInterfaceItem = Assert.Single(baseSupertypes);
        AssertEqualsItem(midInterfaceItem, "IMid", midInterfaceDefinition);
 
        var midInterfaceSupertypes = await RunSupertypesAsync(testLspServer, midInterfaceItem);
        var topInterfaceItem = Assert.Single(midInterfaceSupertypes);
        AssertEqualsItem(topInterfaceItem, "ITop", topInterfaceDefinition);
    }
 
    [Theory, CombinatorialData]
    public async Task TestTypeHierarchySubtypesReturnsOnlyImmediateSubtypes(bool mutatingLspWorkspace)
    {
        var markups = new[]
        {
            """
            interface {|rootDef:IRoot|}
            {
            }
            """,
            """
            interface {|childDef:IChild|} : {|caret:|}IRoot
            {
            }
            """,
            """
            interface {|grandchildDef:IGrandChild|} : IChild
            {
            }
            """,
            """
            class {|directImplDef:DirectImplementation|} : IRoot
            {
            }
            """,
            """
            class {|indirectImplDef:IndirectImplementation|} : IGrandChild
            {
            }
            """,
        };
 
        await using var testLspServer = await CreateTestLspServerAsync(markups, mutatingLspWorkspace);
 
        var preparedItem = Assert.Single(await RunPrepareTypeHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()));
        var subtypes = await RunSubtypesAsync(testLspServer, preparedItem);
 
        var childDefinition = testLspServer.GetLocations("childDef").Single();
        var directImplementationDefinition = testLspServer.GetLocations("directImplDef").Single();
        var grandchildDefinition = testLspServer.GetLocations("grandchildDef").Single();
        var indirectImplementationDefinition = testLspServer.GetLocations("indirectImplDef").Single();
 
        Assert.Equal(2, subtypes.Length);
        AssertContainsItem(subtypes, "IChild", childDefinition);
        AssertContainsItem(subtypes, "DirectImplementation", directImplementationDefinition);
 
        var childItem = Assert.Single(subtypes, static item => item.Name == "IChild");
        var childSubtypes = await RunSubtypesAsync(testLspServer, childItem);
        var grandchildItem = Assert.Single(childSubtypes);
        AssertEqualsItem(grandchildItem, "IGrandChild", grandchildDefinition);
 
        var grandchildSubtypes = await RunSubtypesAsync(testLspServer, grandchildItem);
        var indirectImplementationItem = Assert.Single(grandchildSubtypes);
        AssertEqualsItem(indirectImplementationItem, "IndirectImplementation", indirectImplementationDefinition);
    }
 
    private static void AssertContainsItem(LSP.TypeHierarchyItem[] items, string expectedName, LSP.Location expectedDefinition)
    {
        var item = Assert.Single(items, i => i.Name == expectedName);
        AssertEqualsItem(item, expectedName, expectedDefinition);
    }
 
    private static void AssertEqualsItem(LSP.TypeHierarchyItem item, string expectedName, LSP.Location expectedDefinition)
    {
        Assert.Equal(expectedName, item.Name);
        Assert.Equal(expectedDefinition.DocumentUri, item.Uri);
        Assert.Equal(0, CompareRange(expectedDefinition.Range, item.SelectionRange));
    }
 
    private static async Task<LSP.TypeHierarchyItem[]> RunPrepareTypeHierarchyAsync(TestLspServer testLspServer, LSP.Location caret)
        => await testLspServer.ExecuteRequestAsync<LSP.TypeHierarchyPrepareParams, LSP.TypeHierarchyItem[]?>(
            LSP.Methods.PrepareTypeHierarchyName,
            new LSP.TypeHierarchyPrepareParams
            {
                TextDocument = CreateTextDocumentIdentifier(caret.DocumentUri),
                Position = caret.Range.Start,
            },
            CancellationToken.None) ?? [];
 
    private static async Task<LSP.TypeHierarchyItem[]> RunSupertypesAsync(TestLspServer testLspServer, LSP.TypeHierarchyItem item)
        => await testLspServer.ExecuteRequestAsync<LSP.TypeHierarchySupertypesParams, LSP.TypeHierarchyItem[]?>(
            LSP.Methods.TypeHierarchySupertypesName,
            new LSP.TypeHierarchySupertypesParams
            {
                TextDocument = CreateTextDocumentIdentifier(item.Uri),
                Position = item.SelectionRange.Start,
                Item = item,
            },
            CancellationToken.None) ?? [];
 
    private static async Task<LSP.TypeHierarchyItem[]> RunSubtypesAsync(TestLspServer testLspServer, LSP.TypeHierarchyItem item)
        => await testLspServer.ExecuteRequestAsync<LSP.TypeHierarchySubtypesParams, LSP.TypeHierarchyItem[]?>(
            LSP.Methods.TypeHierarchySubtypesName,
            new LSP.TypeHierarchySubtypesParams
            {
                TextDocument = CreateTextDocumentIdentifier(item.Uri),
                Position = item.SelectionRange.Start,
                Item = item,
            },
            CancellationToken.None) ?? [];
}