zerum 0.1.0

Deterministic-first Python code governance: native checks (ZR001–ZR010), CLI, and SARIF/JSON reporters
Documentation
use crate::core::ast_util::collect_function_metrics;
use crate::core::{Category, Check, CheckContext, Issue, Severity};

const DEFAULT_MAX: usize = 8;

pub struct TooManyBranches;

impl Check for TooManyBranches {
    fn id(&self) -> &'static str {
        "ZR001"
    }

    fn name(&self) -> &'static str {
        "too-many-branches"
    }

    fn category(&self) -> Category {
        Category::Design
    }

    fn severity(&self) -> Severity {
        Severity::Medium
    }

    fn explanation(&self) -> &'static str {
        "Functions with many branches are harder to test and reason about. Each branch is a separate path through the function."
    }

    fn remediation(&self) -> &'static str {
        "Extract helper functions, use early returns, or replace nested conditionals with polymorphism or lookup tables."
    }

    fn run(&self, ctx: &CheckContext) -> Vec<Issue> {
        if !ctx.config.is_check_enabled(self.id()) {
            return Vec::new();
        }
        let max = ctx
            .config
            .check_config(self.id())
            .max_branches
            .unwrap_or(DEFAULT_MAX);

        collect_function_metrics(ctx.parsed)
            .into_iter()
            .filter(|m| m.branch_count > max)
            .map(|m| {
                Issue::from_check(
                    self,
                    format!(
                        "function `{}` has {} branches (max {})",
                        m.name, m.branch_count, max
                    ),
                    ctx.file_path(),
                    m.line,
                    m.column,
                )
            })
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::Config;
    use crate::parser::{PythonParser, RustPythonParser};
    use std::path::Path;

    fn run_check(source: &str) -> Vec<Issue> {
        let parser = RustPythonParser;
        let path = Path::new("sample.py");
        let parsed = parser.parse_file(source, path).unwrap();
        let config = Config::default();
        let check = TooManyBranches;
        let ctx = CheckContext {
            path,
            source,
            parsed: &parsed,
            config: &config,
        };
        check.run(&ctx)
    }

    #[test]
    fn flags_branchy_function() {
        let source = r#"
def branchy(x):
    if x > 0:
        pass
    elif x > 1:
        pass
    elif x > 2:
        pass
    elif x > 3:
        pass
    elif x > 4:
        pass
    elif x > 5:
        pass
    elif x > 6:
        pass
    elif x > 7:
        pass
    elif x > 8:
        pass
    while x:
        for i in range(3):
            if i:
                pass
"#;
        let issues = run_check(source);
        assert!(!issues.is_empty());
        assert!(issues.iter().all(|i| i.id == "ZR001"));
    }

    #[test]
    fn async_function_branches_counted() {
        let source = r#"
async def branchy(x):
    if x:
        pass
    elif x > 1:
        pass
    elif x > 2:
        pass
    elif x > 3:
        pass
    elif x > 4:
        pass
    elif x > 5:
        pass
    elif x > 6:
        pass
    elif x > 7:
        pass
    elif x > 8:
        pass
"#;
        assert!(!run_check(source).is_empty());
    }
}