From 66372f15045f2d32f7bab9826a20e6e82ba594c9 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 19 Feb 2025 14:12:44 +0100 Subject: [PATCH] [red-knot] Allow any `Ranged` argument for `report_lint` and `report_diagnostic` --- crates/red_knot_python_semantic/src/types.rs | 6 +- .../src/types/context.rs | 75 ++-- .../src/types/diagnostic.rs | 14 +- .../src/types/infer.rs | 346 +++++++++--------- .../src/types/string_annotation.rs | 8 +- .../src/types/unpacker.rs | 6 +- 6 files changed, 235 insertions(+), 220 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 7f23500832865..6a885af83f72b 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2264,11 +2264,7 @@ impl<'db> InvalidTypeExpressionError<'db> { invalid_expressions, } = self; for error in invalid_expressions { - context.report_lint( - &INVALID_TYPE_FORM, - node.into(), - format_args!("{}", error.reason()), - ); + context.report_lint(&INVALID_TYPE_FORM, node, format_args!("{}", error.reason())); } fallback_type } diff --git a/crates/red_knot_python_semantic/src/types/context.rs b/crates/red_knot_python_semantic/src/types/context.rs index e3ce0da534bc9..53f3aeaff2e23 100644 --- a/crates/red_knot_python_semantic/src/types/context.rs +++ b/crates/red_knot_python_semantic/src/types/context.rs @@ -5,8 +5,7 @@ use ruff_db::{ diagnostic::{DiagnosticId, SecondaryDiagnosticMessage, Severity}, files::File, }; -use ruff_python_ast::AnyNodeRef; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextRange}; use super::{binding_type, KnownFunction, TypeCheckDiagnostic, TypeCheckDiagnostics}; @@ -67,46 +66,60 @@ impl<'db> InferContext<'db> { self.diagnostics.get_mut().extend(other.diagnostics()); } - /// Reports a lint located at `node`. - pub(super) fn report_lint( + /// Reports a lint located at `ranged`. + pub(super) fn report_lint( &self, lint: &'static LintMetadata, - node: AnyNodeRef, + ranged: T, message: fmt::Arguments, - ) { - self.report_lint_with_secondary_messages(lint, node, message, vec![]); + ) where + T: Ranged, + { + self.report_lint_with_secondary_messages(lint, ranged, message, vec![]); } - /// Reports a lint located at `node`. - pub(super) fn report_lint_with_secondary_messages( + /// Reports a lint located at `ranged`. + pub(super) fn report_lint_with_secondary_messages( &self, lint: &'static LintMetadata, - node: AnyNodeRef, + ranged: T, message: fmt::Arguments, secondary_messages: Vec, - ) { - if !self.db.is_file_open(self.file) { - return; - } + ) where + T: Ranged, + { + fn lint_severity( + context: &InferContext, + lint: &'static LintMetadata, + range: TextRange, + ) -> Option { + if !context.db.is_file_open(context.file) { + return None; + } - // Skip over diagnostics if the rule is disabled. - let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else { - return; - }; + // Skip over diagnostics if the rule is disabled. + let severity = context.db.rule_selection().severity(LintId::of(lint))?; - if self.is_in_no_type_check() { - return; - } + if context.is_in_no_type_check() { + return None; + } - let suppressions = suppressions(self.db, self.file); + let suppressions = suppressions(context.db, context.file); - if let Some(suppression) = suppressions.find_suppression(node.range(), LintId::of(lint)) { - self.diagnostics.borrow_mut().mark_used(suppression.id()); - return; + if let Some(suppression) = suppressions.find_suppression(range, LintId::of(lint)) { + context.diagnostics.borrow_mut().mark_used(suppression.id()); + return None; + } + + Some(severity) } + let Some(severity) = lint_severity(self, lint, ranged.range()) else { + return; + }; + self.report_diagnostic( - node, + ranged, DiagnosticId::Lint(lint.name()), severity, message, @@ -117,14 +130,16 @@ impl<'db> InferContext<'db> { /// Adds a new diagnostic. /// /// The diagnostic does not get added if the rule isn't enabled for this file. - pub(super) fn report_diagnostic( + pub(super) fn report_diagnostic( &self, - node: AnyNodeRef, + ranged: T, id: DiagnosticId, severity: Severity, message: fmt::Arguments, secondary_messages: Vec, - ) { + ) where + T: Ranged, + { if !self.db.is_file_open(self.file) { return; } @@ -139,7 +154,7 @@ impl<'db> InferContext<'db> { file: self.file, id, message: message.to_string(), - range: node.range(), + range: ranged.range(), severity, secondary_messages, }); diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index dd9994a75b32c..63c0fc2b16cf5 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -1047,7 +1047,7 @@ pub(super) fn report_possibly_unresolved_reference( context.report_lint( &POSSIBLY_UNRESOLVED_REFERENCE, - expr_name_node.into(), + expr_name_node, format_args!("Name `{id}` used when possibly not defined"), ); } @@ -1057,7 +1057,7 @@ pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node context.report_lint( &UNRESOLVED_REFERENCE, - expr_name_node.into(), + expr_name_node, format_args!("Name `{id}` used when not defined"), ); } @@ -1065,7 +1065,7 @@ pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) { context.report_lint( &INVALID_EXCEPTION_CAUGHT, - node.into(), + node, format_args!( "Cannot catch object of type `{}` in an exception handler \ (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)", @@ -1077,7 +1077,7 @@ pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast pub(crate) fn report_invalid_exception_raised(context: &InferContext, node: &ast::Expr, ty: Type) { context.report_lint( &INVALID_RAISE, - node.into(), + node, format_args!( "Cannot raise object of type `{}` (must be a `BaseException` subclass or instance)", ty.display(context.db()) @@ -1088,7 +1088,7 @@ pub(crate) fn report_invalid_exception_raised(context: &InferContext, node: &ast pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) { context.report_lint( &INVALID_RAISE, - node.into(), + node, format_args!( "Cannot use object of type `{}` as exception cause \ (must be a `BaseException` subclass or instance or `None`)", @@ -1100,7 +1100,7 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast: pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node: &ast::Expr) { context.report_lint( &INCOMPATIBLE_SLOTS, - node.into(), + node, format_args!("Class base has incompatible `__slots__`"), ); } @@ -1112,7 +1112,7 @@ pub(crate) fn report_invalid_arguments_to_annotated<'db>( ) { context.report_lint( &INVALID_TYPE_FORM, - subscript.into(), + subscript, format_args!( "Special form `{}` expected at least 2 arguments (one type and at least one metadata element)", KnownInstanceType::Annotated.repr(db) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index ab5cb3ae04ee3..48261b798c533 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -589,7 +589,7 @@ impl<'db> TypeInferenceBuilder<'db> { if inheritance_cycle.is_participant() { self.context.report_lint( &CYCLIC_CLASS_DEFINITION, - class_node.into(), + class_node, format_args!( "Cyclic definition of `{}` (class cannot inherit from itself)", class.name(self.db()) @@ -613,7 +613,7 @@ impl<'db> TypeInferenceBuilder<'db> { } self.context.report_lint( &SUBCLASS_OF_FINAL_CLASS, - (&class_node.bases()[i]).into(), + &class_node.bases()[i], format_args!( "Class `{}` cannot inherit from final class `{}`", class.name(self.db()), @@ -631,7 +631,7 @@ impl<'db> TypeInferenceBuilder<'db> { for (index, duplicate) in duplicates { self.context.report_lint( &DUPLICATE_BASE, - (&base_nodes[*index]).into(), + &base_nodes[*index], format_args!("Duplicate base class `{}`", duplicate.name(self.db())), ); } @@ -641,7 +641,7 @@ impl<'db> TypeInferenceBuilder<'db> { for (index, base_ty) in bases { self.context.report_lint( &INVALID_BASE, - (&base_nodes[*index]).into(), + &base_nodes[*index], format_args!( "Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)", base_ty.display(self.db()) @@ -651,7 +651,7 @@ impl<'db> TypeInferenceBuilder<'db> { } MroErrorKind::UnresolvableMro { bases_list } => self.context.report_lint( &INCONSISTENT_MRO, - class_node.into(), + class_node, format_args!( "Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`", class.name(self.db()), @@ -668,12 +668,12 @@ impl<'db> TypeInferenceBuilder<'db> { match metaclass_error.reason() { MetaclassErrorKind::NotCallable(ty) => self.context.report_lint( &INVALID_METACLASS, - class_node.into(), + class_node, format_args!("Metaclass type `{}` is not callable", ty.display(self.db())), ), MetaclassErrorKind::PartlyNotCallable(ty) => self.context.report_lint( &INVALID_METACLASS, - class_node.into(), + class_node, format_args!( "Metaclass type `{}` is partly not callable", ty.display(self.db()) @@ -692,11 +692,10 @@ impl<'db> TypeInferenceBuilder<'db> { }, candidate1_is_base_class, } => { - let node = class_node.into(); if *candidate1_is_base_class { self.context.report_lint( &CONFLICTING_METACLASS, - node, + class_node, format_args!( "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ but `{metaclass1}` (metaclass of base class `{base1}`) and `{metaclass2}` (metaclass of base class `{base2}`) \ @@ -711,7 +710,7 @@ impl<'db> TypeInferenceBuilder<'db> { } else { self.context.report_lint( &CONFLICTING_METACLASS, - node, + class_node, format_args!( "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ but `{metaclass_of_class}` (metaclass of `{class}`) and `{metaclass_of_base}` (metaclass of base class `{base}`) \ @@ -862,7 +861,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.context.report_lint( &DIVISION_BY_ZERO, - expr.into(), + expr, format_args!( "Cannot {op} object of type `{}` {by_zero}", left.display(self.db()) @@ -1263,7 +1262,7 @@ impl<'db> TypeInferenceBuilder<'db> { } else { self.context.report_lint( &INVALID_PARAMETER_DEFAULT, - parameter_with_default.into(), + parameter_with_default, format_args!( "Default value of type `{}` is not assignable to annotated parameter type `{}`", default_ty.display(self.db()), declared_ty.display(self.db())), @@ -1585,7 +1584,7 @@ impl<'db> TypeInferenceBuilder<'db> { (Symbol::Unbound, Symbol::Unbound) => { self.context.report_lint( &INVALID_CONTEXT_MANAGER, - context_expression.into(), + context_expression, format_args!( "Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`", context_expression_ty.display(self.db()) @@ -1596,7 +1595,7 @@ impl<'db> TypeInferenceBuilder<'db> { (Symbol::Unbound, _) => { self.context.report_lint( &INVALID_CONTEXT_MANAGER, - context_expression.into(), + context_expression, format_args!( "Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__`", context_expression_ty.display(self.db()) @@ -1608,7 +1607,7 @@ impl<'db> TypeInferenceBuilder<'db> { if enter_boundness == Boundness::PossiblyUnbound { self.context.report_lint( &INVALID_CONTEXT_MANAGER, - context_expression.into(), + context_expression, format_args!( "Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` is possibly unbound", context_expression = context_expression_ty.display(self.db()), @@ -1625,7 +1624,7 @@ impl<'db> TypeInferenceBuilder<'db> { // distinguish between a not callable `__enter__` attribute and a wrong signature. self.context.report_lint( &INVALID_CONTEXT_MANAGER, - context_expression.into(), + context_expression, format_args!(" Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__enter__`", context_expression = context_expression_ty.display(self.db()), @@ -1638,7 +1637,7 @@ impl<'db> TypeInferenceBuilder<'db> { Symbol::Unbound => { self.context.report_lint( &INVALID_CONTEXT_MANAGER, - context_expression.into(), + context_expression, format_args!( "Object of type `{}` cannot be used with `with` because it doesn't implement `__exit__`", context_expression_ty.display(self.db()) @@ -1651,7 +1650,7 @@ impl<'db> TypeInferenceBuilder<'db> { if exit_boundness == Boundness::PossiblyUnbound { self.context.report_lint( &INVALID_CONTEXT_MANAGER, - context_expression.into(), + context_expression, format_args!( "Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` is possibly unbound", context_expression = context_expression_ty.display(self.db()), @@ -1676,7 +1675,7 @@ impl<'db> TypeInferenceBuilder<'db> { // distinguish between a not callable `__exit__` attribute and a wrong signature. self.context.report_lint( &INVALID_CONTEXT_MANAGER, - context_expression.into(), + context_expression, format_args!( "Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__exit__`", context_expression = context_expression_ty.display(self.db()), @@ -1767,7 +1766,7 @@ impl<'db> TypeInferenceBuilder<'db> { if elts.len() < 2 { self.context.report_lint( &INVALID_TYPE_VARIABLE_CONSTRAINTS, - expr.into(), + expr, format_args!("TypeVar must have at least two constrained types"), ); self.infer_expression(expr); @@ -2219,7 +2218,7 @@ impl<'db> TypeInferenceBuilder<'db> { Err(e) => { self.context.report_lint( &UNSUPPORTED_OPERATOR, - assignment.into(), + assignment, format_args!( "Operator `{op}=` is unsupported between objects of type `{}` and `{}`", target_type.display(self.db()), @@ -2240,7 +2239,7 @@ impl<'db> TypeInferenceBuilder<'db> { .unwrap_or_else(|| { self.context.report_lint( &UNSUPPORTED_OPERATOR, - assignment.into(), + assignment, format_args!( "Operator `{op}=` is unsupported between objects of type `{}` and `{}`", left_ty.display(self.db()), @@ -2269,7 +2268,7 @@ impl<'db> TypeInferenceBuilder<'db> { .unwrap_or_else(|| { self.context.report_lint( &UNSUPPORTED_OPERATOR, - assignment.into(), + assignment, format_args!( "Operator `{op}=` is unsupported between objects of type `{}` and `{}`", left_ty.display(self.db()), @@ -3265,7 +3264,7 @@ impl<'db> TypeInferenceBuilder<'db> { KnownFunction::RevealType => { if let Some(revealed_type) = binding.one_parameter_type() { self.context.report_diagnostic( - call_expression.into(), + call_expression, DiagnosticId::RevealedType, Severity::Info, format_args!( @@ -3281,7 +3280,7 @@ impl<'db> TypeInferenceBuilder<'db> { if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) { self.context.report_lint( &TYPE_ASSERTION_FAILURE, - call_expression.into(), + call_expression, format_args!( "Actual type `{}` is not the same as asserted type `{}`", actual_ty.display(self.db()), @@ -3301,33 +3300,33 @@ impl<'db> TypeInferenceBuilder<'db> { { self.context.report_lint( &STATIC_ASSERT_ERROR, - call_expression.into(), + call_expression, format_args!("Static assertion error: {message}"), ); } else if parameter_ty == Type::BooleanLiteral(false) { self.context.report_lint( - &STATIC_ASSERT_ERROR, - call_expression.into(), - format_args!("Static assertion error: argument evaluates to `False`"), - ); + &STATIC_ASSERT_ERROR, + call_expression, + format_args!("Static assertion error: argument evaluates to `False`"), + ); } else if truthiness.is_always_false() { self.context.report_lint( - &STATIC_ASSERT_ERROR, - call_expression.into(), - format_args!( - "Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy", - parameter_ty=parameter_ty.display(self.db()) - ), - ); + &STATIC_ASSERT_ERROR, + call_expression, + format_args!( + "Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy", + parameter_ty=parameter_ty.display(self.db()) + ), + ); } else { self.context.report_lint( - &STATIC_ASSERT_ERROR, - call_expression.into(), - format_args!( - "Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness", - parameter_ty=parameter_ty.display(self.db()) - ), - ); + &STATIC_ASSERT_ERROR, + call_expression, + format_args!( + "Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness", + parameter_ty=parameter_ty.display(self.db()) + ), + ); }; } } @@ -3351,7 +3350,7 @@ impl<'db> TypeInferenceBuilder<'db> { CallError::NotCallable { not_callable_ty } => { context.report_lint( &CALL_NON_CALLABLE, - call_expression.into(), + call_expression, format_args!( "Object of type `{}` is not callable", not_callable_ty.display(context.db()) @@ -3379,7 +3378,7 @@ impl<'db> TypeInferenceBuilder<'db> { CallError::PossiblyUnboundDunderCall { called_type, .. } => { context.report_lint( &CALL_NON_CALLABLE, - call_expression.into(), + call_expression, format_args!( "Object of type `{}` is not callable (possibly unbound `__call__` method)", called_type.display(context.db()) @@ -3546,7 +3545,7 @@ impl<'db> TypeInferenceBuilder<'db> { if symbol_name == "reveal_type" { self.context.report_lint( &UNDEFINED_REVEAL, - name_node.into(), + name_node, format_args!( "`reveal_type` used without importing it; \ this is allowed for debugging convenience but will fail at runtime" @@ -3597,7 +3596,7 @@ impl<'db> TypeInferenceBuilder<'db> { LookupError::Unbound => { self.context.report_lint( &UNRESOLVED_ATTRIBUTE, - attribute.into(), + attribute, format_args!( "Type `{}` has no attribute `{}`", value_type.display(db), @@ -3609,7 +3608,7 @@ impl<'db> TypeInferenceBuilder<'db> { LookupError::PossiblyUnbound(type_when_bound) => { self.context.report_lint( &POSSIBLY_UNBOUND_ATTRIBUTE, - attribute.into(), + attribute, format_args!( "Attribute `{}` on type `{}` is possibly unbound", attr.id, @@ -3639,7 +3638,7 @@ impl<'db> TypeInferenceBuilder<'db> { if instance_member.is_class_var() { self.context.report_lint( &INVALID_ATTRIBUTE_ACCESS, - attribute.into(), + attribute, format_args!( "Cannot assign to ClassVar `{attr}` from an instance of type `{ty}`", ty = value_ty.display(self.db()), @@ -3728,7 +3727,7 @@ impl<'db> TypeInferenceBuilder<'db> { Err(e) => { self.context.report_lint( &UNSUPPORTED_OPERATOR, - unary.into(), + unary, format_args!( "Unary operator `{op}` is unsupported for type `{}`", operand_type.display(self.db()), @@ -3768,7 +3767,7 @@ impl<'db> TypeInferenceBuilder<'db> { .unwrap_or_else(|| { self.context.report_lint( &UNSUPPORTED_OPERATOR, - binary.into(), + binary, format_args!( "Operator `{op}` is unsupported between objects of type `{}` and `{}`", left_ty.display(self.db()), @@ -4518,10 +4517,9 @@ impl<'db> TypeInferenceBuilder<'db> { // Lookup the rich comparison `__dunder__` methods on instances (Type::Instance(left_instance), Type::Instance(right_instance)) => { let rich_comparison = - |op| perform_rich_comparison(self.db(), left_instance, right_instance, op); - let membership_test_comparison = |op| { - perform_membership_test_comparison(self.db(), left_instance, right_instance, op) - }; + |op| self.infer_rich_comparison(left_instance, right_instance, op); + let membership_test_comparison = + |op| self.infer_membership_test_comparison(left_instance, right_instance, op); match op { ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq), ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne), @@ -4564,6 +4562,112 @@ impl<'db> TypeInferenceBuilder<'db> { } } + /// Rich comparison in Python are the operators `==`, `!=`, `<`, `<=`, `>`, and `>=`. Their + /// behaviour can be edited for classes by implementing corresponding dunder methods. + /// This function performs rich comparison between two instances and returns the resulting type. + /// see `` + fn infer_rich_comparison( + &self, + left: InstanceType<'db>, + right: InstanceType<'db>, + op: RichCompareOperator, + ) -> Result, CompareUnsupportedError<'db>> { + let db = self.db(); + // The following resource has details about the rich comparison algorithm: + // https://snarky.ca/unravelling-rich-comparison-operators/ + let call_dunder = |op: RichCompareOperator, + left: InstanceType<'db>, + right: InstanceType<'db>| { + // TODO: How do we want to handle possibly unbound dunder methods? + match left.class.class_member(db, op.dunder()) { + Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder + .call( + db, + &CallArguments::positional([Type::Instance(left), Type::Instance(right)]), + ) + .map(|outcome| outcome.return_type(db)) + .ok(), + _ => None, + } + }; + + // The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side. + if left != right && right.is_subtype_of(db, left) { + call_dunder(op.reflect(), right, left).or_else(|| call_dunder(op, left, right)) + } else { + call_dunder(op, left, right).or_else(|| call_dunder(op.reflect(), right, left)) + } + .or_else(|| { + // When no appropriate method returns any value other than NotImplemented, + // the `==` and `!=` operators will fall back to `is` and `is not`, respectively. + // refer to `` + if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) { + Some(KnownClass::Bool.to_instance(db)) + } else { + None + } + }) + .ok_or_else(|| CompareUnsupportedError { + op: op.into(), + left_ty: left.into(), + right_ty: right.into(), + }) + } + + /// Performs a membership test (`in` and `not in`) between two instances and returns the resulting type, or `None` if the test is unsupported. + /// The behavior can be customized in Python by implementing `__contains__`, `__iter__`, or `__getitem__` methods. + /// See `` + /// and `` + fn infer_membership_test_comparison( + &self, + left: InstanceType<'db>, + right: InstanceType<'db>, + op: MembershipTestCompareOperator, + ) -> Result, CompareUnsupportedError<'db>> { + let db = self.db(); + + let contains_dunder = right.class.class_member(db, "__contains__"); + let compare_result_opt = match contains_dunder { + Symbol::Type(contains_dunder, Boundness::Bound) => { + // If `__contains__` is available, it is used directly for the membership test. + contains_dunder + .call( + db, + &CallArguments::positional([Type::Instance(right), Type::Instance(left)]), + ) + .map(|outcome| outcome.return_type(db)) + .ok() + } + _ => { + // iteration-based membership test + match Type::Instance(right).iterate(db) { + IterationOutcome::Iterable { .. } => Some(KnownClass::Bool.to_instance(db)), + IterationOutcome::NotIterable { .. } + | IterationOutcome::PossiblyUnboundDunderIter { .. } => None, + } + } + }; + + compare_result_opt + .map(|ty| { + if matches!(ty, Type::Dynamic(DynamicType::Todo(_))) { + return ty; + } + + let truthiness = ty.bool(db); + + match op { + MembershipTestCompareOperator::In => truthiness.into_type(db), + MembershipTestCompareOperator::NotIn => truthiness.negate().into_type(db), + } + }) + .ok_or_else(|| CompareUnsupportedError { + op: op.into(), + left_ty: left.into(), + right_ty: right.into(), + }) + } + /// Simulates rich comparison between tuples and returns the inferred result. /// This performs a lexicographic comparison, returning a union of all possible return types that could result from the comparison. /// @@ -4793,7 +4897,7 @@ impl<'db> TypeInferenceBuilder<'db> { Err(err @ CallDunderError::PossiblyUnbound { .. }) => { self.context.report_lint( &CALL_POSSIBLY_UNBOUND_METHOD, - value_node.into(), + value_node, format_args!( "Method `__getitem__` of type `{}` is possibly unbound", value_ty.display(self.db()), @@ -4805,7 +4909,7 @@ impl<'db> TypeInferenceBuilder<'db> { Err(CallDunderError::Call(err)) => { self.context.report_lint( &CALL_NON_CALLABLE, - value_node.into(), + value_node, format_args!( "Method `__getitem__` of type `{}` is not callable on object of type `{}`", err.called_type().display(self.db()), @@ -4839,7 +4943,7 @@ impl<'db> TypeInferenceBuilder<'db> { if boundness == Boundness::PossiblyUnbound { self.context.report_lint( &CALL_POSSIBLY_UNBOUND_METHOD, - value_node.into(), + value_node, format_args!( "Method `__class_getitem__` of type `{}` is possibly unbound", value_ty.display(self.db()), @@ -4853,7 +4957,7 @@ impl<'db> TypeInferenceBuilder<'db> { .unwrap_or_else(|err| { self.context.report_lint( &CALL_NON_CALLABLE, - value_node.into(), + value_node, format_args!( "Method `__class_getitem__` of type `{}` is not callable on object of type `{}`", err.called_type().display(self.db()), @@ -5000,7 +5104,7 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::BytesLiteral(bytes) => { self.context.report_lint( &BYTE_STRING_TYPE_ANNOTATION, - bytes.into(), + bytes, format_args!("Type expressions cannot use bytes literal"), ); TypeAndQualifiers::unknown() @@ -5009,7 +5113,7 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::FString(fstring) => { self.context.report_lint( &FSTRING_TYPE_ANNOTATION, - fstring.into(), + fstring, format_args!("Type expressions cannot use f-strings"), ); self.infer_fstring_expression(fstring); @@ -5089,7 +5193,7 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::Tuple(..) => { self.context.report_lint( &INVALID_TYPE_FORM, - subscript.into(), + subscript, format_args!( "Type qualifier `{type_qualifier}` expects exactly one type parameter", type_qualifier = known_instance.repr(self.db()), @@ -5468,7 +5572,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_type_expression(slice); self.context.report_lint( &INVALID_TYPE_FORM, - slice.into(), + slice, format_args!("type[...] must have exactly one type argument"), ); Type::unknown() @@ -5580,7 +5684,7 @@ impl<'db> TypeInferenceBuilder<'db> { for node in nodes { self.context.report_lint( &INVALID_TYPE_FORM, - node.into(), + node, format_args!( "Type arguments for `Literal` must be `None`, \ a literal value (int, bool, str, or bytes), or an enum value" @@ -5624,7 +5728,7 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::Tuple(_) => { self.context.report_lint( &INVALID_TYPE_FORM, - subscript.into(), + subscript, format_args!( "Special form `{}` expected exactly one type parameter", known_instance.repr(self.db()) @@ -5653,7 +5757,7 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::Tuple(_) => { self.context.report_lint( &INVALID_TYPE_FORM, - subscript.into(), + subscript, format_args!( "Special form `{}` expected exactly one type parameter", known_instance.repr(self.db()) @@ -5717,7 +5821,7 @@ impl<'db> TypeInferenceBuilder<'db> { KnownInstanceType::ClassVar | KnownInstanceType::Final => { self.context.report_lint( &INVALID_TYPE_FORM, - subscript.into(), + subscript, format_args!( "Type qualifier `{}` is not allowed in type expressions (only in annotation expressions)", known_instance.repr(self.db()) @@ -5752,7 +5856,7 @@ impl<'db> TypeInferenceBuilder<'db> { | KnownInstanceType::AlwaysFalsy => { self.context.report_lint( &INVALID_TYPE_FORM, - subscript.into(), + subscript, format_args!( "Type `{}` expected no type parameter", known_instance.repr(self.db()) @@ -5765,7 +5869,7 @@ impl<'db> TypeInferenceBuilder<'db> { | KnownInstanceType::Unknown => { self.context.report_lint( &INVALID_TYPE_FORM, - subscript.into(), + subscript, format_args!( "Special form `{}` expected no type parameter", known_instance.repr(self.db()) @@ -5776,7 +5880,7 @@ impl<'db> TypeInferenceBuilder<'db> { KnownInstanceType::LiteralString => { self.context.report_lint( &INVALID_TYPE_FORM, - subscript.into(), + subscript, format_args!( "Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?", known_instance.repr(self.db()) @@ -6073,106 +6177,6 @@ impl StringPartsCollector { } } -/// Rich comparison in Python are the operators `==`, `!=`, `<`, `<=`, `>`, and `>=`. Their -/// behaviour can be edited for classes by implementing corresponding dunder methods. -/// This function performs rich comparison between two instances and returns the resulting type. -/// see `` -fn perform_rich_comparison<'db>( - db: &'db dyn Db, - left: InstanceType<'db>, - right: InstanceType<'db>, - op: RichCompareOperator, -) -> Result, CompareUnsupportedError<'db>> { - // The following resource has details about the rich comparison algorithm: - // https://snarky.ca/unravelling-rich-comparison-operators/ - let call_dunder = - |op: RichCompareOperator, left: InstanceType<'db>, right: InstanceType<'db>| { - // TODO: How do we want to handle possibly unbound dunder methods? - match left.class.class_member(db, op.dunder()) { - Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder - .call( - db, - &CallArguments::positional([Type::Instance(left), Type::Instance(right)]), - ) - .map(|outcome| outcome.return_type(db)) - .ok(), - _ => None, - } - }; - - // The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side. - if left != right && right.is_subtype_of(db, left) { - call_dunder(op.reflect(), right, left).or_else(|| call_dunder(op, left, right)) - } else { - call_dunder(op, left, right).or_else(|| call_dunder(op.reflect(), right, left)) - } - .or_else(|| { - // When no appropriate method returns any value other than NotImplemented, - // the `==` and `!=` operators will fall back to `is` and `is not`, respectively. - // refer to `` - if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) { - Some(KnownClass::Bool.to_instance(db)) - } else { - None - } - }) - .ok_or_else(|| CompareUnsupportedError { - op: op.into(), - left_ty: left.into(), - right_ty: right.into(), - }) -} - -/// Performs a membership test (`in` and `not in`) between two instances and returns the resulting type, or `None` if the test is unsupported. -/// The behavior can be customized in Python by implementing `__contains__`, `__iter__`, or `__getitem__` methods. -/// See `` -/// and `` -fn perform_membership_test_comparison<'db>( - db: &'db dyn Db, - left: InstanceType<'db>, - right: InstanceType<'db>, - op: MembershipTestCompareOperator, -) -> Result, CompareUnsupportedError<'db>> { - let contains_dunder = right.class.class_member(db, "__contains__"); - let compare_result_opt = match contains_dunder { - Symbol::Type(contains_dunder, Boundness::Bound) => { - // If `__contains__` is available, it is used directly for the membership test. - contains_dunder - .call( - db, - &CallArguments::positional([Type::Instance(right), Type::Instance(left)]), - ) - .map(|outcome| outcome.return_type(db)) - .ok() - } - _ => { - // iteration-based membership test - match Type::Instance(right).iterate(db) { - IterationOutcome::Iterable { .. } => Some(KnownClass::Bool.to_instance(db)), - IterationOutcome::NotIterable { .. } - | IterationOutcome::PossiblyUnboundDunderIter { .. } => None, - } - } - }; - - compare_result_opt - .map(|ty| { - if matches!(ty, Type::Dynamic(DynamicType::Todo(_))) { - return ty; - } - - match op { - MembershipTestCompareOperator::In => ty.bool(db).into_type(db), - MembershipTestCompareOperator::NotIn => ty.bool(db).negate().into_type(db), - } - }) - .ok_or_else(|| CompareUnsupportedError { - op: op.into(), - left_ty: left.into(), - right_ty: right.into(), - }) -} - #[cfg(test)] mod tests { use crate::db::tests::{setup_db, TestDb}; diff --git a/crates/red_knot_python_semantic/src/types/string_annotation.rs b/crates/red_knot_python_semantic/src/types/string_annotation.rs index d6c3f9e2984d7..1d78d5ff32bf3 100644 --- a/crates/red_knot_python_semantic/src/types/string_annotation.rs +++ b/crates/red_knot_python_semantic/src/types/string_annotation.rs @@ -143,7 +143,7 @@ pub(crate) fn parse_string_annotation( if prefix.is_raw() { context.report_lint( &RAW_STRING_TYPE_ANNOTATION, - string_literal.into(), + string_literal, format_args!("Type expressions cannot use raw string literal"), ); // Compare the raw contents (without quotes) of the expression with the parsed contents @@ -153,7 +153,7 @@ pub(crate) fn parse_string_annotation( Ok(parsed) => return Some(parsed), Err(parse_error) => context.report_lint( &INVALID_SYNTAX_IN_FORWARD_ANNOTATION, - string_literal.into(), + string_literal, format_args!("Syntax error in forward annotation: {}", parse_error.error), ), } @@ -162,7 +162,7 @@ pub(crate) fn parse_string_annotation( // case for annotations that contain escape sequences. context.report_lint( &ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, - string_expr.into(), + string_expr, format_args!("Type expressions cannot contain escape characters"), ); } @@ -170,7 +170,7 @@ pub(crate) fn parse_string_annotation( // String is implicitly concatenated. context.report_lint( &IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, - string_expr.into(), + string_expr, format_args!("Type expressions cannot span multiple string literals"), ); } diff --git a/crates/red_knot_python_semantic/src/types/unpacker.rs b/crates/red_knot_python_semantic/src/types/unpacker.rs index a70312e5cbcc2..3173a9dc28539 100644 --- a/crates/red_knot_python_semantic/src/types/unpacker.rs +++ b/crates/red_knot_python_semantic/src/types/unpacker.rs @@ -124,7 +124,7 @@ impl<'db> Unpacker<'db> { Ordering::Less => { self.context.report_lint( &INVALID_ASSIGNMENT, - target.into(), + target, format_args!( "Too many values to unpack (expected {}, got {})", elts.len(), @@ -135,7 +135,7 @@ impl<'db> Unpacker<'db> { Ordering::Greater => { self.context.report_lint( &INVALID_ASSIGNMENT, - target.into(), + target, format_args!( "Not enough values to unpack (expected {}, got {})", elts.len(), @@ -232,7 +232,7 @@ impl<'db> Unpacker<'db> { } else { self.context.report_lint( &INVALID_ASSIGNMENT, - expr.into(), + expr, format_args!( "Not enough values to unpack (expected {} or more, got {})", targets.len() - 1,