Commit: 066e775
Parent: 0b5e5e6

Reachability from named member

Mårten Åsberg committed on 2026-03-10 at 21:38
Core/CompilationExtensions.cs +1 -1
diff --git a/Core/CompilationExtensions.cs b/Core/CompilationExtensions.cs
index 0e3b2b8..f2018af 100644
@@ -3,7 +3,7 @@ using Microsoft.CodeAnalysis;
namespace Reacher;
public static class CompilationExtensions
internal static class CompilationExtensions
{
public static IEnumerable<IMethodSymbol> GetPublicMembers(this Compilation compilation)
{
Core/Core.csproj +1 -0
diff --git a/Core/Core.csproj b/Core/Core.csproj
index 955cf40..53286dd 100644
@@ -2,6 +2,7 @@
<PropertyGroup>
<AssemblyName>Reacher</AssemblyName>
<RootNamespace>Reacher</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
Core/DocumentationCommentIdMemberCollector.cs +62 -0
diff --git a/Core/DocumentationCommentIdMemberCollector.cs b/Core/DocumentationCommentIdMemberCollector.cs
new file mode 100644
index 0000000..68b4ebf
@@ -0,0 +1,62 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
namespace Reacher;
internal class DocumentationCommentIdMemberCollector(HashSet<string> documentationIds) : SymbolVisitor
{
public static IEnumerable<IMethodSymbol> CollectMembers(
IEnumerable<string> documentationIds,
Compilation[] compilations
)
{
var collector = new DocumentationCommentIdMemberCollector([.. documentationIds]);
foreach (var compilation in compilations)
{
compilation.Assembly.Accept(collector);
}
return collector.Members;
}
public IEnumerable<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 (
member.GetDocumentationCommentId() is not string documentationId
|| !documentationIds.Contains(documentationId)
)
{
return;
}
members.Add(member);
}
}
Core/ReachabilityAnalysis.cs +7 -1
diff --git a/Core/ReachabilityAnalysis.cs b/Core/ReachabilityAnalysis.cs
index f79a18b..a09291c 100644
@@ -9,10 +9,16 @@ using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Reacher;
/// <summary>
/// The results of a reachability analysis.
/// </summary>
public sealed class ReachabilityAnalysis(IReadOnlyCollection<Compilation> compilations)
{
private readonly HashSet<IMethodSymbol> reachableMembers = [];
/// <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)
{
Core/SolutionExtensions.cs +31 -0
diff --git a/Core/SolutionExtensions.cs b/Core/SolutionExtensions.cs
index acaac14..17a992d 100644
@@ -6,8 +6,12 @@ using Microsoft.CodeAnalysis;
namespace Reacher;
/// <summary></summary>
public static class SolutionExtensions
{
/// <summary>
/// Runs a reachability analysis from all entrypoints of all projects in the solution.
/// </summary>
public static async Task<ReachabilityAnalysis> AnalyzeReachabilityFromEntryPoints(
this Solution solution,
CancellationToken cancellationToken
@@ -21,6 +25,9 @@ public static class SolutionExtensions
);
}
/// <summary>
/// Runs a reachability analysis from all public members of public types of all projects in the solution.
/// </summary>
public static async Task<ReachabilityAnalysis> AnalyzeReachabilityFromPublicMembers(
this Solution solution,
CancellationToken cancellationToken
@@ -34,6 +41,30 @@ public static class SolutionExtensions
);
}
/// <summary>
/// Runs a reachability analysis from the provided Documentation Comment IDs for members.
/// </summary>
/// <remarks>
/// Documentation Comment IDs is usually the kind of member (M for method, P for property), the full namespace, the
/// type (and any nested types), and the member name. If it is a method (with one or more parameters) or an indexer,
/// the types of the parameters are listed too, with full namespace and type name.
///
/// For example, this method would be called <c>M:Reacher.SolutionExtensions.AnalyzeReachabilityFromDocumentationCommentIds(Microsoft.CodeAnalysis.Solution,System.Collections.Generic.IEnumerable{System.String},System.Threading.CancellationToken)</c>.
/// </remarks>
public static async Task<ReachabilityAnalysis> AnalyzeReachabilityFromDocumentationCommentIds(
this Solution solution,
IEnumerable<string> documentationIds,
CancellationToken cancellationToken
)
{
var compilations = await solution.GetCompilations(cancellationToken);
return AnalyzeReachability(
compilations,
DocumentationCommentIdMemberCollector.CollectMembers(documentationIds, compilations),
cancellationToken
);
}
private static ReachabilityAnalysis AnalyzeReachability(
IReadOnlyCollection<Compilation> compilations,
IEnumerable<IMethodSymbol> entryPoints,
Poc/Program.cs +4 -2
diff --git a/Poc/Program.cs b/Poc/Program.cs
index 214988b..699ecd8 100644
@@ -12,11 +12,13 @@ var solution = await workspace.OpenSolutionAsync(GetSolutionPath());
Noop();
var analysis = await solution.AnalyzeReachabilityFromEntryPoints(default);
var analysis = args is []
? await solution.AnalyzeReachabilityFromEntryPoints(default)
: await solution.AnalyzeReachabilityFromDocumentationCommentIds(args, default);
foreach (var member in analysis.ReachableMembers)
{
Console.WriteLine(member);
Console.WriteLine(member.GetDocumentationCommentId());
}
return;