zerum 0.1.0

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

pub struct BroadExcept;

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

    fn name(&self) -> &'static str {
        "broad-except"
    }

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

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

    fn explanation(&self) -> &'static str {
        "Catching Exception or using bare except hides bugs and makes failures silent."
    }

    fn remediation(&self) -> &'static str {
        "Catch specific exception types and re-raise or log unexpected errors."
    }

    fn run(&self, ctx: &CheckContext) -> Vec<Issue> {
        if !ctx.config.is_check_enabled(self.id()) {
            return Vec::new();
        }

        let mut issues = Vec::new();
        walk_stmts_for_try(ctx.parsed, &mut |handler| {
            if crate::core::ast_util::is_broad_except_handler(handler) {
                let ExceptHandler::ExceptHandler(h) = handler;
                let (line, column) = line_col(&ctx.parsed.source, h.start().into());
                issues.push(Issue::from_check(
                    self,
                    "broad or bare except handler swallows errors",
                    ctx.file_path(),
                    line,
                    column,
                ));
            }
        });
        issues
    }
}

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

    #[test]
    fn flags_bare_except() {
        let source = "try:\n    pass\nexcept:\n    pass\n";
        let path = Path::new("t.py");
        let parsed = RustPythonParser.parse_file(source, path).unwrap();
        let config = Config::default();
        let ctx = CheckContext {
            path,
            source,
            parsed: &parsed,
            config: &config,
        };
        assert_eq!(BroadExcept.run(&ctx).len(), 1);
    }

    #[test]
    fn specific_except_is_clean() {
        let source = "try:\n    pass\nexcept ValueError:\n    pass\n";
        let path = Path::new("t.py");
        let parsed = RustPythonParser.parse_file(source, path).unwrap();
        let config = Config::default();
        let ctx = CheckContext {
            path,
            source,
            parsed: &parsed,
            config: &config,
        };
        assert!(BroadExcept.run(&ctx).is_empty());
    }
}