Skip to main content

rspack_core/artifacts/
circular_modules_info.rs

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    // Self references
392    DependencyType::CjsSelfReference
393    | DependencyType::ModuleDecorator
394    // Async boundaries. These edges do not synchronously evaluate the target
395    // module while the current module is initializing.
396    | 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    // Resolve/id-only references. They need a target module for resolution or
405    // ID generation, but do not evaluate the target module.
406    | DependencyType::ImportMetaResolve
407    | DependencyType::ImportMetaResolveContext
408    | DependencyType::RequireResolve
409    | DependencyType::RequireResolveContext
410    | DependencyType::IsIncluded
411    // HMR accept/decline references are invoked by the hot runtime later, not
412    // by normal module initialization.
413    | DependencyType::ImportMetaHotAccept
414    | DependencyType::ImportMetaHotDecline
415    | DependencyType::ModuleHotAccept
416    | DependencyType::ModuleHotDecline
417    // URL, worker, asset, and CSS resource references do not synchronously
418    // execute the referenced module in the current JavaScript module graph.
419    | 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    // Build-time or metadata-only dependencies.
431    | DependencyType::ExportInfoApi
432    | DependencyType::StaticExports
433    | DependencyType::LoaderImport
434    | DependencyType::RstestModulePath
435    | DependencyType::RstestMockModuleId
436    | DependencyType::RstestHoistMock
437    | DependencyType::RstestDynamicImportOrigin
438  )
439}