Skip to content

Commit 7712c2f

Browse files
authored
[ty] don't allow first-party code to shadow stdlib types module (#19128)
1 parent 25bdb67 commit 7712c2f

File tree

3 files changed

+28
-10
lines changed

3 files changed

+28
-10
lines changed

crates/ty_python_semantic/resources/mdtest/import/basic.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,18 @@ python-version = "3.13"
205205
import aifc # error: [unresolved-import]
206206
from distutils import sysconfig # error: [unresolved-import]
207207
```
208+
209+
## Cannot shadow core standard library modules
210+
211+
`types.py`:
212+
213+
```py
214+
x: int
215+
```
216+
217+
```py
218+
# error: [unresolved-import]
219+
from types import x
220+
221+
from types import FunctionType
222+
```

crates/ty_python_semantic/src/module_resolver/resolver.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -535,14 +535,23 @@ struct ModuleNameIngredient<'db> {
535535
pub(super) name: ModuleName,
536536
}
537537

538+
/// Returns `true` if the module name refers to a standard library module which can't be shadowed
539+
/// by a first-party module.
540+
///
541+
/// This includes "builtin" modules, which can never be shadowed at runtime either, as well as the
542+
/// `types` module, which tends to be imported early in Python startup, so can't be consistently
543+
/// shadowed, and is important to type checking.
544+
fn is_non_shadowable(minor_version: u8, module_name: &str) -> bool {
545+
module_name == "types" || ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name)
546+
}
547+
538548
/// Given a module name and a list of search paths in which to lookup modules,
539549
/// attempt to resolve the module name
540550
fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<ResolvedName> {
541551
let program = Program::get(db);
542552
let python_version = program.python_version(db);
543553
let resolver_state = ResolverContext::new(db, python_version);
544-
let is_builtin_module =
545-
ruff_python_stdlib::sys::is_builtin_module(python_version.minor, name.as_str());
554+
let is_non_shadowable = is_non_shadowable(python_version.minor, name.as_str());
546555

547556
let name = RelaxedModuleName::new(name);
548557
let stub_name = name.to_stub_package();
@@ -553,7 +562,8 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<ResolvedName> {
553562
// the module name always resolves to the stdlib module,
554563
// even if there's a module of the same name in the first-party root
555564
// (which would normally result in the stdlib module being overridden).
556-
if is_builtin_module && !search_path.is_standard_library() {
565+
// TODO: offer a diagnostic if there is a first-party module of the same name
566+
if is_non_shadowable && !search_path.is_standard_library() {
557567
continue;
558568
}
559569

crates/ty_python_semantic/tests/corpus.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,6 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> {
116116
.with_context(|| format!("Failed to read test file: {path}"))?;
117117

118118
let mut check_with_file_name = |path: &SystemPath| {
119-
if relative_path.file_name() == Some("types.pyi") {
120-
println!(
121-
"Skipping {relative_path:?}: paths with `types.pyi` as their final segment cause a stack overflow"
122-
);
123-
return;
124-
}
125-
126119
db.memory_file_system().write_file_all(path, &code).unwrap();
127120
File::sync_path(&mut db, path);
128121

0 commit comments

Comments
 (0)