📄 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 accesses = syntax
                .DescendantNodesAndSelf()
                .Where(node =>
                    node is SimpleNameSyntax or ElementAccessExpressionSyntax or ElementBindingExpressionSyntax
                );
            foreach (var access in accesses)
            {
                var accessedSymbol = semanticModelForSyntax.GetSymbolInfo(access, cancellationToken).Symbol;
                if (
                    accessedSymbol is not (IMethodSymbol or IPropertySymbol)
                    || !seen.Add(accessedSymbol)
                    || !IsDeclaredInSource(accessedSymbol)
                )
                {
                    continue;
                }

                if (accessedSymbol is IPropertySymbol property && !HasBody(property, cancellationToken))
                {
                    continue;
                }

                callees.Add(
                    new Callee(accessedSymbol, LinkKind.Direct, FindCallees(accessedSymbol, cancellationToken))
                );
            }
        }
        return callees.ToImmutable();
    }

    private static bool IsDeclaredInSource(ISymbol symbol) =>
        symbol.DeclaringSyntaxReferences.Length > 0 && !symbol.IsImplicitlyDeclared;

    private static bool HasBody(IPropertySymbol property, CancellationToken cancellationToken) =>
        property.DeclaringSyntaxReferences.Any(reference =>
            reference.GetSyntax(cancellationToken) switch
            {
                PropertyDeclarationSyntax { ExpressionBody: not null } => true,
                IndexerDeclarationSyntax { ExpressionBody: not null } => true,
                BasePropertyDeclarationSyntax declaration => declaration.AccessorList?.Accessors.Any(accessor =>
                    accessor.Body is not null || accessor.ExpressionBody is not null
                )
                    is true,
                _ => false,
            }
        );
}