Merge remote-tracking branch 'origin/main' into dcreager/callable-return

* origin/main:
  new module for parsing ranged suppressions (#21441)
  [ty] `type[T]` is assignable to an inferable typevar (#21766)
  Fix syntax error false positives for `await` outside functions (#21763)
  [ty] Improve diagnostics for unsupported comparison operations (#21737)
This commit is contained in:
Douglas Creager
2025-12-02 18:42:43 -05:00
33 changed files with 2638 additions and 260 deletions

View File

@@ -2845,6 +2845,11 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
match scope.kind() {
ScopeKind::Class => return false,
ScopeKind::Function | ScopeKind::Lambda => return true,
ScopeKind::Comprehension
if matches!(scope.node(), NodeWithScopeKind::GeneratorExpression(_)) =>
{
return true;
}
ScopeKind::Comprehension
| ScopeKind::Module
| ScopeKind::TypeAlias
@@ -2894,11 +2899,14 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
matches!(kind, ScopeKind::Function | ScopeKind::Lambda)
}
fn in_generator_scope(&self) -> bool {
matches!(
self.scopes[self.current_scope()].node(),
NodeWithScopeKind::GeneratorExpression(_)
)
fn in_generator_context(&self) -> bool {
for scope_info in &self.scope_stack {
let scope = &self.scopes[scope_info.file_scope_id];
if matches!(scope.node(), NodeWithScopeKind::GeneratorExpression(_)) {
return true;
}
}
false
}
fn in_notebook(&self) -> bool {

View File

@@ -2134,33 +2134,52 @@ impl<'db> Type<'db> {
})
.is_never_satisfied(db) =>
{
// TODO: The repetition here isn't great, but we really need the fallthrough logic,
// where this arm only engages if it returns true.
let this_instance = Type::TypeVar(subclass_of.into_type_var().unwrap());
target.to_instance(db).when_some_and(|other_instance| {
this_instance.has_relation_to_impl(
db,
other_instance,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
// TODO: The repetition here isn't great, but we need the fallthrough logic.
subclass_of
.into_type_var()
.zip(target.to_instance(db))
.when_some_and(|(this_instance, other_instance)| {
Type::TypeVar(this_instance).has_relation_to_impl(
db,
other_instance,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
}
(_, Type::SubclassOf(subclass_of)) if subclass_of.is_type_var() => {
let other_instance = Type::TypeVar(subclass_of.into_type_var().unwrap());
self.to_instance(db).when_some_and(|this_instance| {
this_instance.has_relation_to_impl(
db,
other_instance,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
(_, Type::SubclassOf(subclass_of))
if !subclass_of
.into_type_var()
.zip(self.to_instance(db))
.when_some_and(|(other_instance, this_instance)| {
this_instance.has_relation_to_impl(
db,
Type::TypeVar(other_instance),
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
.is_never_satisfied(db) =>
{
// TODO: The repetition here isn't great, but we need the fallthrough logic.
subclass_of
.into_type_var()
.zip(self.to_instance(db))
.when_some_and(|(other_instance, this_instance)| {
this_instance.has_relation_to_impl(
db,
Type::TypeVar(other_instance),
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
}
// A fully static typevar is a subtype of its upper bound, and to something similar to
@@ -2690,7 +2709,9 @@ impl<'db> Type<'db> {
disjointness_visitor,
),
(Type::SubclassOf(subclass_of), _) if subclass_of.is_type_var() => {
(Type::SubclassOf(subclass_of), _) | (_, Type::SubclassOf(subclass_of))
if subclass_of.is_type_var() =>
{
ConstraintSet::from(false)
}
@@ -3150,33 +3171,33 @@ impl<'db> Type<'db> {
// `type[T]` is disjoint from a class object `A` if every instance of `T` is disjoint from an instance of `A`.
(Type::SubclassOf(subclass_of), other) | (other, Type::SubclassOf(subclass_of))
if subclass_of.is_type_var()
&& (other.to_instance(db).is_some()
|| other.as_typevar().is_some_and(|type_var| {
type_var.typevar(db).bound_or_constraints(db).is_none()
})) =>
if !subclass_of
.into_type_var()
.zip(other.to_instance(db))
.when_none_or(|(this_instance, other_instance)| {
Type::TypeVar(this_instance).is_disjoint_from_impl(
db,
other_instance,
inferable,
disjointness_visitor,
relation_visitor,
)
})
.is_always_satisfied(db) =>
{
let this_instance = Type::TypeVar(subclass_of.into_type_var().unwrap());
let other_instance = match other {
// An unbounded typevar `U` may have instances of type `object` if specialized to
// an instance of `type`.
Type::TypeVar(typevar)
if typevar.typevar(db).bound_or_constraints(db).is_none() =>
{
Some(Type::object())
}
_ => other.to_instance(db),
};
other_instance.when_none_or(|other_instance| {
this_instance.is_disjoint_from_impl(
db,
other_instance,
inferable,
disjointness_visitor,
relation_visitor,
)
})
// TODO: The repetition here isn't great, but we need the fallthrough logic.
subclass_of
.into_type_var()
.zip(other.to_instance(db))
.when_none_or(|(this_instance, other_instance)| {
Type::TypeVar(this_instance).is_disjoint_from_impl(
db,
other_instance,
inferable,
disjointness_visitor,
relation_visitor,
)
})
}
// A typevar is never disjoint from itself, since all occurrences of the typevar must

View File

@@ -18,12 +18,14 @@ use crate::types::class::{
CodeGeneratorKind, DisjointBase, DisjointBaseKind, Field, MethodDecorator,
};
use crate::types::function::{FunctionDecorators, FunctionType, KnownFunction, OverloadLiteral};
use crate::types::infer::UnsupportedComparisonError;
use crate::types::overrides::MethodKind;
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::tuple::TupleSpec;
use crate::types::{
BoundTypeVarInstance, ClassType, DynamicType, LintDiagnosticGuard, Protocol,
ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type,
@@ -2410,7 +2412,7 @@ pub(super) fn report_invalid_assignment<'db>(
}
let settings =
DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), target_ty, value_ty);
DisplaySettings::from_possibly_ambiguous_types(context.db(), [target_ty, value_ty]);
let diagnostic_range = if let Some(value_node) = value_node {
// Expand the range to include parentheses around the value, if any. This allows
@@ -2548,7 +2550,7 @@ pub(super) fn report_invalid_return_type(
};
let settings =
DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), expected_ty, actual_ty);
DisplaySettings::from_possibly_ambiguous_types(context.db(), [expected_ty, actual_ty]);
let return_type_span = context.span(return_type_range);
let mut diag = builder.into_diagnostic("Return type does not match returned value");
@@ -4054,6 +4056,112 @@ pub(super) fn report_overridden_final_method<'db>(
}
}
pub(super) fn report_unsupported_comparison<'db>(
context: &InferContext<'db, '_>,
error: &UnsupportedComparisonError<'db>,
range: TextRange,
left: &ast::Expr,
right: &ast::Expr,
left_ty: Type<'db>,
right_ty: Type<'db>,
) {
let db = context.db();
let Some(diagnostic_builder) = context.report_lint(&UNSUPPORTED_OPERATOR, range) else {
return;
};
let display_settings = DisplaySettings::from_possibly_ambiguous_types(
db,
[error.left_ty, error.right_ty, left_ty, right_ty],
);
let mut diagnostic =
diagnostic_builder.into_diagnostic(format_args!("Unsupported `{}` operation", error.op));
if left_ty == right_ty {
diagnostic.set_primary_message(format_args!(
"Both operands have type `{}`",
left_ty.display_with(db, display_settings.clone())
));
diagnostic.annotate(context.secondary(left));
diagnostic.annotate(context.secondary(right));
diagnostic.set_concise_message(format_args!(
"Operator `{}` is not supported between two objects of type `{}`",
error.op,
left_ty.display_with(db, display_settings.clone())
));
} else {
for (ty, expr) in [(left_ty, left), (right_ty, right)] {
diagnostic.annotate(context.secondary(expr).message(format_args!(
"Has type `{}`",
ty.display_with(db, display_settings.clone())
)));
}
diagnostic.set_concise_message(format_args!(
"Operator `{}` is not supported between objects of type `{}` and `{}`",
error.op,
left_ty.display_with(db, display_settings.clone()),
right_ty.display_with(db, display_settings.clone())
));
}
// For non-atomic types like unions and tuples, we now provide context
// on the underlying elements that caused the error.
// If we're emitting a diagnostic for something like `(1, "foo") < (2, 3)`:
//
// - `left_ty` is `tuple[Literal[1], Literal["foo"]]`
// - `right_ty` is `tuple[Literal[2], Literal[3]]
// - `error.left_ty` is `Literal["foo"]`
// - `error.right_ty` is `Literal[3]`
if (error.left_ty, error.right_ty) != (left_ty, right_ty) {
if let Some(TupleSpec::Fixed(lhs_spec)) = left_ty.tuple_instance_spec(db).as_deref()
&& let Some(TupleSpec::Fixed(rhs_spec)) = right_ty.tuple_instance_spec(db).as_deref()
&& lhs_spec.len() == rhs_spec.len()
&& let Some(position) = lhs_spec
.elements()
.zip(rhs_spec.elements())
.position(|tup| tup == (&error.left_ty, &error.right_ty))
{
if error.left_ty == error.right_ty {
diagnostic.info(format_args!(
"Operation fails because operator `{}` is not supported between \
the tuple elements at index {} (both of type `{}`)",
error.op,
position + 1,
error.left_ty.display_with(db, display_settings),
));
} else {
diagnostic.info(format_args!(
"Operation fails because operator `{}` is not supported between \
the tuple elements at index {} (of type `{}` and `{}`)",
error.op,
position + 1,
error.left_ty.display_with(db, display_settings.clone()),
error.right_ty.display_with(db, display_settings),
));
}
} else {
if error.left_ty == error.right_ty {
diagnostic.info(format_args!(
"Operation fails because operator `{}` is not supported \
between two objects of type `{}`",
error.op,
error.left_ty.display_with(db, display_settings),
));
} else {
diagnostic.info(format_args!(
"Operation fails because operator `{}` is not supported \
between objects of type `{}` and `{}`",
error.op,
error.left_ty.display_with(db, display_settings.clone()),
error.right_ty.display_with(db, display_settings)
));
}
}
}
}
/// This function receives an unresolved `from foo import bar` import,
/// where `foo` can be resolved to a module but that module does not
/// have a `bar` member or submodule.

View File

@@ -72,14 +72,15 @@ impl<'db> DisplaySettings<'db> {
}
#[must_use]
pub fn from_possibly_ambiguous_type_pair(
pub fn from_possibly_ambiguous_types(
db: &'db dyn Db,
type_1: Type<'db>,
type_2: Type<'db>,
types: impl IntoIterator<Item = Type<'db>>,
) -> Self {
let collector = AmbiguousClassCollector::default();
collector.visit_type(db, type_1);
collector.visit_type(db, type_2);
for ty in types {
collector.visit_type(db, ty);
}
Self {
qualified: Rc::new(

View File

@@ -57,6 +57,7 @@ use crate::types::{
};
use crate::unpack::Unpack;
use builder::TypeInferenceBuilder;
pub(super) use builder::UnsupportedComparisonError;
mod builder;
#[cfg(test)]

View File

@@ -79,7 +79,7 @@ use crate::types::diagnostic::{
report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore,
report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
report_possibly_missing_attribute, report_possibly_unresolved_reference,
report_rebound_typevar, report_slice_step_size_zero,
report_rebound_typevar, report_slice_step_size_zero, report_unsupported_comparison,
};
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
@@ -158,7 +158,7 @@ impl<'db> DeclaredAndInferredType<'db> {
type BinaryComparisonVisitor<'db> = CycleDetector<
ast::CmpOp,
(Type<'db>, ast::CmpOp, Type<'db>),
Result<Type<'db>, CompareUnsupportedError<'db>>,
Result<Type<'db>, UnsupportedComparisonError<'db>>,
>;
/// We currently store one dataclass field-specifiers inline, because that covers standard
@@ -10056,26 +10056,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
&BinaryComparisonVisitor::new(Ok(Type::BooleanLiteral(true))),
)
.unwrap_or_else(|error| {
if let Some(diagnostic_builder) =
builder.context.report_lint(&UNSUPPORTED_OPERATOR, range)
{
// Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome)
diagnostic_builder.into_diagnostic(format_args!(
"Operator `{}` is not supported for types `{}` and `{}`{}",
error.op,
error.left_ty.display(builder.db()),
error.right_ty.display(builder.db()),
if (left_ty, right_ty) == (error.left_ty, error.right_ty) {
String::new()
} else {
format!(
", in comparing `{}` with `{}`",
left_ty.display(builder.db()),
right_ty.display(builder.db())
)
}
));
}
report_unsupported_comparison(
&builder.context,
&error,
range,
left,
right,
left_ty,
right_ty,
);
match op {
// `in, not in, is, is not` always return bool instances
@@ -10101,13 +10090,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
intersection_on: IntersectionOn,
range: TextRange,
visitor: &BinaryComparisonVisitor<'db>,
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
) -> Result<Type<'db>, UnsupportedComparisonError<'db>> {
enum State<'db> {
// We have not seen any positive elements (yet)
NoPositiveElements,
// The operator was unsupported on all elements that we have seen so far.
// Contains the first error we encountered.
UnsupportedOnAllElements(CompareUnsupportedError<'db>),
UnsupportedOnAllElements(UnsupportedComparisonError<'db>),
// The operator was supported on at least one positive element.
Supported,
}
@@ -10263,7 +10252,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
right: Type<'db>,
range: TextRange,
visitor: &BinaryComparisonVisitor<'db>,
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
) -> Result<Type<'db>, UnsupportedComparisonError<'db>> {
// Note: identity (is, is not) for equal builtin types is unreliable and not part of the
// language spec.
// - `[ast::CompOp::Is]`: return `false` if unequal, `bool` if equal
@@ -10335,7 +10324,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
IntersectionOn::Left,
range,
visitor,
))
).map_err(|err|UnsupportedComparisonError { op, left_ty: left, right_ty: err.right_ty }))
}
(left, Type::Intersection(intersection)) => {
Some(self.infer_binary_intersection_type_comparison(
@@ -10345,7 +10334,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
IntersectionOn::Right,
range,
visitor,
))
).map_err(|err|UnsupportedComparisonError { op, left_ty: err.left_ty, right_ty: right }))
}
(Type::TypeAlias(alias), right) => Some(
@@ -10392,7 +10381,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// Undefined for (int, int)
ast::CmpOp::In | ast::CmpOp::NotIn => Err(CompareUnsupportedError {
ast::CmpOp::In | ast::CmpOp::NotIn => Err(UnsupportedComparisonError {
op,
left_ty: left,
right_ty: right,
@@ -10405,7 +10394,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
right,
range,
visitor,
))
).map_err(|_| UnsupportedComparisonError {op, left_ty: left, right_ty: right}))
}
(Type::NominalInstance(_), Type::IntLiteral(_)) => {
Some(self.infer_binary_type_comparison(
@@ -10414,7 +10403,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
KnownClass::Int.to_instance(self.db()),
range,
visitor,
))
).map_err(|_|UnsupportedComparisonError { op, left_ty: left, right_ty: right }))
}
// Booleans are coded as integers (False = 0, True = 1)
@@ -10425,7 +10414,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::IntLiteral(i64::from(b)),
range,
visitor,
))
).map_err(|_|UnsupportedComparisonError {op, left_ty: left, right_ty: right}))
}
(Type::BooleanLiteral(b), Type::IntLiteral(m)) => {
Some(self.infer_binary_type_comparison(
@@ -10434,7 +10423,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::IntLiteral(m),
range,
visitor,
))
).map_err(|_|UnsupportedComparisonError {op, left_ty: left, right_ty: right}))
}
(Type::BooleanLiteral(a), Type::BooleanLiteral(b)) => {
Some(self.infer_binary_type_comparison(
@@ -10443,37 +10432,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::IntLiteral(i64::from(b)),
range,
visitor,
))
).map_err(|_|UnsupportedComparisonError {op, left_ty: left, right_ty: right}))
}
(Type::StringLiteral(salsa_s1), Type::StringLiteral(salsa_s2)) => {
let s1 = salsa_s1.value(self.db());
let s2 = salsa_s2.value(self.db());
let result = match op {
ast::CmpOp::Eq => Ok(Type::BooleanLiteral(s1 == s2)),
ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(s1 != s2)),
ast::CmpOp::Lt => Ok(Type::BooleanLiteral(s1 < s2)),
ast::CmpOp::LtE => Ok(Type::BooleanLiteral(s1 <= s2)),
ast::CmpOp::Gt => Ok(Type::BooleanLiteral(s1 > s2)),
ast::CmpOp::GtE => Ok(Type::BooleanLiteral(s1 >= s2)),
ast::CmpOp::In => Ok(Type::BooleanLiteral(s2.contains(s1))),
ast::CmpOp::NotIn => Ok(Type::BooleanLiteral(!s2.contains(s1))),
ast::CmpOp::Eq => Type::BooleanLiteral(s1 == s2),
ast::CmpOp::NotEq => Type::BooleanLiteral(s1 != s2),
ast::CmpOp::Lt => Type::BooleanLiteral(s1 < s2),
ast::CmpOp::LtE => Type::BooleanLiteral(s1 <= s2),
ast::CmpOp::Gt => Type::BooleanLiteral(s1 > s2),
ast::CmpOp::GtE => Type::BooleanLiteral(s1 >= s2),
ast::CmpOp::In => Type::BooleanLiteral(s2.contains(s1)),
ast::CmpOp::NotIn => Type::BooleanLiteral(!s2.contains(s1)),
ast::CmpOp::Is => {
if s1 == s2 {
Ok(KnownClass::Bool.to_instance(self.db()))
KnownClass::Bool.to_instance(self.db())
} else {
Ok(Type::BooleanLiteral(false))
Type::BooleanLiteral(false)
}
}
ast::CmpOp::IsNot => {
if s1 == s2 {
Ok(KnownClass::Bool.to_instance(self.db()))
KnownClass::Bool.to_instance(self.db())
} else {
Ok(Type::BooleanLiteral(true))
Type::BooleanLiteral(true)
}
}
};
Some(result)
Some(Ok(result))
}
(Type::StringLiteral(_), _) => Some(self.infer_binary_type_comparison(
KnownClass::Str.to_instance(self.db()),
@@ -10481,14 +10470,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
right,
range,
visitor,
)),
).map_err(|err|UnsupportedComparisonError {op, left_ty: left, right_ty: err.right_ty})),
(_, Type::StringLiteral(_)) => Some(self.infer_binary_type_comparison(
left,
op,
KnownClass::Str.to_instance(self.db()),
range,
visitor,
)),
).map_err(|err|UnsupportedComparisonError {op, left_ty: err.left_ty, right_ty: right})),
(Type::LiteralString, _) => Some(self.infer_binary_type_comparison(
KnownClass::Str.to_instance(self.db()),
@@ -10496,47 +10485,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
right,
range,
visitor,
)),
).map_err(|err|UnsupportedComparisonError {op, left_ty: left, right_ty: err.right_ty})),
(_, Type::LiteralString) => Some(self.infer_binary_type_comparison(
left,
op,
KnownClass::Str.to_instance(self.db()),
range,
visitor,
)),
).map_err(|err|UnsupportedComparisonError {op, left_ty: err.left_ty, right_ty: right})),
(Type::BytesLiteral(salsa_b1), Type::BytesLiteral(salsa_b2)) => {
let b1 = salsa_b1.value(self.db());
let b2 = salsa_b2.value(self.db());
let result = match op {
ast::CmpOp::Eq => Ok(Type::BooleanLiteral(b1 == b2)),
ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(b1 != b2)),
ast::CmpOp::Lt => Ok(Type::BooleanLiteral(b1 < b2)),
ast::CmpOp::LtE => Ok(Type::BooleanLiteral(b1 <= b2)),
ast::CmpOp::Gt => Ok(Type::BooleanLiteral(b1 > b2)),
ast::CmpOp::GtE => Ok(Type::BooleanLiteral(b1 >= b2)),
ast::CmpOp::Eq => Type::BooleanLiteral(b1 == b2),
ast::CmpOp::NotEq => Type::BooleanLiteral(b1 != b2),
ast::CmpOp::Lt => Type::BooleanLiteral(b1 < b2),
ast::CmpOp::LtE => Type::BooleanLiteral(b1 <= b2),
ast::CmpOp::Gt => Type::BooleanLiteral(b1 > b2),
ast::CmpOp::GtE => Type::BooleanLiteral(b1 >= b2),
ast::CmpOp::In => {
Ok(Type::BooleanLiteral(memchr::memmem::find(b2, b1).is_some()))
Type::BooleanLiteral(memchr::memmem::find(b2, b1).is_some())
}
ast::CmpOp::NotIn => {
Ok(Type::BooleanLiteral(memchr::memmem::find(b2, b1).is_none()))
Type::BooleanLiteral(memchr::memmem::find(b2, b1).is_none())
}
ast::CmpOp::Is => {
if b1 == b2 {
Ok(KnownClass::Bool.to_instance(self.db()))
KnownClass::Bool.to_instance(self.db())
} else {
Ok(Type::BooleanLiteral(false))
Type::BooleanLiteral(false)
}
}
ast::CmpOp::IsNot => {
if b1 == b2 {
Ok(KnownClass::Bool.to_instance(self.db()))
KnownClass::Bool.to_instance(self.db())
} else {
Ok(Type::BooleanLiteral(true))
Type::BooleanLiteral(true)
}
}
};
Some(result)
Some(Ok(result))
}
(Type::BytesLiteral(_), _) => Some(self.infer_binary_type_comparison(
KnownClass::Bytes.to_instance(self.db()),
@@ -10544,14 +10533,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
right,
range,
visitor,
)),
).map_err(|err| UnsupportedComparisonError { op, left_ty: left, right_ty: err.right_ty })),
(_, Type::BytesLiteral(_)) => Some(self.infer_binary_type_comparison(
left,
op,
KnownClass::Bytes.to_instance(self.db()),
range,
visitor,
)),
).map_err(|err| UnsupportedComparisonError { op, left_ty: err.left_ty, right_ty: right })),
(Type::EnumLiteral(literal_1), Type::EnumLiteral(literal_2))
if op == ast::CmpOp::Eq =>
@@ -10683,7 +10672,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
right: Type<'db>,
op: RichCompareOperator,
policy: MemberLookupPolicy,
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
) -> Result<Type<'db>, UnsupportedComparisonError<'db>> {
let db = self.db();
// The following resource has details about the rich comparison algorithm:
// https://snarky.ca/unravelling-rich-comparison-operators/
@@ -10719,7 +10708,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
None
}
})
.ok_or_else(|| CompareUnsupportedError {
.ok_or_else(|| UnsupportedComparisonError {
op: op.into(),
left_ty: left,
right_ty: right,
@@ -10736,7 +10725,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
right: Type<'db>,
op: MembershipTestCompareOperator,
range: TextRange,
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
) -> Result<Type<'db>, UnsupportedComparisonError<'db>> {
let db = self.db();
let contains_dunder = right.class_member(db, "__contains__".into()).place;
@@ -10773,7 +10762,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
MembershipTestCompareOperator::NotIn => truthiness.negate().into_type(db),
}
})
.ok_or_else(|| CompareUnsupportedError {
.ok_or_else(|| UnsupportedComparisonError {
op: op.into(),
left_ty: left,
right_ty: right,
@@ -10792,7 +10781,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
right: &TupleSpec<'db>,
range: TextRange,
visitor: &BinaryComparisonVisitor<'db>,
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
) -> Result<Type<'db>, UnsupportedComparisonError<'db>> {
// If either tuple is variable length, we can make no assumptions about the relative
// lengths of the tuples, and therefore neither about how they compare lexicographically.
// TODO: Consider comparing the prefixes of the tuples, since that could give a comparison
@@ -12382,11 +12371,22 @@ impl From<MembershipTestCompareOperator> for ast::CmpOp {
}
}
/// Context for a failed comparison operation.
///
/// `left_ty` and `right_ty` are the "low-level" types
/// that cannot be compared using `op`. For example,
/// when evaluating `(1, "foo") < (2, 3)`, the "high-level"
/// types of the operands are `tuple[Literal[1], Literal["foo"]]`
/// and `tuple[Literal[2], Literal[3]]`. Those aren't captured
/// in this struct, but the "low-level" types that mean that
/// the high-level types cannot be compared *are* captured in
/// this struct. In this case, those would be `Literal["foo"]`
/// and `Literal[3]`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CompareUnsupportedError<'db> {
op: ast::CmpOp,
left_ty: Type<'db>,
right_ty: Type<'db>,
pub(crate) struct UnsupportedComparisonError<'db> {
pub(crate) op: ast::CmpOp,
pub(crate) left_ty: Type<'db>,
pub(crate) right_ty: Type<'db>,
}
fn format_import_from_module(level: u32, module: Option<&str>) -> String {