Directory.Packages.props
+9
-3
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f08c36a..8f399ad 100644
@@ -11,8 +11,14 @@
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Glob" Version="1.1.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="11.0.0-rc.3.26522.148" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="11.0.0-rc.3.26522.148" />
<PackageVersion
Include="Microsoft.Extensions.Configuration.Json"
Version="11.0.0-rc.3.26522.148"
/>
<PackageVersion
Include="Microsoft.Extensions.Options.ConfigurationExtensions"
Version="11.0.0-rc.3.26522.148"
/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.9.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.14" />
@@ -21,4 +27,4 @@
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
</Project>
\ No newline at end of file
</Project>
cli/src/ScanCommand.cs
+19
-9
diff --git a/cli/src/ScanCommand.cs b/cli/src/ScanCommand.cs
index f5073ad..e1aa406 100644
@@ -164,17 +164,24 @@ internal sealed class ScanCommand : Command
var (startFile, startLine) = parseResult.GetRequiredValue(StartFileOption);
var callGraph = await scanner.BuildCallGraph(startFile, startLine, cancellationToken);
var callGraphAnalysis = Analyzer.Analyze(callGraph);
PrintCallGraph(callGraph.Origin, callGraph.CalledFrom, reverse: true);
PrintMember(callGraph.Origin);
PrintCallGraph(callGraph.Origin, callGraph.CallsTo);
PrintCallGraph(
callGraphAnalysis.Origin,
callGraphAnalysis.CalledFrom,
callGraphAnalysis.Appearances,
reverse: true
);
PrintMember(callGraphAnalysis.Origin, callGraphAnalysis.Appearances[callGraphAnalysis.Origin]);
PrintCallGraph(callGraphAnalysis.Origin, callGraphAnalysis.CallsTo, callGraphAnalysis.Appearances);
return 0;
}
private static void PrintCallGraph(
ISymbol origin,
IImmutableDictionary<ISymbol, IImmutableList<Call>> links,
IImmutableDictionary<ISymbol, IImmutableList<CallAnalysis>> links,
IImmutableDictionary<ISymbol, uint> appearances,
bool reverse = false,
string indent = ""
)
@@ -189,24 +196,26 @@ internal sealed class ScanCommand : Command
var target = direct[i].Target;
var isEnd = reverse ? i == 0 : i == direct.Count - 1;
var branch = indent + (isEnd ? (reverse ? "┌─ " : "└─ ") : "├─ ");
var reach = $"({direct[i].Reach}) ";
if (!reverse)
{
PrintMember(target, branch);
PrintMember(target, appearances[target], branch, reach);
}
PrintCallGraph(target, links.Remove(origin), reverse, indent + (isEnd ? " " : "│ "));
PrintCallGraph(target, links.Remove(origin), appearances, reverse, indent + (isEnd ? " " : "│ "));
if (reverse)
{
PrintMember(target, branch);
PrintMember(target, appearances[target], branch, reach);
}
}
}
private static void PrintMember(ISymbol origin, string branch = "")
private static void PrintMember(ISymbol origin, uint appearances, string branch = "", string reach = "")
{
Console.Write(branch);
Console.Write(reach);
var syntaxTree = origin.DeclaringSyntaxReferences.FirstOrDefault()?.SyntaxTree;
if (syntaxTree is not null)
{
@@ -216,11 +225,12 @@ internal sealed class ScanCommand : Command
var column = start.Character + 1;
Console.Write($"\e]8;;file://{path}#{line}:{column}\e\\");
}
Console.Write($"{origin}");
Console.Write(origin);
if (syntaxTree is not null)
{
Console.Write("\e]8;;\e\\");
}
Console.Write($" ({appearances})");
Console.WriteLine();
}
}
core/src/Analyzer.cs
+90
-0
diff --git a/core/src/Analyzer.cs b/core/src/Analyzer.cs
new file mode 100644
index 0000000..237eb0b
@@ -0,0 +1,90 @@
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);
AddVisits(callGraph.CallsTo);
AddVisits(callGraph.CalledFrom);
builder.Add(callGraph.Origin, 0);
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,
}
core/src/CallGraph.cs
+1
-1
diff --git a/core/src/CallGraph.cs b/core/src/CallGraph.cs
index d157ba0..ebc4ff2 100644
@@ -9,7 +9,7 @@ public sealed record CallGraph(
IImmutableDictionary<ISymbol, IImmutableList<Call>> CalledFrom
);
public sealed record Call(ISymbol Target, LinkKind LinkKind);
public record Call(ISymbol Target, LinkKind LinkKind);
public enum LinkKind
{
core/tests/CallGraphBuilderTests/SourceToCallGraphHelper.cs
+0
-1
diff --git a/core/tests/CallGraphBuilderTests/SourceToCallGraphHelper.cs b/core/tests/CallGraphBuilderTests/SourceToCallGraphHelper.cs
index d669185..93f1b7b 100644
@@ -1,5 +1,4 @@
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;