Commit:
519e498Parent:
b39c9bdFind the semantic delcaration from a file:line-number
cli/src/ScanCommand.cs
+31
-4
diff --git a/cli/src/ScanCommand.cs b/cli/src/ScanCommand.cs
index b558e8d..abf8ff0 100644
@@ -71,11 +71,38 @@ internal sealed class ScanCommand : Command
},
};
private static Option<(FileInfo, int)> StartFileOption { get; } =
new("--start-file", "-f")
{
Description = "The start file and line number for the analysis.",
Required = true,
CustomParser = result =>
{
if (result.Tokens is not [var fileAndLine])
{
result.AddError("Start file must be specified.");
return default;
}
if (fileAndLine.Value.Split(":", 2) is not [var file, var lineString])
{
result.AddError("Start file must include a line number separated by ':'.");
return default;
}
if (!int.TryParse(lineString, out var line) || line <= 0)
{
result.AddError("Invalid line number specified.");
return default;
}
return (new FileInfo(file), line);
},
};
public ScanCommand()
: base("scan", "Scan the code for analysis")
{
Action = new ScanAction();
Arguments.Add(PathArgument);
Options.Add(StartFileOption);
}
internal sealed class ScanAction : AsynchronousCommandLineAction
@@ -85,10 +112,10 @@ internal sealed class ScanCommand : Command
var solutionFile = parseResult.GetRequiredValue(PathArgument);
using var scanner = await Scanner.Create(solutionFile, cancellationToken);
await foreach (var method in scanner.FindMethods(cancellationToken))
{
Console.WriteLine($"Found method: {method.Name}");
}
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}");
return 0;
}
core/src/Scanner.cs
+46
-30
diff --git a/core/src/Scanner.cs b/core/src/Scanner.cs
index dbb9ac7..416eba8 100644
@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
@@ -29,37 +28,54 @@ public sealed class Scanner : IDisposable
this.solution = solution;
}
public async IAsyncEnumerable<IMethodSymbol> FindMethods(
[EnumeratorCancellation] CancellationToken cancellationToken = default
public async Task<ISymbol> BuildCallGraph(
FileInfo startFile,
int startLine,
CancellationToken cancellationToken = default
)
{
foreach (var project in solution.Projects)
{
var compilation = await project.GetCompilationAsync(cancellationToken);
if (compilation == null)
{
continue;
}
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 symbol =
semanticModel.GetDeclaredSymbol(startMethod, cancellationToken)
?? throw new InvalidOperationException($"Unable to get declared symbol for method: {startMethod}");
return symbol;
}
foreach (var syntaxTree in compilation.SyntaxTrees)
{
var semanticModel = compilation.GetSemanticModel(syntaxTree);
var methodDeclarations = syntaxTree
.GetRoot(cancellationToken)
.DescendantNodes()
.OfType<MethodDeclarationSyntax>();
foreach (var methodDeclaration in methodDeclarations)
{
if (
semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken: cancellationToken)
is IMethodSymbol methodSymbol
)
{
yield return methodSymbol;
}
}
}
}
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)
?? throw new InvalidOperationException($"Unable to get syntax tree for start file: {startFile.FullName}");
var root =
await syntaxTree.GetRootAsync(cancellationToken)
?? throw new InvalidOperationException($"Unable to get root for start file: {startFile.FullName}");
return root.DescendantNodesAndSelf()
.LastOrDefault(m =>
m
is MethodDeclarationSyntax
or ConstructorDeclarationSyntax
or PropertyDeclarationSyntax
or LambdaExpressionSyntax
&& m.GetLocation().GetLineSpan() is var lineSpan
&& lineSpan.StartLinePosition.Line + 1 <= startLine
&& startLine <= lineSpan.EndLinePosition.Line + 1
)
?? throw new InvalidOperationException(
$"Unable to find method at line {startLine} in start file: {startFile.FullName}"
);
}
public void Dispose()