use crate::domain::policy::{DocumentationScorer, NodeInfo};
trait LanguageDocExtractor: Send + Sync {
fn extract_params(&self, signature: &str) -> Vec<String>;
fn has_return_value(&self, signature: &str) -> bool;
fn mentions_param(&self, doc_lower: &str, param_name: &str) -> bool {
let param_lower = param_name.to_lowercase();
doc_lower
.split(|c: char| !c.is_alphanumeric() && c != '_')
.any(|w| w == param_lower)
}
fn mentions_return(&self, doc_lower: &str) -> bool {
doc_lower.contains("return") || doc_lower.contains("returns") || doc_lower.contains("返回")
}
}
struct GenericExtractor;
impl LanguageDocExtractor for GenericExtractor {
fn extract_params(&self, _signature: &str) -> Vec<String> {
vec![]
}
fn has_return_value(&self, _signature: &str) -> bool {
false
}
}
struct RustExtractor;
impl LanguageDocExtractor for RustExtractor {
fn extract_params(&self, signature: &str) -> Vec<String> {
if let Some(start) = signature.find('(')
&& let Some(end) = signature.find(')')
{
let params_str = &signature[start + 1..end];
return params_str
.split(',')
.filter_map(|p| {
let parts: Vec<&str> = p.split(':').collect();
parts.first().map(|s| s.trim().to_string())
})
.filter(|s| !s.is_empty() && s != "self" && s != "&self" && s != "&mut self")
.collect();
}
vec![]
}
fn has_return_value(&self, signature: &str) -> bool {
signature.contains("->") && !signature.contains("-> ()") && !signature.contains("-> !")
}
}
struct PythonExtractor;
impl LanguageDocExtractor for PythonExtractor {
fn extract_params(&self, signature: &str) -> Vec<String> {
if let Some(start) = signature.find('(')
&& let Some(end) = signature.rfind(')')
{
let params_str = &signature[start + 1..end];
return params_str
.split(',')
.filter_map(|p| {
let name_part = p.split(':').next()?.split('=').next()?.trim();
let clean_name = name_part.trim_start_matches('*');
Some(clean_name.to_string())
})
.filter(|s| !s.is_empty() && s != "self" && s != "cls")
.collect();
}
vec![]
}
fn has_return_value(&self, signature: &str) -> bool {
signature.contains("->") && !signature.contains("-> None")
}
}
pub struct HeuristicDocScorer;
impl Default for HeuristicDocScorer {
fn default() -> Self {
Self::new()
}
}
impl HeuristicDocScorer {
pub fn new() -> Self {
Self
}
fn get_extractor(&self, language: Option<&str>) -> Box<dyn LanguageDocExtractor> {
match language {
Some("rs") | Some("rust") => Box::new(RustExtractor),
Some("py") | Some("python") => Box::new(PythonExtractor),
_ => Box::new(GenericExtractor),
}
}
}
impl DocumentationScorer for HeuristicDocScorer {
fn score(&self, node_info: &NodeInfo, doc_text: Option<&str>) -> f32 {
let doc = match doc_text {
Some(d) if !d.trim().is_empty() => d,
_ => return 0.0,
};
let mut score: f32 = 0.0;
let doc_lower = doc.to_lowercase();
let word_count = doc.split_whitespace().count();
if word_count > 30 {
score += 0.3;
} else if word_count > 15 {
score += 0.2;
} else if word_count > 5 {
score += 0.1;
}
let mut coverage_score: f32 = 0.0;
let extractor = self.get_extractor(node_info.language.as_deref());
if let Some(signature) = &node_info.signature {
let params = extractor.extract_params(signature);
let has_ret = extractor.has_return_value(signature);
let param_contribution = if !params.is_empty() {
let covered_count = params
.iter()
.filter(|p| extractor.mentions_param(&doc_lower, p))
.count();
(covered_count as f32 / params.len() as f32) * 0.4
} else {
0.2
};
let return_contribution = if has_ret {
if extractor.mentions_return(&doc_lower) {
0.3
} else {
0.0
}
} else {
0.1
};
coverage_score = param_contribution + return_contribution;
} else {
if doc_lower.contains("returns")
|| doc_lower.contains("return")
|| doc_lower.contains("返回")
{
coverage_score += 0.3;
}
if doc_lower.contains("args")
|| doc_lower.contains("parameters")
|| doc_lower.contains("param")
{
coverage_score += 0.3;
}
if doc_lower.contains("example") || doc_lower.contains("usage") {
coverage_score += 0.1;
}
}
score += coverage_score;
score.min(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::policy::NodeType;
fn node_info(language: Option<&str>, signature: Option<&str>) -> NodeInfo {
NodeInfo {
node_type: NodeType::Function,
name: "test".into(),
signature: signature.map(|s| s.to_string()),
language: language.map(|s| s.to_string()),
}
}
#[test]
fn test_rust_param_coverage() {
let s = HeuristicDocScorer::new();
let info = node_info(Some("rs"), Some("fn foo(bar: i32, baz: String) -> bool"));
let doc = "Checks if bar and baz are valid. Returns true if so.";
let score = s.score(&info, Some(doc));
assert!(score >= 0.8, "Score should be >= 0.8, got {}", score);
let doc2 = "Checks if bar is valid.";
let score2 = s.score(&info, Some(doc2));
assert!(score2 < 0.5);
assert!(score2 >= 0.2);
}
#[test]
fn test_python_param_coverage() {
let s = HeuristicDocScorer::new();
let info = node_info(Some("py"), Some("def foo(bar: int, baz='hello') -> str:"));
let doc = "Concatenates bar and baz. Returns a string.";
let score = s.score(&info, Some(doc));
assert!(score >= 0.7);
}
#[test]
fn test_no_params_no_return() {
let s = HeuristicDocScorer::new();
let info = node_info(Some("rs"), Some("fn foo()"));
let doc = "A simple function that does nothing.";
let score = s.score(&info, Some(doc));
assert!((score - 0.4).abs() < 0.001);
}
#[test]
fn test_keyword_fallback() {
let s = HeuristicDocScorer::new();
let info = node_info(None, None);
let doc = "This is a function. Returns something. Args: none.";
let score = s.score(&info, Some(doc));
assert!((score - 0.7).abs() < 0.001);
}
#[test]
fn test_empty_doc_is_zero() {
let s = HeuristicDocScorer::new();
let info = node_info(None, None);
assert_eq!(s.score(&info, None), 0.0);
assert_eq!(s.score(&info, Some(" ")), 0.0);
}
#[test]
fn test_partial_parameter_score() {
let s = HeuristicDocScorer::new();
let info = node_info(Some("rs"), Some("fn multi(x: i32, y: i32)"));
let doc = "This function uses x but forgets the other.";
let score = s.score(&info, Some(doc));
assert!((score - 0.4).abs() < 0.001, "Expected 0.4, got {}", score);
}
#[test]
fn test_word_boundary_matching() {
let s = HeuristicDocScorer::new();
let info = node_info(Some("rs"), Some("fn search(id: u32)"));
let doc_bad = "It checks valid_id and verifies identity.";
let score_bad = s.score(&info, Some(doc_bad));
assert!(score_bad < 0.3);
let doc_good = "It checks the id of the user.";
let score_good = s.score(&info, Some(doc_good));
assert!(score_good > score_bad);
}
#[test]
fn test_ignore_self_in_signatures() {
let s = HeuristicDocScorer::new();
let info_rs = node_info(Some("rs"), Some("fn method(&self, data: String)"));
let doc = "Processes the data.";
let score_rs = s.score(&info_rs, Some(doc));
assert!(score_rs >= 0.5);
let info_py = node_info(Some("py"), Some("def method(self, data: str):"));
let score_py = s.score(&info_py, Some(doc));
assert!(score_py >= 0.5);
}
#[test]
fn test_complex_python_args() {
let s = HeuristicDocScorer::new();
let info = node_info(Some("py"), Some("def foo(*args, **kwargs):"));
let doc = "Passes args and kwargs to another function.";
let score = s.score(&info, Some(doc));
assert!(score >= 0.4);
}
}