1use crate::catalogue::{self, RuleMeta};
10
11pub fn did_you_mean(unknown_id: &str) -> Vec<String> {
14 let candidates: Vec<&str> = catalogue::all_rules().iter().map(|r| r.rule_id).collect();
15 close_matches(unknown_id, &candidates, 5, 0.6)
16}
17
18fn close_matches(word: &str, candidates: &[&str], n: usize, cutoff: f32) -> Vec<String> {
27 let mut scored: Vec<(f32, &str)> = candidates
28 .iter()
29 .filter_map(|&cand| {
30 let mut matcher = difflib::sequencematcher::SequenceMatcher::new(cand, word);
31 let ratio = matcher.ratio();
32 if ratio >= cutoff {
33 Some((ratio, cand))
34 } else {
35 None
36 }
37 })
38 .collect();
39 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap().then_with(|| b.1.cmp(a.1)));
40 scored.truncate(n);
41 scored.into_iter().map(|(_, s)| s.to_string()).collect()
42}
43
44fn render_one_terminal(rule_id: &str, meta: &RuleMeta) -> String {
45 let mut lines = vec![
46 format!("{rule_id} ({})", meta.severity.as_str().to_uppercase()),
47 format!(" Category: {}", meta.category),
48 format!(" Authority: {} ({})", meta.authority, meta.authority_ref),
49 ];
50 if meta.confidence != "high" {
51 lines.push(format!(
52 " Confidence: {} (measured corpus precision below the gate; see eval/improvement-log.md)",
53 meta.confidence
54 ));
55 }
56 if !meta.guide_section.is_empty() {
57 lines.push(format!(" JSS guide: {}", meta.guide_section));
58 if let Some(url) = meta.guide_url {
59 lines.push(format!(" See: {url}"));
60 }
61 }
62 lines.push(String::new());
63 let body = if !meta.explanation.is_empty() {
64 meta.explanation
65 } else {
66 meta.message_template
67 };
68 lines.push(body.to_string());
69 lines.join("\n") + "\n"
70}
71
72fn render_one_markdown(rule_id: &str, meta: &RuleMeta) -> String {
73 let mut parts = vec![format!("# {rule_id}"), String::new()];
74 parts.push(format!("- **Severity:** {}", meta.severity.as_str()));
75 parts.push(format!("- **Category:** {}", meta.category));
76 parts.push(format!(
77 "- **Authority:** {} ({})",
78 meta.authority, meta.authority_ref
79 ));
80 if meta.confidence != "high" {
81 parts.push(format!("- **Confidence:** {}", meta.confidence));
82 }
83 if !meta.guide_section.is_empty() {
84 if let Some(url) = meta.guide_url {
85 parts.push(format!("- **JSS guide:** [{}]({url})", meta.guide_section));
86 } else {
87 parts.push(format!("- **JSS guide:** {}", meta.guide_section));
88 }
89 }
90 parts.push(String::new());
91 let body = if !meta.explanation.is_empty() {
92 meta.explanation
93 } else {
94 meta.message_template
95 };
96 parts.push(body.to_string());
97 parts.push(String::new());
98 parts.join("\n")
99}
100
101fn render_listing(fmt: &str) -> String {
102 use std::collections::BTreeMap;
103 let mut by_category: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
104 for meta in catalogue::all_rules() {
105 by_category
106 .entry(meta.category)
107 .or_default()
108 .push(meta.rule_id);
109 }
110 for ids in by_category.values_mut() {
111 ids.sort_unstable();
112 }
113
114 let mut out: Vec<String> = Vec::new();
115 if fmt == "markdown" {
116 out.push("# JSS-lint rule catalogue\n".to_string());
117 } else {
118 out.push("JSS-lint rule catalogue:\n".to_string());
119 }
120
121 for (category, rule_ids) in &by_category {
122 if fmt == "markdown" {
123 out.push(format!("## {category}\n"));
124 for rid in rule_ids {
125 let meta = catalogue::lookup(rid).expect("catalogue rule");
126 out.push(format!("- `{rid}` — {}", meta.message_template));
127 }
128 out.push(String::new());
129 } else {
130 out.push(format!(" {category}:"));
131 for rid in rule_ids {
132 let meta = catalogue::lookup(rid).expect("catalogue rule");
133 out.push(format!(" {rid} {}", meta.message_template));
134 }
135 out.push(String::new());
136 }
137 }
138
139 let joined = out.join("\n");
140 if fmt == "markdown" {
141 joined + "\n"
142 } else {
143 joined
144 }
145}
146
147pub fn render(rule_id: Option<&str>, fmt: &str) -> Result<String, String> {
152 let Some(raw_id) = rule_id else {
153 return Ok(render_listing(fmt));
154 };
155 let normalized = raw_id.trim().to_uppercase();
156 let Some(meta) = catalogue::lookup(&normalized) else {
157 return Err(normalized);
158 };
159 if fmt == "markdown" {
160 Ok(render_one_markdown(&normalized, meta))
161 } else {
162 Ok(render_one_terminal(&normalized, meta))
163 }
164}