Commit: 22eb09c
Parent: 08f5c7d

Only include user authored code

Mårten Åsberg committed on 2026-10-28 at 20:06
Assisted-by: GitHub Copilot:gpt-6.1-sol
core/src/AuthoredSource.cs +45 -0
diff --git a/core/src/AuthoredSource.cs b/core/src/AuthoredSource.cs
new file mode 100644
index 0000000..ed2dcd7
@@ -0,0 +1,45 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace Cfa;
internal static class AuthoredSource
{
public static bool Includes(SyntaxTree tree, Document document)
{
var path = document.FilePath ?? tree.FilePath;
if (
document.Project.FilePath is string projectPath
&& Path.IsPathRooted(path)
&& Path.GetDirectoryName(projectPath) is string projectDirectory
)
{
path = Path.GetRelativePath(projectDirectory, path);
}
if (
path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.Any(segment =>
segment.Equals("obj", StringComparison.OrdinalIgnoreCase)
|| segment.Equals("bin", StringComparison.OrdinalIgnoreCase)
)
)
{
return false;
}
var name = Path.GetFileName(path);
if (
name.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase)
|| name.EndsWith(".g.i.cs", StringComparison.OrdinalIgnoreCase)
|| name.EndsWith(".generated.cs", StringComparison.OrdinalIgnoreCase)
|| name.EndsWith(".designer.cs", StringComparison.OrdinalIgnoreCase)
)
{
return false;
}
return true;
}
}
core/src/CallGraphBuilder.cs +20 -6
diff --git a/core/src/CallGraphBuilder.cs b/core/src/CallGraphBuilder.cs
index a40f8b3..01722f2 100644
@@ -35,6 +35,7 @@ internal sealed class CallGraphBuilder
private readonly Solution solution;
private readonly IReadOnlyCollection<Compilation> compilations;
private readonly HashSet<ProjectId> includedProjects;
private readonly HashSet<SyntaxTree> authoredTrees;
private readonly HashSet<SyntaxTree> includedTrees;
private readonly IReadOnlyCollection<INamedTypeSymbol> types;
private readonly MessageLinks messageLinks;
@@ -56,19 +57,24 @@ internal sealed class CallGraphBuilder
.Projects.Where(project => options.IncludesProject(project.FilePath ?? project.Name))
.Select(project => project.Id),
];
includedTrees =
authoredTrees =
[
.. compilations
.SelectMany(compilation => compilation.SyntaxTrees)
.Where(tree =>
solution.GetDocument(tree) is { Project.Id: var projectId } && includedProjects.Contains(projectId)
),
.Where(tree => solution.GetDocument(tree) is { } document && AuthoredSource.Includes(tree, document)),
];
includedTrees =
[
.. authoredTrees.Where(tree => includedProjects.Contains(solution.GetDocument(tree)!.Project.Id)),
];
types =
[
.. compilations
.SelectMany(compilation => compilation.GetSymbolsWithName(_ => true, SymbolFilter.Type))
.OfType<INamedTypeSymbol>(),
.OfType<INamedTypeSymbol>()
.Where(type =>
type.DeclaringSyntaxReferences.Any(reference => includedTrees.Contains(reference.SyntaxTree))
),
];
messageLinks = new MessageLinks(options.Messages, compilations, includedTrees, solution);
}
@@ -85,7 +91,11 @@ internal sealed class CallGraphBuilder
{
AddCall(handler, LinkKind.Event);
}
foreach (var reference in symbol.DeclaringSyntaxReferences)
foreach (
var reference in symbol.DeclaringSyntaxReferences.Where(reference =>
authoredTrees.Contains(reference.SyntaxTree)
)
)
{
var syntax = reference.GetSyntax(cancellationToken);
var semanticModelForSyntax = compilations.GetSemanticModel(syntax.SyntaxTree);
@@ -240,6 +250,10 @@ internal sealed class CallGraphBuilder
foreach (var reference in referencedSymbol.Locations)
{
var root = await reference.Document.GetSyntaxRootAsync(cancellationToken);
if (root is null || !includedTrees.Contains(root.SyntaxTree))
{
continue;
}
if (
root is not null
&& messageLinks.IsRegistrationArgument(root.FindNode(reference.Location.SourceSpan))
core/src/MessageLinks.cs +5 -1
diff --git a/core/src/MessageLinks.cs b/core/src/MessageLinks.cs
index 76375cc..0611ad4 100644
@@ -79,7 +79,11 @@ internal sealed class MessageLinks
{
return result;
}
foreach (var reference in member.DeclaringSyntaxReferences)
foreach (
var reference in member.DeclaringSyntaxReferences.Where(reference =>
includedTrees.Contains(reference.SyntaxTree)
)
)
{
var syntax = await reference.GetSyntaxAsync(token);
var model = compilations.GetSemanticModel(syntax.SyntaxTree);
core/tests/CallGraphBuilderTests/BuildCallGraphShould.cs +143 -0
diff --git a/core/tests/CallGraphBuilderTests/BuildCallGraphShould.cs b/core/tests/CallGraphBuilderTests/BuildCallGraphShould.cs
index 73ea491..cab71e9 100644
@@ -12,6 +12,149 @@ namespace Cfa.Tests.CallGraphBuilderTests;
public sealed class BuildCallGraphShould
{
[Fact]
public async Task ExcludeGeneratedTreesThatHaveNoRegularWorkspaceDocument()
{
// Arrange
using var workspace = new AdhocWorkspace();
var project = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Default,
"App",
"App",
LanguageNames.CSharp,
compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
metadataReferences: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]
)
);
workspace.AddDocument(
project.Id,
"Example.cs",
SourceText.From(
"""
class Example { void Start() { Generated.Target(); } }
"""
)
);
var solution = workspace.CurrentSolution;
var compilation = (await solution.GetProject(project.Id)!.GetCompilationAsync())
.ShouldNotBeNull()
.AddSyntaxTrees(
CSharpSyntaxTree.ParseText(
"class Generated { public static void Target() { } }",
path: "Generator/Service.cs"
)
);
compilation.GetDiagnostics().ShouldBeEmpty();
var start = compilation.GetTypeByMetadataName("Example")!.GetMembers("Start").Single();
var builder = new CallGraphBuilder(new ScannerOptions(), solution, [compilation]);
// Act
await builder.FindCallees(start, CancellationToken.None);
// Assert
builder.ReachableFromSymbols[start].ShouldBeEmpty();
}
[Theory]
[InlineData("obj/Debug/Service.cs", "", false)]
[InlineData("bin/Debug/Service.cs", "", false)]
[InlineData("Generated/Service.g.cs", "", false)]
[InlineData("Generated/Service.g.i.cs", "", false)]
[InlineData("Generated/Service.generated.cs", "", false)]
[InlineData("Generated/Service.Designer.cs", "", false)]
[InlineData("Service.cs", "// <auto-generated>\n// Generated by the protocol buffer compiler.\n", true)]
[InlineData("Services/ObjectService.cs", "", true)]
public async Task UseOnlyAuthoredFilesForCallsImplementationsAndMessages(string path, string header, bool authored)
{
// Arrange
using var workspace = new AdhocWorkspace();
var project = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Default,
"App",
"App",
LanguageNames.CSharp,
filePath: "/repo/App.csproj",
compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
metadataReferences: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]
)
);
workspace.AddDocument(
project.Id,
"Example.cs",
SourceText.From(
MessageSource.Contracts
+ """
interface Contract { void Run(); }
partial class Example
{
public void Start(Contract contract, Sample.GrainInterfaces.Events.IEventSender sender)
{
Generated.Target();
contract.Run();
sender.Publish(new Updated());
}
}
"""
)
);
var document = DocumentInfo.Create(
DocumentId.CreateNewId(project.Id),
"Service.cs",
filePath: "/repo/" + path,
loader: TextLoader.From(
TextAndVersion.Create(
SourceText.From(
header
+ """
class Generated
{
public static void Target() { }
void Caller(Example example) { example.Start(null, null); }
}
class Implementation : Contract { public void Run() { } }
class Receiver : Sample.Grains.EventHandlers.EventReceiver<Sample.Domain.Message<int>>
{
public Receiver() { On<Updated>(Handle); }
void Handle(Updated message) { }
}
partial class Example { void PartialCaller() { Start(null, null); } }
"""
),
VersionStamp.Default
)
)
);
workspace.AddDocument(document);
var solution = workspace.CurrentSolution;
var compilation = (await solution.GetProject(project.Id)!.GetCompilationAsync()).ShouldNotBeNull();
compilation.GetDiagnostics().ShouldBeEmpty();
var start = compilation.GetTypeByMetadataName("Example")!.GetMembers("Start").Single();
var builder = new CallGraphBuilder(
new ScannerOptions { Messages = MessageSource.Options },
solution,
[compilation]
);
// Act
await builder.FindCallees(start, CancellationToken.None);
await builder.FindCallers(start, CancellationToken.None);
// Assert
builder.ReachableFromSymbols[start].Count.ShouldBe(authored ? 3 : 0);
builder.ReachesToSymbols[start].Count.ShouldBe(authored ? 2 : 0);
if (authored)
{
builder
.ReachableFromSymbols[start]
.Select(call => call.Target.Name)
.ShouldBe(["Target", "Run", "Handle"], ignoreOrder: true);
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]