Skip to main content

kiss/rust_test_refs/
definitions.rs

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
10/// Returns true if the file path is a Rust binary entry point.
11///
12/// Excludes paths that contain a **normal** path component named exactly `tests` (Cargo’s
13/// integration-test tree), not substring matches — so e.g. `legacy_tests/src/main.rs` is still
14/// treated as an entry point.
15pub(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
29/// Returns true if the function is a trivial binary entry point that only delegates.
30/// Such functions are excluded from coverage requirements since they cannot be
31/// directly tested (main cannot be called from tests) and contain no real logic.
32pub(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}