Compare commits

...

3 Commits

Author SHA1 Message Date
David Peter
9452f2c7f6 Update more tests 2025-10-01 10:01:51 +02:00
David Peter
99ebf3746c Do not promote module literals 2025-10-01 09:54:57 +02:00
David Peter
0aa4b9cf78 [ty] No union with Unknown for module-global symbols 2025-10-01 09:54:57 +02:00
23 changed files with 99 additions and 72 deletions

View File

@@ -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

View File

@@ -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)
```

View File

@@ -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
```

View File

@@ -784,7 +784,7 @@ class A: ...
from subexporter import *
# TODO: Should we avoid including `Unknown` for this case?
reveal_type(__all__) # revealed: Unknown | list[Unknown | str]
reveal_type(__all__) # revealed: list[Unknown | str]
__all__.append("B")

View File

@@ -40,10 +40,10 @@ def __getattr__(name: str) -> int:
import mixed_module
# Explicit attribute should take precedence
reveal_type(mixed_module.explicit_attr) # revealed: Unknown | Literal["explicit"]
reveal_type(mixed_module.explicit_attr) # revealed: str
# `__getattr__` should handle unknown attributes
reveal_type(mixed_module.dynamic_attr) # revealed: str
reveal_type(mixed_module.dynamic_attr) # revealed: int
```
`mixed_module.py`:
@@ -51,8 +51,8 @@ reveal_type(mixed_module.dynamic_attr) # revealed: str
```py
explicit_attr = "explicit"
def __getattr__(name: str) -> str:
return "dynamic"
def __getattr__(name: str) -> int:
return 1
```
## Precedence: submodules vs `__getattr__`

View File

@@ -91,19 +91,23 @@ If there's a namespace package with the same name as a module, the module takes
`foo.py`:
```py
x = "module"
class FromModule: ...
x = FromModule
```
`foo/bar.py`:
```py
x = "namespace"
class FromNamespace: ...
x = FromNamespace
```
```py
from foo import x
reveal_type(x) # revealed: Unknown | Literal["module"]
reveal_type(x) # revealed: <class 'FromModule'>
import foo.bar # error: [unresolved-import]
```

View File

@@ -144,8 +144,8 @@ X = (Y := 3) + 4
```py
from exporter import *
reveal_type(X) # revealed: Unknown | Literal[7]
reveal_type(Y) # revealed: Unknown | Literal[3]
reveal_type(X) # revealed: int
reveal_type(Y) # revealed: int
```
### Global-scope symbols defined in many other ways
@@ -781,9 +781,9 @@ else:
from exporter import *
# error: [possibly-unresolved-reference]
reveal_type(A) # revealed: Unknown | Literal[1]
reveal_type(A) # revealed: int
reveal_type(B) # revealed: Unknown | Literal[2, 3]
reveal_type(B) # revealed: int
```
### Reachability constraints in the importing module
@@ -804,7 +804,7 @@ if coinflip():
from exporter import *
# error: [possibly-unresolved-reference]
reveal_type(A) # revealed: Unknown | Literal[1]
reveal_type(A) # revealed: int
```
### Reachability constraints in the exporting module *and* the importing module

View File

@@ -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

View File

@@ -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

View File

@@ -167,11 +167,11 @@ if c.x is not None:
if c.x is not None:
def _():
reveal_type(c.x) # revealed: Unknown | int | None
reveal_type(c.x) # revealed: int | None
def _():
if c.x is not None:
reveal_type(c.x) # revealed: (Unknown & ~None) | int
reveal_type(c.x) # revealed: int
```
## Subscript narrowing

View File

@@ -86,7 +86,7 @@ class B:
reveal_type(a.x) # revealed: Literal["a"]
def f():
reveal_type(a.x) # revealed: Unknown | str | None
reveal_type(a.x) # revealed: str | None
[reveal_type(a.x) for _ in range(1)] # revealed: Literal["a"]
@@ -96,7 +96,7 @@ class C:
reveal_type(a.x) # revealed: str | None
def g():
reveal_type(a.x) # revealed: Unknown | str | None
reveal_type(a.x) # revealed: str | None
[reveal_type(a.x) for _ in range(1)] # revealed: str | None
@@ -109,7 +109,7 @@ class D:
reveal_type(a.x) # revealed: Literal["a"]
def h():
reveal_type(a.x) # revealed: Unknown | str | None
reveal_type(a.x) # revealed: str | None
# TODO: should be `str | None`
[reveal_type(a.x) for _ in range(1)] # revealed: Literal["a"]
@@ -190,7 +190,7 @@ def f(x: str | None):
reveal_type(g) # revealed: str
if a.x is not None:
reveal_type(a.x) # revealed: (Unknown & ~None) | str
reveal_type(a.x) # revealed: str
if l[0] is not None:
reveal_type(l[0]) # revealed: str
@@ -206,7 +206,7 @@ def f(x: str | None):
reveal_type(g) # revealed: str
if a.x is not None:
reveal_type(a.x) # revealed: (Unknown & ~None) | str
reveal_type(a.x) # revealed: str
if l[0] is not None:
reveal_type(l[0]) # revealed: str
@@ -382,12 +382,12 @@ def f():
if a.x is not None:
def _():
# Lazy nested scope narrowing is not performed on attributes/subscripts because it's difficult to track their changes.
reveal_type(a.x) # revealed: Unknown | str | None
reveal_type(a.x) # revealed: str | None
class D:
reveal_type(a.x) # revealed: (Unknown & ~None) | str
reveal_type(a.x) # revealed: str
[reveal_type(a.x) for _ in range(1)] # revealed: (Unknown & ~None) | str
[reveal_type(a.x) for _ in range(1)] # revealed: str
if l[0] is not None:
def _():
@@ -473,11 +473,11 @@ def f():
if a.x is not None:
def _():
if a.x != 1:
reveal_type(a.x) # revealed: (Unknown & ~Literal[1]) | str | None
reveal_type(a.x) # revealed: str | None
class D:
if a.x != 1:
reveal_type(a.x) # revealed: (Unknown & ~Literal[1] & ~None) | str
reveal_type(a.x) # revealed: str
if l[0] is not None:
def _():

View File

@@ -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()

View File

@@ -29,8 +29,8 @@ if flag():
chr: int = 1
def _():
# TODO: Should ideally be `Unknown | Literal[1] | (def abs(x: SupportsAbs[_T], /) -> _T)`
reveal_type(abs) # revealed: Unknown | Literal[1]
# TODO: Should ideally be `Literal[1] | (def abs(x: SupportsAbs[_T], /) -> _T)`
reveal_type(abs) # revealed: int
# TODO: Should ideally be `int | (def chr(i: SupportsIndex, /) -> str)`
reveal_type(chr) # revealed: int
```

View File

@@ -12,7 +12,7 @@ Function definitions are evaluated lazily.
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[1, 2]
reveal_type(x) # revealed: int
x = 2
```
@@ -283,7 +283,7 @@ x = 1
def _():
class C:
# revealed: Unknown | Literal[1]
# revealed: int
[reveal_type(x) for _ in [1]]
x = 2
```
@@ -389,7 +389,7 @@ x = int
class C:
var: ClassVar[x]
reveal_type(C.var) # revealed: Unknown | int | str
reveal_type(C.var) # revealed: int | str
x = str
```

View File

@@ -8,7 +8,7 @@ A name reference to a never-defined symbol in a function is implicitly a global
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[1]
reveal_type(x) # revealed: int
```
## Explicit global in function
@@ -18,7 +18,7 @@ x = 1
def f():
global x
reveal_type(x) # revealed: Unknown | Literal[1]
reveal_type(x) # revealed: int
```
## Unassignable type in function
@@ -201,7 +201,7 @@ x = 42
def f():
global x
reveal_type(x) # revealed: Unknown | Literal[42]
reveal_type(x) # revealed: int
x = "56"
reveal_type(x) # revealed: Literal["56"]
```

View File

@@ -73,10 +73,10 @@ __spec__ = 42 # error: [invalid-assignment] "Object of type `Literal[42]` is no
```py
import module
reveal_type(module.__file__) # revealed: Unknown | None
reveal_type(module.__file__) # revealed: None
reveal_type(module.__path__) # revealed: list[str]
reveal_type(module.__doc__) # revealed: Unknown
reveal_type(module.__spec__) # revealed: Unknown | ModuleSpec | None
reveal_type(module.__spec__) # revealed: ModuleSpec | None
# error: [unresolved-attribute]
reveal_type(module.__warningregistry__) # revealed: Unknown

View File

@@ -17,7 +17,7 @@ class C:
x = 2
# error: [possibly-missing-attribute] "Attribute `x` on type `<class 'C'>` may be missing"
reveal_type(C.x) # revealed: Unknown | Literal[2]
reveal_type(C.x) # revealed: Unknown | int
reveal_type(C.y) # revealed: Unknown | Literal[1]
```
@@ -37,7 +37,7 @@ class C:
# Possibly unbound variables in enclosing scopes are considered bound.
y = x
reveal_type(C.y) # revealed: Unknown | Literal[1, "abc"]
reveal_type(C.y) # revealed: Unknown | Literal[1] | str
```
## Possibly unbound in class scope with multiple declarations

View File

@@ -1,2 +1 @@
spark # too many iterations (in `exported_names` query)
steam.py # hangs (single threaded)

View File

@@ -822,7 +822,11 @@ 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, false))
.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

View File

@@ -1171,20 +1171,37 @@ impl<'db> Type<'db> {
/// Note that this function tries to promote literals to a more user-friendly form than their
/// fallback instance type. For example, `def _() -> int` is promoted to `Callable[[], int]`,
/// as opposed to `FunctionType`.
pub(crate) fn promote_literals(self, db: &'db dyn Db) -> Type<'db> {
self.apply_type_mapping(db, &TypeMapping::PromoteLiterals)
pub(crate) fn promote_literals(
self,
db: &'db dyn Db,
promote_modules_and_functions: bool,
) -> Type<'db> {
self.apply_type_mapping(
db,
&TypeMapping::PromoteLiterals {
promote_modules_and_functions,
},
)
}
/// Like [`Type::promote_literals`], but does not recurse into nested types.
fn promote_literals_impl(self, db: &'db dyn Db) -> Type<'db> {
fn promote_literals_impl(
self,
db: &'db dyn Db,
promote_modules_and_functions: bool,
) -> Type<'db> {
match self {
Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_instance(db),
Type::BooleanLiteral(_) => KnownClass::Bool.to_instance(db),
Type::IntLiteral(_) => KnownClass::Int.to_instance(db),
Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_instance(db),
Type::EnumLiteral(literal) => literal.enum_class_instance(db),
Type::FunctionLiteral(literal) => Type::Callable(literal.into_callable_type(db)),
Type::ModuleLiteral(_) if promote_modules_and_functions => {
KnownClass::ModuleType.to_instance(db)
}
Type::FunctionLiteral(literal) if promote_modules_and_functions => {
Type::Callable(literal.into_callable_type(db))
}
_ => self,
}
}
@@ -6037,7 +6054,7 @@ impl<'db> Type<'db> {
self
}
}
TypeMapping::PromoteLiterals
TypeMapping::PromoteLiterals { .. }
| TypeMapping::BindLegacyTypevars(_)
| TypeMapping::MarkTypeVarsInferable(_) => self,
TypeMapping::Materialize(materialization_kind) => {
@@ -6059,7 +6076,7 @@ impl<'db> Type<'db> {
self
}
}
TypeMapping::PromoteLiterals
TypeMapping::PromoteLiterals { .. }
| TypeMapping::BindLegacyTypevars(_)
| TypeMapping::BindSelf(_)
| TypeMapping::ReplaceSelf { .. }
@@ -6074,7 +6091,7 @@ impl<'db> Type<'db> {
}
TypeMapping::Specialization(_) |
TypeMapping::PartialSpecialization(_) |
TypeMapping::PromoteLiterals |
TypeMapping::PromoteLiterals { .. } |
TypeMapping::BindSelf(_) |
TypeMapping::ReplaceSelf { .. } |
TypeMapping::MarkTypeVarsInferable(_) |
@@ -6085,7 +6102,7 @@ impl<'db> Type<'db> {
let function = Type::FunctionLiteral(function.apply_type_mapping_impl(db, type_mapping, visitor));
match type_mapping {
TypeMapping::PromoteLiterals => function.promote_literals_impl(db),
TypeMapping::PromoteLiterals { promote_modules_and_functions } => function.promote_literals_impl(db, *promote_modules_and_functions),
_ => function
}
}
@@ -6193,7 +6210,7 @@ impl<'db> Type<'db> {
TypeMapping::ReplaceSelf { .. } |
TypeMapping::MarkTypeVarsInferable(_) |
TypeMapping::Materialize(_) => self,
TypeMapping::PromoteLiterals => self.promote_literals_impl(db)
TypeMapping::PromoteLiterals { promote_modules_and_functions } => self.promote_literals_impl(db, *promote_modules_and_functions)
}
Type::Dynamic(_) => match type_mapping {
@@ -6203,7 +6220,7 @@ impl<'db> Type<'db> {
TypeMapping::BindSelf(_) |
TypeMapping::ReplaceSelf { .. } |
TypeMapping::MarkTypeVarsInferable(_) |
TypeMapping::PromoteLiterals => self,
TypeMapping::PromoteLiterals { .. } => self,
TypeMapping::Materialize(materialization_kind) => match materialization_kind {
MaterializationKind::Top => Type::object(),
MaterializationKind::Bottom => Type::Never,
@@ -6730,7 +6747,7 @@ pub enum TypeMapping<'a, 'db> {
PartialSpecialization(PartialSpecialization<'a, 'db>),
/// Replaces any literal types with their corresponding promoted type form (e.g. `Literal["string"]`
/// to `str`, or `def _() -> int` to `Callable[[], int]`).
PromoteLiterals,
PromoteLiterals { promote_modules_and_functions: bool },
/// Binds a legacy typevar with the generic context (class, function, type alias) that it is
/// being used in.
BindLegacyTypevars(BindingContext<'db>),
@@ -6763,7 +6780,7 @@ impl<'db> TypeMapping<'_, 'db> {
match self {
TypeMapping::Specialization(_)
| TypeMapping::PartialSpecialization(_)
| TypeMapping::PromoteLiterals
| TypeMapping::PromoteLiterals { .. }
| TypeMapping::BindLegacyTypevars(_)
| TypeMapping::MarkTypeVarsInferable(_)
| TypeMapping::Materialize(_) => context,

View File

@@ -2498,9 +2498,12 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
// The inherited generic context is used when inferring the specialization of a generic
// class from a constructor call. In this case (only), we promote any typevars that are
// inferred as a literal to the corresponding instance type.
builder
.build(gc)
.apply_type_mapping(self.db, &TypeMapping::PromoteLiterals)
builder.build(gc).apply_type_mapping(
self.db,
&TypeMapping::PromoteLiterals {
promote_modules_and_functions: true,
},
)
});
}

View File

@@ -2662,7 +2662,7 @@ pub(crate) fn report_undeclared_protocol_member(
if definition.kind(db).is_unannotated_assignment() {
let binding_type = binding_type(db, definition);
let suggestion = binding_type.promote_literals(db);
let suggestion = binding_type.promote_literals(db, true);
if should_give_hint(db, suggestion) {
diagnostic.set_primary_message(format_args!(

View File

@@ -5432,7 +5432,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// Convert any element literals to their promoted type form to avoid excessively large
// unions for large nested list literals, which the constraint solver struggles with.
let inferred_elt_ty = inferred_elt_ty.promote_literals(self.db());
let inferred_elt_ty = inferred_elt_ty.promote_literals(self.db(), true);
builder
.infer(Type::TypeVar(*elt_ty), inferred_elt_ty)