@@ -47,10 +47,25 @@ pub fn resolve_real_module<'db>(db: &'db dyn Db, module_name: &ModuleName) -> Op
4747/// Which files should be visible when doing a module query
4848#[ derive( Debug , Copy , Clone , PartialEq , Eq , PartialOrd , Ord , Hash ) ]
4949pub ( crate ) enum ModuleResolveMode {
50+ /// Stubs are allowed to appear.
51+ ///
52+ /// This is the "normal" mode almost everything uses, as type checkers are in fact supposed
53+ /// to *prefer* stubs over the actual implementations.
5054 StubsAllowed ,
55+ /// Stubs are not allowed to appear.
56+ ///
57+ /// This is the "goto definition" mode, where we need to ignore the typing spec and find actual
58+ /// implementations. When querying searchpaths this also notably replaces typeshed with
59+ /// the "real" stdlib.
5160 StubsNotAllowed ,
5261}
5362
63+ #[ salsa:: interned]
64+ #[ derive( Debug ) ]
65+ pub ( crate ) struct ModuleResolveModeIngredient < ' db > {
66+ mode : ModuleResolveMode ,
67+ }
68+
5469impl ModuleResolveMode {
5570 fn stubs_allowed ( self ) -> bool {
5671 matches ! ( self , Self :: StubsAllowed )
@@ -124,7 +139,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module<'_>> {
124139
125140 let path = SystemOrVendoredPathRef :: try_from_file ( db, file) ?;
126141
127- let module_name = search_paths ( db) . find_map ( |candidate| {
142+ let module_name = search_paths ( db, ModuleResolveMode :: StubsAllowed ) . find_map ( |candidate| {
128143 let relative_path = match path {
129144 SystemOrVendoredPathRef :: System ( path) => candidate. relativize_system_path ( path) ,
130145 SystemOrVendoredPathRef :: Vendored ( path) => candidate. relativize_vendored_path ( path) ,
@@ -153,8 +168,8 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module<'_>> {
153168 }
154169}
155170
156- pub ( crate ) fn search_paths ( db : & dyn Db ) -> SearchPathIterator {
157- Program :: get ( db) . search_paths ( db) . iter ( db)
171+ pub ( crate ) fn search_paths ( db : & dyn Db , resolve_mode : ModuleResolveMode ) -> SearchPathIterator {
172+ Program :: get ( db) . search_paths ( db) . iter ( db, resolve_mode )
158173}
159174
160175#[ derive( Clone , Debug , PartialEq , Eq ) ]
@@ -164,7 +179,16 @@ pub struct SearchPaths {
164179 /// config settings themselves change.
165180 static_paths : Vec < SearchPath > ,
166181
167- /// site-packages paths are not included in the above field:
182+ /// Path to typeshed, which should come immediately after static paths.
183+ ///
184+ /// This can currently only be None if the `SystemPath` this points to is already in `static_paths`.
185+ stdlib_path : Option < SearchPath > ,
186+
187+ /// Path to the real stdlib, this replaces typeshed (`stdlib_path`) for goto-definition searches
188+ /// ([`ModuleResolveMode::StubsNotAllowed`]).
189+ real_stdlib_path : Option < SearchPath > ,
190+
191+ /// site-packages paths are not included in the above fields:
168192 /// if there are multiple site-packages paths, editable installations can appear
169193 /// *between* the site-packages paths on `sys.path` at runtime.
170194 /// That means we can't know where a second or third `site-packages` path should sit
@@ -173,8 +197,6 @@ pub struct SearchPaths {
173197 site_packages : Vec < SearchPath > ,
174198
175199 typeshed_versions : TypeshedVersions ,
176-
177- real_stdlib_path : Option < SearchPath > ,
178200}
179201
180202impl SearchPaths {
@@ -243,7 +265,11 @@ impl SearchPaths {
243265 )
244266 } ;
245267
246- static_paths. push ( stdlib_path) ;
268+ let real_stdlib_path = if let Some ( path) = real_stdlib_path {
269+ Some ( SearchPath :: real_stdlib ( system, path. clone ( ) ) ?)
270+ } else {
271+ None
272+ } ;
247273
248274 let mut site_packages: Vec < _ > = Vec :: with_capacity ( site_packages_paths. len ( ) ) ;
249275
@@ -276,17 +302,39 @@ impl SearchPaths {
276302 }
277303 } ) ;
278304
279- let real_stdlib_path = if let Some ( path) = real_stdlib_path {
280- Some ( SearchPath :: real_stdlib ( system, path. clone ( ) ) ?)
305+ // Users probably shouldn't do this but... if they've shadowed their stdlib we should deduplicate it away.
306+ // This notably will mess up anything that checks if a search path "is the standard library" as we won't
307+ // "remember" that fact for static paths.
308+ //
309+ // (We used to shove these into static_paths, so the above retain implicitly did this. I am opting to
310+ // preserve this behaviour to avoid getting into the weeds of corner cases.)
311+ let stdlib_path_is_shadowed = stdlib_path
312+ . as_system_path ( )
313+ . map ( |path| seen_paths. contains ( path) )
314+ . unwrap_or ( false ) ;
315+ let real_stdlib_path_is_shadowed = real_stdlib_path
316+ . as_ref ( )
317+ . and_then ( SearchPath :: as_system_path)
318+ . map ( |path| seen_paths. contains ( path) )
319+ . unwrap_or ( false ) ;
320+
321+ let stdlib_path = if stdlib_path_is_shadowed {
322+ None
281323 } else {
324+ Some ( stdlib_path)
325+ } ;
326+ let real_stdlib_path = if real_stdlib_path_is_shadowed {
282327 None
328+ } else {
329+ real_stdlib_path
283330 } ;
284331
285332 Ok ( SearchPaths {
286333 static_paths,
334+ stdlib_path,
335+ real_stdlib_path,
287336 site_packages,
288337 typeshed_versions,
289- real_stdlib_path,
290338 } )
291339 }
292340
@@ -301,22 +349,32 @@ impl SearchPaths {
301349 }
302350 }
303351
304- pub ( super ) fn iter < ' a > ( & ' a self , db : & ' a dyn Db ) -> SearchPathIterator < ' a > {
352+ pub ( super ) fn iter < ' a > (
353+ & ' a self ,
354+ db : & ' a dyn Db ,
355+ mode : ModuleResolveMode ,
356+ ) -> SearchPathIterator < ' a > {
357+ let stdlib_path = self . stdlib ( mode) ;
305358 SearchPathIterator {
306359 db,
307360 static_paths : self . static_paths . iter ( ) ,
361+ stdlib_path,
308362 dynamic_paths : None ,
363+ mode : ModuleResolveModeIngredient :: new ( db, mode) ,
364+ }
365+ }
366+
367+ pub ( crate ) fn stdlib ( & self , mode : ModuleResolveMode ) -> Option < & SearchPath > {
368+ match mode {
369+ ModuleResolveMode :: StubsAllowed => self . stdlib_path . as_ref ( ) ,
370+ ModuleResolveMode :: StubsNotAllowed => self . real_stdlib_path . as_ref ( ) ,
309371 }
310372 }
311373
312374 pub ( crate ) fn custom_stdlib ( & self ) -> Option < & SystemPath > {
313- self . static_paths . iter ( ) . find_map ( |search_path| {
314- if search_path. is_standard_library ( ) {
315- search_path. as_system_path ( )
316- } else {
317- None
318- }
319- } )
375+ self . stdlib_path
376+ . as_ref ( )
377+ . and_then ( SearchPath :: as_system_path)
320378 }
321379
322380 pub ( crate ) fn typeshed_versions ( & self ) -> & TypeshedVersions {
@@ -333,11 +391,15 @@ impl SearchPaths {
333391/// should come between the two `site-packages` directories when it comes to
334392/// module-resolution priority.
335393#[ salsa:: tracked( returns( deref) , heap_size=ruff_memory_usage:: heap_size) ]
336- pub ( crate ) fn dynamic_resolution_paths ( db : & dyn Db ) -> Vec < SearchPath > {
394+ pub ( crate ) fn dynamic_resolution_paths < ' db > (
395+ db : & ' db dyn Db ,
396+ mode : ModuleResolveModeIngredient < ' db > ,
397+ ) -> Vec < SearchPath > {
337398 tracing:: debug!( "Resolving dynamic module resolution paths" ) ;
338399
339400 let SearchPaths {
340401 static_paths,
402+ stdlib_path,
341403 site_packages,
342404 typeshed_versions : _,
343405 real_stdlib_path,
@@ -355,6 +417,15 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
355417 . map ( Cow :: Borrowed )
356418 . collect ( ) ;
357419
420+ // Use the `ModuleResolveMode` to determine which stdlib (if any) to mark as existing
421+ let stdlib = match mode. mode ( db) {
422+ ModuleResolveMode :: StubsAllowed => stdlib_path,
423+ ModuleResolveMode :: StubsNotAllowed => real_stdlib_path,
424+ } ;
425+ if let Some ( path) = stdlib. as_ref ( ) . and_then ( SearchPath :: as_system_path) {
426+ existing_paths. insert ( Cow :: Borrowed ( path) ) ;
427+ }
428+
358429 let files = db. files ( ) ;
359430 let system = db. system ( ) ;
360431
@@ -427,15 +498,6 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
427498 }
428499 }
429500
430- // Append the real stdlib as the very last option in search.
431- // Normally this means it will always be shadowed by typeshed.
432- //
433- // FIXME(Gankra): ideally this should be completely disabled unless we're in
434- // `ModuleResolveMode::NoStubsAllowed`.
435- if let Some ( real_stdlib_path) = real_stdlib_path {
436- dynamic_paths. push ( real_stdlib_path. clone ( ) ) ;
437- }
438-
439501 dynamic_paths
440502}
441503
@@ -449,7 +511,9 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
449511pub ( crate ) struct SearchPathIterator < ' db > {
450512 db : & ' db dyn Db ,
451513 static_paths : std:: slice:: Iter < ' db , SearchPath > ,
514+ stdlib_path : Option < & ' db SearchPath > ,
452515 dynamic_paths : Option < std:: slice:: Iter < ' db , SearchPath > > ,
516+ mode : ModuleResolveModeIngredient < ' db > ,
453517}
454518
455519impl < ' db > Iterator for SearchPathIterator < ' db > {
@@ -459,14 +523,19 @@ impl<'db> Iterator for SearchPathIterator<'db> {
459523 let SearchPathIterator {
460524 db,
461525 static_paths,
526+ stdlib_path,
527+ mode,
462528 dynamic_paths,
463529 } = self ;
464530
465- static_paths. next ( ) . or_else ( || {
466- dynamic_paths
467- . get_or_insert_with ( || dynamic_resolution_paths ( * db) . iter ( ) )
468- . next ( )
469- } )
531+ static_paths
532+ . next ( )
533+ . or_else ( || stdlib_path. take ( ) )
534+ . or_else ( || {
535+ dynamic_paths
536+ . get_or_insert_with ( || dynamic_resolution_paths ( * db, * mode) . iter ( ) )
537+ . next ( )
538+ } )
470539 }
471540}
472541
@@ -603,7 +672,7 @@ fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Opti
603672 let stub_name = name. to_stub_package ( ) ;
604673 let mut is_namespace_package = false ;
605674
606- for search_path in search_paths ( db) {
675+ for search_path in search_paths ( db, mode ) {
607676 // When a builtin module is imported, standard module resolution is bypassed:
608677 // the module name always resolves to the stdlib module,
609678 // even if there's a module of the same name in the first-party root
@@ -994,9 +1063,7 @@ mod tests {
9941063 use ruff_db:: Db ;
9951064 use ruff_db:: files:: { File , FilePath , system_path_to_file} ;
9961065 use ruff_db:: system:: { DbWithTestSystem as _, DbWithWritableSystem as _} ;
997- use ruff_db:: testing:: {
998- assert_const_function_query_was_not_run, assert_function_query_was_not_run,
999- } ;
1066+ use ruff_db:: testing:: assert_function_query_was_not_run;
10001067 use ruff_python_ast:: PythonVersion ;
10011068
10021069 use crate :: db:: tests:: TestDb ;
@@ -1928,7 +1995,12 @@ not_a_directory
19281995 & FilePath :: system( "/y/src/bar.py" )
19291996 ) ;
19301997 let events = db. take_salsa_events ( ) ;
1931- assert_const_function_query_was_not_run ( & db, dynamic_resolution_paths, & events) ;
1998+ assert_function_query_was_not_run (
1999+ & db,
2000+ dynamic_resolution_paths,
2001+ ModuleResolveModeIngredient :: new ( & db, ModuleResolveMode :: StubsAllowed ) ,
2002+ & events,
2003+ ) ;
19322004 }
19332005
19342006 #[ test]
@@ -1997,7 +2069,8 @@ not_a_directory
19972069 . with_site_packages_files ( & [ ( "_foo.pth" , "/src" ) ] )
19982070 . build ( ) ;
19992071
2000- let search_paths: Vec < & SearchPath > = search_paths ( & db) . collect ( ) ;
2072+ let search_paths: Vec < & SearchPath > =
2073+ search_paths ( & db, ModuleResolveMode :: StubsAllowed ) . collect ( ) ;
20012074
20022075 assert ! ( search_paths. contains(
20032076 &&SearchPath :: first_party( db. system( ) , SystemPathBuf :: from( "/src" ) ) . unwrap( )
0 commit comments