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 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}