1use fallow_types::discover::FileId;
8use fallow_types::extract::ModuleLoadMechanism;
9use rustc_hash::FxHashSet;
10
11use super::types::{
12 ReferencePathId, ReferencePathNode, ReferenceRouteGraphId, ReferenceRouteNodeId,
13};
14use super::{ExportNamespace, ExportSymbol, ModuleGraph, TestReachabilityIndex};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum ExportTestReach {
19 Direct,
22 Transitive,
24 MockedOnly,
27 None,
29 NoneFile,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum TestMaskingState {
36 Active,
38 NoMocks,
40 Capped,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ExportTestReachResult {
48 pub reach: ExportTestReach,
50 pub tests: Vec<FileId>,
54 pub mocked_targets: Vec<FileId>,
58}
59
60#[derive(Default)]
62struct MaskedBits {
63 mocked: bool,
64 targets: FxHashSet<FileId>,
65}
66
67impl ModuleGraph {
68 #[must_use]
70 pub fn test_masking_state(&self) -> TestMaskingState {
71 if self.test_reachability_index.profile_count > 0 {
72 TestMaskingState::Active
73 } else if self.test_reachability_index.masking_capped {
74 TestMaskingState::Capped
75 } else {
76 TestMaskingState::NoMocks
77 }
78 }
79
80 #[must_use]
87 pub fn export_test_reach(
88 &self,
89 file: FileId,
90 export: &ExportSymbol,
91 is_test_file: &dyn Fn(FileId) -> bool,
92 ) -> ExportTestReachResult {
93 let value_references = || {
94 export
95 .references
96 .iter()
97 .enumerate()
98 .filter(|(_, reference)| reference.namespace == ExportNamespace::Value)
99 };
100
101 let mut covered = false;
102 let mut direct_tests: Vec<FileId> = Vec::new();
103 for (index, reference) in value_references() {
104 if !self.is_test_reference_covered(export, index) {
105 continue;
106 }
107 covered = true;
108 if is_test_file(reference.from_file) {
109 direct_tests.push(reference.from_file);
110 }
111 }
112 if !direct_tests.is_empty() {
113 return ExportTestReachResult {
114 reach: ExportTestReach::Direct,
115 tests: sorted_unique(direct_tests),
116 mocked_targets: Vec::new(),
117 };
118 }
119 if covered {
120 return ExportTestReachResult {
121 reach: ExportTestReach::Transitive,
122 tests: Vec::new(),
123 mocked_targets: Vec::new(),
124 };
125 }
126
127 if self.test_masking_state() == TestMaskingState::Active {
128 let mut mocked_tests = Vec::new();
129 let mut mocked_targets = FxHashSet::default();
130 for (index, reference) in value_references() {
131 let Some(path) = export.reference_path(index) else {
132 continue;
133 };
134 let masked = self.test_reachability_index.masked_reference_bits(
135 reference.from_file,
136 path,
137 self,
138 );
139 if !masked.mocked {
140 continue;
141 }
142 mocked_targets.extend(masked.targets);
143 if is_test_file(reference.from_file) {
144 mocked_tests.push(reference.from_file);
145 }
146 }
147 if !mocked_targets.is_empty() {
148 return ExportTestReachResult {
149 reach: ExportTestReach::MockedOnly,
150 tests: sorted_unique(mocked_tests),
151 mocked_targets: sorted_unique(mocked_targets.into_iter().collect()),
152 };
153 }
154 }
155
156 let reach = if self.is_test_reachable(file) {
157 ExportTestReach::None
158 } else {
159 ExportTestReach::NoneFile
160 };
161 ExportTestReachResult {
162 reach,
163 tests: Vec::new(),
164 mocked_targets: Vec::new(),
165 }
166 }
167}
168
169fn sorted_unique(mut files: Vec<FileId>) -> Vec<FileId> {
170 files.sort_unstable_by_key(|file| file.0);
171 files.dedup();
172 files
173}
174
175impl TestReachabilityIndex {
176 fn masked_reference_bits(
184 &self,
185 source: FileId,
186 path: ReferencePathId,
187 graph: &ModuleGraph,
188 ) -> MaskedBits {
189 let mut result = MaskedBits::default();
190 let Some(source_profiles) = self.profiles_for(&self.reachable_profiles, source) else {
191 return result;
192 };
193 let mut chain: Vec<ReferencePathNode> = Vec::new();
194 let mut next = Some(path);
195 while let Some(path_id) = next {
196 let Some(&node) = graph.reference_paths.get(path_id.index()) else {
197 return result;
198 };
199 next = node.parent();
200 chain.push(node);
201 }
202 chain.reverse();
203
204 for (word_index, &source_word) in source_profiles.iter().enumerate() {
205 let mut alive = source_word;
206 for node in &chain {
207 if alive == 0 {
208 break;
209 }
210 alive = match *node {
211 ReferencePathNode::Hop {
212 target, mechanism, ..
213 } => self.masked_hop(target, mechanism, word_index, alive, &mut result),
214 ReferencePathNode::Route {
215 graph: route_graph,
216 start,
217 terminal,
218 start_mechanism,
219 ..
220 } => self.masked_route(
221 graph,
222 RouteQuery {
223 graph_id: route_graph,
224 start,
225 terminal,
226 start_mechanism,
227 word_index,
228 },
229 alive,
230 &mut result,
231 ),
232 };
233 }
234 }
235 result
236 }
237
238 fn masked_hop(
241 &self,
242 target: FileId,
243 mechanism: ModuleLoadMechanism,
244 word_index: usize,
245 alive: u64,
246 result: &mut MaskedBits,
247 ) -> u64 {
248 let mut alive = alive;
249 if matches!(mechanism, ModuleLoadMechanism::EsModule)
250 && let Some(masked_word) = self
251 .masked_profiles_for(target)
252 .and_then(|profiles| profiles.get(word_index))
253 {
254 let masked = alive & masked_word;
255 if masked != 0 {
256 result.mocked = true;
257 result.targets.insert(target);
258 alive &= !masked_word;
259 }
260 }
261 let reachable = self
262 .profiles_for(&self.reachable_profiles, target)
263 .and_then(|profiles| profiles.get(word_index))
264 .copied()
265 .unwrap_or(0);
266 alive & reachable
267 }
268
269 fn masked_route(
272 &self,
273 graph: &ModuleGraph,
274 query: RouteQuery,
275 candidate_profiles: u64,
276 result: &mut MaskedBits,
277 ) -> u64 {
278 let routes = &graph.reference_routes;
279 let Some(route_graph) = routes.graphs.get(query.graph_id.0 as usize) else {
280 return 0;
281 };
282 let node_count = route_graph
283 .nodes
284 .end
285 .saturating_sub(route_graph.nodes.start) as usize;
286 let start_index = query.start.0 as usize;
287 let terminal_index = query.terminal.0 as usize;
288 if start_index >= node_count || terminal_index >= node_count {
289 return 0;
290 }
291
292 let mut attempted = vec![0_u64; node_count];
293 let mut pending = vec![0_u64; node_count];
294 let mut queued = vec![false; node_count];
295 let mut queue = std::collections::VecDeque::from([start_index]);
296 pending[start_index] = candidate_profiles;
297 queued[start_index] = true;
298 let mut successful_profiles = 0_u64;
299
300 while let Some(local_index) = queue.pop_front() {
301 queued[local_index] = false;
302 let incoming = pending[local_index] & !attempted[local_index];
303 pending[local_index] = 0;
304 attempted[local_index] |= incoming;
305 if incoming == 0 {
306 continue;
307 }
308 let Some(node) = routes
309 .nodes
310 .get(route_graph.nodes.start as usize + local_index)
311 else {
312 return 0;
313 };
314 let mechanism = if local_index == start_index {
315 query.start_mechanism
316 } else {
317 Some(node.mechanism)
318 };
319 let active = match mechanism {
322 Some(mechanism) => {
323 self.masked_hop(node.target, mechanism, query.word_index, incoming, result)
324 }
325 None => incoming,
326 };
327 if active == 0 {
328 continue;
329 }
330 if local_index == terminal_index {
331 successful_profiles |= active;
332 continue;
333 }
334 let Some(successors) = routes
335 .edges
336 .get(node.successors.start as usize..node.successors.end as usize)
337 else {
338 return 0;
339 };
340 for successor in successors {
341 let successor_index = successor.0 as usize;
342 if successor_index >= node_count {
343 return 0;
344 }
345 let new_profiles = active & !attempted[successor_index] & !pending[successor_index];
346 if new_profiles == 0 {
347 continue;
348 }
349 pending[successor_index] |= new_profiles;
350 if !queued[successor_index] {
351 queued[successor_index] = true;
352 queue.push_back(successor_index);
353 }
354 }
355 }
356 successful_profiles
357 }
358}
359
360#[derive(Clone, Copy)]
362struct RouteQuery {
363 graph_id: ReferenceRouteGraphId,
364 start: ReferenceRouteNodeId,
365 terminal: ReferenceRouteNodeId,
366 start_mechanism: Option<ModuleLoadMechanism>,
367 word_index: usize,
368}