11use crate :: { JsImportPath , ModuleDb , ModuleGraphGeneration , ModuleInfoKind } ;
2+ use biome_fs:: is_node_modules_path;
23use camino:: { Utf8Path , Utf8PathBuf } ;
34use rustc_hash:: FxHashMap ;
45
56/// Strongly connected components of the JavaScript import graph.
67///
78/// Two modules belong to the same component when each is reachable from the
8- /// other by following imports. All resolved edges between indexed JavaScript
9- /// modules are included, so callers may use this as a conservative prefilter
10- /// when they traverse a narrower graph.
9+ /// other by following imports. Resolved edges between indexed JavaScript
10+ /// modules outside `node_modules` are included, so callers may use this as a
11+ /// conservative prefilter when they traverse a narrower graph.
1112///
1213/// See: <https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm>
1314#[ derive( Debug , Eq , PartialEq ) ]
@@ -41,8 +42,9 @@ pub fn js_module_sccs(db: &dyn ModuleDb, generation: ModuleGraphGeneration) -> J
4142 let mut id_by_path = FxHashMap :: default ( ) ;
4243
4344 db. for_each_module ( & mut |module| {
44- if matches ! ( module. kind( db) , ModuleInfoKind :: Js ( _) ) {
45- id_by_path. insert ( module. path ( db) . to_path_buf ( ) , id_by_path. len ( ) as u32 ) ;
45+ let path = module. path ( db) ;
46+ if matches ! ( module. kind( db) , ModuleInfoKind :: Js ( _) ) && !is_node_modules_path ( path) {
47+ id_by_path. insert ( path. to_path_buf ( ) , id_by_path. len ( ) as u32 ) ;
4648 }
4749 } ) ;
4850
@@ -84,7 +86,7 @@ pub fn js_module_sccs(db: &dyn ModuleDb, generation: ModuleGraphGeneration) -> J
8486pub ( super ) fn compute_sccs ( edges : & [ Vec < u32 > ] ) -> ( Vec < u32 > , Vec < u32 > ) {
8587 let node_count = edges. len ( ) ;
8688 let mut visited = vec ! [ false ; node_count] ;
87- let mut next_edge = vec ! [ 0 ; node_count] ;
89+ let mut next_edge = vec ! [ 0u32 ; node_count] ;
8890 let mut finish_order = Vec :: with_capacity ( node_count) ;
8991 let mut stack = Vec :: new ( ) ;
9092
@@ -101,7 +103,7 @@ pub(super) fn compute_sccs(edges: &[Vec<u32>]) -> (Vec<u32>, Vec<u32>) {
101103
102104 while let Some ( & node) = stack. last ( ) {
103105 let node = node as usize ;
104- if let Some ( & next) = edges[ node] . get ( next_edge[ node] ) {
106+ if let Some ( & next) = edges[ node] . get ( next_edge[ node] as usize ) {
105107 next_edge[ node] += 1 ;
106108 if !visited[ next as usize ] {
107109 visited[ next as usize ] = true ;
@@ -158,7 +160,23 @@ pub(super) fn compute_sccs(edges: &[Vec<u32>]) -> (Vec<u32>, Vec<u32>) {
158160
159161#[ cfg( test) ]
160162mod tests {
161- use super :: compute_sccs;
163+ use super :: { JsModuleSccs , compute_sccs} ;
164+ use camino:: { Utf8Path , Utf8PathBuf } ;
165+
166+ fn module_sccs ( paths : & [ & str ] , edges : & [ Vec < u32 > ] ) -> JsModuleSccs {
167+ assert_eq ! ( paths. len( ) , edges. len( ) ) ;
168+ let ( component_by_id, component_sizes) = compute_sccs ( edges) ;
169+ let component_by_path = paths
170+ . iter ( )
171+ . enumerate ( )
172+ . map ( |( id, path) | ( Utf8PathBuf :: from ( * path) , component_by_id[ id] ) )
173+ . collect ( ) ;
174+
175+ JsModuleSccs {
176+ component_by_path,
177+ component_sizes : component_sizes. into_boxed_slice ( ) ,
178+ }
179+ }
162180
163181 #[ test]
164182 fn separates_acyclic_nodes ( ) {
@@ -182,4 +200,76 @@ mod tests {
182200 assert_eq ! ( components[ 4 ] , components[ 5 ] ) ;
183201 assert_eq ! ( sizes[ components[ 4 ] as usize ] , 2 ) ;
184202 }
203+
204+ #[ test]
205+ fn importing_into_cycle_does_not_join_cycle ( ) {
206+ let ( components, sizes) = compute_sccs ( & [ vec ! [ 1 ] , vec ! [ 0 ] , vec ! [ 0 ] ] ) ;
207+
208+ assert_eq ! ( components[ 0 ] , components[ 1 ] ) ;
209+ assert_eq ! ( sizes[ components[ 0 ] as usize ] , 2 ) ;
210+ assert_ne ! ( components[ 2 ] , components[ 0 ] ) ;
211+ assert_eq ! ( sizes[ components[ 2 ] as usize ] , 1 ) ;
212+ }
213+
214+ #[ test]
215+ fn convergence_without_cycle_separates_all_nodes ( ) {
216+ let ( components, sizes) = compute_sccs ( & [ vec ! [ 1 , 2 ] , vec ! [ 3 ] , vec ! [ 3 ] , vec ! [ ] ] ) ;
217+
218+ for ( node, & component) in components. iter ( ) . enumerate ( ) {
219+ assert_eq ! ( sizes[ component as usize ] , 1 ) ;
220+ assert ! ( components[ ..node] . iter( ) . all( |& other| other != component) ) ;
221+ }
222+ }
223+
224+ #[ test]
225+ fn chord_does_not_split_cycle ( ) {
226+ let ( components, sizes) = compute_sccs ( & [ vec ! [ 1 ] , vec ! [ 2 , 3 ] , vec ! [ 3 ] , vec ! [ 0 ] ] ) ;
227+
228+ assert ! (
229+ components
230+ . iter( )
231+ . all( |& component| component == components[ 0 ] )
232+ ) ;
233+ assert_eq ! ( sizes[ components[ 0 ] as usize ] , 4 ) ;
234+ }
235+
236+ #[ test]
237+ fn contains_cycle_between_nodes_in_cycle ( ) {
238+ let sccs = module_sccs ( & [ "/a.js" , "/b.js" ] , & [ vec ! [ 1 ] , vec ! [ 0 ] ] ) ;
239+
240+ assert ! ( sccs. contains_cycle_between( Utf8Path :: new( "/a.js" ) , Utf8Path :: new( "/b.js" ) ) ) ;
241+ assert ! ( sccs. contains_cycle_between( Utf8Path :: new( "/b.js" ) , Utf8Path :: new( "/a.js" ) ) ) ;
242+ }
243+
244+ #[ test]
245+ fn does_not_contain_cycle_for_edge_exiting_cycle ( ) {
246+ let sccs = module_sccs ( & [ "/a.js" , "/b.js" , "/c.js" ] , & [ vec ! [ 1 , 2 ] , vec ! [ 0 ] , vec ! [ ] ] ) ;
247+
248+ assert ! ( sccs. contains_cycle_between( Utf8Path :: new( "/a.js" ) , Utf8Path :: new( "/b.js" ) ) ) ;
249+ assert ! ( !sccs. contains_cycle_between( Utf8Path :: new( "/a.js" ) , Utf8Path :: new( "/c.js" ) ) ) ;
250+ }
251+
252+ #[ test]
253+ fn does_not_contain_cycle_for_single_self_import ( ) {
254+ let sccs = module_sccs ( & [ "/a.js" ] , & [ vec ! [ 0 ] ] ) ;
255+
256+ assert ! ( !sccs. contains_cycle_between( Utf8Path :: new( "/a.js" ) , Utf8Path :: new( "/a.js" ) ) ) ;
257+ }
258+
259+ #[ test]
260+ fn does_not_contain_cycle_between_unrelated_cycles ( ) {
261+ let sccs = module_sccs (
262+ & [ "/a.js" , "/b.js" , "/c.js" , "/d.js" ] ,
263+ & [ vec ! [ 1 ] , vec ! [ 0 ] , vec ! [ 3 ] , vec ! [ 2 ] ] ,
264+ ) ;
265+
266+ assert ! ( !sccs. contains_cycle_between( Utf8Path :: new( "/a.js" ) , Utf8Path :: new( "/c.js" ) ) ) ;
267+ }
268+
269+ #[ test]
270+ fn does_not_contain_cycle_for_unknown_path ( ) {
271+ let sccs = module_sccs ( & [ "/a.js" , "/b.js" ] , & [ vec ! [ 1 ] , vec ! [ 0 ] ] ) ;
272+
273+ assert ! ( !sccs. contains_cycle_between( Utf8Path :: new( "/a.js" ) , Utf8Path :: new( "/unknown.js" ) ) ) ;
274+ }
185275}
0 commit comments