📄 core/tests/CallGraphBuilderTests/FindCallersShould.cs
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Shouldly;
using Xunit;

namespace Cfa.Tests.CallGraphBuilderTests;

public sealed class FindCallersShould
{
    [Theory]
    [InlineData("_ = Start;", "int Start { get => 1; set { } }")]
    [InlineData("Start = 2;", "int Start { get => 1; set { } }")]
    public async Task FindPropertyAccesses(string access, string declaration)
    {
        // Arrange
        var source = $$"""
            class Example
            {
                {{declaration}}
                void Caller() { {{access}} }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        var caller = callers[start].ShouldHaveSingleItem();
        caller.Target.Name.ShouldBe("Caller");
        caller.LinkKind.ShouldBe(LinkKind.Direct);
    }

    [Theory]
    [InlineData("_ = this[0];", "int this[int index] => Start(index);")]
    [InlineData("this[0] = 1;", "int this[int index] { get => index; set => Start(index + value); }")]
    public async Task FindIndexer(string access, string declaration)
    {
        // Arrange
        var source = $$"""
            class Example
            {
                int Start(int index) => 1;
                {{declaration}}
                void Caller() { {{access}} }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        var indexer = callers[start].ShouldHaveSingleItem();
        var caller = callers[indexer.Target].ShouldHaveSingleItem();
        caller.Target.Name.ShouldBe("Caller");
        caller.LinkKind.ShouldBe(LinkKind.Direct);
    }

    [Theory]
    [InlineData("get { return Start(); }")]
    [InlineData("get => Start();")]
    [InlineData("set { Start(); }")]
    public async Task AttributeAccessorCallsToTheirProperty(string accessor)
    {
        // Arrange
        var source = $$"""
            class Example
            {
                int Value { {{accessor}} }
                int Start() => 1;
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        callers[start].ShouldHaveSingleItem().Target.Name.ShouldBe("Value");
    }

    [Theory]
    [InlineData("new Example()")]
    [InlineData("new()")]
    public async Task FindConstructorCalls(string creation)
    {
        // Arrange
        var source = $$"""
            class Example
            {
                public Example() { Start(); }
                void Start() { }
                static void Caller() { Example example = {{creation}}; }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        var constructor = callers[start].ShouldHaveSingleItem().Target;
        constructor.ShouldBeAssignableTo<IMethodSymbol>().MethodKind.ShouldBe(MethodKind.Constructor);
        callers[constructor].ShouldHaveSingleItem().Target.Name.ShouldBe("Caller");
    }

    [Fact]
    public async Task FindCallsThroughMetadataInterfaces()
    {
        // Arrange
        const string source = """
            class Example : System.IDisposable
            {
                public void Dispose() { Start(); }
                void Start() { }
                void Caller(System.IDisposable service) { service.Dispose(); }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        var dispose = callers[start].ShouldHaveSingleItem().Target;
        var caller = callers[dispose].ShouldHaveSingleItem();
        caller.Target.Name.ShouldBe("Caller");
        caller.LinkKind.ShouldBe(LinkKind.Abstract);
    }

    [Fact]
    public async Task IgnoreDirectCallsToOtherImplementations()
    {
        // Arrange
        const string source = """
            interface IService { void Run(); }
            class Other : IService { public void Run() { } }
            class Example : IService
            {
                public void Run() { Start(); }
                void Start() { }
                void Caller(Other other) { other.Run(); }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        var run = callers[start].ShouldHaveSingleItem().Target;
        callers[run].ShouldBeEmpty();
    }

    [Fact]
    public async Task DeduplicateCallersAndStopAtCycles()
    {
        // Arrange
        const string source = """
            class Example
            {
                void Start() { Caller(); }
                void Caller() { Start(); Start(); }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        var caller = callers[start].ShouldHaveSingleItem().Target;
        callers[caller].ShouldHaveSingleItem().Target.ShouldBe(start);
        callers.Count.ShouldBe(2);
    }

    [Fact]
    public async Task FindCallsThroughVirtualBaseMembers()
    {
        // Arrange
        const string source = """
            class Base { public virtual void Run() { } }
            class Example : Base
            {
                public override void Run() { Start(); }
                void Start() { }
                void Caller(Base service) { service.Run(); }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        var run = callers[start].ShouldHaveSingleItem().Target;
        var caller = callers[run].ShouldHaveSingleItem();
        caller.Target.Name.ShouldBe("Caller");
        caller.LinkKind.ShouldBe(LinkKind.Abstract);
    }

    [Fact]
    public async Task FindCallsInsideLambdas()
    {
        // Arrange
        const string source = """
            class Example
            {
                void Start() { }
                void Caller() { System.Action action = () => Start(); action(); }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        callers[start].ShouldHaveSingleItem().Target.Name.ShouldBe("Caller");
    }

    [Fact]
    public async Task ReturnAnEmptyListWhenThereAreNoCallers()
    {
        // Arrange
        const string source = """
            class Example { void Start() { } }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        callers[start].ShouldBeEmpty();
    }

    [Fact]
    public async Task FollowCallers()
    {
        // Arrange
        var source = """
            class Example
            {
                void TwoUp() { OneUp(); }
                void OneUp() { Start(); }
                void Start() { }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        var oneUp = callers[start].ShouldHaveSingleItem().Target;
        oneUp.Name.ShouldBe("OneUp");
        var twoUp = callers[oneUp].ShouldHaveSingleItem().Target;
        twoUp.Name.ShouldBe("TwoUp");
    }

    [Fact]
    public async Task FindExpressionBodiedProperties()
    {
        // Arrange
        var source = """
            class Example
            {
                int Value => Start();
                int Start() => 42;
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        callers[start].ShouldHaveSingleItem().Target.Name.ShouldBe("Value");
    }

    [Fact]
    public async Task FindCallsToTheAbstractMethodThatTheOriginImplements()
    {
        // Arrange
        var source = """
            abstract class Base
            {
                public abstract void Run();
            }
            class Example : Base
            {
                void ShouldBeFound(Base b) { b.Run(); }
                public override void Run() { Start(); }
                void Start() { }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        var concreteRun = callers[start].ShouldHaveSingleItem();
        concreteRun.Target.ContainingType.Name.ShouldBe("Example");
        var shouldBeFound = callers[concreteRun.Target].ShouldHaveSingleItem();
        shouldBeFound.Target.Name.ShouldBe("ShouldBeFound");
        shouldBeFound.LinkKind.ShouldBe(LinkKind.Abstract);
    }

    [Theory]
    [InlineData("service.Run(); Run();")]
    [InlineData("Run(); service.Run();")]
    public async Task PreferDirectCallsOverInterfaceLinks(string body)
    {
        // Arrange
        var source = $$"""
            interface IService { void Run(); }
            class Example : IService
            {
                void ShouldBeFound(IService service) { {{body}} }
                public void Run() { Start(); }
                void Start() { }
            }
            """;

        // Act
        var (start, callers) = await source.FindCallers();

        // Assert
        var concreteRun = callers[start].ShouldHaveSingleItem();
        concreteRun.Target.ContainingType.Name.ShouldBe("Example");
        var shouldBeFound = callers[concreteRun.Target].ShouldHaveSingleItem();
        shouldBeFound.Target.Name.ShouldBe("ShouldBeFound");
        shouldBeFound.LinkKind.ShouldBe(LinkKind.Direct);
    }
}