Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Dependencies/StaticReachabilityAnalyzer.cs
2026-01-22 19:08:46 +02:00

214 lines
6.7 KiB
C#

using System.Collections.Immutable;
using StellaOps.Concelier.SbomIntegration.Models;
namespace StellaOps.Scanner.Reachability.Dependencies;
/// <summary>
/// Static graph traversal reachability for dependency graphs.
/// </summary>
public sealed class StaticReachabilityAnalyzer
{
public ReachabilityReport Analyze(
DependencyGraph graph,
ImmutableArray<string> entryPoints,
ReachabilityPolicy? policy = null)
{
ArgumentNullException.ThrowIfNull(graph);
var resolvedPolicy = policy ?? new ReachabilityPolicy();
var results = new Dictionary<string, ReachabilityStatus>(StringComparer.Ordinal);
var findings = new List<ReachabilityFinding>();
if (entryPoints.IsDefaultOrEmpty)
{
foreach (var node in graph.Nodes)
{
results[node] = ReachabilityStatus.Unknown;
findings.Add(new ReachabilityFinding
{
ComponentRef = node,
Status = ReachabilityStatus.Unknown,
Reason = "no-entrypoints"
});
}
return ReachabilityReportBuilder.Build(graph, results, findings);
}
var reachable = new HashSet<string>(StringComparer.Ordinal);
var potential = new HashSet<string>(StringComparer.Ordinal);
var predecessor = new Dictionary<string, string?>(StringComparer.Ordinal);
var pathKind = new Dictionary<string, PathKind>(StringComparer.Ordinal);
var queue = new Queue<(string Node, PathKind Kind)>();
foreach (var entry in entryPoints)
{
if (string.IsNullOrWhiteSpace(entry))
{
continue;
}
var normalized = entry.Trim();
if (reachable.Add(normalized))
{
predecessor[normalized] = null;
pathKind[normalized] = PathKind.Required;
queue.Enqueue((normalized, PathKind.Required));
}
}
while (queue.Count > 0)
{
var (node, kind) = queue.Dequeue();
if (!graph.Edges.TryGetValue(node, out var edges))
{
continue;
}
foreach (var edge in edges)
{
if (!IsScopeIncluded(edge.Scope, resolvedPolicy.ScopeHandling))
{
continue;
}
var nextKind = Combine(kind, edge.Scope, resolvedPolicy.ScopeHandling);
var target = edge.To;
if (nextKind == PathKind.Required)
{
if (reachable.Add(target))
{
predecessor[target] = node;
pathKind[target] = PathKind.Required;
queue.Enqueue((target, PathKind.Required));
}
else if (pathKind.TryGetValue(target, out var existing) &&
existing == PathKind.Optional)
{
predecessor[target] = node;
pathKind[target] = PathKind.Required;
queue.Enqueue((target, PathKind.Required));
}
}
else
{
if (reachable.Contains(target))
{
continue;
}
if (potential.Add(target))
{
predecessor[target] = node;
pathKind[target] = PathKind.Optional;
queue.Enqueue((target, PathKind.Optional));
}
}
}
}
foreach (var node in graph.Nodes)
{
if (reachable.Contains(node))
{
results[node] = ReachabilityStatus.Reachable;
findings.Add(new ReachabilityFinding
{
ComponentRef = node,
Status = ReachabilityStatus.Reachable,
Path = BuildPath(node, predecessor)
});
continue;
}
if (potential.Contains(node))
{
results[node] = ReachabilityStatus.PotentiallyReachable;
findings.Add(new ReachabilityFinding
{
ComponentRef = node,
Status = ReachabilityStatus.PotentiallyReachable,
Path = BuildPath(node, predecessor),
Reason = "optional-dependency"
});
continue;
}
results[node] = ReachabilityStatus.Unreachable;
findings.Add(new ReachabilityFinding
{
ComponentRef = node,
Status = ReachabilityStatus.Unreachable,
Reason = "no-path"
});
}
return ReachabilityReportBuilder.Build(graph, results, findings);
}
private static ImmutableArray<string> BuildPath(
string target,
IReadOnlyDictionary<string, string?> predecessor)
{
var path = new List<string>();
var current = target;
var seen = new HashSet<string>(StringComparer.Ordinal);
while (true)
{
if (!seen.Add(current))
{
break;
}
path.Add(current);
if (!predecessor.TryGetValue(current, out var previous) ||
string.IsNullOrWhiteSpace(previous))
{
break;
}
current = previous;
}
path.Reverse();
return path.ToImmutableArray();
}
private static bool IsScopeIncluded(DependencyScope scope, ReachabilityScopePolicy policy)
{
return scope switch
{
DependencyScope.Runtime => policy.IncludeRuntime,
DependencyScope.Development => policy.IncludeDevelopment,
DependencyScope.Test => policy.IncludeTest,
DependencyScope.Optional => policy.IncludeOptional != OptionalDependencyHandling.Exclude,
_ => true
};
}
private static PathKind Combine(
PathKind current,
DependencyScope scope,
ReachabilityScopePolicy policy)
{
if (scope != DependencyScope.Optional)
{
return current;
}
return policy.IncludeOptional switch
{
OptionalDependencyHandling.Reachable => current,
OptionalDependencyHandling.AsPotentiallyReachable => PathKind.Optional,
_ => current
};
}
private enum PathKind
{
Required,
Optional
}
}