zerum 0.1.0

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

const PHRASE_PATTERNS: &[&str] = &["TODO: AI", "FIXME: AI", "GENERATED BY AI", "AUTO-GENERATED"];
const TOKEN_PATTERNS: &[&str] = &["CHATGPT", "COPILOT"];

pub struct AiPlaceholderComment;

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

    fn name(&self) -> &'static str {
        "ai-generated-placeholder-comment"
    }

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

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

    fn explanation(&self) -> &'static str {
        "Placeholder comments often indicate incomplete AI-assisted edits that were not finished or reviewed."
    }

    fn remediation(&self) -> &'static str {
        "Replace placeholders with real implementation, tests, and documentation; remove marketing boilerplate."
    }

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

        ctx.source
            .lines()
            .enumerate()
            .filter_map(|(idx, line)| {
                let comment = comment_text(line)?;
                find_pattern(comment).map(|pattern| {
                    let column = comment_position(line, pattern);
                    Issue::from_check(
                        self,
                        format!("suspicious AI-related placeholder: `{pattern}`"),
                        ctx.file_path(),
                        idx + 1,
                        column,
                    )
                })
            })
            .collect()
    }
}

fn comment_text(line: &str) -> Option<&str> {
    line.find('#').map(|i| &line[i..])
}

fn find_pattern(comment: &str) -> Option<&'static str> {
    let upper = comment.to_uppercase();
    PHRASE_PATTERNS
        .iter()
        .find(|p| upper.contains(&p.to_ascii_uppercase()))
        .copied()
        .or_else(|| {
            TOKEN_PATTERNS
                .iter()
                .find(|p| contains_token(&upper, &p.to_ascii_uppercase()))
                .copied()
        })
}

fn contains_token(haystack: &str, token: &str) -> bool {
    if token.is_empty() {
        return false;
    }
    let bytes = haystack.as_bytes();
    let tok = token.as_bytes();
    for i in 0..=bytes.len().saturating_sub(tok.len()) {
        if bytes[i..i + tok.len()].eq_ignore_ascii_case(tok)
            && boundary_before(bytes, i)
            && boundary_after(bytes, i + tok.len())
        {
            return true;
        }
    }
    false
}

fn boundary_before(bytes: &[u8], index: usize) -> bool {
    index == 0 || !bytes[index - 1].is_ascii_alphanumeric()
}

fn boundary_after(bytes: &[u8], index: usize) -> bool {
    index >= bytes.len() || !bytes[index].is_ascii_alphanumeric()
}

fn comment_position(line: &str, pattern: &str) -> usize {
    let hash = line.find('#').unwrap_or(0);
    let comment = &line[hash..];
    comment
        .to_uppercase()
        .find(&pattern.to_uppercase())
        .map(|i| hash + i + 1)
        .unwrap_or(hash + 1)
}

#[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<crate::core::Issue> {
        let parser = RustPythonParser;
        let path = Path::new("x.py");
        let parsed = parser.parse_file(source, path).unwrap();
        let config = Config::default();
        let ctx = CheckContext {
            path,
            source,
            parsed: &parsed,
            config: &config,
        };
        AiPlaceholderComment.run(&ctx)
    }

    #[test]
    fn detects_ai_placeholder_in_comment() {
        let issues = run_check("# TODO: AI fill in implementation\npass\n");
        assert_eq!(issues.len(), 1);
        assert_eq!(issues[0].id, "ZR009");
    }

    #[test]
    fn ignores_pattern_inside_string() {
        let issues = run_check("s = \"TODO: AI guide\"\n");
        assert!(issues.is_empty());
    }

    #[test]
    fn ignores_chatgpt_in_identifier() {
        let issues = run_check("chatgpt_client = 1\n");
        assert!(issues.is_empty());
    }

    #[test]
    fn flags_chatgpt_in_comment() {
        let issues = run_check("# built with CHATGPT\n");
        assert_eq!(issues.len(), 1);
    }
}