use crate::engine::{CompiledPolicy, Hit};
use crate::extract::Doc;
use crate::input::{line_ranges, FormatData, Prepared};
use crate::Config;
pub const HANDLED: &[&str] = &["SLOP-X001", "SLOP-X002", "SLOP-X003", "SLOP-X004"];
pub fn evaluate(
cp: &CompiledPolicy,
prepared: &Prepared,
doc: &Doc,
config: &Config,
hits: &mut Vec<Hit>,
) {
if let Some(idx) = super::active(cp, config, "SLOP-X001") {
let rule = &cp.pkg.rules[idx];
let set: Vec<String> = rule
.params
.as_table()
.and_then(|t| t.get("heading_set"))
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let min = super::param_i64(rule, "min_matches").unwrap_or(3) as usize;
let matching: Vec<&crate::extract::Heading> = doc
.headings
.iter()
.filter(|h| set.iter().any(|s| s == &h.text.to_ascii_lowercase()))
.collect();
if matching.len() >= min {
hits.push(Hit::new(idx, matching[0].range.clone()));
}
}
if let Some(idx) = super::active(cp, config, "SLOP-X002") {
if let FormatData::Commit(split) = &prepared.format {
let rule = &cp.pkg.rules[idx];
let min_lines = super::param_i64(rule, "min_body_lines").unwrap_or(10) as usize;
let min_pct = super::param_i64(rule, "min_bullet_line_pct").unwrap_or(50) as usize;
let body = &prepared.text[split.body.clone()];
let mut lines = 0usize;
let mut bullet_lines = 0usize;
for lr in line_ranges(body) {
let line = body[lr].trim_start();
if line.is_empty() {
continue;
}
lines += 1;
if line.starts_with("- ") || line.starts_with("* ") || line.starts_with("+ ") {
bullet_lines += 1;
}
}
if lines >= min_lines && bullet_lines * 100 >= min_pct * lines {
hits.push(Hit::new(idx, split.body.clone()));
}
}
}
if let Some(idx) = super::active(cp, config, "SLOP-X003") {
let rule = &cp.pkg.rules[idx];
let cap = rule
.params
.as_table()
.and_then(|t| t.get("max_words"))
.and_then(|v| v.as_table())
.and_then(|t| t.get(config.profile.as_str()))
.and_then(|v| v.as_integer());
if let Some(cap) = cap {
if doc.stats.word_count > cap as u64 {
hits.push(Hit::new(idx, super::document_anchor(&prepared.text)));
}
}
}
if let Some(idx) = super::active(cp, config, "SLOP-X004") {
let rule = &cp.pkg.rules[idx];
let max_words = super::param_i64(rule, "max_words").unwrap_or(300) as u64;
let min_headings = super::param_i64(rule, "min_headings").unwrap_or(4) as usize;
if doc.stats.word_count <= max_words
&& doc.stats.word_count > 0
&& doc.headings.len() >= min_headings
&& !x004_heading_set_exempt(rule, doc, config)
{
hits.push(Hit::new(idx, doc.headings[0].range.clone()));
}
}
}
fn x004_heading_set_exempt(rule: &crate::policy::Rule, doc: &Doc, config: &Config) -> bool {
let Some(sets) = rule
.params
.as_table()
.and_then(|t| t.get("exempt_heading_sets"))
.and_then(|v| v.as_table())
.and_then(|t| t.get(config.profile.as_str()))
.and_then(|v| v.as_array())
else {
return false;
};
let got: Vec<String> = doc
.headings
.iter()
.skip(1)
.map(|h| h.text.trim().to_lowercase())
.collect();
sets.iter().any(|set| match set.as_array() {
Some(want) => {
want.len() == got.len()
&& want
.iter()
.zip(&got)
.all(|(w, g)| w.as_str() == Some(g.as_str()))
}
None => false,
})
}