📄 core/src/CalleBuilder.cs
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Cfa;

internal sealed class CalleeBuilder(IReadOnlyCollection<Compilation> compilations)
{
    private readonly IReadOnlyCollection<Compilation> compilations = compilations;
    private readonly HashSet<ISymbol> visitedSymbols = [with(SymbolEqualityComparer.Default)];

    public IImmutableList<Callee> FindCallees(ISymbol symbol, CancellationToken cancellationToken)
    {
        if (!visitedSymbols.Add(symbol))
        {
            return [];
        }

        var seen = new HashSet<ISymbol>(SymbolEqualityComparer.Default);
        var callees = ImmutableList.CreateBuilder<Callee>();
        foreach (var reference in symbol.DeclaringSyntaxReferences)
        {
            var syntax = reference.GetSyntax(cancellationToken);
            var semanticModelForSyntax = compilations.GetSemanticModel(syntax.SyntaxTree);
            var invocations = syntax.DescendantNodesAndSelf().OfType<InvocationExpressionSyntax>();
            foreach (var invocation in invocations)
            {
                var invokedSymbol = semanticModelForSyntax.GetSymbolInfo(invocation, cancellationToken).Symbol;
                if (invokedSymbol is { DeclaringSyntaxReferences.Length: > 0 } && seen.Add(invokedSymbol))
                {
                    callees.Add(
                        new Callee(invokedSymbol, LinkKind.Direct, FindCallees(invokedSymbol, cancellationToken))
                    );
                }
            }
        }
        return callees.ToImmutable();
    }
}