From 0aa4b9cf786844eee2bedd43aa03ff3a2e589bed Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 30 Sep 2025 09:56:56 +0200 Subject: [PATCH] [ty] No union with `Unknown` for module-global symbols --- .../resources/mdtest/boundness_declaredness/public.md | 8 ++++---- crates/ty_python_semantic/resources/mdtest/del.md | 2 +- .../resources/mdtest/import/conditional.md | 6 +++--- .../resources/mdtest/known_constants.md | 2 +- .../resources/mdtest/narrow/assignment.md | 10 +++++----- .../resources/mdtest/public_types.md | 2 +- crates/ty_python_semantic/src/place.rs | 4 +++- crates/ty_python_semantic/src/types.rs | 6 ++++++ 8 files changed, 24 insertions(+), 16 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md b/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md index 5dc20fa1d4..fd596758ba 100644 --- a/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md +++ b/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md @@ -237,11 +237,11 @@ b: SomeUnknownName = 1 # error: [unresolved-reference] ```py from mod import a, b -reveal_type(a) # revealed: Unknown | Literal[1] +reveal_type(a) # revealed: int reveal_type(b) # revealed: Unknown # All external modifications of `a` are allowed: -a = None +a = None # error: [invalid-assignment] ``` ### Undeclared and possibly unbound @@ -265,11 +265,11 @@ if flag: # on top of this document. from mod import a, b -reveal_type(a) # revealed: Unknown | Literal[1] +reveal_type(a) # revealed: int reveal_type(b) # revealed: Unknown # All external modifications of `a` are allowed: -a = None +a = None # error: [invalid-assignment] ``` ### Undeclared and unbound diff --git a/crates/ty_python_semantic/resources/mdtest/del.md b/crates/ty_python_semantic/resources/mdtest/del.md index e0d8a495c2..ece1b79d7c 100644 --- a/crates/ty_python_semantic/resources/mdtest/del.md +++ b/crates/ty_python_semantic/resources/mdtest/del.md @@ -108,7 +108,7 @@ def foo(): global x def bar(): # allowed, refers to `x` in the global scope - reveal_type(x) # revealed: Unknown | Literal[1] + reveal_type(x) # revealed: int bar() del x # allowed, deletes `x` in the global scope (though we don't track that) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/conditional.md b/crates/ty_python_semantic/resources/mdtest/import/conditional.md index d2896ae2cd..679a96ba2e 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/conditional.md +++ b/crates/ty_python_semantic/resources/mdtest/import/conditional.md @@ -25,8 +25,8 @@ reveal_type(y) # error: [possibly-missing-import] "Member `y` of module `maybe_unbound` may be missing" from maybe_unbound import x, y -reveal_type(x) # revealed: Unknown | Literal[3] -reveal_type(y) # revealed: Unknown | Literal[3] +reveal_type(x) # revealed: int +reveal_type(y) # revealed: int ``` ## Maybe unbound annotated @@ -56,7 +56,7 @@ Importing an annotated name prefers the declared type over the inferred type: # error: [possibly-missing-import] "Member `y` of module `maybe_unbound_annotated` may be missing" from maybe_unbound_annotated import x, y -reveal_type(x) # revealed: Unknown | Literal[3] +reveal_type(x) # revealed: int reveal_type(y) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/known_constants.md b/crates/ty_python_semantic/resources/mdtest/known_constants.md index db53b807dc..91b8b579a9 100644 --- a/crates/ty_python_semantic/resources/mdtest/known_constants.md +++ b/crates/ty_python_semantic/resources/mdtest/known_constants.md @@ -95,7 +95,7 @@ TYPE_CHECKING: bool = ... ```py from constants import TYPE_CHECKING -reveal_type(TYPE_CHECKING) # revealed: Literal[True] +reveal_type(TYPE_CHECKING) # revealed: bool from stub import TYPE_CHECKING diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md index 8f32950252..f692c59835 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md @@ -34,7 +34,7 @@ class _: [reveal_type(a.z) for _ in range(1)] # revealed: Literal[0] def _(): - reveal_type(a.x) # revealed: Unknown | int | None + reveal_type(a.x) # revealed: int | None reveal_type(a.y) # revealed: Unknown | None reveal_type(a.z) # revealed: Unknown | None @@ -75,7 +75,7 @@ class _: if cond(): a = A() - reveal_type(a.x) # revealed: int | None | Unknown + reveal_type(a.x) # revealed: int | None reveal_type(a.y) # revealed: Unknown | None reveal_type(a.z) # revealed: Unknown | None @@ -295,10 +295,10 @@ class C: def _(): # error: [possibly-missing-attribute] - reveal_type(b.a.x[0]) # revealed: Unknown | int | None + reveal_type(b.a.x[0]) # revealed: int | None # error: [possibly-missing-attribute] - reveal_type(b.a.x) # revealed: Unknown | list[int | None] - reveal_type(b.a) # revealed: Unknown | A | None + reveal_type(b.a.x) # revealed: list[int | None] + reveal_type(b.a) # revealed: A | None ``` ## Invalid assignments are not used for narrowing diff --git a/crates/ty_python_semantic/resources/mdtest/public_types.md b/crates/ty_python_semantic/resources/mdtest/public_types.md index 4a6ef1c6fb..1f4d0a5cca 100644 --- a/crates/ty_python_semantic/resources/mdtest/public_types.md +++ b/crates/ty_python_semantic/resources/mdtest/public_types.md @@ -263,7 +263,7 @@ if flag(): x = 1 def f() -> None: - reveal_type(x) # revealed: Unknown | Literal[1, 2] + reveal_type(x) # revealed: int # Function only used inside this branch f() diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 993075cd5d..4ba6a98225 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -822,7 +822,9 @@ fn place_by_id<'db>( ) }); - if scope.file(db).is_stub(db) || scope.scope(db).visibility().is_private() { + if scope.node(db).scope_kind().is_module() { + inferred.map_type(|ty| ty.promote_literals(db)).into() + } else if scope.file(db).is_stub(db) || scope.scope(db).visibility().is_private() { // We generally trust module-level undeclared places in stubs and do not union // with `Unknown`. If we don't do this, simple aliases like `IOError = OSError` in // stubs would result in `IOError` being a union of `OSError` and `Unknown`, which diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index a9111f3581..3021acd23c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6227,6 +6227,12 @@ impl<'db> Type<'db> { } } + /// Replaces any literal types with their corresponding promoted type form (e.g. `Literal["string"]` + /// to `str`, or `def _() -> int` to `Callable[[], int]`). + pub(crate) fn promote_literals(self, db: &'db dyn Db) -> Type<'db> { + self.apply_type_mapping(db, &TypeMapping::PromoteLiterals) + } + /// Locates any legacy `TypeVar`s in this type, and adds them to a set. This is used to build /// up a generic context from any legacy `TypeVar`s that appear in a function parameter list or /// `Generic` specialization.