From 2802cbde29ea85ae6a01870aac34b5b788346fc0 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 18 Dec 2024 14:37:17 -0500 Subject: [PATCH] Don't special-case class instances in unary expression inference (#15045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have a handy `to_meta_type` that does the right thing for class instances, and also works for all of the other types that are “instances of” something. Unless I'm missing something, this should let us get rid of the catch-all clause in one fell swoop. cf #14548 --- .../resources/mdtest/unary/custom.md | 165 ++++++++++++++++++ .../src/types/infer.rs | 41 +++-- 2 files changed, 193 insertions(+), 13 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/unary/custom.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md b/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md new file mode 100644 index 0000000000000..931521f75c340 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md @@ -0,0 +1,165 @@ +# Custom unary operations + +## Class instances + +```py +class Yes: + def __pos__(self) -> bool: + return False + + def __neg__(self) -> str: + return "negative" + + def __invert__(self) -> int: + return 17 + +class Sub(Yes): ... +class No: ... + +reveal_type(+Yes()) # revealed: bool +reveal_type(-Yes()) # revealed: str +reveal_type(~Yes()) # revealed: int + +reveal_type(+Sub()) # revealed: bool +reveal_type(-Sub()) # revealed: str +reveal_type(~Sub()) # revealed: int + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `No`" +reveal_type(+No()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `No`" +reveal_type(-No()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `No`" +reveal_type(~No()) # revealed: Unknown +``` + +## Classes + +```py +class Yes: + def __pos__(self) -> bool: + return False + + def __neg__(self) -> str: + return "negative" + + def __invert__(self) -> int: + return 17 + +class Sub(Yes): ... +class No: ... + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Yes]`" +reveal_type(+Yes) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Yes]`" +reveal_type(-Yes) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Yes]`" +reveal_type(~Yes) # revealed: Unknown + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Sub]`" +reveal_type(+Sub) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Sub]`" +reveal_type(-Sub) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Sub]`" +reveal_type(~Sub) # revealed: Unknown + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`" +reveal_type(+No) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`" +reveal_type(-No) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`" +reveal_type(~No) # revealed: Unknown +``` + +## Function literals + +```py +def f(): + pass + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[f]`" +reveal_type(+f) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[f]`" +reveal_type(-f) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[f]`" +reveal_type(~f) # revealed: Unknown +``` + +## Subclass + +```py +class Yes: + def __pos__(self) -> bool: + return False + + def __neg__(self) -> str: + return "negative" + + def __invert__(self) -> int: + return 17 + +class Sub(Yes): ... +class No: ... + +def yes() -> type[Yes]: + return Yes + +def sub() -> type[Sub]: + return Sub + +def no() -> type[No]: + return No + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Yes]`" +reveal_type(+yes()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Yes]`" +reveal_type(-yes()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Yes]`" +reveal_type(~yes()) # revealed: Unknown + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Sub]`" +reveal_type(+sub()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Sub]`" +reveal_type(-sub()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Sub]`" +reveal_type(~sub()) # revealed: Unknown + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[No]`" +reveal_type(+no()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[No]`" +reveal_type(-no()) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[No]`" +reveal_type(~no()) # revealed: Unknown +``` + +## Metaclass + +```py +class Meta(type): + def __pos__(self) -> bool: + return False + + def __neg__(self) -> str: + return "negative" + + def __invert__(self) -> int: + return 17 + +class Yes(metaclass=Meta): ... +class Sub(Yes): ... +class No: ... + +reveal_type(+Yes) # revealed: bool +reveal_type(-Yes) # revealed: str +reveal_type(~Yes) # revealed: int + +reveal_type(+Sub) # revealed: bool +reveal_type(-Sub) # revealed: str +reveal_type(~Sub) # revealed: int + +# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`" +reveal_type(+No) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`" +reveal_type(-No) # revealed: Unknown +# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`" +reveal_type(~No) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 7ff8431f345e9..95e7eca3f1dfd 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -62,11 +62,11 @@ use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, todo_type, - typing_extensions_symbol, Boundness, Class, ClassLiteralType, FunctionType, InstanceType, - IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction, - KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, Symbol, - Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay, TypeVarBoundOrConstraints, - TypeVarInstance, UnionBuilder, UnionType, + typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType, FunctionType, + InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, + KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, + Symbol, Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay, + TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType, }; use crate::unpack::Unpack; use crate::util::subscript::{PyIndex, PySlice}; @@ -3201,6 +3201,11 @@ impl<'db> TypeInferenceBuilder<'db> { let operand_type = self.infer_expression(operand); match (op, operand_type) { + (_, Type::Any) => Type::Any, + (_, Type::Todo(_)) => operand_type, + (_, Type::Never) => Type::Never, + (_, Type::Unknown) => Type::Unknown, + (UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value), (UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value), (UnaryOp::Invert, Type::IntLiteral(value)) => Type::IntLiteral(!value), @@ -3210,11 +3215,23 @@ impl<'db> TypeInferenceBuilder<'db> { (UnaryOp::Invert, Type::BooleanLiteral(bool)) => Type::IntLiteral(!i64::from(bool)), (UnaryOp::Not, ty) => ty.bool(self.db()).negate().into_type(self.db()), - (_, Type::Any) => Type::Any, - (_, Type::Unknown) => Type::Unknown, ( op @ (UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert), - Type::Instance(InstanceType { class }), + Type::FunctionLiteral(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::SubclassOf(_) + | Type::Instance(_) + | Type::KnownInstance(_) + | Type::Union(_) + | Type::Intersection(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) + | Type::SliceLiteral(_) + | Type::Tuple(_), ) => { let unary_dunder_method = match op { UnaryOp::Invert => "__invert__", @@ -3225,11 +3242,10 @@ impl<'db> TypeInferenceBuilder<'db> { } }; - if let Symbol::Type(class_member, _) = - class.class_member(self.db(), unary_dunder_method) + if let CallDunderResult::CallOutcome(call) + | CallDunderResult::PossiblyUnbound(call) = + operand_type.call_dunder(self.db(), unary_dunder_method, &[operand_type]) { - let call = class_member.call(self.db(), &[operand_type]); - match call.return_ty_result(&self.context, AnyNodeRef::ExprUnaryOp(unary)) { Ok(t) => t, Err(e) => { @@ -3257,7 +3273,6 @@ impl<'db> TypeInferenceBuilder<'db> { Type::Unknown } } - _ => todo_type!(), // TODO other unary op types } }