1use std::path::PathBuf;
4
5use anyhow::{Result, bail};
6use syn::visit::Visit;
7
8use crate::files::collect_rust_files_with_exclusions;
9
10#[derive(Debug)]
11pub struct MatchReport {
12 pub enum_name: String,
13 pub all_variants: Vec<String>,
14 pub match_sites: Vec<MatchSite>,
15}
16
17#[derive(Debug)]
18pub struct MatchSite {
19 pub fn_name: String,
20 pub file_path: String,
21 pub line: usize,
22 pub missing_variants: Vec<String>,
23 pub has_wildcard: bool,
24}
25
26pub fn run(paths: &[PathBuf], enum_name: &str, exclude: &[String]) -> Result<MatchReport> {
27 let files = collect_rust_files_with_exclusions(paths, exclude)?;
28
29 let mut all_variants: Vec<String> = Vec::new();
31
32 for file in &files {
33 let content = match std::fs::read_to_string(file) {
34 Ok(c) => c,
35 Err(e) => {
36 eprintln!("⚠️ Skipping {}: {}", file.display(), e);
37 continue;
38 }
39 };
40 let syntax = match syn::parse_file(&content) {
41 Ok(s) => s,
42 Err(e) => {
43 eprintln!("⚠️ Skipping {} (parse error): {}", file.display(), e);
44 continue;
45 }
46 };
47
48 for item in &syntax.items {
49 if let syn::Item::Enum(e) = item
50 && e.ident == enum_name
51 {
52 all_variants = e.variants.iter().map(|v| v.ident.to_string()).collect();
53 break;
54 }
55 }
56 if !all_variants.is_empty() {
57 break;
58 }
59 }
60
61 if all_variants.is_empty() {
62 bail!(
63 "Enum '{}' not found in any of the scanned files. \
64 Make sure the paths include the file that defines this enum.",
65 enum_name
66 );
67 }
68
69 let mut match_sites: Vec<MatchSite> = Vec::new();
71
72 for file in &files {
73 let content = match std::fs::read_to_string(file) {
74 Ok(c) => c,
75 Err(e) => {
76 eprintln!("⚠️ Skipping {}: {}", file.display(), e);
77 continue;
78 }
79 };
80 let syntax = match syn::parse_file(&content) {
81 Ok(s) => s,
82 Err(e) => {
83 eprintln!("⚠️ Skipping {} (parse error): {}", file.display(), e);
84 continue;
85 }
86 };
87
88 let mut visitor = MatchAuditVisitor {
89 enum_name,
90 all_variants: &all_variants,
91 file_path: file.to_string_lossy().to_string(),
92 fn_stack: Vec::new(),
93 sites: Vec::new(),
94 };
95 visitor.visit_file(&syntax);
96 match_sites.extend(visitor.sites);
97 }
98
99 Ok(MatchReport {
100 enum_name: enum_name.to_string(),
101 all_variants,
102 match_sites,
103 })
104}
105
106pub fn render(report: &MatchReport) {
107 println!("Match audit for enum {}:", report.enum_name);
108 println!(" Known variants: {}", report.all_variants.join(", "));
109 println!();
110
111 if report.match_sites.is_empty() {
112 println!(
113 " No match expressions found for enum {}.",
114 report.enum_name
115 );
116 return;
117 }
118
119 println!("Missing variants:");
120 let mut any_missing = false;
121 for site in &report.match_sites {
122 if site.has_wildcard {
123 println!(
124 " {} ({}:{}): (wildcard — covers all)",
125 site.fn_name, site.file_path, site.line
126 );
127 } else if site.missing_variants.is_empty() {
128 println!(" {}: complete", site.fn_name);
129 } else {
130 println!(
131 " {} ({}:{}): {}",
132 site.fn_name,
133 site.file_path,
134 site.line,
135 site.missing_variants.join(", ")
136 );
137 any_missing = true;
138 }
139 }
140 if !any_missing {
141 println!(" All match expressions are complete.");
142 }
143}
144
145struct MatchAuditVisitor<'a> {
148 enum_name: &'a str,
149 all_variants: &'a [String],
150 file_path: String,
151 fn_stack: Vec<String>,
152 sites: Vec<MatchSite>,
153}
154
155impl<'a> MatchAuditVisitor<'a> {
156 fn current_fn(&self) -> String {
157 self.fn_stack
158 .last()
159 .cloned()
160 .unwrap_or_else(|| "<top-level>".to_string())
161 }
162}
163
164impl<'ast, 'a> Visit<'ast> for MatchAuditVisitor<'a> {
165 fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
166 self.fn_stack.push(node.sig.ident.to_string());
167 syn::visit::visit_item_fn(self, node);
168 self.fn_stack.pop();
169 }
170
171 fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
172 self.fn_stack.push(node.sig.ident.to_string());
173 syn::visit::visit_impl_item_fn(self, node);
174 self.fn_stack.pop();
175 }
176
177 fn visit_expr_match(&mut self, node: &'ast syn::ExprMatch) {
178 let mut variants_seen: Vec<String> = Vec::new();
180 let mut has_wildcard = false;
181
182 for arm in &node.arms {
183 if arm_has_wildcard(&arm.pat) {
184 has_wildcard = true;
185 }
186 collect_enum_variants_from_pat(&arm.pat, self.enum_name, &mut variants_seen);
187 }
188
189 if !variants_seen.is_empty() {
192 let missing: Vec<String> = if has_wildcard {
193 vec![]
194 } else {
195 self.all_variants
196 .iter()
197 .filter(|v| !variants_seen.contains(v))
198 .cloned()
199 .collect()
200 };
201
202 let line = node.match_token.span.start().line;
204
205 self.sites.push(MatchSite {
206 fn_name: self.current_fn(),
207 file_path: self.file_path.clone(),
208 line,
209 missing_variants: missing,
210 has_wildcard,
211 });
212 }
213
214 syn::visit::visit_expr_match(self, node);
215 }
216}
217
218fn arm_has_wildcard(pat: &syn::Pat) -> bool {
220 match pat {
221 syn::Pat::Wild(_) => true,
222 syn::Pat::Ident(pi) if pi.ident == "_" => true,
223 syn::Pat::Or(po) => po.cases.iter().any(arm_has_wildcard),
224 _ => false,
225 }
226}
227
228fn collect_enum_variants_from_pat(pat: &syn::Pat, enum_name: &str, out: &mut Vec<String>) {
231 match pat {
232 syn::Pat::Path(pp) => {
233 check_path(&pp.path, enum_name, out);
234 }
235 syn::Pat::TupleStruct(pts) => {
236 check_path(&pts.path, enum_name, out);
237 }
238 syn::Pat::Struct(ps) => {
239 check_path(&ps.path, enum_name, out);
240 }
241 syn::Pat::Or(po) => {
242 for case in &po.cases {
243 collect_enum_variants_from_pat(case, enum_name, out);
244 }
245 }
246 syn::Pat::Tuple(pt) => {
247 for elem in &pt.elems {
248 collect_enum_variants_from_pat(elem, enum_name, out);
249 }
250 }
251 syn::Pat::Reference(pr) => collect_enum_variants_from_pat(&pr.pat, enum_name, out),
252 syn::Pat::Paren(pp) => collect_enum_variants_from_pat(&pp.pat, enum_name, out),
253 _ => {}
254 }
255}
256
257fn check_path(path: &syn::Path, enum_name: &str, out: &mut Vec<String>) {
258 let segs: Vec<&syn::PathSegment> = path.segments.iter().collect();
259 if segs.len() >= 2 {
260 let second_to_last = segs[segs.len() - 2].ident.to_string();
261 if second_to_last == enum_name {
262 let variant = segs.last().unwrap().ident.to_string();
263 if !out.contains(&variant) {
264 out.push(variant);
265 }
266 }
267 }
268}