@@ -78,10 +78,12 @@ pub fn scan_path_with_config(
7878 source,
7979 } ) ?;
8080 let config = load_project_plugins ( & root, config) ?;
81- // Workspace discovery runs once per scan. The map is built eagerly
82- // so future slices can wire it into `crate::` resolution and
83- // cross-crate import classification without changing this seam.
84- let _workspace = crate :: workspace:: discover ( & root, & config) ;
81+ // Workspace discovery runs once per scan and feeds Rust resolution
82+ // so `crate::X` targets the importing member's `src/` and
83+ // `use my_workspace_crate::X` classifies as Local instead of
84+ // External. Empty workspace map -> resolution falls back to the
85+ // pre-workspace generic path-walk.
86+ let workspace = crate :: workspace:: discover ( & root, & config) ;
8587
8688 let mut files = Vec :: new ( ) ;
8789 let mut functions = Vec :: new ( ) ;
@@ -256,7 +258,7 @@ pub fn scan_path_with_config(
256258
257259 let alias_map = build_alias_map ( & root, & config) ;
258260 apply_alias_rewrites ( & mut imports, & files, & alias_map) ;
259- resolve_imports ( & files, & mut imports, & config) ;
261+ resolve_imports ( & files, & mut imports, & config, & workspace ) ;
260262 let call_edges = resolve_call_edges ( & files, & functions, & calls) ;
261263
262264 let snapshot_id = snapshot_id ( & files) ;
@@ -3210,7 +3212,12 @@ fn apply_alias_replacement(replacement: &str, suffix: &str) -> String {
32103212 }
32113213}
32123214
3213- fn resolve_imports ( files : & [ FileFact ] , imports : & mut [ ImportFact ] , config : & RaysenseConfig ) {
3215+ fn resolve_imports (
3216+ files : & [ FileFact ] ,
3217+ imports : & mut [ ImportFact ] ,
3218+ config : & RaysenseConfig ,
3219+ workspace : & crate :: workspace:: WorkspaceMap ,
3220+ ) {
32143221 let mut by_path = HashMap :: new ( ) ;
32153222 let mut by_module = HashMap :: new ( ) ;
32163223 let profile = ProjectProfile :: infer ( files) ;
@@ -3234,8 +3241,9 @@ fn resolve_imports(files: &[FileFact], imports: &mut [ImportFact], config: &Rays
32343241 & by_module,
32353242 & profile. include_roots ,
32363243 config,
3244+ workspace,
32373245 ) ;
3238- import. resolution = classify_import ( from_file, import, config) ;
3246+ import. resolution = classify_import ( from_file, import, config, workspace ) ;
32393247 }
32403248}
32413249
@@ -3324,6 +3332,7 @@ fn classify_import(
33243332 from_file : & FileFact ,
33253333 import : & ImportFact ,
33263334 config : & RaysenseConfig ,
3335+ workspace : & crate :: workspace:: WorkspaceMap ,
33273336) -> ImportResolution {
33283337 if import. resolved_file . is_some ( ) {
33293338 return ImportResolution :: Local ;
@@ -3344,7 +3353,9 @@ fn classify_import(
33443353 {
33453354 ImportResolution :: Local
33463355 }
3347- Language :: Rust if rust_target_is_local ( & import. target ) => ImportResolution :: Unresolved ,
3356+ Language :: Rust if rust_target_is_local ( & import. target , workspace) => {
3357+ ImportResolution :: Unresolved
3358+ }
33483359 Language :: TypeScript if import. target . starts_with ( '.' ) => ImportResolution :: Unresolved ,
33493360 Language :: Python if import. target . starts_with ( '.' ) => ImportResolution :: Unresolved ,
33503361 Language :: C | Language :: Cpp if import. kind == "include" => ImportResolution :: Unresolved ,
@@ -3359,8 +3370,9 @@ fn resolve_import(
33593370 by_module : & HashMap < String , usize > ,
33603371 include_roots : & [ PathBuf ] ,
33613372 config : & RaysenseConfig ,
3373+ workspace : & crate :: workspace:: WorkspaceMap ,
33623374) -> Option < usize > {
3363- let candidates = import_candidates ( from_file, import, include_roots, config) ;
3375+ let candidates = import_candidates ( from_file, import, include_roots, config, workspace ) ;
33643376 candidates
33653377 . iter ( )
33663378 . find_map ( |candidate| by_path. get ( candidate) . copied ( ) )
@@ -3374,9 +3386,10 @@ fn import_candidates(
33743386 import : & ImportFact ,
33753387 include_roots : & [ PathBuf ] ,
33763388 config : & RaysenseConfig ,
3389+ workspace : & crate :: workspace:: WorkspaceMap ,
33773390) -> Vec < String > {
33783391 match from_file. language {
3379- Language :: Rust => rust_import_candidates ( & from_file. path , & import. target ) ,
3392+ Language :: Rust => rust_import_candidates ( & from_file. path , & import. target , workspace ) ,
33803393 Language :: Python => python_import_candidates ( & import. target ) ,
33813394 Language :: TypeScript => typescript_import_candidates ( & from_file. path , & import. target ) ,
33823395 Language :: C | Language :: Cpp => {
@@ -3473,21 +3486,57 @@ fn plugin_import_candidates(
34733486 candidates
34743487}
34753488
3476- fn rust_import_candidates ( from_path : & Path , target : & str ) -> Vec < String > {
3477- if !rust_target_is_local ( target) {
3489+ fn rust_import_candidates (
3490+ from_path : & Path ,
3491+ target : & str ,
3492+ workspace : & crate :: workspace:: WorkspaceMap ,
3493+ ) -> Vec < String > {
3494+ if !rust_target_is_local ( target, workspace) {
34783495 return Vec :: new ( ) ;
34793496 }
34803497
3481- let target = normalize_rust_target ( target) ;
34823498 let mut candidates = Vec :: new ( ) ;
34833499
3500+ // Cross-crate target: first segment is a workspace member name.
3501+ // `use beta::thing` -> search inside `beta`'s `src/` rather than
3502+ // walking from the importer's path.
3503+ let trimmed = target. trim ( ) ;
3504+ if !trimmed. starts_with ( "crate::" )
3505+ && !trimmed. starts_with ( "self::" )
3506+ && !trimmed. starts_with ( "super::" )
3507+ {
3508+ if let Some ( ( first, rest) ) = trimmed. split_once ( "::" ) {
3509+ if let Some ( member) = workspace. members_by_crate . get ( first) {
3510+ let prefix_target = rest. replace ( "::" , "/" ) ;
3511+ let prefix_target = prefix_target
3512+ . split ( '/' )
3513+ . filter ( |segment| !segment. is_empty ( ) )
3514+ . collect :: < Vec < _ > > ( )
3515+ . join ( "/" ) ;
3516+ if !prefix_target. is_empty ( ) {
3517+ candidates. push ( normalize_path (
3518+ member. src_dir . join ( format ! ( "{prefix_target}.rs" ) ) ,
3519+ ) ) ;
3520+ candidates. push ( normalize_path (
3521+ member. src_dir . join ( format ! ( "{prefix_target}/mod.rs" ) ) ,
3522+ ) ) ;
3523+ }
3524+ candidates. push ( normalize_path ( member. src_dir . join ( "lib.rs" ) ) ) ;
3525+ candidates. push ( normalize_path ( member. src_dir . join ( "mod.rs" ) ) ) ;
3526+ return candidates;
3527+ }
3528+ }
3529+ }
3530+
3531+ let target = normalize_rust_target ( target) ;
3532+
34843533 for prefix in rust_module_prefixes ( & target) {
34853534 candidates. push ( format ! ( "{prefix}.rs" ) ) ;
34863535 candidates. push ( format ! ( "{prefix}/mod.rs" ) ) ;
34873536 candidates. push ( format ! ( "src/{prefix}.rs" ) ) ;
34883537 candidates. push ( format ! ( "src/{prefix}/mod.rs" ) ) ;
34893538
3490- if let Some ( crate_src) = rust_crate_src_dir ( from_path) {
3539+ if let Some ( crate_src) = rust_crate_src_dir ( from_path, workspace ) {
34913540 candidates. push ( normalize_path ( crate_src. join ( format ! ( "{prefix}.rs" ) ) ) ) ;
34923541 candidates. push ( normalize_path ( crate_src. join ( format ! ( "{prefix}/mod.rs" ) ) ) ) ;
34933542 }
@@ -3496,14 +3545,24 @@ fn rust_import_candidates(from_path: &Path, target: &str) -> Vec<String> {
34963545 candidates
34973546}
34983547
3499- fn rust_target_is_local ( target : & str ) -> bool {
3548+ fn rust_target_is_local ( target : & str , workspace : & crate :: workspace :: WorkspaceMap ) -> bool {
35003549 let target = target. trim ( ) ;
3501- target. starts_with ( "crate::" )
3550+ if target. starts_with ( "crate::" )
35023551 || target. starts_with ( "self::" )
35033552 || target. starts_with ( "super::" )
35043553 || target == "super"
35053554 || target == "self"
35063555 || !target. contains ( "::" )
3556+ {
3557+ return true ;
3558+ }
3559+ // Cross-crate workspace target: `use my_workspace_crate::Foo`.
3560+ if let Some ( ( first, _) ) = target. split_once ( "::" ) {
3561+ if workspace. members_by_crate . contains_key ( first) {
3562+ return true ;
3563+ }
3564+ }
3565+ false
35073566}
35083567
35093568fn normalize_rust_target ( target : & str ) -> String {
@@ -3543,7 +3602,48 @@ fn rust_module_prefixes(target: &str) -> Vec<String> {
35433602 . collect ( )
35443603}
35453604
3546- fn rust_crate_src_dir ( from_path : & Path ) -> Option < PathBuf > {
3605+ fn rust_crate_src_dir (
3606+ from_path : & Path ,
3607+ workspace : & crate :: workspace:: WorkspaceMap ,
3608+ ) -> Option < PathBuf > {
3609+ // Workspace-aware path: use the importing member's `src/` so
3610+ // `crate::X` from `crates/a/src/lib.rs` resolves under
3611+ // `crates/a/src/` rather than whatever `src/` raysense hits first.
3612+ if !workspace. members_by_crate . is_empty ( ) {
3613+ let normalized = normalize_path ( from_path) ;
3614+ let normalized = normalized. trim_end_matches ( '/' ) ;
3615+ let mut best: Option < & crate :: workspace:: MemberCrate > = None ;
3616+ for member in workspace. members_by_crate . values ( ) {
3617+ let manifest_dir = normalize_path ( & member. manifest_dir ) ;
3618+ let manifest_dir = manifest_dir. trim_end_matches ( '/' ) ;
3619+ let prefix = if manifest_dir. is_empty ( ) || manifest_dir == "." {
3620+ String :: new ( )
3621+ } else {
3622+ format ! ( "{manifest_dir}/" )
3623+ } ;
3624+ if prefix. is_empty ( ) || normalized. starts_with ( & prefix) {
3625+ let len = prefix. len ( ) ;
3626+ if best
3627+ . map ( |m| {
3628+ let cur = normalize_path ( & m. manifest_dir ) ;
3629+ let cur = cur. trim_end_matches ( '/' ) ;
3630+ let cur_len = if cur. is_empty ( ) || cur == "." {
3631+ 0
3632+ } else {
3633+ cur. len ( ) + 1
3634+ } ;
3635+ len > cur_len
3636+ } )
3637+ . unwrap_or ( true )
3638+ {
3639+ best = Some ( member) ;
3640+ }
3641+ }
3642+ }
3643+ if let Some ( member) = best {
3644+ return Some ( member. src_dir . clone ( ) ) ;
3645+ }
3646+ }
35473647 let mut out = PathBuf :: new ( ) ;
35483648 for component in from_path. components ( ) {
35493649 let Component :: Normal ( part) = component else {
@@ -4110,6 +4210,97 @@ fn helper() {}
41104210 assert_eq ! ( imports[ 0 ] . alias. as_deref( ) , Some ( "Baz" ) ) ;
41114211 }
41124212
4213+ #[ test]
4214+ fn workspace_internal_use_resolves_as_local ( ) {
4215+ let root = temp_scan_root ( "workspace_local" ) ;
4216+ fs:: create_dir_all ( & root) . unwrap ( ) ;
4217+ std:: fs:: write (
4218+ root. join ( "Cargo.toml" ) ,
4219+ "[workspace]\n members = [\" crates/alpha\" , \" crates/beta\" ]\n " ,
4220+ )
4221+ . unwrap ( ) ;
4222+ for ( member, name, body) in [
4223+ (
4224+ "crates/alpha" ,
4225+ "alpha" ,
4226+ "use beta::thing;\n pub fn use_beta() { let _ = thing(); }\n " ,
4227+ ) ,
4228+ ( "crates/beta" , "beta" , "pub fn thing() {}\n " ) ,
4229+ ] {
4230+ let manifest_dir = root. join ( member) ;
4231+ fs:: create_dir_all ( manifest_dir. join ( "src" ) ) . unwrap ( ) ;
4232+ fs:: write (
4233+ manifest_dir. join ( "Cargo.toml" ) ,
4234+ format ! ( "[package]\n name = \" {name}\" \n version = \" 0.0.0\" \n edition = \" 2021\" \n " ) ,
4235+ )
4236+ . unwrap ( ) ;
4237+ fs:: write ( manifest_dir. join ( "src/lib.rs" ) , body) . unwrap ( ) ;
4238+ }
4239+
4240+ let report = scan_path_with_config ( & root, & RaysenseConfig :: default ( ) ) . unwrap ( ) ;
4241+ fs:: remove_dir_all ( & root) . unwrap ( ) ;
4242+
4243+ let beta_thing_import = report
4244+ . imports
4245+ . iter ( )
4246+ . find ( |import| import. target == "beta::thing" )
4247+ . expect ( "the use beta::thing import is captured" ) ;
4248+ assert_eq ! (
4249+ beta_thing_import. resolution,
4250+ ImportResolution :: Local ,
4251+ "cross-crate workspace import resolves as Local instead of External"
4252+ ) ;
4253+ assert ! ( beta_thing_import. resolved_file. is_some( ) ) ;
4254+ }
4255+
4256+ #[ test]
4257+ fn workspace_crate_self_import_uses_member_src_dir ( ) {
4258+ let root = temp_scan_root ( "workspace_crate_self" ) ;
4259+ fs:: create_dir_all ( & root) . unwrap ( ) ;
4260+ std:: fs:: write (
4261+ root. join ( "Cargo.toml" ) ,
4262+ "[workspace]\n members = [\" crates/alpha\" , \" crates/beta\" ]\n " ,
4263+ )
4264+ . unwrap ( ) ;
4265+ // Both members declare a `module` sibling. The importer in
4266+ // crates/alpha/src/lib.rs uses `crate::module`, which must
4267+ // resolve to crates/alpha/src/module.rs (its own member's
4268+ // source root) -- never crates/beta/src/module.rs.
4269+ for member in [ "crates/alpha" , "crates/beta" ] {
4270+ let name = member. rsplit ( '/' ) . next ( ) . unwrap ( ) ;
4271+ let manifest_dir = root. join ( member) ;
4272+ fs:: create_dir_all ( manifest_dir. join ( "src" ) ) . unwrap ( ) ;
4273+ fs:: write (
4274+ manifest_dir. join ( "Cargo.toml" ) ,
4275+ format ! ( "[package]\n name = \" {name}\" \n version = \" 0.0.0\" \n edition = \" 2021\" \n " ) ,
4276+ )
4277+ . unwrap ( ) ;
4278+ fs:: write ( manifest_dir. join ( "src/module.rs" ) , "pub fn item() {}\n " ) . unwrap ( ) ;
4279+ }
4280+ fs:: write ( root. join ( "crates/alpha/src/lib.rs" ) , "use crate::module;\n " ) . unwrap ( ) ;
4281+ fs:: write ( root. join ( "crates/beta/src/lib.rs" ) , "" ) . unwrap ( ) ;
4282+
4283+ let report = scan_path_with_config ( & root, & RaysenseConfig :: default ( ) ) . unwrap ( ) ;
4284+ fs:: remove_dir_all ( & root) . unwrap ( ) ;
4285+
4286+ let import = report
4287+ . imports
4288+ . iter ( )
4289+ . find ( |import| import. target == "crate::module" )
4290+ . expect ( "crate::module import captured" ) ;
4291+ let resolved_id = import. resolved_file . expect ( "resolved against alpha's src" ) ;
4292+ let resolved_path = report
4293+ . files
4294+ . iter ( )
4295+ . find ( |file| file. file_id == resolved_id)
4296+ . map ( |file| file. path . to_string_lossy ( ) . to_string ( ) )
4297+ . unwrap ( ) ;
4298+ assert ! (
4299+ resolved_path. contains( "crates/alpha/src/module.rs" ) ,
4300+ "crate:: resolution must use the importing member's src dir, got {resolved_path}"
4301+ ) ;
4302+ }
4303+
41134304 #[ test]
41144305 fn extracts_rust_trait_impl_single ( ) {
41154306 let content = "trait Greet {}\n struct Dog;\n impl Greet for Dog {}\n " ;
@@ -4940,7 +5131,12 @@ int run(void) {
49405131 ] ;
49415132 let mut imports = vec ! [ new_import( 0 , "crate::graph" , "use" ) ] ;
49425133
4943- resolve_imports ( & files, & mut imports, & RaysenseConfig :: default ( ) ) ;
5134+ resolve_imports (
5135+ & files,
5136+ & mut imports,
5137+ & RaysenseConfig :: default ( ) ,
5138+ & crate :: workspace:: WorkspaceMap :: default ( ) ,
5139+ ) ;
49445140
49455141 assert_eq ! ( imports[ 0 ] . resolved_file, Some ( 1 ) ) ;
49465142 assert_eq ! ( imports[ 0 ] . resolution, ImportResolution :: Local ) ;
@@ -4958,7 +5154,12 @@ int run(void) {
49585154 new_import( 0 , "../widgets" , "import" ) ,
49595155 ] ;
49605156
4961- resolve_imports ( & files, & mut imports, & RaysenseConfig :: default ( ) ) ;
5157+ resolve_imports (
5158+ & files,
5159+ & mut imports,
5160+ & RaysenseConfig :: default ( ) ,
5161+ & crate :: workspace:: WorkspaceMap :: default ( ) ,
5162+ ) ;
49625163
49635164 assert_eq ! ( imports[ 0 ] . resolved_file, Some ( 1 ) ) ;
49645165 assert_eq ! ( imports[ 1 ] . resolved_file, Some ( 2 ) ) ;
@@ -4974,7 +5175,12 @@ int run(void) {
49745175 ] ;
49755176 let mut imports = vec ! [ new_import( 0 , "crate::memory" , "use" ) ] ;
49765177
4977- resolve_imports ( & files, & mut imports, & RaysenseConfig :: default ( ) ) ;
5178+ resolve_imports (
5179+ & files,
5180+ & mut imports,
5181+ & RaysenseConfig :: default ( ) ,
5182+ & crate :: workspace:: WorkspaceMap :: default ( ) ,
5183+ ) ;
49785184
49795185 assert_eq ! ( imports[ 0 ] . resolved_file, Some ( 1 ) ) ;
49805186 assert_eq ! ( imports[ 0 ] . resolution, ImportResolution :: Local ) ;
@@ -4985,7 +5191,12 @@ int run(void) {
49855191 let files = vec ! [ file( 0 , "src/main.rs" , Language :: Rust ) ] ;
49865192 let mut imports = vec ! [ new_import( 0 , "serde::Serialize" , "use" ) ] ;
49875193
4988- resolve_imports ( & files, & mut imports, & RaysenseConfig :: default ( ) ) ;
5194+ resolve_imports (
5195+ & files,
5196+ & mut imports,
5197+ & RaysenseConfig :: default ( ) ,
5198+ & crate :: workspace:: WorkspaceMap :: default ( ) ,
5199+ ) ;
49895200
49905201 assert_eq ! ( imports[ 0 ] . resolved_file, None ) ;
49915202 assert_eq ! ( imports[ 0 ] . resolution, ImportResolution :: External ) ;
@@ -5005,7 +5216,12 @@ int run(void) {
50055216 new_import( 0 , "missing.h" , "include" ) ,
50065217 ] ;
50075218
5008- resolve_imports ( & files, & mut imports, & RaysenseConfig :: default ( ) ) ;
5219+ resolve_imports (
5220+ & files,
5221+ & mut imports,
5222+ & RaysenseConfig :: default ( ) ,
5223+ & crate :: workspace:: WorkspaceMap :: default ( ) ,
5224+ ) ;
50095225
50105226 assert_eq ! ( imports[ 0 ] . resolution, ImportResolution :: System ) ;
50115227 assert_eq ! ( imports[ 1 ] . resolved_file, Some ( 1 ) ) ;
0 commit comments