use crate::detectors::base::{Detector, DetectorConfig};
use crate::graph::GraphClient;
use crate::models::{Finding, Severity};
use anyhow::Result;
use std::path::PathBuf;
use uuid::Uuid;
pub struct GeneratorMisuseDetector {
config: DetectorConfig,
max_findings: usize,
}
impl GeneratorMisuseDetector {
pub fn new() -> Self {
Self {
config: DetectorConfig::new(),
max_findings: 50,
}
}
}
impl Default for GeneratorMisuseDetector {
fn default() -> Self {
Self::new()
}
}
impl Detector for GeneratorMisuseDetector {
fn name(&self) -> &'static str {
"GeneratorMisuseDetector"
}
fn description(&self) -> &'static str {
"Detects single-yield generators that add unnecessary complexity"
}
fn config(&self) -> Option<&DetectorConfig> {
Some(&self.config)
}
fn detect(&self, graph: &GraphClient) -> Result<Vec<Finding>> {
let mut findings = Vec::new();
let query = r#"
MATCH (f:Function)
WHERE f.isGenerator = true OR f.hasYield = true
RETURN f.qualifiedName AS func_name,
f.name AS func_simple_name,
f.filePath AS func_file,
f.lineStart AS func_line
LIMIT 100
"#;
let results = graph.execute(query)?;
for row in results {
let func_name = row
.get("func_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let func_simple_name = row
.get("func_simple_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let file_path = row
.get("func_file")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let line_start = row
.get("func_line")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
if func_name.is_empty() {
continue;
}
if findings.len() >= self.max_findings {
break;
}
let description = format!(
"Generator function `{}` may be unnecessarily complex.\n\n\
Consider:\n\
- If it yields only once, use a simple return instead\n\
- Check if generator protocol overhead is justified\n\n\
**Exception:** Context managers using `@contextmanager` are valid.",
func_simple_name
);
findings.push(Finding {
id: Uuid::new_v4().to_string(),
detector: self.name().to_string(),
severity: Severity::Low,
title: format!("Potential generator misuse: {}", func_simple_name),
description,
affected_files: vec![PathBuf::from(&file_path)],
line_start,
line_end: None,
suggested_fix: Some(format!(
"Consider converting `{}` to a regular function if it only yields once.",
func_simple_name
)),
estimated_effort: Some("Small (< 1 hour)".to_string()),
category: Some("complexity".to_string()),
cwe_id: None,
why_it_matters: Some(
"Single-yield generators add unnecessary complexity and are harder to understand.".to_string()
),
});
}
Ok(findings)
}
}