1use rspack_collections::{IdentifierMap, IdentifierSet};
2use smallvec::SmallVec;
3
4use crate::{Compilation, ContextTypePrefix, DependencyType, ModuleGraph, ModuleIdentifier};
5
6#[derive(Debug, Default)]
7enum CollectState<T> {
8 #[default]
9 NoCollect,
10 NeedCollect,
11 Collected(T),
12}
13
14#[derive(Debug, Default)]
15pub struct CircularModulesInfo {
16 modules: CollectState<IdentifierSet>,
17 cycle_paths: CollectState<Vec<Vec<ModuleIdentifier>>>,
18}
19
20impl CircularModulesInfo {
21 pub fn enable_collect_modules(&mut self) {
22 self.modules = CollectState::NeedCollect;
23 }
24
25 pub fn enable_collect_cycle_paths(&mut self) {
26 self.cycle_paths = CollectState::NeedCollect;
27 }
28
29 pub fn ensure_circular_modules_info(&mut self, compilation: &Compilation) {
30 let collect_modules = matches!(self.modules, CollectState::NeedCollect);
31 let collect_cycle_paths = matches!(self.cycle_paths, CollectState::NeedCollect);
32 if collect_modules || collect_cycle_paths {
33 if collect_modules {
34 self.modules = CollectState::Collected(Default::default());
35 }
36 if collect_cycle_paths {
37 self.cycle_paths = CollectState::Collected(Default::default());
38 }
39 let graph = CycleGraph::build(compilation.get_module_graph());
40 let detector = CycleDetector::new(&graph, self);
41 detector.find_circular_modules_info();
42 }
43 }
44
45 pub fn modules_mut(&mut self) -> Option<&mut IdentifierSet> {
46 match &mut self.modules {
47 CollectState::Collected(modules) => Some(modules),
48 _ => None,
49 }
50 }
51
52 pub fn is_circular_module(&self, module: &ModuleIdentifier) -> Option<bool> {
53 match &self.modules {
54 CollectState::Collected(modules) => Some(modules.contains(module)),
55 _ => None,
56 }
57 }
58
59 pub fn cycle_paths_mut(&mut self) -> Option<&mut Vec<Vec<ModuleIdentifier>>> {
60 match &mut self.cycle_paths {
61 CollectState::Collected(cycle_paths) => Some(cycle_paths),
62 _ => None,
63 }
64 }
65
66 pub fn cycle_paths(&self) -> Option<&[Vec<ModuleIdentifier>]> {
67 match &self.cycle_paths {
68 CollectState::Collected(cycle_paths) => Some(cycle_paths),
69 _ => None,
70 }
71 }
72}
73
74#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
75struct ModuleIndex(u32);
76
77impl ModuleIndex {
78 #[inline]
79 fn from_usize(index: usize) -> Self {
80 Self(u32::try_from(index).expect("module index should fit in u32"))
81 }
82
83 #[inline]
84 fn to_usize(self) -> usize {
85 usize::try_from(self.0).expect("module index should fit in usize")
86 }
87
88 #[inline]
89 fn incremented(self) -> Self {
90 Self(
91 self
92 .0
93 .checked_add(1)
94 .expect("module index should fit in u32"),
95 )
96 }
97}
98
99type ModuleEdges = SmallVec<[ModuleIndex; 6]>;
100
101struct CycleGraph {
102 modules: Vec<ModuleIdentifier>,
103 edges: Vec<ModuleEdges>,
104 self_loop_modules: Vec<bool>,
105}
106
107impl CycleGraph {
108 fn build(module_graph: &ModuleGraph) -> Self {
109 let mut modules = module_graph
110 .modules()
111 .filter_map(|(&id, module)| module.source().is_some().then_some(id))
112 .collect::<Vec<_>>();
113 modules.sort_unstable();
114
115 let mut indexes = IdentifierMap::default();
116 indexes.reserve(modules.len());
117 for (index, id) in modules.iter().enumerate() {
118 indexes.insert(*id, ModuleIndex::from_usize(index));
119 }
120
121 let mut edges = vec![ModuleEdges::new(); modules.len()];
122 let mut self_loop_modules = vec![false; modules.len()];
123 for (index, module_id) in modules.iter().enumerate() {
124 let module_edges = &mut edges[index];
125 for connection in module_graph.get_outgoing_connections(module_id) {
126 let dependency = module_graph.dependency_by_id(&connection.dependency_id);
127 if should_ignore_dependency_type(*dependency.dependency_type()) {
128 continue;
129 }
130
131 let target_id = *connection.module_identifier();
132 if target_id == *module_id {
133 self_loop_modules[index] = true;
134 continue;
135 }
136
137 if let Some(&target_index) = indexes.get(&target_id) {
138 module_edges.push(target_index);
139 }
140 }
141
142 module_edges.sort_unstable();
143 module_edges.dedup();
144 }
145
146 Self {
147 modules,
148 edges,
149 self_loop_modules,
150 }
151 }
152
153 #[inline]
154 fn edges(&self, index: ModuleIndex) -> &ModuleEdges {
155 &self.edges[index.to_usize()]
156 }
157
158 #[inline]
159 fn module(&self, index: ModuleIndex) -> ModuleIdentifier {
160 self.modules[index.to_usize()]
161 }
162
163 #[inline]
164 fn has_self_loop(&self, index: ModuleIndex) -> bool {
165 self.self_loop_modules[index.to_usize()]
166 }
167}
168
169#[derive(Clone, Copy, Default)]
170struct NodeState {
171 index: Option<ModuleIndex>,
172 low_link: ModuleIndex,
173 on_stack: bool,
174}
175
176struct ConnectFrame {
177 module_index: ModuleIndex,
178 next_edge_index: usize,
179 parent_index: Option<ModuleIndex>,
180}
181
182struct CycleDetector<'a> {
183 graph: &'a CycleGraph,
184 next_index: ModuleIndex,
185 states: Vec<NodeState>,
186 stack: Vec<ModuleIndex>,
187 circular_info: &'a mut CircularModulesInfo,
188}
189
190impl<'a> CycleDetector<'a> {
191 fn new(graph: &'a CycleGraph, circular_info: &'a mut CircularModulesInfo) -> Self {
192 let size = graph.modules.len();
193 Self {
194 graph,
195 next_index: ModuleIndex::default(),
196 states: vec![NodeState::default(); size],
197 stack: Vec::with_capacity(size),
198 circular_info,
199 }
200 }
201
202 fn find_circular_modules_info(mut self) {
203 for module_index in 0..self.graph.modules.len() {
204 let module_index = ModuleIndex::from_usize(module_index);
205 if self.state(module_index).index.is_none() {
206 self.connect(module_index);
207 }
208 }
209 }
210
211 fn connect(&mut self, module_index: ModuleIndex) {
212 self.start_connect(module_index);
213 let mut visit_stack = vec![ConnectFrame {
214 module_index,
215 next_edge_index: 0,
216 parent_index: None,
217 }];
218
219 while !visit_stack.is_empty() {
220 let next_target = {
221 let frame = visit_stack
222 .last_mut()
223 .expect("visit stack should not be empty");
224 let module_index = frame.module_index;
225 let edges = self.graph.edges(module_index);
226 if frame.next_edge_index < edges.len() {
227 let target_index = edges[frame.next_edge_index];
228 frame.next_edge_index += 1;
229 Some((module_index, target_index))
230 } else {
231 None
232 }
233 };
234
235 if let Some((module_index, target_index)) = next_target {
236 if self.state(target_index).index.is_none() {
237 self.start_connect(target_index);
238 visit_stack.push(ConnectFrame {
239 module_index: target_index,
240 next_edge_index: 0,
241 parent_index: Some(module_index),
242 });
243 } else if self.state(target_index).on_stack {
244 self.state_mut(module_index).low_link = self
245 .state(module_index)
246 .low_link
247 .min(self.state(target_index).index.expect("indexed"));
248 }
249 continue;
250 }
251
252 let frame = visit_stack.pop().expect("visit stack should not be empty");
253 let module_index = frame.module_index;
254 self.finish_connect(module_index);
255
256 if let Some(parent_index) = frame.parent_index {
257 self.state_mut(parent_index).low_link = self
258 .state(parent_index)
259 .low_link
260 .min(self.state(module_index).low_link);
261 }
262 }
263 }
264
265 fn start_connect(&mut self, module_index: ModuleIndex) {
266 let index = self.next_index;
267 self.next_index = self.next_index.incremented();
268 self.states[module_index.to_usize()] = NodeState {
269 index: Some(index),
270 low_link: index,
271 on_stack: true,
272 };
273 self.stack.push(module_index);
274 }
275
276 fn finish_connect(&mut self, module_index: ModuleIndex) {
277 if self.state(module_index).low_link == self.state(module_index).index.expect("indexed") {
278 let mut component = Vec::new();
279 loop {
280 let current = self.stack.pop().expect("root should be on the stack");
281 self.state_mut(current).on_stack = false;
282 component.push(current);
283 if current == module_index {
284 break;
285 }
286 }
287
288 if component.len() > 1 || self.graph.has_self_loop(module_index) {
289 self.record_circular_component(component);
290 }
291 }
292 }
293
294 fn record_circular_component(&mut self, mut component: Vec<ModuleIndex>) {
295 if let Some(modules) = self.circular_info.modules_mut() {
296 for m in component.iter() {
297 let module = self.graph.module(*m);
298 modules.insert(module);
299 }
300 }
301
302 let Some(cycle_paths) = self.circular_info.cycle_paths_mut() else {
303 return;
304 };
305
306 if component.len() == 1 {
307 let module_index = component[0];
308 let module = self.graph.module(module_index);
309 cycle_paths.push(vec![module, module]);
310 return;
311 }
312
313 component.sort_unstable();
314 let mut in_component = vec![false; self.graph.modules.len()];
315 for &module_index in &component {
316 in_component[module_index.to_usize()] = true;
317 }
318
319 let module_index = component[0];
320 if let Some(path) = find_cycle_path(self.graph, module_index, &in_component) {
321 cycle_paths.push(path);
322 }
323 }
324
325 #[inline]
326 fn state(&self, index: ModuleIndex) -> &NodeState {
327 &self.states[index.to_usize()]
328 }
329
330 #[inline]
331 fn state_mut(&mut self, index: ModuleIndex) -> &mut NodeState {
332 &mut self.states[index.to_usize()]
333 }
334}
335
336fn find_cycle_path(
337 graph: &CycleGraph,
338 start_index: ModuleIndex,
339 in_component: &[bool],
340) -> Option<Vec<ModuleIdentifier>> {
341 let mut visited = vec![false; graph.modules.len()];
342 let mut stack = vec![(start_index, 0)];
343 let mut path = vec![start_index];
344 visited[start_index.to_usize()] = true;
345
346 while !stack.is_empty() {
347 let next_target = {
348 let (module_index, next_edge_index) = stack.last_mut().expect("stack should not be empty");
349 let edges = graph.edges(*module_index);
350 if *next_edge_index < edges.len() {
351 let target_index = edges[*next_edge_index];
352 *next_edge_index += 1;
353 Some(target_index)
354 } else {
355 None
356 }
357 };
358
359 let Some(target_index) = next_target else {
360 let module_index = path.pop().expect("path should not be empty");
361 visited[module_index.to_usize()] = false;
362 stack.pop();
363 continue;
364 };
365
366 if target_index == start_index {
367 let mut cycle = path.clone();
368 cycle.push(start_index);
369 return Some(
370 cycle
371 .into_iter()
372 .map(|module_index| graph.module(module_index))
373 .collect(),
374 );
375 }
376
377 let target_usize = target_index.to_usize();
378 if in_component[target_usize] && !visited[target_usize] {
379 visited[target_usize] = true;
380 path.push(target_index);
381 stack.push((target_index, 0));
382 }
383 }
384
385 None
386}
387
388fn should_ignore_dependency_type(ty: DependencyType) -> bool {
389 matches!(
390 ty,
391 DependencyType::CjsSelfReference
393 | DependencyType::ModuleDecorator
394 | DependencyType::DynamicImport
397 | DependencyType::DynamicImportEager
398 | DependencyType::DynamicImportWeak
399 | DependencyType::ImportContext
400 | DependencyType::LazyImport
401 | DependencyType::ContextElement(ContextTypePrefix::Import)
402 | DependencyType::RequireEnsure
403 | DependencyType::RequireEnsureItem
404 | DependencyType::ImportMetaResolve
407 | DependencyType::ImportMetaResolveContext
408 | DependencyType::RequireResolve
409 | DependencyType::RequireResolveContext
410 | DependencyType::IsIncluded
411 | DependencyType::ImportMetaHotAccept
414 | DependencyType::ImportMetaHotDecline
415 | DependencyType::ModuleHotAccept
416 | DependencyType::ModuleHotDecline
417 | DependencyType::NewUrl
420 | DependencyType::NewUrlContext
421 | DependencyType::NewWorker
422 | DependencyType::CreateScriptUrl
423 | DependencyType::CssUrl
424 | DependencyType::CssImport
425 | DependencyType::CssCompose
426 | DependencyType::CssExport
427 | DependencyType::CssLocalIdent
428 | DependencyType::CssSelfReferenceLocalIdent
429 | DependencyType::ExtractCSS
430 | DependencyType::ExportInfoApi
432 | DependencyType::StaticExports
433 | DependencyType::LoaderImport
434 | DependencyType::RstestModulePath
435 | DependencyType::RstestMockModuleId
436 | DependencyType::RstestHoistMock
437 | DependencyType::RstestDynamicImportOrigin
438 )
439}