Skip to main content

kiss/rust_test_refs/
mod.rs

1use crate::graph::DependencyGraph;
2use crate::rust_parsing::ParsedRustFile;
3use crate::units::CodeUnitKind;
4use std::collections::{HashMap, HashSet};
5use std::path::{Path, PathBuf};
6use syn::Attribute;
7
8mod coverage;
9mod coverage_map;
10mod scope;
11mod definitions;
12mod propagation;
13mod references;
14mod trivial_expr;
15
16#[cfg(test)]
17mod tests_coverage;
18#[cfg(test)]
19mod tests_coverage_witness;
20
21#[cfg(test)]
22mod tests_1;
23#[cfg(test)]
24mod tests_2;
25#[cfg(test)]
26mod tests_vault;
27
28pub use coverage::compute_rs_weighted_file_pcts;
29pub use definitions::RustCodeDefinition;
30use definitions::{
31    collect_inline_test_module_witnesses, collect_rust_definitions, collect_test_module_references,
32};
33use coverage_map::build_rust_coverage_map;
34use propagation::propagate_transitive_production_refs;
35use references::{
36    collect_per_test_usage, collect_rust_call_references, collect_rust_references,
37    QualifiedModuleRef,
38};
39
40pub use references::rust_test_functions_in;
41
42use crate::test_refs::disambiguation::crate_qualified_module_matches_def;
43use crate::test_refs::file_to_module_suffix;
44use crate::test_refs::CoveringTest;
45
46type PerTestUsage = Vec<(PathBuf, Vec<(String, HashSet<String>)>)>;
47
48#[derive(Debug, Clone)]
49pub struct RustTestRefAnalysis {
50    pub definitions: Vec<RustCodeDefinition>,
51    pub test_references: HashSet<String>,
52    pub call_references: HashSet<String>,
53    pub propagated_references: HashSet<String>,
54    pub unreferenced: Vec<RustCodeDefinition>,
55    /// For each covered definition (file, name), the list of tests that reference it.
56    pub coverage_map: HashMap<(PathBuf, String), Vec<CoveringTest>>,
57}
58
59fn is_rs_file(path: &Path) -> bool {
60    crate::rust_include::is_rust_source_path(path)
61}
62
63fn has_test_naming_pattern(path: &Path) -> bool {
64    path.file_stem()
65        .and_then(|n| n.to_str())
66        .is_some_and(|name| {
67            name.ends_with("_test") || name.starts_with("test_") || name.ends_with("_integration")
68        })
69}
70
71#[must_use]
72pub fn is_rust_test_file(path: &Path) -> bool {
73    is_rs_file(path)
74        && (has_test_naming_pattern(path) || crate::test_refs::is_in_test_directory(path))
75}
76
77pub(crate) fn has_test_attribute(attrs: &[Attribute]) -> bool {
78    attrs.iter().any(|a| a.path().is_ident("test"))
79}
80
81fn cfg_contains_test(tokens: proc_macro2::TokenStream) -> bool {
82    let mut iter = tokens.into_iter();
83    while let Some(token) = iter.next() {
84        match &token {
85            proc_macro2::TokenTree::Ident(ident) if ident == "test" => return true,
86            proc_macro2::TokenTree::Ident(ident) if ident == "not" => {
87                let _ = iter.next();
88            }
89            proc_macro2::TokenTree::Ident(ident) if *ident == "all" || *ident == "any" => {
90                if let Some(proc_macro2::TokenTree::Group(group)) = iter.next()
91                    && cfg_contains_test(group.stream())
92                {
93                    return true;
94                }
95            }
96            proc_macro2::TokenTree::Group(group) => {
97                if cfg_contains_test(group.stream()) {
98                    return true;
99                }
100            }
101            _ => {}
102        }
103    }
104    false
105}
106
107pub(crate) fn has_cfg_test_attribute(attrs: &[Attribute]) -> bool {
108    attrs.iter().any(|a| {
109        if !a.path().is_ident("cfg") {
110            return false;
111        }
112        if let syn::Meta::List(ref list) = a.meta {
113            return cfg_contains_test(list.tokens.clone());
114        }
115        false
116    })
117}
118
119fn is_directly_referenced(
120    def: &RustCodeDefinition,
121    refs: &HashSet<String>,
122    name_files: &HashMap<String, HashSet<PathBuf>>,
123    disambiguation: &HashMap<String, PathBuf>,
124) -> bool {
125    if !refs.contains(&def.name) {
126        return false;
127    }
128    let unique = name_files.get(&def.name).is_none_or(|f| f.len() <= 1);
129    if unique {
130        return true;
131    }
132    if let Some(winner) = disambiguation.get(&def.name) {
133        return *winner == def.file;
134    }
135    false
136}
137
138fn is_impl_method_covered_by_type_and_name(
139    def: &RustCodeDefinition,
140    refs: &HashSet<String>,
141) -> bool {
142    matches!(
143        def.kind,
144        CodeUnitKind::TraitImplMethod | CodeUnitKind::Method
145    ) && refs.contains(&def.name)
146        && def
147            .impl_for_type
148            .as_ref()
149            .is_some_and(|t| refs.contains(t))
150}
151
152pub(super) fn is_covered_by_qualified_ref(
153    def: &RustCodeDefinition,
154    qualified_refs: &HashSet<QualifiedModuleRef>,
155) -> bool {
156    let def_suffix = file_to_module_suffix(&def.file);
157    let stem = def
158        .file
159        .file_stem()
160        .and_then(|s| s.to_str())
161        .unwrap_or("");
162    qualified_refs.iter().any(|(module, name)| {
163        if name != &def.name {
164            return false;
165        }
166        crate_qualified_module_matches_def(&def_suffix, module)
167            || (!stem.is_empty()
168                && module.contains('.')
169                && module.ends_with(&format!(".{stem}")))
170    })
171}
172
173pub(crate) fn is_covered_by_tests(
174    def: &RustCodeDefinition,
175    refs: &HashSet<String>,
176    qualified_refs: &HashSet<QualifiedModuleRef>,
177    name_files: &HashMap<String, HashSet<PathBuf>>,
178    disambiguation: &HashMap<String, PathBuf>,
179) -> bool {
180    is_directly_referenced(def, refs, name_files, disambiguation)
181        || is_impl_method_covered_by_type_and_name(def, refs)
182        || is_covered_by_qualified_ref(def, qualified_refs)
183}
184
185pub fn is_binary_entry_point(path: &Path) -> bool {
186    definitions::is_binary_entry_point(path)
187}
188
189fn collect_non_test_file_refs(
190    ast: &syn::File,
191    test_references: &mut HashSet<String>,
192    test_direct_references: &mut HashSet<String>,
193    call_references: &mut HashSet<String>,
194) {
195    collect_test_module_references(ast, test_references);
196    collect_inline_test_module_witnesses(ast, test_direct_references, call_references);
197}
198
199fn ingest_parsed_rust_file(
200    parsed: &ParsedRustFile,
201    definitions: &mut Vec<RustCodeDefinition>,
202    test_references: &mut HashSet<String>,
203    test_direct_references: &mut HashSet<String>,
204    call_references: &mut HashSet<String>,
205    qualified_references: &mut HashSet<QualifiedModuleRef>,
206    per_test_usage: &mut PerTestUsage,
207) {
208    if is_rust_test_file(&parsed.path) {
209        collect_rust_references(
210            &parsed.ast,
211            test_references,
212            qualified_references,
213        );
214        collect_rust_references(
215            &parsed.ast,
216            test_direct_references,
217            &mut HashSet::new(),
218        );
219        collect_rust_call_references(
220            &parsed.ast,
221            call_references,
222            &mut HashSet::new(),
223        );
224    } else if definitions::is_binary_entry_point(&parsed.path) {
225        collect_non_test_file_refs(
226            &parsed.ast,
227            test_references,
228            test_direct_references,
229            call_references,
230        );
231    } else {
232        collect_rust_definitions(&parsed.ast, &parsed.path, definitions);
233        collect_non_test_file_refs(
234            &parsed.ast,
235            test_references,
236            test_direct_references,
237            call_references,
238        );
239    }
240    let test_funcs = collect_per_test_usage(&parsed.ast);
241    if !test_funcs.is_empty() {
242        per_test_usage.push((parsed.path.clone(), test_funcs));
243    }
244}
245
246fn build_rust_disambiguation(
247    per_test_usage: &PerTestUsage,
248    name_files: &HashMap<String, HashSet<PathBuf>>,
249    test_references: &HashSet<String>,
250    graph: Option<&DependencyGraph>,
251) -> HashMap<String, PathBuf> {
252    #[allow(clippy::type_complexity)]
253    let py_style_usage: Vec<(PathBuf, Vec<(String, HashSet<String>, HashSet<String>)>)> =
254        per_test_usage
255            .iter()
256            .map(|(path, funcs)| {
257                (
258                    path.clone(),
259                    funcs
260                        .iter()
261                        .map(|(id, refs)| (id.clone(), refs.clone(), HashSet::new()))
262                        .collect(),
263                )
264            })
265            .collect();
266    crate::test_refs::build_disambiguation_map(name_files, test_references, &py_style_usage, graph)
267}
268
269pub fn analyze_rust_test_refs(
270    parsed_files: &[&ParsedRustFile],
271    graph: Option<&DependencyGraph>,
272) -> RustTestRefAnalysis {
273    let mut definitions = Vec::new();
274    let mut test_references = HashSet::new();
275    let mut test_direct_references = HashSet::new();
276    let mut call_references = HashSet::new();
277    let mut qualified_references = HashSet::new();
278    let mut per_test_usage: PerTestUsage = Vec::new();
279    for parsed in parsed_files {
280        ingest_parsed_rust_file(
281            parsed,
282            &mut definitions,
283            &mut test_references,
284            &mut test_direct_references,
285            &mut call_references,
286            &mut qualified_references,
287            &mut per_test_usage,
288        );
289    }
290    let production_files: Vec<&ParsedRustFile> = parsed_files
291        .iter()
292        .copied()
293        .filter(|p| {
294            !is_rust_test_file(&p.path) && !definitions::is_binary_entry_point(&p.path)
295        })
296        .collect();
297    let name_files = crate::test_refs::build_name_file_map(
298        definitions
299            .iter()
300            .map(|d| (d.name.as_str(), d.file.as_path())),
301    );
302    propagate_transitive_production_refs(
303        &production_files,
304        &definitions,
305        &name_files,
306        &mut test_references,
307        &mut qualified_references,
308    );
309    let propagated_references: HashSet<String> = test_references
310        .iter()
311        .filter(|name| !test_direct_references.contains(*name))
312        .cloned()
313        .collect();
314    let disambiguation =
315        build_rust_disambiguation(&per_test_usage, &name_files, &test_references, graph);
316    let unreferenced = definitions
317        .iter()
318        .filter(|d| {
319            !is_covered_by_tests(
320                d,
321                &test_references,
322                &qualified_references,
323                &name_files,
324                &disambiguation,
325            )
326        })
327        .cloned()
328        .collect();
329    let coverage_map = build_rust_coverage_map(
330        &definitions,
331        &per_test_usage,
332        &name_files,
333        &disambiguation,
334        &qualified_references,
335    );
336    RustTestRefAnalysis {
337        definitions,
338        test_references,
339        call_references,
340        propagated_references,
341        unreferenced,
342        coverage_map,
343    }
344}