ailint_core/rules/semantic/
vague_instruction.rs1use 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#[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 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}