Commit:
56162aaParent:
f9ae0ceFollow calls though interfaces/abstract classes
Assisted-by: GitHub Copilot:gpt-6.1-sol
core/src/CallGraph.cs
+1
-0
diff --git a/core/src/CallGraph.cs b/core/src/CallGraph.cs
index e5ec0f9..c0d2401 100644
@@ -10,4 +10,5 @@ public sealed record Call(ISymbol Target, LinkKind LinkKind);
public enum LinkKind
{
Direct,
Abstract,
}
core/src/CalleBuilder.cs
+85
-14
diff --git a/core/src/CalleBuilder.cs b/core/src/CalleBuilder.cs
index 9c0381c..2c6f8b9 100644
@@ -15,19 +15,31 @@ internal sealed class CalleeBuilder
CancellationToken cancellationToken
)
{
var builder = new CalleeBuilder(compilations);
var builder = new CalleeBuilder(
compilations,
[
.. compilations
.SelectMany(compilation =>
compilation.GetSymbolsWithName(_ => true, SymbolFilter.Type, cancellationToken)
)
.OfType<INamedTypeSymbol>(),
]
);
builder.FindCallees(symbol, cancellationToken);
return builder.reachableFromSymbols.ToImmutable();
}
private readonly IReadOnlyCollection<Compilation> compilations;
private readonly IReadOnlyCollection<INamedTypeSymbol> types;
private readonly HashSet<ISymbol> seenMember = [with(SymbolEqualityComparer.Default)];
private readonly ImmutableDictionary<ISymbol, IImmutableList<Call>>.Builder reachableFromSymbols =
ImmutableDictionary.CreateBuilder<ISymbol, IImmutableList<Call>>(SymbolEqualityComparer.Default);
private CalleeBuilder(IReadOnlyCollection<Compilation> compilations)
private CalleeBuilder(IReadOnlyCollection<Compilation> compilations, IReadOnlyCollection<INamedTypeSymbol> types)
{
this.compilations = compilations;
this.types = types;
}
private void FindCallees(ISymbol symbol, CancellationToken cancellationToken)
@@ -37,8 +49,7 @@ internal sealed class CalleeBuilder
return;
}
var seenCall = new HashSet<ISymbol>(SymbolEqualityComparer.Default);
var callees = ImmutableList.CreateBuilder<Call>();
var seenCall = new Dictionary<ISymbol, LinkKind>(SymbolEqualityComparer.Default);
foreach (var reference in symbol.DeclaringSyntaxReferences)
{
var syntax = reference.GetSyntax(cancellationToken);
@@ -51,27 +62,87 @@ internal sealed class CalleeBuilder
foreach (var access in accesses)
{
var accessedSymbol = semanticModelForSyntax.GetSymbolInfo(access, cancellationToken).Symbol;
if (
accessedSymbol is not (IMethodSymbol or IPropertySymbol)
|| !seenCall.Add(accessedSymbol)
|| !IsDeclaredInSource(accessedSymbol)
)
if (accessedSymbol is not (IMethodSymbol or IPropertySymbol))
{
continue;
}
if (accessedSymbol is IPropertySymbol property && !HasBody(property, cancellationToken))
if (accessedSymbol.IsAbstract)
{
continue;
foreach (var implementation in FindImplementations(accessedSymbol))
{
AddCall(implementation, LinkKind.Abstract);
}
}
else
{
AddCall(accessedSymbol, LinkKind.Direct);
}
}
}
reachableFromSymbols.Add(symbol, seenCall.Select(call => new Call(call.Key, call.Value)).ToImmutableList());
void AddCall(ISymbol target, LinkKind linkKind)
{
if (
!IsDeclaredInSource(target)
|| linkKind == LinkKind.Abstract && target.IsAbstract
|| target is IPropertySymbol property && !HasBody(property, cancellationToken)
)
{
return;
}
if (seenCall.TryAdd(target, linkKind))
{
FindCallees(target, cancellationToken);
}
else if (linkKind is LinkKind.Direct)
{
seenCall[target] = LinkKind.Direct;
}
}
}
private IEnumerable<ISymbol> FindImplementations(ISymbol member)
{
foreach (var type in types)
{
if (member.ContainingType.TypeKind is TypeKind.Interface)
{
if (type.FindImplementationForInterfaceMember(member) is { } implementation)
{
yield return implementation;
}
continue;
}
callees.Add(new Call(accessedSymbol, LinkKind.Direct));
FindCallees(accessedSymbol, cancellationToken);
foreach (var candidate in type.GetMembers(member.Name))
{
for (
var overridden = GetOverriddenMember(candidate);
overridden is not null;
overridden = GetOverriddenMember(overridden)
)
{
if (SymbolEqualityComparer.Default.Equals(overridden.OriginalDefinition, member.OriginalDefinition))
{
yield return candidate;
break;
}
}
}
}
reachableFromSymbols.Add(symbol, callees.ToImmutable());
}
private static ISymbol? GetOverriddenMember(ISymbol member) =>
member switch
{
IMethodSymbol method => method.OverriddenMethod,
IPropertySymbol property => property.OverriddenProperty,
_ => null,
};
private static bool IsDeclaredInSource(ISymbol symbol) =>
symbol.DeclaringSyntaxReferences.Length > 0 && !symbol.IsImplicitlyDeclared;
core/tests/CalleeBuilderTests/FindCalleesShould.cs
+191
-0
diff --git a/core/tests/CalleeBuilderTests/FindCalleesShould.cs b/core/tests/CalleeBuilderTests/FindCalleesShould.cs
index db23c1b..f360857 100644
@@ -170,6 +170,196 @@ public sealed class FindCalleesShould
calls[start].ShouldBeEmpty();
}
[Theory]
[InlineData("void Run();", "public void Run() => Helper();", "service.Run();")]
[InlineData("void Run();", "void IService.Run() => Helper();", "service.Run();")]
[InlineData("int Value { get; }", "public int Value => Helper();", "_ = service.Value;")]
[InlineData("int Value { get; }", "int IService.Value => Helper();", "_ = service.Value;")]
[InlineData("int this[int index] { get; }", "public int this[int index] => Helper();", "_ = service[0];")]
public void FollowSourceImplementationsOfInterfaces(string contract, string implementation, string access)
{
// Arrange
var source = $$"""
interface IService { {{contract}} }
class Example : IService
{
{{implementation}}
int Helper() => 1;
void Start(IService service) { {{access}} }
}
""";
// Act
var (start, calls) = FindCallees(source);
// Assert
var call = calls[start].ShouldHaveSingleItem();
call.Target.ContainingType.Name.ShouldBe("Example");
calls[call.Target].ShouldHaveSingleItem().Target.Name.ShouldBe("Helper");
}
[Fact]
public void NotListTheAbstractMemberOnlyTheImplementation()
{
// Arrange
const string source = """
interface IService { void Run(); }
class Example : IService
{
public void Run() { }
void Start(IService service) { service.Run(); }
}
""";
// Act
var (start, calls) = FindCallees(source);
// Assert
var call = calls[start].ShouldHaveSingleItem();
call.Target.ContainingType.Name.ShouldBe("Example");
}
[Fact]
public void FollowImplementationsOfInterfacesFromMetadata()
{
// Arrange
const string source = """
class Example : System.IDisposable
{
public void Dispose() => Cleanup();
void Cleanup() { }
void Start(System.IDisposable service) { service.Dispose(); }
}
""";
// Act
var (start, calls) = FindCallees(source);
// Assert
var call = calls[start].ShouldHaveSingleItem();
call.Target.Name.ShouldBe("Dispose");
call.LinkKind.ShouldBe(LinkKind.Abstract);
calls[call.Target].ShouldHaveSingleItem().Target.Name.ShouldBe("Cleanup");
}
[Theory]
[InlineData("service.Run(); Run();")]
[InlineData("Run(); service.Run();")]
public void PreferDirectCallsOverInterfaceLinks(string body)
{
// Arrange
var source = $$"""
interface IService { void Run(); }
class Example : IService
{
public void Run() { }
void Start(IService service) { {{body}} }
}
""";
// Act
var (start, calls) = FindCallees(source);
// Assert
var call = calls[start].ShouldHaveSingleItem();
call.LinkKind.ShouldBe(LinkKind.Direct);
}
[Fact]
public void IgnoreInterfaceImplementationsWithoutPropertyBodies()
{
// Arrange
const string source = """
interface IService { int Value { get; } }
class Example : IService
{
public int Value { get; set; }
void Start(IService service) { _ = service.Value; }
}
""";
// Act
var (start, calls) = FindCallees(source);
// Assert
calls[start].ShouldBeEmpty();
}
[Theory]
[InlineData("public abstract void Run();", "public override void Run() => Helper();", "service.Run();")]
[InlineData("public abstract int Value { get; }", "public override int Value => Helper();", "_ = service.Value;")]
[InlineData(
"public abstract int this[int index] { get; }",
"public override int this[int index] => Helper();",
"_ = service[0];"
)]
public void FollowOverridesOfAbstractMembers(string contract, string implementation, string access)
{
// Arrange
var source = $$"""
abstract class Service { {{contract}} }
class Example : Service
{
{{implementation}}
int Helper() => 1;
void Start(Service service) { {{access}} }
}
""";
// Act
var (start, calls) = FindCallees(source);
// Assert
var call = calls[start].ShouldHaveSingleItem();
call.Target.ContainingType.Name.ShouldBe("Example");
calls[call.Target].ShouldHaveSingleItem().Target.Name.ShouldBe("Helper");
}
[Fact]
public void FollowOverridesThroughIntermediateAbstractClasses()
{
// Arrange
const string source = """
abstract class Service { public abstract void Run(); }
abstract class Middle : Service { public abstract override void Run(); }
class Example : Middle
{
public override void Run() { }
void Start(Service service) { service.Run(); }
}
""";
// Act
var (start, calls) = FindCallees(source);
// Assert
var call = calls[start].ShouldHaveSingleItem();
call.Target.ContainingType.Name.ShouldBe("Example");
}
[Theory]
[InlineData("service.Run(); Run();")]
[InlineData("Run(); service.Run();")]
public void PreferDirectCallsOverAbstractDispatch(string body)
{
// Arrange
var source = $$"""
abstract class Service { public abstract void Run(); }
class Example : Service
{
public override void Run() { }
void Start(Service service) { {{body}} }
}
""";
// Act
var (start, calls) = FindCallees(source);
// Assert
var call = calls[start].ShouldHaveSingleItem();
call.LinkKind.ShouldBe(LinkKind.Direct);
}
private static (ISymbol, IImmutableDictionary<ISymbol, IImmutableList<Call>>) FindCallees(string source)
{
var compilation = CSharpCompilation.Create(
@@ -178,6 +368,7 @@ public sealed class FindCalleesShould
[MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ShouldBeEmpty();
var start = compilation.GetTypeByMetadataName("Example")!.GetMembers("Start").Single();
return (start, CalleeBuilder.FindCallees([compilation], start, CancellationToken.None));
}