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);
}
}