From e4ba29392bb768965ba16ba86aba7d482e4f2ea8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jan 2026 09:20:32 -0500 Subject: [PATCH] [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 --- crates/ty_ide/src/completion.rs | 11 ++++++++ crates/ty_ide/src/hover.rs | 28 +++++++++++++++++++ crates/ty_python_semantic/src/place.rs | 28 ++++++++++++++++++- .../ty_python_semantic/src/semantic_model.rs | 14 ++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index f5d33d43c7..85f2ac819e 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -8059,6 +8059,17 @@ def f(x: Intersection[int, Any] | str): ); } + #[test] + fn dunder_file_completion() { + let builder = completion_test_builder("__fil"); + + // __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. /// diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index c44ea3a3d8..8b6918813c 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -4166,6 +4166,34 @@ def function(): "); } + #[test] + fn hover_dunder_file() { + let test = cursor_test( + r#" + __file__ + "#, + ); + + // __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; diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index bd1a616d59..e831cb5d91 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -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)> + '_ { + // 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::*; diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index d4c7078b60..aaeacb2cd6 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -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));