1use super::trivial_expr::is_delegation_only_block;
2use super::{has_cfg_test_attribute, has_test_attribute};
3use crate::units::CodeUnitKind;
4use std::collections::HashSet;
5use std::path::{Path, PathBuf};
6use syn::{ImplItem, Item};
7
8use super::references::{collect_rust_call_references, collect_rust_references};
9
10pub(super) fn is_binary_entry_point(path: &Path) -> bool {
16 if path
17 .components()
18 .any(|c| matches!(c, std::path::Component::Normal(s) if s == "tests"))
19 {
20 return false;
21 }
22 let path_str = path.to_string_lossy();
23 if path.file_name().is_some_and(|n| n == "main.rs") {
24 return true;
25 }
26 path_str.contains("src/bin/") || path_str.contains("src\\bin\\")
27}
28
29pub(super) fn is_trivial_binary_main(f: &syn::ItemFn, path: &Path) -> bool {
33 if f.sig.ident != "main" {
34 return false;
35 }
36 if !f.sig.inputs.is_empty() {
37 return false;
38 }
39 if !is_binary_entry_point(path) {
40 return false;
41 }
42 is_delegation_only_block(&f.block)
43}
44
45#[derive(Debug, Clone)]
46pub struct RustCodeDefinition {
47 pub name: String,
48 pub kind: CodeUnitKind,
49 pub file: PathBuf,
50 pub line: usize,
51 pub impl_for_type: Option<String>,
52}
53
54pub(super) fn collect_rust_definitions(
55 ast: &syn::File,
56 file: &Path,
57 defs: &mut Vec<RustCodeDefinition>,
58) {
59 if is_binary_entry_point(file) {
60 return;
61 }
62 for item in &ast.items {
63 collect_definitions_from_item(item, file, defs);
64 }
65}
66
67pub(crate) fn is_private(name: &str) -> bool {
68 name.starts_with('_')
69}
70
71pub(super) fn try_add_def(
72 defs: &mut Vec<RustCodeDefinition>,
73 name: &str,
74 kind: CodeUnitKind,
75 file: &Path,
76 line: usize,
77 impl_for_type: Option<String>,
78) {
79 if !is_private(name) {
80 defs.push(RustCodeDefinition {
81 name: name.to_string(),
82 kind,
83 file: file.to_path_buf(),
84 line,
85 impl_for_type,
86 });
87 }
88}
89
90pub(super) fn extract_type_name(ty: &syn::Type) -> Option<String> {
91 if let syn::Type::Path(p) = ty {
92 p.path.segments.last().map(|s| s.ident.to_string())
93 } else {
94 None
95 }
96}
97
98pub(super) fn collect_impl_methods(
99 impl_block: &syn::ItemImpl,
100 file: &Path,
101 defs: &mut Vec<RustCodeDefinition>,
102) {
103 let is_trait_impl = impl_block.trait_.is_some();
104 let impl_type_name = extract_type_name(&impl_block.self_ty);
105 for impl_item in &impl_block.items {
106 if let ImplItem::Fn(m) = impl_item {
107 if has_test_attribute(&m.attrs) {
108 continue;
109 }
110 let (kind, impl_for) = if is_trait_impl {
111 (CodeUnitKind::TraitImplMethod, impl_type_name.clone())
112 } else {
113 (CodeUnitKind::Method, impl_type_name.clone())
114 };
115 try_add_def(
116 defs,
117 &m.sig.ident.to_string(),
118 kind,
119 file,
120 m.sig.ident.span().start().line,
121 impl_for,
122 );
123 }
124 }
125}
126
127pub(super) fn collect_definitions_from_item(
128 item: &Item,
129 file: &Path,
130 defs: &mut Vec<RustCodeDefinition>,
131) {
132 match item {
133 Item::Fn(f) if !has_test_attribute(&f.attrs) && !is_trivial_binary_main(f, file) => {
134 try_add_def(
135 defs,
136 &f.sig.ident.to_string(),
137 CodeUnitKind::Function,
138 file,
139 f.sig.ident.span().start().line,
140 None,
141 );
142 }
143 Item::Struct(s) => try_add_def(
144 defs,
145 &s.ident.to_string(),
146 CodeUnitKind::Class,
147 file,
148 s.ident.span().start().line,
149 None,
150 ),
151 Item::Enum(e) => try_add_def(
152 defs,
153 &e.ident.to_string(),
154 CodeUnitKind::Class,
155 file,
156 e.ident.span().start().line,
157 None,
158 ),
159 Item::Impl(i) if !has_cfg_test_attribute(&i.attrs) => collect_impl_methods(i, file, defs),
160 Item::Mod(m) if !has_cfg_test_attribute(&m.attrs) => {
161 if let Some((_, items)) = &m.content {
162 for i in items {
163 collect_definitions_from_item(i, file, defs);
164 }
165 }
166 }
167 _ => {}
168 }
169}
170
171fn inline_test_items(ast: &syn::File) -> Vec<syn::Item> {
172 let mut out = Vec::new();
173 for item in &ast.items {
174 match item {
175 Item::Mod(m) if has_cfg_test_attribute(&m.attrs) => {
176 if let Some((_, items)) = &m.content {
177 out.extend(items.iter().cloned());
178 }
179 }
180 Item::Fn(f) if has_test_attribute(&f.attrs) => {
181 out.push(Item::Fn(f.clone()));
182 }
183 _ => {}
184 }
185 }
186 out
187}
188
189pub(super) fn collect_test_module_references(ast: &syn::File, refs: &mut HashSet<String>) {
190 let items = inline_test_items(ast);
191 if items.is_empty() {
192 return;
193 }
194 collect_rust_references(
195 &syn::File {
196 shebang: None,
197 attrs: vec![],
198 items,
199 },
200 refs,
201 &mut HashSet::new(),
202 );
203}
204
205pub(super) fn collect_inline_test_module_witnesses(
206 ast: &syn::File,
207 direct_refs: &mut HashSet<String>,
208 call_refs: &mut HashSet<String>,
209) {
210 let items = inline_test_items(ast);
211 if items.is_empty() {
212 return;
213 }
214 let file = syn::File {
215 shebang: None,
216 attrs: vec![],
217 items,
218 };
219 collect_rust_references(&file, direct_refs, &mut HashSet::new());
220 collect_rust_call_references(&file, call_refs, &mut HashSet::new());
221}
222
223#[cfg(test)]
224mod definitions_coverage {
225 use super::super::trivial_expr::{
226 is_qualified_or_known_call, is_trivial_expr, is_trivial_stmt, is_well_known_constructor,
227 };
228 use super::*;
229
230 #[test]
231 fn well_known_constructors_recognized() {
232 for name in ["Ok", "Err", "Some", "None"] {
233 assert!(is_well_known_constructor(name));
234 }
235 assert!(!is_well_known_constructor("MyType"));
236 }
237
238 #[test]
239 fn is_delegation_only_block_variants() {
240 assert!(is_delegation_only_block(&syn::parse_str("{}").unwrap()));
241 assert!(is_delegation_only_block(
242 &syn::parse_str("{ crate::run() }").unwrap()
243 ));
244 assert!(!is_delegation_only_block(
245 &syn::parse_str("{ struct Foo; }").unwrap()
246 ));
247 }
248
249 #[test]
250 fn is_trivial_expr_variants() {
251 assert!(is_trivial_expr(&syn::parse_str("42").unwrap()));
252 assert!(is_trivial_expr(&syn::parse_str("x").unwrap()));
253 assert!(is_trivial_expr(&syn::parse_str("lib::run()").unwrap()));
254 assert!(!is_trivial_expr(&syn::parse_str("|| {}").unwrap()));
255 }
256
257 #[test]
258 fn is_trivial_stmt_variants() {
259 assert!(is_trivial_stmt(
260 &syn::parse_str::<syn::Stmt>("Ok(());").unwrap()
261 ));
262 let trivial: syn::Block = syn::parse_str("{ let x = 42; }").unwrap();
263 assert!(trivial.stmts.iter().all(is_trivial_stmt));
264 let non_trivial: syn::Block = syn::parse_str("{ fn inner() {} }").unwrap();
265 assert!(!non_trivial.stmts.iter().all(is_trivial_stmt));
266 }
267
268 #[test]
269 fn is_qualified_or_known_call_variants() {
270 assert!(is_qualified_or_known_call(
271 &syn::parse_str("module::func()").unwrap()
272 ));
273 assert!(is_qualified_or_known_call(
274 &syn::parse_str("Ok(())").unwrap()
275 ));
276 assert!(!is_qualified_or_known_call(
277 &syn::parse_str("unknown_func()").unwrap()
278 ));
279 }
280
281 #[test]
282 fn try_add_def_public_and_private() {
283 let mut defs = Vec::new();
284 try_add_def(
285 &mut defs,
286 "my_func",
287 CodeUnitKind::Function,
288 Path::new("t.rs"),
289 1,
290 None,
291 );
292 assert_eq!(defs.len(), 1);
293 assert_eq!(defs[0].name, "my_func");
294 try_add_def(
295 &mut defs,
296 "_private",
297 CodeUnitKind::Function,
298 Path::new("t.rs"),
299 1,
300 None,
301 );
302 assert_eq!(defs.len(), 1);
303 }
304
305 #[test]
306 fn collect_rust_definitions_on_file() {
307 let code = "fn public_fn() {}\nfn _private_fn() {}\nstruct MyStruct;";
308 let ast: syn::File = syn::parse_str(code).unwrap();
309 let mut defs = Vec::new();
310 collect_rust_definitions(&ast, Path::new("test.rs"), &mut defs);
311 let names: Vec<&str> = defs.iter().map(|d| d.name.as_str()).collect();
312 assert!(names.contains(&"public_fn"));
313 assert!(names.contains(&"MyStruct"));
314 assert!(!names.contains(&"_private_fn"));
315 }
316
317 #[test]
318 fn collect_test_module_references_finds_refs() {
319 let code = r"
320 fn production_fn() {}
321 #[cfg(test)]
322 mod tests {
323 use super::*;
324 #[test]
325 fn test_it() { production_fn(); }
326 }
327 ";
328 let ast: syn::File = syn::parse_str(code).unwrap();
329 let mut refs = HashSet::new();
330 collect_test_module_references(&ast, &mut refs);
331 assert!(refs.contains("production_fn"));
332 }
333}