[ty] Improve diagnostics for unsupported binary operations and unsupported augmented assignments (#21947)

## Summary

This PR takes the improvements we made to unsupported-comparison
diagnostics in https://github.com/astral-sh/ruff/pull/21737, and extends
them to other `unsupported-operator` diagnostics.

## Test Plan

Mdtests and snapshots
This commit is contained in:
Alex Waygood
2025-12-12 21:53:29 +00:00
committed by GitHub
parent ca5f099481
commit 5a2aba237b
15 changed files with 401 additions and 107 deletions

View File

@@ -40,7 +40,7 @@ use ruff_db::{
use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::name::Name;
use ruff_python_ast::token::parentheses_iterator;
use ruff_python_ast::{self as ast, AnyNodeRef, StringFlags};
use ruff_python_ast::{self as ast, AnyNodeRef, PythonVersion, StringFlags};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
use std::fmt::{self, Formatter};
@@ -4155,6 +4155,120 @@ pub(super) fn report_unsupported_comparison<'db>(
}
}
pub(super) fn report_unsupported_augmented_assignment<'db>(
context: &InferContext<'db, '_>,
stmt: &ast::StmtAugAssign,
left_ty: Type<'db>,
right_ty: Type<'db>,
) {
report_unsupported_binary_operation_impl(
context,
stmt.range(),
&stmt.target,
&stmt.value,
left_ty,
right_ty,
OperatorDisplay {
operator: stmt.op,
is_augmented_assignment: true,
},
);
}
pub(super) fn report_unsupported_binary_operation<'db>(
context: &InferContext<'db, '_>,
binary_expression: &ast::ExprBinOp,
left_ty: Type<'db>,
right_ty: Type<'db>,
operator: ast::Operator,
) {
let Some(mut diagnostic) = report_unsupported_binary_operation_impl(
context,
binary_expression.range(),
&binary_expression.left,
&binary_expression.right,
left_ty,
right_ty,
OperatorDisplay {
operator,
is_augmented_assignment: false,
},
) else {
return;
};
let db = context.db();
if operator == ast::Operator::BitOr
&& (left_ty.is_subtype_of(db, KnownClass::Type.to_instance(db))
|| right_ty.is_subtype_of(db, KnownClass::Type.to_instance(db)))
&& Program::get(db).python_version(db) < PythonVersion::PY310
{
diagnostic.info(
"Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later",
);
add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, "resolving types");
}
}
#[derive(Debug, Copy, Clone)]
struct OperatorDisplay {
operator: ast::Operator,
is_augmented_assignment: bool,
}
impl std::fmt::Display for OperatorDisplay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_augmented_assignment {
write!(f, "{}=", self.operator)
} else {
write!(f, "{}", self.operator)
}
}
}
fn report_unsupported_binary_operation_impl<'a>(
context: &'a InferContext<'a, 'a>,
range: TextRange,
left: &ast::Expr,
right: &ast::Expr,
left_ty: Type<'a>,
right_ty: Type<'a>,
operator: OperatorDisplay,
) -> Option<LintDiagnosticGuard<'a, 'a>> {
let db = context.db();
let diagnostic_builder = context.report_lint(&UNSUPPORTED_OPERATOR, range)?;
let display_settings = DisplaySettings::from_possibly_ambiguous_types(db, [left_ty, right_ty]);
let mut diagnostic =
diagnostic_builder.into_diagnostic(format_args!("Unsupported `{operator}` operation"));
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 `{operator}` is not supported between two objects of type `{}`",
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 `{operator}` is not supported between objects of type `{}` and `{}`",
left_ty.display_with(db, display_settings.clone()),
right_ty.display_with(db, display_settings.clone())
));
}
Some(diagnostic)
}
/// 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

@@ -80,7 +80,8 @@ 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_unsupported_comparison,
report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
report_unsupported_binary_operation, report_unsupported_comparison,
};
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
@@ -5925,22 +5926,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let op = assignment.op;
let db = self.db();
let report_unsupported_augmented_op = |ctx: &mut InferContext| {
let Some(builder) = ctx.report_lint(&UNSUPPORTED_OPERATOR, assignment) else {
return;
};
builder.into_diagnostic(format_args!(
"Operator `{op}=` is not supported between objects of type `{}` and `{}`",
target_type.display(db),
value_type.display(db)
));
};
// Fall back to non-augmented binary operator inference.
let mut binary_return_ty = || {
self.infer_binary_expression_type(assignment.into(), false, target_type, value_type, op)
.unwrap_or_else(|| {
report_unsupported_augmented_op(&mut self.context);
report_unsupported_augmented_assignment(
&self.context,
assignment,
target_type,
value_type,
);
Type::unknown()
})
};
@@ -5964,7 +5959,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
UnionType::from_elements(db, [outcome.return_type(db), binary_return_ty()])
}
Err(CallDunderError::CallError(_, bindings)) => {
report_unsupported_augmented_op(&mut self.context);
report_unsupported_augmented_assignment(
&self.context,
assignment,
target_type,
value_type,
);
bindings.return_type(db)
}
}
@@ -9699,7 +9699,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.context.report_lint(&UNSUPPORTED_OPERATOR, unary)
{
builder.into_diagnostic(format_args!(
"Unary operator `{op}` is not supported for type `{}`",
"Unary operator `{op}` is not supported for object of type `{}`",
operand_type.display(self.db()),
));
}
@@ -9732,26 +9732,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_binary_expression_type(binary.into(), false, left_ty, right_ty, *op)
.unwrap_or_else(|| {
let db = self.db();
if let Some(builder) = self.context.report_lint(&UNSUPPORTED_OPERATOR, binary) {
let mut diag = builder.into_diagnostic(format_args!(
"Operator `{op}` is not supported between objects of type `{}` and `{}`",
left_ty.display(db),
right_ty.display(db)
));
if op == &ast::Operator::BitOr
&& (left_ty.is_subtype_of(db, KnownClass::Type.to_instance(db))
|| right_ty.is_subtype_of(db, KnownClass::Type.to_instance(db)))
&& Program::get(db).python_version(db) < PythonVersion::PY310
{
diag.info(
"Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later",
);
add_inferred_python_version_hint_to_diagnostic(db, &mut diag, "resolving types");
}
}
report_unsupported_binary_operation(&self.context, binary, left_ty, right_ty, *op);
Type::unknown()
})
}