using System; using System.Collections.Immutable; using StellaOps.PolicyDsl; namespace StellaOps.Policy.Engine.Compilation; /// /// Computes deterministic complexity metrics for compiled policies. /// 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 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 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 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);