📄 core/tests/CallGraphBuilderTests/BuildCallGraphShould.cs
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GlobExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using Shouldly;
using Xunit;

namespace Cfa.Tests.CallGraphBuilderTests;

public sealed class BuildCallGraphShould
{
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task LinkMessagesAcrossProjectsUnlessTheReceiverProjectIsExcluded(bool excludeReceiver)
    {
        // Arrange
        using var workspace = new AdhocWorkspace();
        var sender = workspace.AddProject(
            ProjectInfo.Create(
                ProjectId.CreateNewId(),
                VersionStamp.Default,
                "Sender",
                "Sender",
                LanguageNames.CSharp,
                filePath: "/repo/Sender.csproj",
                compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
                metadataReferences: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]
            )
        );
        var receiver = workspace.AddProject(
            ProjectInfo.Create(
                ProjectId.CreateNewId(),
                VersionStamp.Default,
                "Receiver.Tests",
                "Receiver.Tests",
                LanguageNames.CSharp,
                filePath: "/repo/Receiver.Tests.csproj",
                compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
                projectReferences: [new ProjectReference(sender.Id)],
                metadataReferences: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]
            )
        );
        workspace.AddDocument(
            sender.Id,
            "Sender.cs",
            SourceText.From(
                MessageSource.Contracts
                    + """
                    public class Example
                    {
                        public void Start(Sample.GrainInterfaces.Events.IEventSender publisher)
                        {
                            publisher.Publish(new Updated());
                        }
                    }
                    """
            )
        );
        workspace.AddDocument(
            receiver.Id,
            "Receiver.cs",
            SourceText.From(
                """
                class Receiver : Sample.Grains.EventHandlers.EventReceiver<Sample.Domain.Message<int>>
                {
                    public Receiver() { On<Updated>(Handle); }
                    void Handle(Updated evt) { Save(); }
                    void Save() { }
                }
                """
            )
        );
        var solution = workspace.CurrentSolution;
        var senderCompilation = (await solution.GetProject(sender.Id)!.GetCompilationAsync()).ShouldNotBeNull();
        var receiverCompilation = (await solution.GetProject(receiver.Id)!.GetCompilationAsync()).ShouldNotBeNull();
        senderCompilation.GetDiagnostics().ShouldBeEmpty();
        receiverCompilation.GetDiagnostics().ShouldBeEmpty();
        var start = senderCompilation.GetTypeByMetadataName("Example")!.GetMembers("Start").Single();
        var options = new ScannerOptions
        {
            ExcludePatterns = excludeReceiver ? [new("*Tests*")] : [],
            Messages = MessageSource.Options,
        };

        // Act
        var graph = await CallGraphBuilder.BuildCallGraph(
            options,
            solution,
            [senderCompilation, receiverCompilation],
            start,
            CancellationToken.None
        );

        // Assert
        if (excludeReceiver)
        {
            graph.CallsTo[start].ShouldBeEmpty();
        }
        else
        {
            var handler = graph.CallsTo[start].ShouldHaveSingleItem();
            handler.LinkKind.ShouldBe(LinkKind.Event);
            handler.Target.Name.ShouldBe("Handle");
            graph.CallsTo[handler.Target].ShouldHaveSingleItem().Target.Name.ShouldBe("Save");
            graph.CalledFrom[handler.Target].ShouldHaveSingleItem().Target.ShouldBe(start);
        }
    }

    [Fact]
    public async Task ExcludeTestProjectMembersButNotProductionFilesNamedTests()
    {
        // Arrange
        using var workspace = new AdhocWorkspace();
        var app = workspace.AddProject(
            ProjectInfo.Create(
                ProjectId.CreateNewId(),
                VersionStamp.Default,
                "App",
                "App",
                LanguageNames.CSharp,
                filePath: "/repo/App.csproj",
                compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
                metadataReferences: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]
            )
        );
        var tests = workspace.AddProject(
            ProjectInfo.Create(
                ProjectId.CreateNewId(),
                VersionStamp.Default,
                "App.Tests",
                "App.Tests",
                LanguageNames.CSharp,
                filePath: "/repo/App.Tests.csproj",
                compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
                metadataReferences: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
                projectReferences: [new ProjectReference(app.Id)]
            )
        );
        workspace.AddDocument(
            app.Id,
            "Tests.cs",
            SourceText.From(
                """
                public class App
                {
                    public static void Target() { }
                    public static void Caller() { Target(); }
                }
                """
            )
        );
        workspace.AddDocument(
            tests.Id,
            "Production.cs",
            SourceText.From(
                """
                class Tests
                {
                    void Start() { Helper(); App.Target(); }
                    void Helper() { }
                }
                """
            )
        );
        var solution = workspace.CurrentSolution;
        var appCompilation = (await solution.GetProject(app.Id)!.GetCompilationAsync()).ShouldNotBeNull();
        var testCompilation = (await solution.GetProject(tests.Id)!.GetCompilationAsync()).ShouldNotBeNull();
        appCompilation.GetDiagnostics().ShouldBeEmpty();
        testCompilation.GetDiagnostics().ShouldBeEmpty();
        var start = testCompilation.GetTypeByMetadataName("Tests")!.GetMembers("Start").Single();
        var target = appCompilation.GetTypeByMetadataName("App")!.GetMembers("Target").Single();
        var builder = new CallGraphBuilder(
            new ScannerOptions { ExcludePatterns = [new("*Tests*")] },
            solution,
            [appCompilation, testCompilation]
        );

        // Act
        await builder.FindCallees(start, CancellationToken.None);
        await builder.FindCallers(target, CancellationToken.None);

        // Assert
        builder.ReachableFromSymbols[start].ShouldHaveSingleItem().Target.Name.ShouldBe("Target");
        builder.ReachesToSymbols[target].ShouldHaveSingleItem().Target.Name.ShouldBe("Caller");
    }
}