cli/src/ScanCommand.cs
+40
-3
diff --git a/cli/src/ScanCommand.cs b/cli/src/ScanCommand.cs
index abf8ff0..4532a3e 100644
@@ -1,10 +1,12 @@
using System;
using System.Collections.Immutable;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
namespace Cfa.Cli;
@@ -113,11 +115,46 @@ internal sealed class ScanCommand : Command
using var scanner = await Scanner.Create(solutionFile, cancellationToken);
var (startFile, startLine) = parseResult.GetRequiredValue(StartFileOption);
var symbol = await scanner.BuildCallGraph(startFile, startLine, cancellationToken);
Console.WriteLine($"Found symbol: {symbol}");
Console.WriteLine($"Symbol type: {symbol.GetType().FullName}");
var callGraph = await scanner.BuildCallGraph(startFile, startLine, cancellationToken);
PrintCallGraph(callGraph.Origin, callGraph.Callees);
return 0;
}
private static void PrintCallGraph(
ISymbol origin,
IImmutableList<Callee> callees,
string indent = "",
string branch = ""
)
{
Console.Write(branch);
var syntaxTree = origin.DeclaringSyntaxReferences.FirstOrDefault()?.SyntaxTree;
if (syntaxTree is not null)
{
var path = syntaxTree.FilePath;
var start = syntaxTree.GetLineSpan(origin.DeclaringSyntaxReferences.First().Span).StartLinePosition;
var line = start.Line + 1;
var column = start.Character + 1;
Console.Write($"\e]8;;file://{path}#{line}:{column}\e\\");
}
Console.Write($"{origin}");
if (syntaxTree is not null)
{
Console.Write("\e]8;;\e\\");
}
Console.WriteLine();
for (var i = 0; i < callees.Count; i++)
{
PrintCallGraph(
callees[i].Target,
callees[i].Callees,
indent + (i < callees.Count - 1 ? "│ " : " "),
indent + (i < callees.Count - 1 ? "├─ " : "└─ ")
);
}
}
}
}
core/src/CallGraph.cs
+13
-0
diff --git a/core/src/CallGraph.cs b/core/src/CallGraph.cs
new file mode 100644
index 0000000..fc92386
@@ -0,0 +1,13 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
namespace Cfa;
public sealed record CallGraph(ISymbol Origin, IImmutableList<Callee> Callees);
public sealed record Callee(ISymbol Target, LinkKind LinkKind, IImmutableList<Callee> Callees);
public enum LinkKind
{
Direct,
}
core/src/CalleBuilder.cs
+41
-0
diff --git a/core/src/CalleBuilder.cs b/core/src/CalleBuilder.cs
new file mode 100644
index 0000000..3ec1640
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Cfa;
internal sealed class CalleeBuilder(IReadOnlyCollection<Compilation> compilations)
{
private readonly IReadOnlyCollection<Compilation> compilations = compilations;
private readonly HashSet<ISymbol> visitedSymbols = [];
public IImmutableList<Callee> FindCallees(ISymbol symbol, CancellationToken cancellationToken)
{
if (!visitedSymbols.Add(symbol))
{
return [];
}
var callees = ImmutableList.CreateBuilder<Callee>();
foreach (var reference in symbol.DeclaringSyntaxReferences)
{
var syntax = reference.GetSyntax(cancellationToken);
var semanticModelForSyntax = compilations.GetSemanticModel(syntax.SyntaxTree);
var invocations = syntax.DescendantNodesAndSelf().OfType<InvocationExpressionSyntax>();
foreach (var invocation in invocations)
{
var invokedSymbol = semanticModelForSyntax.GetSymbolInfo(invocation, cancellationToken).Symbol;
if (invokedSymbol is { DeclaringSyntaxReferences.Length: > 0 })
{
callees.Add(
new Callee(invokedSymbol, LinkKind.Direct, FindCallees(invokedSymbol, cancellationToken))
);
}
}
}
return callees.ToImmutable();
}
}
core/src/CompilationsExtensions.cs
+15
-0
diff --git a/core/src/CompilationsExtensions.cs b/core/src/CompilationsExtensions.cs
new file mode 100644
index 0000000..214aa38
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace Cfa;
internal static class CompilationsExtensions
{
public static SemanticModel GetSemanticModel(this IEnumerable<Compilation> compilations, SyntaxTree syntaxTree) =>
compilations.FirstOrDefault(c => c.ContainsSyntaxTree(syntaxTree))?.GetSemanticModel(syntaxTree)
?? throw new InvalidOperationException(
$"Unable to get semantic model for syntax tree at {syntaxTree.FilePath}"
);
}
core/src/Scanner.cs
+16
-18
diff --git a/core/src/Scanner.cs b/core/src/Scanner.cs
index 416eba8..9053e48 100644
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
@@ -16,48 +17,45 @@ public sealed class Scanner : IDisposable
{
var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(solutionFile.FullName, cancellationToken: cancellationToken);
return new Scanner(workspace, solution);
var compilations = await solution
.Projects.ToAsyncEnumerable()
.Select(async (p, ct) => await p.GetCompilationAsync(ct))
.OfType<Compilation>()
.ToListAsync(cancellationToken);
return new Scanner(workspace, solution, compilations);
}
private readonly Workspace workspace;
private readonly Solution solution;
private readonly IReadOnlyCollection<Compilation> compilations;
private Scanner(Workspace workspace, Solution solution)
private Scanner(Workspace workspace, Solution solution, IReadOnlyCollection<Compilation> compilations)
{
this.workspace = workspace;
this.solution = solution;
this.compilations = compilations;
}
public async Task<ISymbol> BuildCallGraph(
public async Task<CallGraph> BuildCallGraph(
FileInfo startFile,
int startLine,
CancellationToken cancellationToken = default
)
{
var startMethod = await FindMethodAt(startFile, startLine, cancellationToken);
var semanticModel =
await (
solution.GetDocument(startMethod.SyntaxTree)
?? throw new InvalidOperationException(
$"Unable to get document for syntax tree: {startMethod.SyntaxTree}"
)
).GetSemanticModelAsync(cancellationToken)
?? throw new InvalidOperationException(
$"Unable to get semantic model for syntax tree: {startMethod.SyntaxTree}"
);
var semanticModel = compilations.GetSemanticModel(startMethod.SyntaxTree);
var symbol =
semanticModel.GetDeclaredSymbol(startMethod, cancellationToken)
?? throw new InvalidOperationException($"Unable to get declared symbol for method: {startMethod}");
return symbol;
var calleeBuilder = new CalleeBuilder(compilations);
return new(symbol, calleeBuilder.FindCallees(symbol, cancellationToken));
}
private async Task<SyntaxNode> FindMethodAt(FileInfo startFile, int startLine, CancellationToken cancellationToken)
{
var document =
solution.Projects.SelectMany(p => p.Documents).FirstOrDefault(d => d.FilePath == startFile.FullName)
?? throw new FileNotFoundException($"Start file not found in solution: {startFile.FullName}");
var syntaxTree =
await document.GetSyntaxTreeAsync(cancellationToken)
compilations.SelectMany(c => c.SyntaxTrees).FirstOrDefault(st => st.FilePath == startFile.FullName)
?? throw new InvalidOperationException($"Unable to get syntax tree for start file: {startFile.FullName}");
var root =
await syntaxTree.GetRootAsync(cancellationToken)