Add call graph fixtures for various languages and scenarios
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

- Introduced `all-edge-reasons.json` to test edge resolution reasons in .NET.
- Added `all-visibility-levels.json` to validate method visibility levels in .NET.
- Created `dotnet-aspnetcore-minimal.json` for a minimal ASP.NET Core application.
- Included `go-gin-api.json` for a Go Gin API application structure.
- Added `java-spring-boot.json` for the Spring PetClinic application in Java.
- Introduced `legacy-no-schema.json` for legacy application structure without schema.
- Created `node-express-api.json` for an Express.js API application structure.
This commit is contained in:
master
2025-12-16 10:44:24 +02:00
parent 4391f35d8a
commit 5a480a3c2a
223 changed files with 19367 additions and 727 deletions

View File

@@ -173,6 +173,14 @@ internal sealed class DotNetCallgraphBuilder
var isVirtual = (methodDef.Attributes & MethodAttributes.Virtual) != 0;
var isGeneric = methodDef.GetGenericParameters().Count > 0;
// Extract visibility from MethodAttributes
var visibility = ExtractVisibility(methodDef.Attributes);
// Determine if this method is an entrypoint candidate
var isTypePublic = (typeDef.Attributes & TypeAttributes.Public) != 0 ||
(typeDef.Attributes & TypeAttributes.NestedPublic) != 0;
var isEntrypointCandidate = isPublic && isTypePublic && !methodName.StartsWith("<");
var node = new DotNetMethodNode(
MethodId: methodId,
AssemblyName: assemblyName,
@@ -186,7 +194,9 @@ internal sealed class DotNetCallgraphBuilder
IsStatic: isStatic,
IsPublic: isPublic,
IsVirtual: isVirtual,
IsGeneric: isGeneric);
IsGeneric: isGeneric,
Visibility: visibility,
IsEntrypointCandidate: isEntrypointCandidate);
_methods.TryAdd(methodId, node);
@@ -254,6 +264,7 @@ internal sealed class DotNetCallgraphBuilder
!methodName.StartsWith("get_") && !methodName.StartsWith("set_") &&
methodName != ".ctor")
{
var (routeTemplate, httpMethod) = ExtractRouteInfo(metadata, methodDef.GetCustomAttributes());
var rootId = DotNetGraphIdentifiers.ComputeRootId(DotNetRootPhase.Runtime, rootOrder++, methodId);
_roots.Add(new DotNetSyntheticRoot(
RootId: rootId,
@@ -262,14 +273,29 @@ internal sealed class DotNetCallgraphBuilder
Source: "ControllerAction",
AssemblyPath: assemblyPath,
Phase: DotNetRootPhase.Runtime,
Order: rootOrder - 1));
Order: rootOrder - 1,
RouteTemplate: routeTemplate,
HttpMethod: httpMethod,
Framework: DotNetEntrypointFramework.AspNetCore));
}
// Test methods (xUnit, NUnit, MSTest)
var testFramework = DotNetEntrypointFramework.Unknown;
if (HasAttribute(metadata, methodDef.GetCustomAttributes(), "Xunit.FactAttribute") ||
HasAttribute(metadata, methodDef.GetCustomAttributes(), "Xunit.TheoryAttribute") ||
HasAttribute(metadata, methodDef.GetCustomAttributes(), "NUnit.Framework.TestAttribute") ||
HasAttribute(metadata, methodDef.GetCustomAttributes(), "Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute"))
HasAttribute(metadata, methodDef.GetCustomAttributes(), "Xunit.TheoryAttribute"))
{
testFramework = DotNetEntrypointFramework.XUnit;
}
else if (HasAttribute(metadata, methodDef.GetCustomAttributes(), "NUnit.Framework.TestAttribute"))
{
testFramework = DotNetEntrypointFramework.NUnit;
}
else if (HasAttribute(metadata, methodDef.GetCustomAttributes(), "Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute"))
{
testFramework = DotNetEntrypointFramework.MSTest;
}
if (testFramework != DotNetEntrypointFramework.Unknown)
{
var rootId = DotNetGraphIdentifiers.ComputeRootId(DotNetRootPhase.Runtime, rootOrder++, methodId);
_roots.Add(new DotNetSyntheticRoot(
@@ -279,7 +305,8 @@ internal sealed class DotNetCallgraphBuilder
Source: "TestMethod",
AssemblyPath: assemblyPath,
Phase: DotNetRootPhase.Runtime,
Order: rootOrder - 1));
Order: rootOrder - 1,
Framework: testFramework));
}
// Azure Functions
@@ -294,7 +321,8 @@ internal sealed class DotNetCallgraphBuilder
Source: "AzureFunction",
AssemblyPath: assemblyPath,
Phase: DotNetRootPhase.Runtime,
Order: rootOrder - 1));
Order: rootOrder - 1,
Framework: DotNetEntrypointFramework.AzureFunctions));
}
// AWS Lambda
@@ -308,10 +336,120 @@ internal sealed class DotNetCallgraphBuilder
Source: "LambdaHandler",
AssemblyPath: assemblyPath,
Phase: DotNetRootPhase.Runtime,
Order: rootOrder - 1));
Order: rootOrder - 1,
Framework: DotNetEntrypointFramework.AwsLambda));
}
}
private static (string? RouteTemplate, string? HttpMethod) ExtractRouteInfo(
MetadataReader metadata,
CustomAttributeHandleCollection attributes)
{
string? routeTemplate = null;
string? httpMethod = null;
foreach (var attrHandle in attributes)
{
var attr = metadata.GetCustomAttribute(attrHandle);
var ctorHandle = attr.Constructor;
string? typeName = null;
switch (ctorHandle.Kind)
{
case HandleKind.MemberReference:
var memberRef = metadata.GetMemberReference((MemberReferenceHandle)ctorHandle);
if (memberRef.Parent.Kind == HandleKind.TypeReference)
{
var typeRef = metadata.GetTypeReference((TypeReferenceHandle)memberRef.Parent);
typeName = GetTypeRefName(metadata, typeRef);
}
break;
case HandleKind.MethodDefinition:
var methodDef = metadata.GetMethodDefinition((MethodDefinitionHandle)ctorHandle);
var declaringType = metadata.GetTypeDefinition(methodDef.GetDeclaringType());
typeName = GetFullTypeName(metadata, declaringType);
break;
}
if (typeName is null)
{
continue;
}
// Extract route template from [Route] attribute
if (typeName.Contains("RouteAttribute"))
{
routeTemplate ??= TryExtractStringArgument(metadata, attr);
}
// Extract HTTP method and optional route from Http*Attribute
if (typeName.Contains("HttpGetAttribute"))
{
httpMethod = "GET";
routeTemplate ??= TryExtractStringArgument(metadata, attr);
}
else if (typeName.Contains("HttpPostAttribute"))
{
httpMethod = "POST";
routeTemplate ??= TryExtractStringArgument(metadata, attr);
}
else if (typeName.Contains("HttpPutAttribute"))
{
httpMethod = "PUT";
routeTemplate ??= TryExtractStringArgument(metadata, attr);
}
else if (typeName.Contains("HttpDeleteAttribute"))
{
httpMethod = "DELETE";
routeTemplate ??= TryExtractStringArgument(metadata, attr);
}
else if (typeName.Contains("HttpPatchAttribute"))
{
httpMethod = "PATCH";
routeTemplate ??= TryExtractStringArgument(metadata, attr);
}
}
return (routeTemplate, httpMethod);
}
private static string? TryExtractStringArgument(MetadataReader metadata, CustomAttribute attr)
{
// Simplified extraction - read first string argument from attribute blob
// Full implementation would properly parse the custom attribute blob
try
{
var value = attr.DecodeValue(new SimpleAttributeProvider());
if (value.FixedArguments.Length > 0 &&
value.FixedArguments[0].Value is string strValue &&
!string.IsNullOrEmpty(strValue))
{
return strValue;
}
}
catch
{
// Attribute decoding failed - not critical
}
return null;
}
/// <summary>
/// Simple attribute type provider for decoding custom attributes.
/// </summary>
private sealed class SimpleAttributeProvider : ICustomAttributeTypeProvider<object?>
{
public object? GetPrimitiveType(PrimitiveTypeCode typeCode) => null;
public object? GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => null;
public object? GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => null;
public object? GetSZArrayType(object? elementType) => null;
public object? GetSystemType() => typeof(Type);
public object? GetTypeFromSerializedName(string name) => Type.GetType(name);
public PrimitiveTypeCode GetUnderlyingEnumType(object? type) => PrimitiveTypeCode.Int32;
public bool IsSystemType(object? type) => type is Type;
}
private void ExtractCallEdgesFromType(
MetadataReader metadata,
TypeDefinition typeDef,
@@ -390,15 +528,15 @@ internal sealed class DotNetCallgraphBuilder
var token = BitConverter.ToInt32(ilBytes, offset);
offset += 4;
var edgeType = opcode switch
var (edgeType, edgeReason) = opcode switch
{
0x28 => DotNetEdgeType.Call,
0x6F => DotNetEdgeType.CallVirt,
0x73 => DotNetEdgeType.NewObj,
_ => DotNetEdgeType.Call,
0x28 => (DotNetEdgeType.Call, DotNetEdgeReason.DirectCall),
0x6F => (DotNetEdgeType.CallVirt, DotNetEdgeReason.VirtualCall),
0x73 => (DotNetEdgeType.NewObj, DotNetEdgeReason.NewObj),
_ => (DotNetEdgeType.Call, DotNetEdgeReason.DirectCall),
};
AddCallEdge(metadata, callerId, token, ilOffset, edgeType, assemblyName, assemblyPath);
AddCallEdge(metadata, callerId, token, ilOffset, edgeType, edgeReason, assemblyName, assemblyPath);
break;
}
case 0xFE06: // ldftn (0xFE 0x06)
@@ -413,7 +551,7 @@ internal sealed class DotNetCallgraphBuilder
offset += 4;
var edgeType = opcode == 0xFE06 ? DotNetEdgeType.LdFtn : DotNetEdgeType.LdVirtFtn;
AddCallEdge(metadata, callerId, token, ilOffset, edgeType, assemblyName, assemblyPath);
AddCallEdge(metadata, callerId, token, ilOffset, edgeType, DotNetEdgeReason.DelegateCreate, assemblyName, assemblyPath);
break;
}
case 0x29: // calli
@@ -436,6 +574,7 @@ internal sealed class DotNetCallgraphBuilder
CalleePurl: null,
CalleeMethodDigest: null,
EdgeType: DotNetEdgeType.CallI,
EdgeReason: DotNetEdgeReason.IndirectCall,
ILOffset: ilOffset,
IsResolved: false,
Confidence: 0.2));
@@ -470,6 +609,7 @@ internal sealed class DotNetCallgraphBuilder
int token,
int ilOffset,
DotNetEdgeType edgeType,
DotNetEdgeReason edgeReason,
string assemblyName,
string assemblyPath)
{
@@ -517,8 +657,8 @@ internal sealed class DotNetCallgraphBuilder
case HandleKind.MethodSpecification:
{
var methodSpec = metadata.GetMethodSpecification((MethodSpecificationHandle)handle);
// Recursively resolve the generic method
AddCallEdge(metadata, callerId, MetadataTokens.GetToken(methodSpec.Method), ilOffset, edgeType, assemblyName, assemblyPath);
// Recursively resolve the generic method - use GenericInstantiation reason
AddCallEdge(metadata, callerId, MetadataTokens.GetToken(methodSpec.Method), ilOffset, edgeType, DotNetEdgeReason.GenericInstantiation, assemblyName, assemblyPath);
return;
}
default:
@@ -549,6 +689,7 @@ internal sealed class DotNetCallgraphBuilder
CalleePurl: calleePurl,
CalleeMethodDigest: null,
EdgeType: edgeType,
EdgeReason: edgeReason,
ILOffset: ilOffset,
IsResolved: isResolved,
Confidence: isResolved ? 1.0 : 0.7));
@@ -788,4 +929,19 @@ internal sealed class DotNetCallgraphBuilder
_ => 1, // default for unrecognized
};
}
private static DotNetVisibility ExtractVisibility(MethodAttributes attributes)
{
var accessMask = attributes & MethodAttributes.MemberAccessMask;
return accessMask switch
{
MethodAttributes.Public => DotNetVisibility.Public,
MethodAttributes.Private => DotNetVisibility.Private,
MethodAttributes.Family => DotNetVisibility.Protected,
MethodAttributes.Assembly => DotNetVisibility.Internal,
MethodAttributes.FamORAssem => DotNetVisibility.ProtectedInternal,
MethodAttributes.FamANDAssem => DotNetVisibility.PrivateProtected,
_ => DotNetVisibility.Private
};
}
}

View File

@@ -32,6 +32,8 @@ public sealed record DotNetReachabilityGraph(
/// <param name="IsPublic">Whether the method is public.</param>
/// <param name="IsVirtual">Whether the method is virtual.</param>
/// <param name="IsGeneric">Whether the method has generic parameters.</param>
/// <param name="Visibility">Access visibility (public, private, protected, internal, etc.).</param>
/// <param name="IsEntrypointCandidate">Whether this method could be an entrypoint (public, controller action, etc.).</param>
public sealed record DotNetMethodNode(
string MethodId,
string AssemblyName,
@@ -45,7 +47,33 @@ public sealed record DotNetMethodNode(
bool IsStatic,
bool IsPublic,
bool IsVirtual,
bool IsGeneric);
bool IsGeneric,
DotNetVisibility Visibility,
bool IsEntrypointCandidate);
/// <summary>
/// Access visibility levels for .NET methods.
/// </summary>
public enum DotNetVisibility
{
/// <summary>Accessible from anywhere.</summary>
Public,
/// <summary>Accessible only within the same type.</summary>
Private,
/// <summary>Accessible within the same type or derived types.</summary>
Protected,
/// <summary>Accessible within the same assembly.</summary>
Internal,
/// <summary>Accessible within the same assembly or derived types.</summary>
ProtectedInternal,
/// <summary>Accessible only within derived types in the same assembly.</summary>
PrivateProtected
}
/// <summary>
/// A call edge in the .NET call graph.
@@ -56,6 +84,7 @@ public sealed record DotNetMethodNode(
/// <param name="CalleePurl">PURL of the callee if resolvable.</param>
/// <param name="CalleeMethodDigest">Method digest of the callee.</param>
/// <param name="EdgeType">Type of edge (call instruction type).</param>
/// <param name="EdgeReason">Semantic reason for the edge (DirectCall, VirtualCall, etc.).</param>
/// <param name="ILOffset">IL offset where call occurs.</param>
/// <param name="IsResolved">Whether the callee was successfully resolved.</param>
/// <param name="Confidence">Confidence level (1.0 for resolved, lower for heuristic).</param>
@@ -66,6 +95,7 @@ public sealed record DotNetCallEdge(
string? CalleePurl,
string? CalleeMethodDigest,
DotNetEdgeType EdgeType,
DotNetEdgeReason EdgeReason,
int ILOffset,
bool IsResolved,
double Confidence);
@@ -103,6 +133,52 @@ public enum DotNetEdgeType
Dynamic,
}
/// <summary>
/// Semantic reason for why a .NET edge exists.
/// Maps to the schema's EdgeReason enum for explainability.
/// </summary>
public enum DotNetEdgeReason
{
/// <summary>Direct method call (call opcode).</summary>
DirectCall,
/// <summary>Virtual/interface dispatch (callvirt opcode).</summary>
VirtualCall,
/// <summary>Reflection-based invocation (Type.GetMethod, etc.).</summary>
ReflectionString,
/// <summary>Dependency injection binding.</summary>
DiBinding,
/// <summary>Dynamic import or late binding.</summary>
DynamicImport,
/// <summary>Constructor/object instantiation (newobj opcode).</summary>
NewObj,
/// <summary>Delegate/function pointer creation (ldftn, ldvirtftn).</summary>
DelegateCreate,
/// <summary>Async/await continuation.</summary>
AsyncContinuation,
/// <summary>Event handler subscription.</summary>
EventHandler,
/// <summary>Generic type instantiation.</summary>
GenericInstantiation,
/// <summary>Native interop (P/Invoke).</summary>
NativeInterop,
/// <summary>Indirect call through function pointer (calli).</summary>
IndirectCall,
/// <summary>Reason could not be determined.</summary>
Unknown
}
/// <summary>
/// A synthetic root in the .NET call graph.
/// </summary>
@@ -114,6 +190,9 @@ public enum DotNetEdgeType
/// <param name="Phase">Execution phase.</param>
/// <param name="Order">Order within the phase.</param>
/// <param name="IsResolved">Whether the target was successfully resolved.</param>
/// <param name="RouteTemplate">HTTP route template if applicable (e.g., "/api/orders/{id}").</param>
/// <param name="HttpMethod">HTTP method if applicable (GET, POST, etc.).</param>
/// <param name="Framework">Framework exposing this entrypoint.</param>
public sealed record DotNetSyntheticRoot(
string RootId,
string TargetId,
@@ -122,7 +201,43 @@ public sealed record DotNetSyntheticRoot(
string AssemblyPath,
DotNetRootPhase Phase,
int Order,
bool IsResolved = true);
bool IsResolved = true,
string? RouteTemplate = null,
string? HttpMethod = null,
DotNetEntrypointFramework Framework = DotNetEntrypointFramework.Unknown);
/// <summary>
/// Frameworks that expose .NET entrypoints.
/// </summary>
public enum DotNetEntrypointFramework
{
/// <summary>Unknown framework.</summary>
Unknown,
/// <summary>ASP.NET Core MVC/WebAPI.</summary>
AspNetCore,
/// <summary>ASP.NET Core Minimal APIs.</summary>
MinimalApi,
/// <summary>gRPC for .NET.</summary>
Grpc,
/// <summary>Azure Functions.</summary>
AzureFunctions,
/// <summary>AWS Lambda.</summary>
AwsLambda,
/// <summary>xUnit test framework.</summary>
XUnit,
/// <summary>NUnit test framework.</summary>
NUnit,
/// <summary>MSTest framework.</summary>
MSTest
}
/// <summary>
/// Execution phase for .NET synthetic roots.