Skip to content

Commit 9712de0

Browse files
committed
More rigorously handle real stdlibs
1 parent 31987c9 commit 9712de0

File tree

5 files changed

+120
-44
lines changed

5 files changed

+120
-44
lines changed

crates/ty_python_semantic/src/module_resolver/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub use resolver::{resolve_module, resolve_real_module};
99
use ruff_db::system::SystemPath;
1010

1111
use crate::Db;
12-
use crate::module_resolver::resolver::search_paths;
12+
use crate::module_resolver::resolver::{ModuleResolveMode, search_paths};
1313
use resolver::SearchPathIterator;
1414

1515
mod module;
@@ -23,7 +23,9 @@ mod testing;
2323
/// Returns an iterator over all search paths pointing to a system path
2424
pub fn system_module_search_paths(db: &dyn Db) -> SystemModuleSearchPathsIter {
2525
SystemModuleSearchPathsIter {
26-
inner: search_paths(db),
26+
// Always run in `StubsAllowed` mode because we want to include as much as possible
27+
// and we don't care about the "real" stdlib
28+
inner: search_paths(db, ModuleResolveMode::StubsAllowed),
2729
}
2830
}
2931

crates/ty_python_semantic/src/module_resolver/path.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,9 @@ impl SearchPath {
524524
pub(crate) fn is_standard_library(&self) -> bool {
525525
matches!(
526526
&*self.0,
527-
SearchPathInner::StandardLibraryCustom(_) | SearchPathInner::StandardLibraryVendored(_)
527+
SearchPathInner::StandardLibraryCustom(_)
528+
| SearchPathInner::StandardLibraryVendored(_)
529+
| SearchPathInner::StandardLibraryReal(_)
528530
)
529531
}
530532

crates/ty_python_semantic/src/module_resolver/resolver.rs

Lines changed: 112 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
4949
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.
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+
5469
impl 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

180202
impl 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> {
449511
pub(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

455519
impl<'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()

crates/ty_python_semantic/src/site_packages.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ System site-packages will not be used for module resolution.",
501501
root_path,
502502
// We don't need to respect this setting
503503
include_system_site_packages: _,
504-
// FIXME(Gankra): should we inherit info from the parent environment?
504+
// We don't need to inherit any info from the parent environment
505505
parent_environment: _,
506506
} = self;
507507

crates/ty_test/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,6 @@ fn run_test(
283283
extra_paths: configuration.extra_paths().unwrap_or_default().to_vec(),
284284
custom_typeshed: custom_typeshed_path.map(SystemPath::to_path_buf),
285285
site_packages_paths,
286-
// FIXME(Gankra): should this be computed properly?
287286
real_stdlib_path: None,
288287
}
289288
.to_search_paths(db.system(), db.vendored())

0 commit comments

Comments
 (0)