ailint_core/rules/semantic/
missing_examples.rs1use serde::Deserialize;
6
7use crate::parser::{DocumentContent, ParsedDocument};
8use crate::rules::semantic::AIL101;
9use crate::rules::{Rule, RuleContext, RuleId, Severity, Violation};
10
11const DEFAULT_REQUIRED_IN: &[&str] = &["Examples", "Usage", "Example"];
12const DEFAULT_MIN_WORDS: usize = 20;
13
14#[derive(Debug, Default, Deserialize)]
15#[serde(default, deny_unknown_fields)]
16struct Options {
17 required_in: Option<Vec<String>>,
18 min_words: Option<usize>,
19}
20
21#[derive(Debug, Default)]
23pub struct NoMissingExamplesRule;
24
25impl Rule for NoMissingExamplesRule {
26 fn id(&self) -> RuleId {
27 AIL101
28 }
29
30 fn default_severity(&self) -> Severity {
31 Severity::Info
32 }
33
34 fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
35 let md = match &doc.content {
36 DocumentContent::Markdown(m) => m,
37 _ => return Vec::new(),
38 };
39
40 let opts: Options = ctx
41 .options
42 .and_then(|v| serde_yaml::from_value(v.clone()).ok())
43 .unwrap_or_default();
44 let required_in: Vec<String> = opts
45 .required_in
46 .unwrap_or_else(|| {
47 DEFAULT_REQUIRED_IN
48 .iter()
49 .map(|s| s.to_lowercase())
50 .collect()
51 })
52 .into_iter()
53 .map(|s| s.to_lowercase())
54 .collect();
55 let min_words = opts.min_words.unwrap_or(DEFAULT_MIN_WORDS);
56
57 let mut out = Vec::new();
58 for section in &md.sections {
59 let heading_idx = match section.heading_index {
60 Some(i) => i,
61 None => continue,
62 };
63 let heading = match md.headings.get(heading_idx) {
64 Some(h) => h,
65 None => continue,
66 };
67 let heading_lc = heading.text.to_lowercase();
68 if !required_in.iter().any(|r| heading_lc.contains(r)) {
69 continue;
70 }
71 let body = doc.raw.get(section.byte_range.clone()).unwrap_or("");
72 if body.split_whitespace().count() < min_words {
73 continue;
74 }
75 let has_code = md.code_blocks.iter().any(|cb| {
76 cb.byte_range.start >= section.byte_range.start
77 && cb.byte_range.end <= section.byte_range.end
78 });
79 let body_lc = body.to_lowercase();
80 let has_eg = body_lc.contains("e.g.") || body_lc.contains("example:");
81 if has_code || has_eg {
82 continue;
83 }
84 let mut v = Violation::new(
85 AIL101,
86 self.default_severity(),
87 doc.path.clone(),
88 format!("section '{}' lacks concrete examples", heading.text),
89 )
90 .at(heading.line, 1);
91 v.fix_hint =
92 Some("add a fenced code block or an `e.g.` clause with a concrete example".into());
93 out.push(v);
94 }
95 out
96 }
97}