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