📄 Core/ReachabilityAnalyzer.cs
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Reacher.MemberCollectors;

namespace Reacher;

internal sealed class ReachabilityAnalyzer(IReadOnlyCollection<Compilation> compilations)
{
    private readonly HashSet<ISymbol> reachableMembers = [];

    public void Analyze(ISymbol member, CancellationToken cancellationToken)
    {
        if (
            !IsUserDefined(member)
            || !TryGetAssociatedCompilation(member, out var compilation)
            || !reachableMembers.Add(member)
        )
        {
            return;
        }

        foreach (var declaration in member.DeclaringSyntaxReferences)
        {
            AnalyzeDeclaration(compilation, declaration, cancellationToken);
        }
    }

    private static bool IsUserDefined(ISymbol member) =>
        !member.IsImplicitlyDeclared && (member.CanBeReferencedByName || member.Name is "<Main>$");

    private bool TryGetAssociatedCompilation(ISymbol member, [NotNullWhen(true)] out Compilation? compilation)
    {
        compilation = compilations.FirstOrDefault(c =>
            member.DeclaringSyntaxReferences.Any(d => !d.Span.IsEmpty && c.ContainsSyntaxTree(d.SyntaxTree))
        );
        return compilation is not null;
    }

    private void AnalyzeDeclaration(
        Compilation compilation,
        SyntaxReference declaration,
        CancellationToken cancellationToken
    )
    {
        var originalNode = declaration.GetSyntax(cancellationToken);
        var expressions = originalNode
            .DescendantNodes(ShouldDescendInto(originalNode))
            .OfType<ExpressionSyntax>()
            .Where(e =>
                e is IdentifierNameSyntax or ImplicitObjectCreationExpressionSyntax or ObjectCreationExpressionSyntax
            );

        var semanticModel = compilation.GetSemanticModel(declaration.SyntaxTree);

        foreach (var identifier in expressions)
        {
            if (!semanticModel.IsReachable(identifier))
            {
                continue;
            }
            if (
                semanticModel.GetSymbolInfo(identifier, cancellationToken) is not { Symbol: ISymbol member }
                || member is not (IMethodSymbol or IPropertySymbol)
            )
            {
                continue;
            }
            Analyze(member, cancellationToken);
        }
    }

    private static Func<SyntaxNode, bool> ShouldDescendInto(SyntaxNode originalNode) =>
        syntaxNode =>
            syntaxNode == originalNode
            || !(
                syntaxNode.IsKind(SyntaxKind.LocalFunctionStatement)
                || syntaxNode.IsKind(SyntaxKind.SimpleLambdaExpression)
                || syntaxNode.IsKind(SyntaxKind.ParenthesizedLambdaExpression)
            );

    public ReachabilityAnalysis GetAnalysis() => new(reachableMembers, GetUnreachableMembers());

    private IReadOnlySet<ISymbol> GetUnreachableMembers()
    {
        var collector = new PredicateMembersCollector(m => IsUserDefined(m) && !reachableMembers.Contains(m));
        compilations.Accept(collector);
        return collector.Members;
    }
}