Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
284 lines
9.2 KiB
C#
284 lines
9.2 KiB
C#
using System;
|
|
using System.Collections.Immutable;
|
|
using StellaOps.PolicyDsl;
|
|
|
|
namespace StellaOps.Policy.Engine.Compilation;
|
|
|
|
/// <summary>
|
|
/// Computes deterministic complexity metrics for compiled policies.
|
|
/// </summary>
|
|
internal sealed class PolicyComplexityAnalyzer
|
|
{
|
|
public PolicyComplexityReport Analyze(PolicyIrDocument document)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(document);
|
|
|
|
var metrics = new ComplexityMetrics();
|
|
metrics.RuleCount = document.Rules.IsDefault ? 0 : document.Rules.Length;
|
|
|
|
VisitMetadata(document.Metadata.Values, metrics);
|
|
VisitMetadata(document.Settings.Values, metrics);
|
|
VisitProfiles(document.Profiles, metrics);
|
|
|
|
if (!document.Rules.IsDefaultOrEmpty)
|
|
{
|
|
foreach (var rule in document.Rules)
|
|
{
|
|
metrics.ConditionCount++;
|
|
VisitExpression(rule.When, metrics, depth: 0);
|
|
|
|
VisitActions(rule.ThenActions, metrics);
|
|
VisitActions(rule.ElseActions, metrics);
|
|
}
|
|
}
|
|
|
|
var score = CalculateScore(metrics);
|
|
var roundedScore = Math.Round(score, 3, MidpointRounding.AwayFromZero);
|
|
|
|
return new PolicyComplexityReport(
|
|
roundedScore,
|
|
metrics.RuleCount,
|
|
metrics.ActionCount,
|
|
metrics.ExpressionCount,
|
|
metrics.InvocationCount,
|
|
metrics.MemberAccessCount,
|
|
metrics.IdentifierCount,
|
|
metrics.LiteralCount,
|
|
metrics.MaxDepth,
|
|
metrics.ProfileCount,
|
|
metrics.ProfileBindings,
|
|
metrics.ConditionCount,
|
|
metrics.ListItems);
|
|
}
|
|
|
|
private static void VisitProfiles(ImmutableArray<PolicyIrProfile> profiles, ComplexityMetrics metrics)
|
|
{
|
|
if (profiles.IsDefaultOrEmpty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var profile in profiles)
|
|
{
|
|
metrics.ProfileCount++;
|
|
|
|
if (!profile.Maps.IsDefaultOrEmpty)
|
|
{
|
|
foreach (var map in profile.Maps)
|
|
{
|
|
if (map.Entries.IsDefaultOrEmpty)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var entry in map.Entries)
|
|
{
|
|
metrics.ProfileBindings++;
|
|
metrics.LiteralCount++; // weight values contribute to literal count
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!profile.Environments.IsDefaultOrEmpty)
|
|
{
|
|
foreach (var environment in profile.Environments)
|
|
{
|
|
if (environment.Entries.IsDefaultOrEmpty)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var entry in environment.Entries)
|
|
{
|
|
metrics.ProfileBindings++;
|
|
metrics.ConditionCount++;
|
|
VisitExpression(entry.Condition, metrics, depth: 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!profile.Scalars.IsDefaultOrEmpty)
|
|
{
|
|
foreach (var scalar in profile.Scalars)
|
|
{
|
|
metrics.ProfileBindings++;
|
|
VisitLiteral(scalar.Value, metrics);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void VisitMetadata(IEnumerable<PolicyIrLiteral> literals, ComplexityMetrics metrics)
|
|
{
|
|
foreach (var literal in literals)
|
|
{
|
|
VisitLiteral(literal, metrics);
|
|
}
|
|
}
|
|
|
|
private static void VisitLiteral(PolicyIrLiteral literal, ComplexityMetrics metrics)
|
|
{
|
|
switch (literal)
|
|
{
|
|
case PolicyIrListLiteral list when !list.Items.IsDefaultOrEmpty:
|
|
foreach (var item in list.Items)
|
|
{
|
|
VisitLiteral(item, metrics);
|
|
}
|
|
break;
|
|
}
|
|
|
|
metrics.LiteralCount++;
|
|
}
|
|
|
|
private static void VisitActions(ImmutableArray<PolicyIrAction> actions, ComplexityMetrics metrics)
|
|
{
|
|
if (actions.IsDefaultOrEmpty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var action in actions)
|
|
{
|
|
metrics.ActionCount++;
|
|
switch (action)
|
|
{
|
|
case PolicyIrAssignmentAction assignment:
|
|
VisitExpression(assignment.Value, metrics, depth: 0);
|
|
break;
|
|
case PolicyIrAnnotateAction annotate:
|
|
VisitExpression(annotate.Value, metrics, depth: 0);
|
|
break;
|
|
case PolicyIrIgnoreAction ignore when ignore.Until is not null:
|
|
VisitExpression(ignore.Until, metrics, depth: 0);
|
|
break;
|
|
case PolicyIrEscalateAction escalate:
|
|
VisitExpression(escalate.To, metrics, depth: 0);
|
|
VisitExpression(escalate.When, metrics, depth: 0);
|
|
break;
|
|
case PolicyIrRequireVexAction require when !require.Conditions.IsEmpty:
|
|
foreach (var condition in require.Conditions.Values)
|
|
{
|
|
VisitExpression(condition, metrics, depth: 0);
|
|
}
|
|
break;
|
|
case PolicyIrWarnAction warn when warn.Message is not null:
|
|
VisitExpression(warn.Message, metrics, depth: 0);
|
|
break;
|
|
case PolicyIrDeferAction defer when defer.Until is not null:
|
|
VisitExpression(defer.Until, metrics, depth: 0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void VisitExpression(PolicyExpression? expression, ComplexityMetrics metrics, int depth)
|
|
{
|
|
if (expression is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
metrics.ExpressionCount++;
|
|
var currentDepth = depth + 1;
|
|
if (currentDepth > metrics.MaxDepth)
|
|
{
|
|
metrics.MaxDepth = currentDepth;
|
|
}
|
|
|
|
switch (expression)
|
|
{
|
|
case PolicyLiteralExpression:
|
|
metrics.LiteralCount++;
|
|
break;
|
|
case PolicyListExpression listExpression:
|
|
if (!listExpression.Items.IsDefaultOrEmpty)
|
|
{
|
|
foreach (var item in listExpression.Items)
|
|
{
|
|
metrics.ListItems++;
|
|
VisitExpression(item, metrics, currentDepth);
|
|
}
|
|
}
|
|
break;
|
|
case PolicyIdentifierExpression:
|
|
metrics.IdentifierCount++;
|
|
break;
|
|
case PolicyMemberAccessExpression member:
|
|
metrics.MemberAccessCount++;
|
|
VisitExpression(member.Target, metrics, currentDepth);
|
|
break;
|
|
case PolicyInvocationExpression invocation:
|
|
metrics.InvocationCount++;
|
|
VisitExpression(invocation.Target, metrics, currentDepth);
|
|
if (!invocation.Arguments.IsDefaultOrEmpty)
|
|
{
|
|
foreach (var argument in invocation.Arguments)
|
|
{
|
|
VisitExpression(argument, metrics, currentDepth);
|
|
}
|
|
}
|
|
break;
|
|
case PolicyIndexerExpression indexer:
|
|
VisitExpression(indexer.Target, metrics, currentDepth);
|
|
VisitExpression(indexer.Index, metrics, currentDepth);
|
|
break;
|
|
case PolicyUnaryExpression unary:
|
|
VisitExpression(unary.Operand, metrics, currentDepth);
|
|
break;
|
|
case PolicyBinaryExpression binary:
|
|
VisitExpression(binary.Left, metrics, currentDepth);
|
|
VisitExpression(binary.Right, metrics, currentDepth);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static double CalculateScore(ComplexityMetrics metrics)
|
|
{
|
|
return metrics.RuleCount * 5d
|
|
+ metrics.ActionCount * 1.5d
|
|
+ metrics.ExpressionCount * 0.75d
|
|
+ metrics.InvocationCount * 1.5d
|
|
+ metrics.MemberAccessCount * 1.0d
|
|
+ metrics.IdentifierCount * 0.5d
|
|
+ metrics.LiteralCount * 0.25d
|
|
+ metrics.ProfileBindings * 0.5d
|
|
+ metrics.ConditionCount * 1.25d
|
|
+ metrics.MaxDepth * 2d
|
|
+ metrics.ListItems * 0.25d;
|
|
}
|
|
|
|
private sealed class ComplexityMetrics
|
|
{
|
|
public int RuleCount;
|
|
public int ActionCount;
|
|
public int ExpressionCount;
|
|
public int InvocationCount;
|
|
public int MemberAccessCount;
|
|
public int IdentifierCount;
|
|
public int LiteralCount;
|
|
public int ProfileCount;
|
|
public int ProfileBindings;
|
|
public int ConditionCount;
|
|
public int MaxDepth;
|
|
public int ListItems;
|
|
}
|
|
}
|
|
|
|
internal sealed record PolicyComplexityReport(
|
|
double Score,
|
|
int RuleCount,
|
|
int ActionCount,
|
|
int ExpressionCount,
|
|
int InvocationCount,
|
|
int MemberAccessCount,
|
|
int IdentifierCount,
|
|
int LiteralCount,
|
|
int MaxExpressionDepth,
|
|
int ProfileCount,
|
|
int ProfileBindingCount,
|
|
int ConditionCount,
|
|
int ListItemCount);
|