tests fixes and sprints work
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user