1use std::path::{Path, PathBuf};
7
8use syn::spanned::Spanned;
9use syn::visit::Visit;
10
11use crate::fingerprint::Fingerprint;
12use crate::node::NormalizedNode;
13
14use crate::node;
15use crate::rust::normalizer;
16use normalizer::normalize;
17use std::fs;
18use syn::Attribute;
19use syn::ExprClosure;
20use syn::Ident;
21use syn::ItemFn;
22use syn::ItemImpl;
23use syn::ItemMod;
24use syn::Type;
25use syn::parse_file as parse_syn_file;
26use syn::visit;
27
28pub use crate::code_unit::{CodeUnit, CodeUnitKind};
29
30#[must_use]
32pub fn has_test_attr(attrs: &[Attribute]) -> bool {
33 attrs.iter().any(|attr| attr.path().is_ident("test"))
34}
35
36#[must_use]
38pub fn has_cfg_test_attr(attrs: &[Attribute]) -> bool {
39 attrs.iter().any(|attr| {
40 attr.path().is_ident("cfg")
41 && attr
42 .parse_args::<Ident>()
43 .is_ok_and(|ident| ident == "test")
44 })
45}
46
47struct CodeUnitExtractor {
49 file: PathBuf,
50 min_node_count: usize,
51 min_line_count: usize,
52 units: Vec<CodeUnit>,
53 current_impl: Option<String>,
55 in_trait_impl: bool,
57 in_test_context: bool,
59}
60
61impl CodeUnitExtractor {
62 const fn new(file: PathBuf, min_node_count: usize, min_line_count: usize) -> Self {
63 Self {
64 file,
65 min_node_count,
66 min_line_count,
67 units: Vec::new(),
68 current_impl: None,
69 in_trait_impl: false,
70 in_test_context: false,
71 }
72 }
73
74 #[allow(clippy::too_many_arguments)]
75 fn add_unit(
76 &mut self,
77 kind: CodeUnitKind,
78 name: String,
79 line_start: usize,
80 line_end: usize,
81 sig: NormalizedNode,
82 body: NormalizedNode,
83 is_test: bool,
84 ) {
85 let node_count = node::count_nodes(&sig) + node::count_nodes(&body);
86 if node_count < self.min_node_count {
87 return;
88 }
89 let line_count = line_end.saturating_sub(line_start) + 1;
90 if self.min_line_count > 0 && line_count < self.min_line_count {
91 return;
92 }
93 let fingerprint = Fingerprint::from_sig_and_body(&sig, &body);
94 self.units.push(CodeUnit {
95 kind,
96 name,
97 file: self.file.clone(),
98 line_start,
99 line_end,
100 signature: sig,
101 body,
102 fingerprint,
103 node_count,
104 parent_name: None,
105 is_test,
106 });
107 }
108}
109
110impl<'ast> Visit<'ast> for CodeUnitExtractor {
111 fn visit_item_fn(&mut self, node: &'ast ItemFn) {
112 let is_test =
113 self.in_test_context || has_test_attr(&node.attrs) || has_cfg_test_attr(&node.attrs);
114
115 let name = node.sig.ident.to_string();
116 let line_start = node.sig.ident.span().start().line;
117 let line_end = node.block.brace_token.span.close().end().line;
118 let (sig, body) = normalize::normalize_item_fn(node);
119 self.add_unit(
120 CodeUnitKind::Function,
121 name,
122 line_start,
123 line_end,
124 sig,
125 body,
126 is_test,
127 );
128
129 let prev = self.in_test_context;
131 self.in_test_context = is_test;
132 visit::visit_item_fn(self, node);
133 self.in_test_context = prev;
134 }
135
136 fn visit_item_mod(&mut self, node: &'ast ItemMod) {
137 let prev = self.in_test_context;
138 if has_cfg_test_attr(&node.attrs) {
139 self.in_test_context = true;
140 }
141 visit::visit_item_mod(self, node);
142 self.in_test_context = prev;
143 }
144
145 fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
146 let prev_test = self.in_test_context;
147 if has_cfg_test_attr(&node.attrs) {
148 self.in_test_context = true;
149 }
150
151 let type_name = quote_type(&node.self_ty);
152 let is_trait_impl = node.trait_.is_some();
153 let trait_name = node
154 .trait_
155 .as_ref()
156 .map(|(_, path, _)| {
157 path.segments
158 .iter()
159 .map(|s| s.ident.to_string())
160 .collect::<Vec<_>>()
161 .join("::")
162 })
163 .unwrap_or_default();
164
165 let prev_impl = self.current_impl.take();
166 let prev_trait = self.in_trait_impl;
167
168 self.current_impl = Some(type_name.clone());
169 self.in_trait_impl = is_trait_impl;
170
171 for item in &node.items {
173 if let syn::ImplItem::Fn(method) = item {
174 let method_name = method.sig.ident.to_string();
175 let full_name = if is_trait_impl {
176 format!("<{type_name} as {trait_name}>::{method_name}")
177 } else {
178 format!("{type_name}::{method_name}")
179 };
180
181 let line_start = method.sig.ident.span().start().line;
182 let line_end = method.block.brace_token.span.close().end().line;
183
184 let (sig, body) = normalize::normalize_impl_item_fn(method);
185 let kind = if is_trait_impl {
186 CodeUnitKind::TraitImplBlock
187 } else {
188 CodeUnitKind::Method
189 };
190
191 self.add_unit(
192 kind,
193 full_name,
194 line_start,
195 line_end,
196 sig,
197 body,
198 self.in_test_context,
199 );
200 }
201 }
202
203 self.current_impl = prev_impl;
204 self.in_trait_impl = prev_trait;
205 self.in_test_context = prev_test;
206 }
207
208 fn visit_expr_closure(&mut self, node: &'ast ExprClosure) {
209 let line_start = node.or1_token.span.start().line;
210 let line_end = match &*node.body {
211 syn::Expr::Block(eb) => eb.block.brace_token.span.close().end().line,
212 other => {
213 let end = other.span().end().line;
214 if end > 0 { end } else { line_start }
215 }
216 };
217
218 let normalized = normalize::normalize_closure_expr(node);
219 let node_count = node::count_nodes(&normalized);
220 let line_count = line_end.saturating_sub(line_start) + 1;
221 if node_count >= self.min_node_count
222 && (self.min_line_count == 0 || line_count >= self.min_line_count)
223 {
224 let name = format!("closure at {}:{}", self.file.display(), line_start);
225 let fingerprint = Fingerprint::from_node(&normalized);
226 self.units.push(CodeUnit {
227 kind: CodeUnitKind::Closure,
228 name,
229 file: self.file.clone(),
230 line_start,
231 line_end,
232 signature: NormalizedNode::leaf(crate::node::NodeKind::Opaque),
233 body: normalized,
234 fingerprint,
235 node_count,
236 parent_name: None,
237 is_test: self.in_test_context,
238 });
239 }
240
241 visit::visit_expr_closure(self, node);
243 }
244}
245
246fn quote_type(ty: &Type) -> String {
248 match ty {
249 Type::Path(tp) => tp
250 .path
251 .segments
252 .iter()
253 .map(|s| s.ident.to_string())
254 .collect::<Vec<_>>()
255 .join("::"),
256 _ => "Unknown".to_string(),
257 }
258}
259
260pub fn parse_source(
267 path: &Path,
268 source: &str,
269 min_node_count: usize,
270 min_line_count: usize,
271) -> Result<Vec<CodeUnit>, String> {
272 let file =
273 parse_syn_file(source).map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?;
274
275 let mut extractor = CodeUnitExtractor::new(path.to_path_buf(), min_node_count, min_line_count);
276 extractor.visit_file(&file);
277
278 Ok(extractor.units)
279}
280
281pub fn parse_file(
286 path: &Path,
287 min_node_count: usize,
288 min_line_count: usize,
289) -> Result<Vec<CodeUnit>, String> {
290 let content = fs::read_to_string(path)
291 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
292
293 parse_source(path, &content, min_node_count, min_line_count)
294}
295
296#[must_use]
301pub fn parse_files(
302 paths: &[PathBuf],
303 min_node_count: usize,
304 min_line_count: usize,
305) -> (Vec<CodeUnit>, Vec<String>) {
306 let mut units = Vec::new();
307 let mut warnings = Vec::new();
308
309 for path in paths {
310 match parse_file(path, min_node_count, min_line_count) {
311 Ok(file_units) => units.extend(file_units),
312 Err(warning) => warnings.push(warning),
313 }
314 }
315
316 (units, warnings)
317}