📄 core/src/Analyzer.cs
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace Cfa;

public static class Analyzer
{
    public static CallGraphAnalysis Analyze(CallGraph callGraph) =>
        new(
            callGraph.Origin,
            AnalyzeCalls(callGraph.CallsTo),
            AnalyzeCalls(callGraph.CalledFrom),
            CountAppearances(callGraph)
        );

    private static IImmutableDictionary<ISymbol, IImmutableList<CallAnalysis>> AnalyzeCalls(
        IImmutableDictionary<ISymbol, IImmutableList<Call>> calls
    ) =>
        calls.ToImmutableDictionary(
            kvp => kvp.Key,
            kvp => (IImmutableList<CallAnalysis>)[.. kvp.Value.Select(c => AnalyzeCall(kvp.Key, c))],
            SymbolEqualityComparer.Default
        );

    private static CallAnalysis AnalyzeCall(ISymbol source, Call call) =>
        new(call.Target, call.LinkKind, DetermineReach(source, call.Target));

    private static CallReach DetermineReach(ISymbol source, ISymbol target) =>
        SymbolEqualityComparer.Default.Equals(source.ContainingType, target.ContainingType) ? CallReach.SameType
        : SymbolEqualityComparer.Default.Equals(source.ContainingNamespace, target.ContainingNamespace)
            ? CallReach.SameNamespace
        : SymbolEqualityComparer.Default.Equals(source.ContainingAssembly, target.ContainingAssembly)
            ? CallReach.SameAssembly
        : CallReach.CrossAssembly;

    private static IImmutableDictionary<ISymbol, uint> CountAppearances(CallGraph callGraph)
    {
        var builder = ImmutableDictionary.CreateBuilder<ISymbol, uint>(SymbolEqualityComparer.Default);

        builder.Add(callGraph.Origin, 0);
        AddVisits(callGraph.CallsTo);
        AddVisits(callGraph.CalledFrom);

        return builder.ToImmutable();

        void AddVisits(IImmutableDictionary<ISymbol, IImmutableList<Call>> map)
        {
            var toVisit = new Queue<ISymbol>();
            toVisit.Enqueue(callGraph.Origin);
            var visited = new HashSet<ISymbol>(SymbolEqualityComparer.Default);
            while (toVisit.TryDequeue(out var current))
            {
                if (!visited.Add(current) || !map.TryGetValue(current, out var calls))
                {
                    continue;
                }

                foreach (var call in calls)
                {
                    var target = call.Target;
                    builder[target] = builder.GetValueOrDefault(target) + 1;
                    toVisit.Enqueue(target);
                }
            }
        }
    }
}

public sealed record CallGraphAnalysis(
    ISymbol Origin,
    IImmutableDictionary<ISymbol, IImmutableList<CallAnalysis>> CallsTo,
    IImmutableDictionary<ISymbol, IImmutableList<CallAnalysis>> CalledFrom,
    IImmutableDictionary<ISymbol, uint> Appearances
)
{
    public uint TotalAppearances { get; } = Appearances.Values.Aggregate(0u, (acc, val) => acc + val);
}

public record CallAnalysis(ISymbol Target, LinkKind LinkKind, CallReach Reach) : Call(Target, LinkKind);

public enum CallReach
{
    SameType,
    SameNamespace,
    SameAssembly,
    CrossAssembly,
}