diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md index f696cd4ea414f..c1fee047feab6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md @@ -122,3 +122,10 @@ class Foo: ... x = Foo() reveal_type(x) # revealed: Foo ``` + +## Annotated assignments in stub files are inferred correctly + +```pyi path=main.pyi +x: int = 1 +reveal_type(x) # revealed: Literal[1] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsis.md b/crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsis.md new file mode 100644 index 0000000000000..fd5c826b98684 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsis.md @@ -0,0 +1,78 @@ +# Ellipsis + +## Function and methods + +The ellipsis literal `...` can be used as a placeholder default value for a function parameter, in a +stub file only, regardless of the type of the parameter. + +```py path=test.pyi +def f(x: int = ...) -> None: + reveal_type(x) # revealed: int + +def f2(x: str = ...) -> None: + reveal_type(x) # revealed: str +``` + +## Class and module symbols + +The ellipsis literal can be assigned to a class or module symbol, regardless of its declared type, +in a stub file only. + +```py path=test.pyi +y: bytes = ... +reveal_type(y) # revealed: bytes +x = ... +reveal_type(x) # revealed: Unknown + +class Foo: + y: int = ... + +reveal_type(Foo.y) # revealed: int +``` + +## Unpacking ellipsis literal in assignment + +No diagnostic is emitted if an ellipsis literal is "unpacked" in a stub file as part of an +assignment statement: + +```py path=test.pyi +x, y = ... +reveal_type(x) # revealed: Unknown +reveal_type(y) # revealed: Unknown +``` + +## Unpacking ellipsis literal in for loops + +Iterating over an ellipsis literal as part of a `for` loop in a stub is invalid, however, and +results in a diagnostic: + +```py path=test.pyi +# error: [not-iterable] "Object of type `ellipsis` is not iterable" +for a, b in ...: + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown +``` + +## Ellipsis usage in non stub file + +In a non-stub file, there's no special treatment of ellipsis literals. An ellipsis literal can only +be assigned if `EllipsisType` is actually assignable to the annotated type. + +```py +# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`" +def f(x: int = ...) -> None: ... + +# error: 1 [invalid-assignment] "Object of type `ellipsis` is not assignable to `int`" +a: int = ... +b = ... +reveal_type(b) # revealed: ellipsis +``` + +## Use of `Ellipsis` symbol + +There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals. + +```py path=test.pyi +# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`" +def f(x: int = Ellipsis) -> None: ... +``` diff --git a/crates/red_knot_python_semantic/src/types/context.rs b/crates/red_knot_python_semantic/src/types/context.rs index 49ea2af6c7ccf..60ea5aaf8469a 100644 --- a/crates/red_knot_python_semantic/src/types/context.rs +++ b/crates/red_knot_python_semantic/src/types/context.rs @@ -162,6 +162,11 @@ impl<'db> InferContext<'db> { } } + /// Are we currently inferring types in a stub file? + pub(crate) fn in_stub(&self) -> bool { + self.file.is_stub(self.db().upcast()) + } + #[must_use] pub(crate) fn finish(mut self) -> TypeCheckDiagnostics { self.bomb.defuse(); diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 8650d7f436a9a..4fe26fca1d83e 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -435,6 +435,10 @@ impl<'db> TypeInferenceBuilder<'db> { matches!(self.region, InferenceRegion::Deferred(_)) || self.deferred_state.is_deferred() } + fn in_stub(&self) -> bool { + self.context.in_stub() + } + /// Get the already-inferred type of an expression node. /// /// ## Panics @@ -1174,6 +1178,12 @@ impl<'db> TypeInferenceBuilder<'db> { let inferred_ty = if let Some(default_ty) = default_ty { if default_ty.is_assignable_to(self.db(), declared_ty) { UnionType::from_elements(self.db(), [declared_ty, default_ty]) + } else if self.in_stub() + && default + .as_ref() + .is_some_and(|d| d.is_ellipsis_literal_expr()) + { + declared_ty } else { self.context.report_lint( &INVALID_PARAMETER_DEFAULT, @@ -1896,7 +1906,13 @@ impl<'db> TypeInferenceBuilder<'db> { let name_ast_id = name.scoped_expression_id(self.db(), self.scope()); unpacked.get(name_ast_id).unwrap_or(Type::Unknown) } - TargetKind::Name => value_ty, + TargetKind::Name => { + if self.in_stub() && value.is_ellipsis_literal_expr() { + Type::Unknown + } else { + value_ty + } + } }; if let Some(known_instance) = @@ -1963,6 +1979,11 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(value) = value.as_deref() { let value_ty = self.infer_expression(value); + let value_ty = if self.in_stub() && value.is_ellipsis_literal_expr() { + annotation_ty + } else { + value_ty + }; self.add_declaration_with_binding( assignment.into(), definition, diff --git a/crates/red_knot_python_semantic/src/types/unpacker.rs b/crates/red_knot_python_semantic/src/types/unpacker.rs index a7da2b42ea356..aaafe18e6aace 100644 --- a/crates/red_knot_python_semantic/src/types/unpacker.rs +++ b/crates/red_knot_python_semantic/src/types/unpacker.rs @@ -45,6 +45,15 @@ impl<'db> Unpacker<'db> { let mut value_ty = infer_expression_types(self.db(), value.expression()) .expression_ty(value.scoped_expression_id(self.db(), self.scope)); + if value.is_assign() + && self.context.in_stub() + && value + .expression() + .node_ref(self.db()) + .is_ellipsis_literal_expr() + { + value_ty = Type::Unknown; + } if value.is_iterable() { // If the value is an iterable, then the type that needs to be unpacked is the iterator // type. diff --git a/crates/red_knot_python_semantic/src/unpack.rs b/crates/red_knot_python_semantic/src/unpack.rs index e0b3be92c4b45..470041a4dc9c7 100644 --- a/crates/red_knot_python_semantic/src/unpack.rs +++ b/crates/red_knot_python_semantic/src/unpack.rs @@ -76,6 +76,11 @@ impl<'db> UnpackValue<'db> { matches!(self, UnpackValue::Iterable(_)) } + /// Returns `true` if the value is being assigned to a target. + pub(crate) const fn is_assign(self) -> bool { + matches!(self, UnpackValue::Assign(_)) + } + /// Returns the underlying [`Expression`] that is being unpacked. pub(crate) const fn expression(self) -> Expression<'db> { match self {