1- use std:: collections:: HashMap ;
1+ use std:: collections:: { BTreeSet , HashMap } ;
22use std:: path:: { Path , PathBuf } ;
33
44use console:: style;
@@ -29,13 +29,17 @@ pub enum CrateMemberBuilderError {
2929}
3030
3131// Type aliases to reduce complexity
32- type WorkspaceProcessResult = ( PathBuf , WorkspaceInfo , Vec < ( String , PathBuf ) > ) ;
32+ pub type CrateWorkspaceMap = HashMap < String , BTreeSet < PathBuf > > ;
33+ pub type CratePathToWorkspaceMap = HashMap < PathBuf , PathBuf > ;
34+
35+ type WorkspaceProcessResult = ( PathBuf , WorkspaceInfo ) ;
3336type ParallelProcessResults = Vec < WorkspaceProcessResult > ;
3437
3538#[ derive( Debug , Clone ) ]
3639pub struct WorkspaceAnalyzer {
3740 workspaces : HashMap < PathBuf , WorkspaceInfo > ,
38- crate_to_workspace : HashMap < String , PathBuf > ,
41+ crate_to_workspaces : CrateWorkspaceMap ,
42+ crate_path_to_workspace : CratePathToWorkspaceMap ,
3943 crate_to_paths : HashMap < String , Vec < PathBuf > > ,
4044}
4145
@@ -226,6 +230,8 @@ impl CrateMemberBuilder {
226230pub struct Dependency {
227231 name : String ,
228232 target : Option < String > ,
233+ path : Option < PathBuf > ,
234+ is_workspace : bool ,
229235}
230236
231237impl Dependency {
@@ -240,12 +246,22 @@ impl Dependency {
240246 pub fn target ( & self ) -> Option < & str > {
241247 self . target . as_deref ( )
242248 }
249+
250+ pub fn path ( & self ) -> Option < & PathBuf > {
251+ self . path . as_ref ( )
252+ }
253+
254+ pub fn is_workspace ( & self ) -> bool {
255+ self . is_workspace
256+ }
243257}
244258
245259#[ derive( Default ) ]
246260pub struct DependencyBuilder {
247261 name : Option < String > ,
248262 target : Option < String > ,
263+ path : Option < PathBuf > ,
264+ is_workspace : bool ,
249265}
250266
251267#[ derive( Error , Debug , Diagnostic ) ]
@@ -263,6 +279,8 @@ impl From<&Dependency> for DependencyBuilder {
263279 Self {
264280 name : Some ( dep. name ( ) . to_string ( ) ) ,
265281 target : dep. target ( ) . map ( |t| t. to_string ( ) ) ,
282+ path : dep. path ( ) . cloned ( ) ,
283+ is_workspace : dep. is_workspace ( ) ,
266284 }
267285 }
268286}
@@ -278,10 +296,22 @@ impl DependencyBuilder {
278296 self
279297 }
280298
299+ pub fn with_path ( mut self , path : impl Into < PathBuf > ) -> Self {
300+ self . path = Some ( path. into ( ) ) ;
301+ self
302+ }
303+
304+ pub fn with_is_workspace ( mut self , is_workspace : bool ) -> Self {
305+ self . is_workspace = is_workspace;
306+ self
307+ }
308+
281309 pub fn build ( self ) -> Result < Dependency , DependencyBuilderError > {
282310 Ok ( Dependency {
283311 name : self . name . ok_or ( DependencyBuilderError :: MissingName ) ?,
284312 target : self . target ,
313+ path : self . path ,
314+ is_workspace : self . is_workspace ,
285315 } )
286316 }
287317}
@@ -296,7 +326,8 @@ impl WorkspaceAnalyzer {
296326 pub fn new ( ) -> Self {
297327 Self {
298328 workspaces : HashMap :: new ( ) ,
299- crate_to_workspace : HashMap :: new ( ) ,
329+ crate_to_workspaces : HashMap :: new ( ) ,
330+ crate_path_to_workspace : HashMap :: new ( ) ,
300331 crate_to_paths : HashMap :: new ( ) ,
301332 }
302333 }
@@ -305,8 +336,12 @@ impl WorkspaceAnalyzer {
305336 & self . workspaces
306337 }
307338
308- pub fn crate_to_workspace ( & self ) -> & HashMap < String , PathBuf > {
309- & self . crate_to_workspace
339+ pub fn crate_to_workspace ( & self ) -> & CrateWorkspaceMap {
340+ & self . crate_to_workspaces
341+ }
342+
343+ pub fn crate_path_to_workspace ( & self ) -> & CratePathToWorkspaceMap {
344+ & self . crate_path_to_workspace
310345 }
311346
312347 pub fn crate_to_paths ( & self ) -> & HashMap < String , Vec < PathBuf > > {
@@ -395,19 +430,41 @@ impl WorkspaceAnalyzer {
395430 }
396431
397432 fn merge_results ( & mut self , results : ParallelProcessResults ) {
398- for ( path, info, crate_mappings) in results {
399- // Populate crate_to_paths mapping from the workspace info
400- for member in & info. members {
401- self . crate_to_paths
433+ for ( workspace_path, mut info) in results {
434+ let workspace_key = workspace_path
435+ . canonicalize ( )
436+ . unwrap_or_else ( |_| workspace_path. clone ( ) ) ;
437+
438+ // Populate crate lookups from the workspace info
439+ for member in & mut info. members {
440+ let crate_path = member
441+ . path
442+ . canonicalize ( )
443+ . unwrap_or_else ( |_| member. path . clone ( ) ) ;
444+
445+ member. path = crate_path. clone ( ) ;
446+
447+ if let Some ( entry) = self . crate_to_paths . get_mut ( & member. name ) {
448+ if !entry. iter ( ) . any ( |existing| existing == & crate_path) {
449+ entry. push ( crate_path. clone ( ) ) ;
450+ }
451+ } else {
452+ self . crate_to_paths
453+ . entry ( member. name . clone ( ) )
454+ . or_default ( )
455+ . push ( crate_path. clone ( ) ) ;
456+ }
457+
458+ self . crate_to_workspaces
402459 . entry ( member. name . clone ( ) )
403460 . or_default ( )
404- . push ( member. path . clone ( ) ) ;
405- }
461+ . insert ( workspace_key. clone ( ) ) ;
406462
407- self . workspaces . insert ( path, info) ;
408- for ( crate_name, workspace_path) in crate_mappings {
409- self . crate_to_workspace . insert ( crate_name, workspace_path) ;
463+ self . crate_path_to_workspace
464+ . insert ( crate_path, workspace_key. clone ( ) ) ;
410465 }
466+
467+ self . workspaces . insert ( workspace_key, info) ;
411468 }
412469 }
413470
@@ -445,7 +502,7 @@ impl WorkspaceAnalyzer {
445502 root : WorkspaceRoot ,
446503 ) -> Result < WorkspaceProcessResult > {
447504 // Process members in parallel and collect both results and errors
448- let results: Vec < Result < ( CrateMember , String ) > > = root
505+ let results: Vec < Result < CrateMember > > = root
449506 . members ( )
450507 . par_iter ( )
451508 . map ( |member| {
@@ -456,18 +513,17 @@ impl WorkspaceAnalyzer {
456513 root. workspace_dependencies ( ) ,
457514 root. path ( ) ,
458515 )
459- . map ( |crate_member| ( crate_member, member. name ( ) . to_string ( ) ) )
460516 . wrap_err_with ( || format ! ( "Failed to analyze crate '{}'" , member. name( ) ) )
461517 } )
462518 . collect ( ) ;
463519
464520 // Separate successful results from errors
465- let mut members_with_mappings = Vec :: new ( ) ;
521+ let mut members = Vec :: new ( ) ;
466522 let mut crate_errors = Vec :: new ( ) ;
467523
468524 for result in results {
469525 match result {
470- Ok ( data ) => members_with_mappings . push ( data ) ,
526+ Ok ( member ) => members . push ( member ) ,
471527 Err ( e) => crate_errors. push ( e) ,
472528 }
473529 }
@@ -477,23 +533,13 @@ impl WorkspaceAnalyzer {
477533 eprintln ! ( "{} {}" , style( "⚠" ) . yellow( ) , error) ;
478534 }
479535
480- let members: Vec < CrateMember > = members_with_mappings
481- . iter ( )
482- . map ( |( m, _) | m. clone ( ) )
483- . collect ( ) ;
484-
485- let crate_mappings: Vec < ( String , PathBuf ) > = members_with_mappings
486- . into_iter ( )
487- . map ( |( _, name) | ( name, root. path ( ) . clone ( ) ) )
488- . collect ( ) ;
489-
490536 let workspace_info = WorkspaceInfo {
491537 name : root. name ( ) . to_string ( ) ,
492538 members,
493539 is_standalone : root. is_standalone ( ) ,
494540 } ;
495541
496- Ok ( ( root. path ( ) . clone ( ) , workspace_info, crate_mappings ) )
542+ Ok ( ( root. path ( ) . clone ( ) , workspace_info) )
497543 }
498544
499545 fn analyze_crate_member (
@@ -520,7 +566,9 @@ impl WorkspaceAnalyzer {
520566
521567#[ cfg( test) ]
522568mod tests {
569+ use std:: collections:: BTreeSet ;
523570 use std:: fs;
571+ use std:: path:: PathBuf ;
524572
525573 use tempfile:: TempDir ;
526574
@@ -601,4 +649,90 @@ crate-a = { path = "../crate-a" }
601649 let crate_b = ws. members . iter ( ) . find ( |m| m. name == "crate-b" ) . unwrap ( ) ;
602650 assert_eq ! ( crate_b. dev_dependencies. len( ) , 1 ) ; // crate-a
603651 }
652+
653+ #[ test]
654+ fn test_duplicate_crate_names_map_to_multiple_workspaces ( ) {
655+ let temp = TempDir :: new ( ) . unwrap ( ) ;
656+ let root = temp. path ( ) ;
657+
658+ let workspace_a = root. join ( "workspace-a" ) ;
659+ let workspace_b = root. join ( "workspace-b" ) ;
660+
661+ fs:: create_dir_all ( workspace_a. join ( "shared/src" ) ) . unwrap ( ) ;
662+ fs:: create_dir_all ( workspace_b. join ( "shared/src" ) ) . unwrap ( ) ;
663+
664+ fs:: write (
665+ workspace_a. join ( "Cargo.toml" ) ,
666+ r#"
667+ [workspace]
668+ members = ["shared"]
669+ "# ,
670+ )
671+ . unwrap ( ) ;
672+ fs:: write (
673+ workspace_a. join ( "shared/Cargo.toml" ) ,
674+ "[package]\n name = \" shared\" \n " ,
675+ )
676+ . unwrap ( ) ;
677+ fs:: write ( workspace_a. join ( "shared/src/lib.rs" ) , "pub fn a() {}" ) . unwrap ( ) ;
678+
679+ fs:: write (
680+ workspace_b. join ( "Cargo.toml" ) ,
681+ r#"
682+ [workspace]
683+ members = ["shared"]
684+ "# ,
685+ )
686+ . unwrap ( ) ;
687+ fs:: write (
688+ workspace_b. join ( "shared/Cargo.toml" ) ,
689+ "[package]\n name = \" shared\" \n " ,
690+ )
691+ . unwrap ( ) ;
692+ fs:: write ( workspace_b. join ( "shared/src/lib.rs" ) , "pub fn b() {}" ) . unwrap ( ) ;
693+
694+ let mut analyzer = WorkspaceAnalyzer :: new ( ) ;
695+ analyzer
696+ . discover_workspaces ( & [ root. to_path_buf ( ) ] , None )
697+ . unwrap ( ) ;
698+
699+ let shared_entries = analyzer
700+ . crate_to_workspace ( )
701+ . get ( "shared" )
702+ . expect ( "shared crate should be indexed" ) ;
703+
704+ let expected_ws_paths: BTreeSet < PathBuf > = [
705+ workspace_a. canonicalize ( ) . unwrap ( ) ,
706+ workspace_b. canonicalize ( ) . unwrap ( ) ,
707+ ]
708+ . into_iter ( )
709+ . collect ( ) ;
710+
711+ let actual_ws_paths: BTreeSet < PathBuf > = shared_entries
712+ . iter ( )
713+ . map ( |p| p. canonicalize ( ) . unwrap ( ) )
714+ . collect ( ) ;
715+
716+ assert_eq ! ( actual_ws_paths, expected_ws_paths) ;
717+
718+ let crate_paths = analyzer
719+ . crate_to_paths ( )
720+ . get ( "shared" )
721+ . expect ( "crate paths should be tracked" ) ;
722+ assert_eq ! ( crate_paths. len( ) , 2 ) ;
723+
724+ for crate_path in crate_paths {
725+ let resolved = crate_path. canonicalize ( ) . unwrap ( ) ;
726+ let ws = analyzer
727+ . crate_path_to_workspace ( )
728+ . get ( crate_path)
729+ . expect ( "crate path should map to workspace" ) ;
730+ let ws_abs = ws. canonicalize ( ) . unwrap ( ) ;
731+ assert ! ( expected_ws_paths. contains( & ws_abs) ) ;
732+ assert ! (
733+ resolved. starts_with( & ws_abs) ,
734+ "crate {resolved:?} should live under workspace {ws_abs:?}"
735+ ) ;
736+ }
737+ }
604738}
0 commit comments