Commit: 447dbae
Parent: 066e775

List unreachable members

Mårten Åsberg committed on 2026-03-10 at 22:06
Core/ExcludingMembersCollector.cs +46 -0
diff --git a/Core/ExcludingMembersCollector.cs b/Core/ExcludingMembersCollector.cs
new file mode 100644
index 0000000..33cd9fa
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
namespace Reacher;
internal class ExcludingMembersCollector(IReadOnlySet<IMethodSymbol> excludedMembers) : SymbolVisitor
{
public IReadOnlySet<IMethodSymbol> Members => members;
private readonly HashSet<IMethodSymbol> members = [];
public override void VisitAssembly(IAssemblySymbol symbol)
{
symbol.GlobalNamespace.Accept(this);
}
public override void VisitNamespace(INamespaceSymbol symbol)
{
foreach (var namespaceOrType in symbol.GetMembers())
{
namespaceOrType.Accept(this);
}
}
public override void VisitNamedType(INamedTypeSymbol type)
{
foreach (var member in type.GetMembers())
{
member.Accept(this);
}
foreach (var nestedType in type.GetTypeMembers())
{
nestedType.Accept(this);
}
}
public override void VisitMethod(IMethodSymbol member)
{
if (excludedMembers.Contains(member))
{
return;
}
members.Add(member);
}
}
Core/ReachabilityAnalysis.cs +8 -69
diff --git a/Core/ReachabilityAnalysis.cs b/Core/ReachabilityAnalysis.cs
index a09291c..c32feaf 100644
@@ -1,77 +1,16 @@
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;
namespace Reacher;
/// <summary>
/// The results of a reachability analysis.
/// </summary>
public sealed class ReachabilityAnalysis(IReadOnlyCollection<Compilation> compilations)
{
/// <summary>
/// A set of all reachable members found by the analysis.
/// </summary>
public IReadOnlySet<IMethodSymbol> ReachableMembers => reachableMembers;
private readonly HashSet<IMethodSymbol> reachableMembers = [];
internal void Analyze(IMethodSymbol member, CancellationToken cancellationToken)
{
if (!TryGetAssociatedCompilation(member, out var compilation) || !reachableMembers.Add(member))
{
return;
}
foreach (var declaration in member.DeclaringSyntaxReferences)
{
AnalyzeDeclaration(compilation, declaration, cancellationToken);
}
}
private bool TryGetAssociatedCompilation(IMethodSymbol member, [NotNullWhen(true)] out Compilation? compilation)
{
compilation = compilations.FirstOrDefault(c =>
member.DeclaringSyntaxReferences.Any(d => c.ContainsSyntaxTree(d.SyntaxTree))
);
return compilation is not null;
}
private void AnalyzeDeclaration(
Compilation compilation,
SyntaxReference declaration,
CancellationToken cancellationToken
)
{
var originalNode = declaration.GetSyntax(cancellationToken);
var identifiers = originalNode.DescendantNodes(ShouldDescendInto(originalNode)).OfType<IdentifierNameSyntax>();
var semanticModel = compilation.GetSemanticModel(declaration.SyntaxTree);
foreach (var identifier in identifiers)
{
if (!semanticModel.IsReachable(identifier))
{
continue;
}
if (semanticModel.GetSymbolInfo(identifier, cancellationToken) is not { Symbol: IMethodSymbol member })
{
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)
);
}
/// <param name="ReachableMembers">The members the analysis deemed reachable from the original starting members.</param>
/// <param name="UnreachableMembers">
/// The members the analysis deemed unreachable from the original starting members.
/// </param>
public sealed record ReachabilityAnalysis(
IReadOnlySet<IMethodSymbol> ReachableMembers,
IReadOnlySet<IMethodSymbol> UnreachableMembers
);
Core/ReachabilityAnalyzer.cs +84 -0
diff --git a/Core/ReachabilityAnalyzer.cs b/Core/ReachabilityAnalyzer.cs
new file mode 100644
index 0000000..1af4ff6
@@ -0,0 +1,84 @@
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;
namespace Reacher;
internal sealed class ReachabilityAnalyzer(IReadOnlyCollection<Compilation> compilations)
{
private readonly HashSet<IMethodSymbol> reachableMembers = [];
public void Analyze(IMethodSymbol member, CancellationToken cancellationToken)
{
if (!TryGetAssociatedCompilation(member, out var compilation) || !reachableMembers.Add(member))
{
return;
}
foreach (var declaration in member.DeclaringSyntaxReferences)
{
AnalyzeDeclaration(compilation, declaration, cancellationToken);
}
}
private bool TryGetAssociatedCompilation(IMethodSymbol member, [NotNullWhen(true)] out Compilation? compilation)
{
compilation = compilations.FirstOrDefault(c =>
member.DeclaringSyntaxReferences.Any(d => c.ContainsSyntaxTree(d.SyntaxTree))
);
return compilation is not null;
}
private void AnalyzeDeclaration(
Compilation compilation,
SyntaxReference declaration,
CancellationToken cancellationToken
)
{
var originalNode = declaration.GetSyntax(cancellationToken);
var identifiers = originalNode.DescendantNodes(ShouldDescendInto(originalNode)).OfType<IdentifierNameSyntax>();
var semanticModel = compilation.GetSemanticModel(declaration.SyntaxTree);
foreach (var identifier in identifiers)
{
if (!semanticModel.IsReachable(identifier))
{
continue;
}
if (semanticModel.GetSymbolInfo(identifier, cancellationToken) is not { Symbol: IMethodSymbol member })
{
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<IMethodSymbol> GetUnreachableMembers()
{
var collector = new ExcludingMembersCollector(reachableMembers);
foreach (var compilation in compilations)
{
compilation.Assembly.Accept(collector);
}
return collector.Members;
}
}
Core/SolutionExtensions.cs +3 -3
diff --git a/Core/SolutionExtensions.cs b/Core/SolutionExtensions.cs
index 17a992d..9ea9d23 100644
@@ -71,14 +71,14 @@ public static class SolutionExtensions
CancellationToken cancellationToken
)
{
var analysis = new ReachabilityAnalysis(compilations);
var analyzer = new ReachabilityAnalyzer(compilations);
foreach (var entryPoint in entryPoints)
{
analysis.Analyze(entryPoint, cancellationToken);
analyzer.Analyze(entryPoint, cancellationToken);
}
return analysis;
return analyzer.GetAnalysis();
}
private static async Task<Compilation[]> GetCompilations(
Poc/Program.cs +7 -1
diff --git a/Poc/Program.cs b/Poc/Program.cs
index 699ecd8..6cdf0ac 100644
@@ -16,9 +16,15 @@ var analysis = args is []
? await solution.AnalyzeReachabilityFromEntryPoints(default)
: await solution.AnalyzeReachabilityFromDocumentationCommentIds(args, default);
Console.WriteLine("Reachable members:");
foreach (var member in analysis.ReachableMembers)
{
Console.WriteLine(member.GetDocumentationCommentId());
Console.WriteLine($"\t{member.GetDocumentationCommentId()}");
}
Console.WriteLine("Unreachable members:");
foreach (var member in analysis.UnreachableMembers)
{
Console.WriteLine($"\t{member.GetDocumentationCommentId()}");
}
return;