[ty] Fix __file__ type in completions to show str instead of str | None (#22510)

## Summary

The type inference system already correctly special-cases `__file__` to
return `str` for the current module (since the code is executing from an
existing file). However, the completion system was bypassing this logic
and pulling `__file__: str | None` directly from `types.ModuleType` in
typeshed.

This PR adds implicit module globals (like `__file__`, `__name__`, etc.)
with their correctly-typed values to completions, reusing the existing
`module_type_implicit_global_symbol` function that already handles the
special-casing.

Closes https://github.com/astral-sh/ty/issues/2445.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Charlie Marsh
2026-01-12 09:20:32 -05:00
committed by GitHub
parent 29064034ba
commit e4ba29392b
4 changed files with 80 additions and 1 deletions

View File

@@ -8059,6 +8059,17 @@ def f(x: Intersection[int, Any] | str):
);
}
#[test]
fn dunder_file_completion() {
let builder = completion_test_builder("__fil<CURSOR>");
// __file__ should be `str` when accessed within a module, not `str | None`
assert_snapshot!(
builder.skip_keywords().skip_auto_import().type_signatures().build().snapshot(),
@"__file__ :: str",
);
}
/// A way to create a simple single-file (named `main.py`) completion test
/// builder.
///

View File

@@ -4166,6 +4166,34 @@ def function():
");
}
#[test]
fn hover_dunder_file() {
let test = cursor_test(
r#"
__fil<CURSOR>e__
"#,
);
// __file__ should be `str` when accessed within a module, not `str | None`
assert_snapshot!(test.hover(), @r"
str
---------------------------------------------
```python
str
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:1
|
2 | __file__
| ^^^^^-^^
| | |
| | Cursor offset
| source
|
");
}
impl CursorTest {
fn hover(&self) -> String {
use std::fmt::Write;

View File

@@ -1583,7 +1583,7 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool {
all_names.contains(symbol_name)
}
mod implicit_globals {
pub(crate) mod implicit_globals {
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
@@ -1763,6 +1763,32 @@ mod implicit_globals {
smallvec::SmallVec::default()
}
/// Returns an iterator over all implicit module global symbols and their types.
///
/// This is used for completions in the global scope of a module. It returns
/// the correct types for special-cased symbols like `__file__` (which is `str`
/// for the current module, not `str | None`).
pub(crate) fn all_implicit_module_globals(
db: &dyn Db,
) -> impl Iterator<Item = (Name, Type<'_>)> + '_ {
// Special-cased implicit globals that are not in `module_type_symbols`
let special_cased = ["__builtins__", "__debug__", "__warningregistry__"]
.into_iter()
.map(Name::new_static);
// All symbols from ModuleType (already includes `__file__`, `__name__`, etc.)
let module_type_syms = module_type_symbols(db).iter().cloned();
// Combine and map to (name, type) pairs
special_cased
.chain(module_type_syms)
.filter_map(move |name| {
let place = module_type_implicit_global_symbol(db, name.as_str());
// Only include bound symbols (not undefined or possibly-undefined)
place.place.ignore_possibly_undefined().map(|ty| (name, ty))
})
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -10,6 +10,7 @@ use ty_module_resolver::{
};
use crate::Db;
use crate::place::implicit_globals::all_implicit_module_globals;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::FileScopeId;
use crate::semantic_index::semantic_index;
@@ -234,6 +235,19 @@ impl<'db> SemanticModel<'db> {
),
);
}
// Add implicit module globals (like `__file__`, `__name__`, etc.) with their
// correct types. These are added before builtins so that the deduplication
// keeps the correct types (e.g., `__file__` is `str` for the current module,
// not `str | None`).
completions.extend(
all_implicit_module_globals(self.db).map(|(name, ty)| Completion {
name,
ty: Some(ty),
builtin: true,
}),
);
// Builtins are available in all scopes.
let builtins = ModuleName::new_static("builtins").expect("valid module name");
completions.extend(self.module_completions(&builtins));