Skip to content

Commit a0d64b0

Browse files
committed
More rigorously handle real stdlibs
1 parent 05f9c2f commit a0d64b0

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
@@ -8,7 +8,7 @@ pub use resolver::{resolve_module, resolve_real_module};
88
use ruff_db::system::SystemPath;
99

1010
use crate::Db;
11-
use crate::module_resolver::resolver::search_paths;
11+
use crate::module_resolver::resolver::{ModuleResolveMode, search_paths};
1212
use resolver::SearchPathIterator;
1313

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

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
@@ -37,10 +37,25 @@ pub fn resolve_real_module(db: &dyn Db, module_name: &ModuleName) -> Option<Modu
3737
/// Which files should be visible when doing a module query
3838
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3939
pub(crate) enum ModuleResolveMode {
40+
/// Stubs are allowed to appear.
41+
///
42+
/// This is the "normal" mode almost everything uses, as type checkers are in fact supposed
43+
/// to *prefer* stubs over the actual implementations.
4044
StubsAllowed,
45+
/// Stubs are not allowed to appear.
46+
///
47+
/// This is the "goto definition" mode, where we need to ignore the typing spec and find actual
48+
/// implementations. When querying searchpaths this also notably replaces typeshed with
49+
/// the "real" stdlib.
4150
StubsNotAllowed,
4251
}
4352

53+
#[salsa::interned]
54+
#[derive(Debug)]
55+
pub(crate) struct ModuleResolveModeIngredient<'db> {
56+
mode: ModuleResolveMode,
57+
}
58+
4459
impl ModuleResolveMode {
4560
fn stubs_allowed(self) -> bool {
4661
matches!(self, Self::StubsAllowed)
@@ -108,7 +123,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
108123

109124
let path = SystemOrVendoredPathRef::try_from_file(db, file)?;
110125

111-
let module_name = search_paths(db).find_map(|candidate| {
126+
let module_name = search_paths(db, ModuleResolveMode::StubsAllowed).find_map(|candidate| {
112127
let relative_path = match path {
113128
SystemOrVendoredPathRef::System(path) => candidate.relativize_system_path(path),
114129
SystemOrVendoredPathRef::Vendored(path) => candidate.relativize_vendored_path(path),
@@ -137,8 +152,8 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
137152
}
138153
}
139154

140-
pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
141-
Program::get(db).search_paths(db).iter(db)
155+
pub(crate) fn search_paths(db: &dyn Db, resolve_mode: ModuleResolveMode) -> SearchPathIterator {
156+
Program::get(db).search_paths(db).iter(db, resolve_mode)
142157
}
143158

144159
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -147,7 +162,16 @@ pub struct SearchPaths {
147162
/// These shouldn't ever change unless the config settings themselves change.
148163
static_paths: Vec<SearchPath>,
149164

150-
/// site-packages paths are not included in the above field:
165+
/// Path to typeshed, which should come immediately after static paths.
166+
///
167+
/// This can currently only be None if the `SystemPath` this points to is already in `static_paths`.
168+
stdlib_path: Option<SearchPath>,
169+
170+
/// Path to the real stdlib, this replaces typeshed (`stdlib_path`) for goto-definition searches
171+
/// ([`ModuleResolveMode::StubsNotAllowed`]).
172+
real_stdlib_path: Option<SearchPath>,
173+
174+
/// site-packages paths are not included in the above fields:
151175
/// if there are multiple site-packages paths, editable installations can appear
152176
/// *between* the site-packages paths on `sys.path` at runtime.
153177
/// That means we can't know where a second or third `site-packages` path should sit
@@ -156,8 +180,6 @@ pub struct SearchPaths {
156180
site_packages: Vec<SearchPath>,
157181

158182
typeshed_versions: TypeshedVersions,
159-
160-
real_stdlib_path: Option<SearchPath>,
161183
}
162184

163185
impl SearchPaths {
@@ -226,7 +248,11 @@ impl SearchPaths {
226248
)
227249
};
228250

229-
static_paths.push(stdlib_path);
251+
let real_stdlib_path = if let Some(path) = real_stdlib_path {
252+
Some(SearchPath::real_stdlib(system, path.clone())?)
253+
} else {
254+
None
255+
};
230256

231257
let mut site_packages: Vec<_> = Vec::with_capacity(site_packages_paths.len());
232258

@@ -254,17 +280,39 @@ impl SearchPaths {
254280
}
255281
});
256282

257-
let real_stdlib_path = if let Some(path) = real_stdlib_path {
258-
Some(SearchPath::real_stdlib(system, path.clone())?)
283+
// Users probably shouldn't do this but... if they've shadowed their stdlib we should deduplicate it away.
284+
// This notably will mess up anything that checks if a search path "is the standard library" as we won't
285+
// "remember" that fact for static paths.
286+
//
287+
// (We used to shove these into static_paths, so the above retain implicitly did this. I am opting to
288+
// preserve this behaviour to avoid getting into the weeds of corner cases.)
289+
let stdlib_path_is_shadowed = stdlib_path
290+
.as_system_path()
291+
.map(|path| seen_paths.contains(path))
292+
.unwrap_or(false);
293+
let real_stdlib_path_is_shadowed = real_stdlib_path
294+
.as_ref()
295+
.and_then(SearchPath::as_system_path)
296+
.map(|path| seen_paths.contains(path))
297+
.unwrap_or(false);
298+
299+
let stdlib_path = if stdlib_path_is_shadowed {
300+
None
259301
} else {
302+
Some(stdlib_path)
303+
};
304+
let real_stdlib_path = if real_stdlib_path_is_shadowed {
260305
None
306+
} else {
307+
real_stdlib_path
261308
};
262309

263310
Ok(SearchPaths {
264311
static_paths,
312+
stdlib_path,
313+
real_stdlib_path,
265314
site_packages,
266315
typeshed_versions,
267-
real_stdlib_path,
268316
})
269317
}
270318

@@ -279,22 +327,32 @@ impl SearchPaths {
279327
}
280328
}
281329

282-
pub(super) fn iter<'a>(&'a self, db: &'a dyn Db) -> SearchPathIterator<'a> {
330+
pub(super) fn iter<'a>(
331+
&'a self,
332+
db: &'a dyn Db,
333+
mode: ModuleResolveMode,
334+
) -> SearchPathIterator<'a> {
335+
let stdlib_path = self.stdlib(mode);
283336
SearchPathIterator {
284337
db,
285338
static_paths: self.static_paths.iter(),
339+
stdlib_path,
286340
dynamic_paths: None,
341+
mode: ModuleResolveModeIngredient::new(db, mode),
342+
}
343+
}
344+
345+
pub(crate) fn stdlib(&self, mode: ModuleResolveMode) -> Option<&SearchPath> {
346+
match mode {
347+
ModuleResolveMode::StubsAllowed => self.stdlib_path.as_ref(),
348+
ModuleResolveMode::StubsNotAllowed => self.real_stdlib_path.as_ref(),
287349
}
288350
}
289351

290352
pub(crate) fn custom_stdlib(&self) -> Option<&SystemPath> {
291-
self.static_paths.iter().find_map(|search_path| {
292-
if search_path.is_standard_library() {
293-
search_path.as_system_path()
294-
} else {
295-
None
296-
}
297-
})
353+
self.stdlib_path
354+
.as_ref()
355+
.and_then(SearchPath::as_system_path)
298356
}
299357

300358
pub(crate) fn typeshed_versions(&self) -> &TypeshedVersions {
@@ -311,11 +369,15 @@ impl SearchPaths {
311369
/// should come between the two `site-packages` directories when it comes to
312370
/// module-resolution priority.
313371
#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)]
314-
pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
372+
pub(crate) fn dynamic_resolution_paths<'db>(
373+
db: &'db dyn Db,
374+
mode: ModuleResolveModeIngredient<'db>,
375+
) -> Vec<SearchPath> {
315376
tracing::debug!("Resolving dynamic module resolution paths");
316377

317378
let SearchPaths {
318379
static_paths,
380+
stdlib_path,
319381
site_packages,
320382
typeshed_versions: _,
321383
real_stdlib_path,
@@ -333,6 +395,15 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
333395
.map(Cow::Borrowed)
334396
.collect();
335397

398+
// Use the `ModuleResolveMode` to determine which stdlib (if any) to mark as existing
399+
let stdlib = match mode.mode(db) {
400+
ModuleResolveMode::StubsAllowed => stdlib_path,
401+
ModuleResolveMode::StubsNotAllowed => real_stdlib_path,
402+
};
403+
if let Some(path) = stdlib.as_ref().and_then(SearchPath::as_system_path) {
404+
existing_paths.insert(Cow::Borrowed(path));
405+
}
406+
336407
let files = db.files();
337408
let system = db.system();
338409

@@ -405,15 +476,6 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
405476
}
406477
}
407478

408-
// Append the real stdlib as the very last option in search.
409-
// Normally this means it will always be shadowed by typeshed.
410-
//
411-
// FIXME(Gankra): ideally this should be completely disabled unless we're in
412-
// `ModuleResolveMode::NoStubsAllowed`.
413-
if let Some(real_stdlib_path) = real_stdlib_path {
414-
dynamic_paths.push(real_stdlib_path.clone());
415-
}
416-
417479
dynamic_paths
418480
}
419481

@@ -427,7 +489,9 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
427489
pub(crate) struct SearchPathIterator<'db> {
428490
db: &'db dyn Db,
429491
static_paths: std::slice::Iter<'db, SearchPath>,
492+
stdlib_path: Option<&'db SearchPath>,
430493
dynamic_paths: Option<std::slice::Iter<'db, SearchPath>>,
494+
mode: ModuleResolveModeIngredient<'db>,
431495
}
432496

433497
impl<'db> Iterator for SearchPathIterator<'db> {
@@ -437,14 +501,19 @@ impl<'db> Iterator for SearchPathIterator<'db> {
437501
let SearchPathIterator {
438502
db,
439503
static_paths,
504+
stdlib_path,
505+
mode,
440506
dynamic_paths,
441507
} = self;
442508

443-
static_paths.next().or_else(|| {
444-
dynamic_paths
445-
.get_or_insert_with(|| dynamic_resolution_paths(*db).iter())
446-
.next()
447-
})
509+
static_paths
510+
.next()
511+
.or_else(|| stdlib_path.take())
512+
.or_else(|| {
513+
dynamic_paths
514+
.get_or_insert_with(|| dynamic_resolution_paths(*db, *mode).iter())
515+
.next()
516+
})
448517
}
449518
}
450519

@@ -581,7 +650,7 @@ fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Opti
581650
let stub_name = name.to_stub_package();
582651
let mut is_namespace_package = false;
583652

584-
for search_path in search_paths(db) {
653+
for search_path in search_paths(db, mode) {
585654
// When a builtin module is imported, standard module resolution is bypassed:
586655
// the module name always resolves to the stdlib module,
587656
// even if there's a module of the same name in the first-party root
@@ -932,9 +1001,7 @@ mod tests {
9321001
use ruff_db::Db;
9331002
use ruff_db::files::{File, FilePath, system_path_to_file};
9341003
use ruff_db::system::{DbWithTestSystem as _, DbWithWritableSystem as _};
935-
use ruff_db::testing::{
936-
assert_const_function_query_was_not_run, assert_function_query_was_not_run,
937-
};
1004+
use ruff_db::testing::assert_function_query_was_not_run;
9381005
use ruff_python_ast::PythonVersion;
9391006

9401007
use crate::db::tests::TestDb;
@@ -1848,7 +1915,12 @@ not_a_directory
18481915
&FilePath::system("/y/src/bar.py")
18491916
);
18501917
let events = db.take_salsa_events();
1851-
assert_const_function_query_was_not_run(&db, dynamic_resolution_paths, &events);
1918+
assert_function_query_was_not_run(
1919+
&db,
1920+
dynamic_resolution_paths,
1921+
ModuleResolveModeIngredient::new(&db, ModuleResolveMode::StubsAllowed),
1922+
&events,
1923+
);
18521924
}
18531925

18541926
#[test]
@@ -1917,7 +1989,8 @@ not_a_directory
19171989
.with_site_packages_files(&[("_foo.pth", "/src")])
19181990
.build();
19191991

1920-
let search_paths: Vec<&SearchPath> = search_paths(&db).collect();
1992+
let search_paths: Vec<&SearchPath> =
1993+
search_paths(&db, ModuleResolveMode::StubsAllowed).collect();
19211994

19221995
assert!(search_paths.contains(
19231996
&&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)