Skip to main content

ailint_core/rules/semantic/
vague_instruction.rs

1//! AIL100 `no-vague-instruction` — list items containing hand-wavy phrases.
2//!
3//! See: `docs/rules/semantic/AIL100.md`
4
5use aho_corasick::AhoCorasick;
6use serde::Deserialize;
7
8use crate::parser::{DocumentContent, ParsedDocument};
9use crate::rules::semantic::AIL100;
10use crate::rules::{dictionary_lines, Rule, RuleContext, RuleId, Severity, Violation};
11
12const DEFAULT_PHRASES: &str = include_str!("vague_phrases.txt");
13
14#[derive(Debug, Default, Deserialize)]
15#[serde(default, deny_unknown_fields)]
16struct Options {
17    phrases: Option<Vec<String>>,
18    extra_phrases: Option<Vec<String>>,
19    case_sensitive: Option<bool>,
20}
21
22/// AIL100 no-vague-instruction: flags unactionable phrasing like "be careful".
23#[derive(Debug, Default)]
24pub struct NoVagueInstructionRule;
25
26impl Rule for NoVagueInstructionRule {
27    fn id(&self) -> RuleId {
28        AIL100
29    }
30
31    fn default_severity(&self) -> Severity {
32        Severity::Warning
33    }
34
35    fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
36        let md = match &doc.content {
37            DocumentContent::Markdown(m) => m,
38            _ => return Vec::new(),
39        };
40
41        let opts: Options = ctx
42            .options
43            .and_then(|v| serde_yaml::from_value(v.clone()).ok())
44            .unwrap_or_default();
45        let case_sensitive = opts.case_sensitive.unwrap_or(false);
46
47        let mut phrases: Vec<String> = match opts.phrases {
48            Some(p) => p,
49            None => dictionary_lines(DEFAULT_PHRASES)
50                .into_iter()
51                .map(String::from)
52                .collect(),
53        };
54        if let Some(extra) = opts.extra_phrases {
55            phrases.extend(extra);
56        }
57        if !case_sensitive {
58            for p in &mut phrases {
59                *p = p.to_lowercase();
60            }
61        }
62        // Single-pass search over all phrases at once.
63        let Ok(ac) = AhoCorasick::new(&phrases) else {
64            return Vec::new();
65        };
66
67        let mut out = Vec::new();
68        for item in &md.list_items {
69            let haystack = if case_sensitive {
70                item.text.clone()
71            } else {
72                item.text.to_lowercase()
73            };
74            if let Some(m) = ac.find(&haystack) {
75                let phrase = &phrases[m.pattern().as_usize()];
76                let snippet: String = item.text.chars().take(120).collect();
77                let mut v = Violation::new(
78                    AIL100,
79                    self.default_severity(),
80                    doc.path.clone(),
81                    format!("vague instruction: contains phrase '{}'", phrase),
82                )
83                .at(item.line, 1);
84                v.fix_hint = Some("replace with a concrete, testable rule".into());
85                v.snippet = Some(snippet);
86                out.push(v);
87            }
88        }
89        out
90    }
91}