///|
/// CI-oriented limits applied to the result of static program analysis.
pub(all) struct AnalysisPolicy {
max_ast_nodes : Int
max_ast_depth : Int
max_estimated_cost : Int
reject_warnings : Bool
allow_dynamic_regex : Bool
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn AnalysisPolicy::default() -> AnalysisPolicy {
{
max_ast_nodes: 2048,
max_ast_depth: 64,
max_estimated_cost: 10000,
reject_warnings: true,
allow_dynamic_regex: false,
}
}
///|
/// Static analysis plus the policy violations that determine CI acceptance.
pub(all) struct AnalysisGateReport {
passed : Bool
analysis : ProgramAnalysis
violations : Array[AnalysisFinding]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn AnalysisGateReport::to_json_string(
self : AnalysisGateReport,
indent? : Int = 2,
) -> String {
self.to_json().stringify(indent~)
}
///|
fn validate_analysis_policy(
policy : AnalysisPolicy,
) -> Result[Unit, Diagnostic] {
if policy.max_ast_nodes <= 0 ||
policy.max_ast_depth <= 0 ||
policy.max_estimated_cost <= 0 {
Err(
Diagnostic::new(
Configure,
"C030",
"analysis policy limits must be positive integers",
Span::new(0, 0),
hint="Set every numeric AnalysisPolicy field to at least 1.",
),
)
} else {
Ok(())
}
}
///|
fn gate_finding(
code : String,
message : String,
span : Span,
hint : String,
) -> AnalysisFinding {
{ level: Warning, code, message, span, hint: Some(hint) }
}
///|
/// Analyze one program and decide whether it satisfies a configured CI gate.
pub fn analyze_with_policy(
program : Program,
policy : AnalysisPolicy,
) -> Result[AnalysisGateReport, Diagnostic] {
match validate_analysis_policy(policy) {
Err(diagnostic) => return Err(diagnostic)
Ok(_) => ()
}
let analysis = analyze(program)
let violations = []
if analysis.node_count > policy.max_ast_nodes {
violations.push(
gate_finding(
"A050",
"AST node count \{analysis.node_count} exceeds policy limit \{policy.max_ast_nodes}",
program.span(),
"Simplify the expression or explicitly raise max_ast_nodes.",
),
)
}
if analysis.ast_depth > policy.max_ast_depth {
violations.push(
gate_finding(
"A051",
"AST depth \{analysis.ast_depth} exceeds policy limit \{policy.max_ast_depth}",
program.span(),
"Reduce nesting or explicitly raise max_ast_depth.",
),
)
}
if analysis.estimated_cost > policy.max_estimated_cost {
violations.push(
gate_finding(
"A052",
"estimated cost \{analysis.estimated_cost} exceeds policy limit \{policy.max_estimated_cost}",
program.span(),
"Split the rule or explicitly raise max_estimated_cost after benchmarking.",
),
)
}
for finding in analysis.findings {
if policy.reject_warnings && finding.level == Warning {
violations.push(finding)
} else if !policy.allow_dynamic_regex && finding.code == "A005" {
violations.push({
..finding,
level: Warning,
code: "A053",
message: "dynamic regular expression is not allowed by analysis policy",
hint: Some(
"Use a literal pattern or explicitly allow dynamic regular expressions.",
),
})
}
}
Ok({ passed: violations.is_empty(), analysis, violations })
}
///|
/// Gate result associated with one named rule.
pub(all) struct RuleAnalysisGate {
name : String
severity : Severity
report : AnalysisGateReport
} derive(Eq, Debug, ToJson, FromJson)
///|
/// Aggregate CI gate result for a compiled rule set.
pub(all) struct RuleSetAnalysisGateReport {
passed : Bool
rule_count : Int
failed_rule_count : Int
rules : Array[RuleAnalysisGate]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn RuleSetAnalysisGateReport::to_json_string(
self : RuleSetAnalysisGateReport,
indent? : Int = 2,
) -> String {
self.to_json().stringify(indent~)
}
///|
/// Apply one static-analysis policy to every rule in a compiled rule set.
pub fn RuleSet::analyze_with_policy(
self : RuleSet,
policy : AnalysisPolicy,
) -> Result[RuleSetAnalysisGateReport, Diagnostic] {
match validate_analysis_policy(policy) {
Err(diagnostic) => return Err(diagnostic)
Ok(_) => ()
}
let rules = []
let mut failed_rule_count = 0
for rule in self.rules {
let report = analyze_with_policy(rule.program, policy).unwrap()
if !report.passed {
failed_rule_count = failed_rule_count + 1
}
rules.push({
name: rule.definition.name,
severity: rule.definition.severity,
report,
})
}
Ok({
passed: failed_rule_count == 0,
rule_count: rules.length(),
failed_rule_count,
rules,
})
}