@@ -47,10 +47,25 @@ pub fn resolve_real_module<'db>(db: &'db dyn Db, module_name: &ModuleName) -> Op
47
47
/// Which files should be visible when doing a module query
48
48
#[ derive( Debug , Copy , Clone , PartialEq , Eq , PartialOrd , Ord , Hash ) ]
49
49
pub ( 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.
50
54
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.
51
60
StubsNotAllowed ,
52
61
}
53
62
63
+ #[ salsa:: interned]
64
+ #[ derive( Debug ) ]
65
+ pub ( crate ) struct ModuleResolveModeIngredient < ' db > {
66
+ mode : ModuleResolveMode ,
67
+ }
68
+
54
69
impl ModuleResolveMode {
55
70
fn stubs_allowed ( self ) -> bool {
56
71
matches ! ( self , Self :: StubsAllowed )
@@ -124,7 +139,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module<'_>> {
124
139
125
140
let path = SystemOrVendoredPathRef :: try_from_file ( db, file) ?;
126
141
127
- let module_name = search_paths ( db) . find_map ( |candidate| {
142
+ let module_name = search_paths ( db, ModuleResolveMode :: StubsAllowed ) . find_map ( |candidate| {
128
143
let relative_path = match path {
129
144
SystemOrVendoredPathRef :: System ( path) => candidate. relativize_system_path ( path) ,
130
145
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<'_>> {
153
168
}
154
169
}
155
170
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 )
158
173
}
159
174
160
175
#[ derive( Clone , Debug , PartialEq , Eq ) ]
@@ -164,7 +179,16 @@ pub struct SearchPaths {
164
179
/// config settings themselves change.
165
180
static_paths : Vec < SearchPath > ,
166
181
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:
168
192
/// if there are multiple site-packages paths, editable installations can appear
169
193
/// *between* the site-packages paths on `sys.path` at runtime.
170
194
/// That means we can't know where a second or third `site-packages` path should sit
@@ -173,8 +197,6 @@ pub struct SearchPaths {
173
197
site_packages : Vec < SearchPath > ,
174
198
175
199
typeshed_versions : TypeshedVersions ,
176
-
177
- real_stdlib_path : Option < SearchPath > ,
178
200
}
179
201
180
202
impl SearchPaths {
@@ -243,7 +265,11 @@ impl SearchPaths {
243
265
)
244
266
} ;
245
267
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
+ } ;
247
273
248
274
let mut site_packages: Vec < _ > = Vec :: with_capacity ( site_packages_paths. len ( ) ) ;
249
275
@@ -276,17 +302,39 @@ impl SearchPaths {
276
302
}
277
303
} ) ;
278
304
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
281
323
} else {
324
+ Some ( stdlib_path)
325
+ } ;
326
+ let real_stdlib_path = if real_stdlib_path_is_shadowed {
282
327
None
328
+ } else {
329
+ real_stdlib_path
283
330
} ;
284
331
285
332
Ok ( SearchPaths {
286
333
static_paths,
334
+ stdlib_path,
335
+ real_stdlib_path,
287
336
site_packages,
288
337
typeshed_versions,
289
- real_stdlib_path,
290
338
} )
291
339
}
292
340
@@ -301,22 +349,32 @@ impl SearchPaths {
301
349
}
302
350
}
303
351
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) ;
305
358
SearchPathIterator {
306
359
db,
307
360
static_paths : self . static_paths . iter ( ) ,
361
+ stdlib_path,
308
362
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 ( ) ,
309
371
}
310
372
}
311
373
312
374
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)
320
378
}
321
379
322
380
pub ( crate ) fn typeshed_versions ( & self ) -> & TypeshedVersions {
@@ -333,11 +391,15 @@ impl SearchPaths {
333
391
/// should come between the two `site-packages` directories when it comes to
334
392
/// module-resolution priority.
335
393
#[ 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 > {
337
398
tracing:: debug!( "Resolving dynamic module resolution paths" ) ;
338
399
339
400
let SearchPaths {
340
401
static_paths,
402
+ stdlib_path,
341
403
site_packages,
342
404
typeshed_versions : _,
343
405
real_stdlib_path,
@@ -355,6 +417,15 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
355
417
. map ( Cow :: Borrowed )
356
418
. collect ( ) ;
357
419
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
+
358
429
let files = db. files ( ) ;
359
430
let system = db. system ( ) ;
360
431
@@ -427,15 +498,6 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
427
498
}
428
499
}
429
500
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
-
439
501
dynamic_paths
440
502
}
441
503
@@ -449,7 +511,9 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
449
511
pub ( crate ) struct SearchPathIterator < ' db > {
450
512
db : & ' db dyn Db ,
451
513
static_paths : std:: slice:: Iter < ' db , SearchPath > ,
514
+ stdlib_path : Option < & ' db SearchPath > ,
452
515
dynamic_paths : Option < std:: slice:: Iter < ' db , SearchPath > > ,
516
+ mode : ModuleResolveModeIngredient < ' db > ,
453
517
}
454
518
455
519
impl < ' db > Iterator for SearchPathIterator < ' db > {
@@ -459,14 +523,19 @@ impl<'db> Iterator for SearchPathIterator<'db> {
459
523
let SearchPathIterator {
460
524
db,
461
525
static_paths,
526
+ stdlib_path,
527
+ mode,
462
528
dynamic_paths,
463
529
} = self ;
464
530
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
+ } )
470
539
}
471
540
}
472
541
@@ -603,7 +672,7 @@ fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Opti
603
672
let stub_name = name. to_stub_package ( ) ;
604
673
let mut is_namespace_package = false ;
605
674
606
- for search_path in search_paths ( db) {
675
+ for search_path in search_paths ( db, mode ) {
607
676
// When a builtin module is imported, standard module resolution is bypassed:
608
677
// the module name always resolves to the stdlib module,
609
678
// even if there's a module of the same name in the first-party root
@@ -994,9 +1063,7 @@ mod tests {
994
1063
use ruff_db:: Db ;
995
1064
use ruff_db:: files:: { File , FilePath , system_path_to_file} ;
996
1065
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;
1000
1067
use ruff_python_ast:: PythonVersion ;
1001
1068
1002
1069
use crate :: db:: tests:: TestDb ;
@@ -1928,7 +1995,12 @@ not_a_directory
1928
1995
& FilePath :: system( "/y/src/bar.py" )
1929
1996
) ;
1930
1997
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
+ ) ;
1932
2004
}
1933
2005
1934
2006
#[ test]
@@ -1997,7 +2069,8 @@ not_a_directory
1997
2069
. with_site_packages_files ( & [ ( "_foo.pth" , "/src" ) ] )
1998
2070
. build ( ) ;
1999
2071
2000
- let search_paths: Vec < & SearchPath > = search_paths ( & db) . collect ( ) ;
2072
+ let search_paths: Vec < & SearchPath > =
2073
+ search_paths ( & db, ModuleResolveMode :: StubsAllowed ) . collect ( ) ;
2001
2074
2002
2075
assert ! ( search_paths. contains(
2003
2076
&&SearchPath :: first_party( db. system( ) , SystemPathBuf :: from( "/src" ) ) . unwrap( )
0 commit comments