normalize_native_rules/
check_examples.rs1use normalize_output::OutputFormatter;
4use normalize_output::diagnostics::{DiagnosticsReport, Issue, Severity};
5use serde::Serialize;
6use std::path::Path;
7
8static MARKER_START_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
9static REF_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
10
11#[derive(Debug, Serialize, schemars::JsonSchema)]
13struct MissingExample {
14 doc_file: String,
15 line: usize,
16 reference: String, }
18
19#[derive(Debug, Serialize, schemars::JsonSchema)]
21pub struct CheckExamplesReport {
22 defined_examples: usize,
23 references_found: usize,
24 missing: Vec<MissingExample>,
25}
26
27impl OutputFormatter for CheckExamplesReport {
28 fn format_text(&self) -> String {
29 let mut lines = Vec::new();
30 lines.push("Example Reference Check".to_string());
31 lines.push(String::new());
32 lines.push(format!("Defined examples: {}", self.defined_examples));
33 lines.push(format!("References found: {}", self.references_found));
34 lines.push(String::new());
35
36 if self.missing.is_empty() {
37 lines.push("All example references are valid.".to_string());
38 } else {
39 lines.push(format!("Missing examples ({}):", self.missing.len()));
40 lines.push(String::new());
41 for m in &self.missing {
42 lines.push(format!(
43 " {}:{}: {{{{{}}}}}",
44 m.doc_file, m.line, m.reference
45 ));
46 }
47 }
48
49 lines.join("\n")
50 }
51}
52
53pub fn build_check_examples_report(
55 root: &Path,
56 walk_config: &normalize_rules_config::WalkConfig,
57) -> CheckExamplesReport {
58 use std::collections::HashSet;
59
60 let marker_start_re =
62 MARKER_START_RE.get_or_init(|| regex::Regex::new(r"//\s*\[example:\s*([^\]]+)\]").unwrap());
63 let ref_re = REF_RE.get_or_init(|| regex::Regex::new(r"\{\{example:\s*([^}]+)\}\}").unwrap());
65
66 let mut defined_examples: HashSet<String> = HashSet::new();
67
68 for entry in crate::walk::gitignore_walk(root, walk_config)
69 .filter(|e| e.file_type().is_some_and(|ft| ft.is_file()))
70 {
71 let path = entry.path();
72
73 if normalize_languages::support_for_path(path).is_none() {
74 continue;
75 }
76
77 let content = match std::fs::read_to_string(path) {
78 Ok(c) => c,
79 Err(_) => continue,
80 };
81
82 let rel_path = path
83 .strip_prefix(root)
84 .unwrap_or(path)
85 .display()
86 .to_string();
87
88 for cap in marker_start_re.captures_iter(&content) {
89 let name = cap[1].trim();
90 let key = format!("{}#{}", rel_path, name);
91 defined_examples.insert(key);
92 }
93 }
94
95 let mut missing: Vec<MissingExample> = Vec::new();
96 let mut refs_found = 0;
97
98 for entry in crate::walk::gitignore_walk(root, walk_config)
99 .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("md"))
100 {
101 let path = entry.path();
102 let content = match std::fs::read_to_string(path) {
103 Ok(c) => c,
104 Err(_) => continue,
105 };
106
107 let rel_path = path
108 .strip_prefix(root)
109 .unwrap_or(path)
110 .display()
111 .to_string();
112
113 let mut in_code_block = false;
114 for (line_num, line) in content.lines().enumerate() {
115 if line.trim().starts_with("```") {
116 in_code_block = !in_code_block;
117 continue;
118 }
119 if in_code_block {
120 continue;
121 }
122
123 for cap in ref_re.captures_iter(line) {
124 let match_start = cap.get(0).unwrap().start();
126 let match_end = cap.get(0).unwrap().end();
127 let before = &line[..match_start];
129 let after = &line[match_end..];
130
131 if before.chars().filter(|&c| c == '`').count() % 2 == 1 && after.contains('`') {
132 continue;
133 }
134
135 refs_found += 1;
136 let reference = cap[1].trim();
137
138 if !defined_examples.contains(reference) {
139 missing.push(MissingExample {
140 doc_file: rel_path.clone(),
141 line: line_num + 1,
142 reference: reference.to_string(),
143 });
144 }
145 }
146 }
147 }
148
149 CheckExamplesReport {
150 defined_examples: defined_examples.len(),
151 references_found: refs_found,
152 missing,
153 }
154}
155
156impl From<CheckExamplesReport> for DiagnosticsReport {
157 fn from(report: CheckExamplesReport) -> Self {
158 DiagnosticsReport {
159 issues: report
160 .missing
161 .into_iter()
162 .map(|m| Issue {
163 file: m.doc_file,
164 line: Some(m.line),
165 column: None,
166 end_line: None,
167 end_column: None,
168 rule_id: "missing-example".into(),
169 message: format!("example `{}` not found in source", m.reference),
170 severity: Severity::Warning,
171 source: "check-examples".into(),
172 related: vec![],
173 suggestion: None,
174 })
175 .collect(),
176 files_checked: 0, sources_run: vec!["check-examples".into()],
178 tool_errors: vec![],
179 daemon_cached: false,
180 }
181 }
182}