📄 core/tests/CalleeBuilderTests/FindCalleesShould.cs
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Shouldly;
using Xunit;

namespace Cfa.Tests.CalleeBuilderTests;

public sealed class FindCalleesShould
{
    [Theory]
    [InlineData("Value")]
    [InlineData("this.Value")]
    public void FindExpressionBodiedProperties(string access)
    {
        // Arrange
        var source = $$"""
            class Example
            {
                int Value => 1;
                void Start() { _ = {{access}}; }
            }
            """;

        // Act
        var callees = FindCallees(source);

        // Assert
        callees.ShouldHaveSingleItem().Target.Name.ShouldBe("Value");
    }

    [Fact]
    public void FollowMethodCallsInsideProperties()
    {
        // Arrange
        const string source = """
            class Example
            {
                int Value => GetValue();
                int GetValue() => 1;
                void Start() { _ = Value; }
            }
            """;

        // Act
        var callees = FindCallees(source);

        // Assert
        var property = callees.ShouldHaveSingleItem();
        property.Target.Name.ShouldBe("Value");
        property.Callees.ShouldHaveSingleItem().Target.Name.ShouldBe("GetValue");
    }

    [Theory]
    [InlineData("int Value { get { return 1; } }", "_ = Value;")]
    [InlineData("int Value { get => 1; }", "_ = Value;")]
    [InlineData("int Value { set { } }", "Value = 1;")]
    public void FindPropertiesWithAccessorBodies(string declaration, string access)
    {
        // Arrange
        var source = $$"""
            class Example
            {
                {{declaration}}
                void Start() { {{access}} }
            }
            """;

        // Act
        var callees = FindCallees(source);

        // Assert
        callees.ShouldHaveSingleItem().Target.Name.ShouldBe("Value");
    }

    [Theory]
    [InlineData("set { Save(value); }", "Value = 1;")]
    [InlineData("set { Save(value); }", "this.Value = 1;")]
    [InlineData("set => Save(value);", "Value = 1;")]
    [InlineData("set => Save(value);", "this.Value = 1;")]
    public void FollowAssignedPropertiesAndMethodCallsInsideTheirSetters(string setter, string assignment)
    {
        // Arrange
        var source = $$"""
            class Example
            {
                int Value { {{setter}} }
                void Save(int value) { }
                void Start() { {{assignment}} }
            }
            """;

        // Act
        var callees = FindCallees(source);

        // Assert
        var property = callees.ShouldHaveSingleItem();
        property.Target.Name.ShouldBe("Value");
        property.Callees.ShouldHaveSingleItem().Target.Name.ShouldBe("Save");
    }

    [Theory]
    [InlineData("_ = this.Value;")]
    [InlineData("this.Value = 2;")]
    public void IgnoreAutoPropertiesEvenWhenTheyHaveInitializers(string access)
    {
        // Arrange
        var source = $$"""
            class Example
            {
                int Value { get; set; } = 1;
                void Start() { {{access}} }
            }
            """;

        // Act
        var callees = FindCallees(source);

        // Assert
        callees.ShouldBeEmpty();
    }

    [Theory]
    [InlineData("int this[int index] => Read();", "_ = this[0];", "Read")]
    [InlineData("int this[int index] => Read();", "_ = this?[0];", "Read")]
    [InlineData("int this[int index] { get { return Read(); } }", "_ = this[0];", "Read")]
    [InlineData("int this[int index] { get => Read(); }", "_ = this[0];", "Read")]
    [InlineData("int this[int index] { set { Save(value); } }", "this[0] = 1;", "Save")]
    [InlineData("int this[int index] { set => Save(value); }", "this[0] = 1;", "Save")]
    public void FollowIndexersWithBodies(string declaration, string access, string method)
    {
        // Arrange
        var source = $$"""
            class Example
            {
                {{declaration}}
                int Read() => 1;
                void Save(int value) { }
                void Start() { {{access}} }
            }
            """;

        // Act
        var callees = FindCallees(source);

        // Assert
        var indexer = callees.ShouldHaveSingleItem();
        indexer.Target.ShouldBeAssignableTo<IPropertySymbol>().IsIndexer.ShouldBeTrue();
        indexer.Callees.ShouldHaveSingleItem().Target.Name.ShouldBe(method);
    }

    [Fact]
    public void IgnoreIndexersWithoutBodies()
    {
        // Arrange
        const string source = """
            abstract class Example
            {
                public abstract int this[int index] { get; set; }
                void Start() { _ = this[0]; this[0] = 1; }
            }
            """;

        // Act
        var callees = FindCallees(source);

        // Assert
        callees.ShouldBeEmpty();
    }

    private static IImmutableList<Callee> FindCallees(string source)
    {
        var compilation = CSharpCompilation.Create(
            "Example",
            [CSharpSyntaxTree.ParseText(source)],
            [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
        );
        var start = compilation.GetTypeByMetadataName("Example")!.GetMembers("Start").Single();
        return new CalleeBuilder([compilation]).FindCallees(start, CancellationToken.None);
    }
}