mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-25 14:24:10 +00:00
[red-knot] Allow any Ranged argument for report_lint and report_diagnostic (#16252)
This commit is contained in:
parent
3032867603
commit
55ea09401a
6 changed files with 236 additions and 221 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T>(
|
||||
&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<T>(
|
||||
&self,
|
||||
lint: &'static LintMetadata,
|
||||
node: AnyNodeRef,
|
||||
ranged: T,
|
||||
message: fmt::Arguments,
|
||||
secondary_messages: Vec<SecondaryDiagnosticMessage>,
|
||||
) {
|
||||
if !self.db.is_file_open(self.file) {
|
||||
return;
|
||||
) where
|
||||
T: Ranged,
|
||||
{
|
||||
fn lint_severity(
|
||||
context: &InferContext,
|
||||
lint: &'static LintMetadata,
|
||||
range: TextRange,
|
||||
) -> Option<Severity> {
|
||||
if !context.db.is_file_open(context.file) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Skip over diagnostics if the rule is disabled.
|
||||
let severity = context.db.rule_selection().severity(LintId::of(lint))?;
|
||||
|
||||
if context.is_in_no_type_check() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let suppressions = suppressions(context.db, context.file);
|
||||
|
||||
if let Some(suppression) = suppressions.find_suppression(range, LintId::of(lint)) {
|
||||
context.diagnostics.borrow_mut().mark_used(suppression.id());
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(severity)
|
||||
}
|
||||
|
||||
// Skip over diagnostics if the rule is disabled.
|
||||
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
|
||||
let Some(severity) = lint_severity(self, lint, ranged.range()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if self.is_in_no_type_check() {
|
||||
return;
|
||||
}
|
||||
|
||||
let suppressions = suppressions(self.db, self.file);
|
||||
|
||||
if let Some(suppression) = suppressions.find_suppression(node.range(), LintId::of(lint)) {
|
||||
self.diagnostics.borrow_mut().mark_used(suppression.id());
|
||||
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<T>(
|
||||
&self,
|
||||
node: AnyNodeRef,
|
||||
ranged: T,
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
message: fmt::Arguments,
|
||||
secondary_messages: Vec<SecondaryDiagnosticMessage>,
|
||||
) {
|
||||
) 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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 `<https://docs.python.org/3/reference/datamodel.html#object.__lt__>`
|
||||
fn infer_rich_comparison(
|
||||
&self,
|
||||
left: InstanceType<'db>,
|
||||
right: InstanceType<'db>,
|
||||
op: RichCompareOperator,
|
||||
) -> Result<Type<'db>, 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 `<https://docs.python.org/3/reference/datamodel.html#object.__eq__>`
|
||||
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 `<https://docs.python.org/3/reference/datamodel.html#object.__contains__>`
|
||||
/// and `<https://docs.python.org/3/reference/expressions.html#membership-test-details>`
|
||||
fn infer_membership_test_comparison(
|
||||
&self,
|
||||
left: InstanceType<'db>,
|
||||
right: InstanceType<'db>,
|
||||
op: MembershipTestCompareOperator,
|
||||
) -> Result<Type<'db>, 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 `<https://docs.python.org/3/reference/datamodel.html#object.__lt__>`
|
||||
fn perform_rich_comparison<'db>(
|
||||
db: &'db dyn Db,
|
||||
left: InstanceType<'db>,
|
||||
right: InstanceType<'db>,
|
||||
op: RichCompareOperator,
|
||||
) -> Result<Type<'db>, 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 `<https://docs.python.org/3/reference/datamodel.html#object.__eq__>`
|
||||
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 `<https://docs.python.org/3/reference/datamodel.html#object.__contains__>`
|
||||
/// and `<https://docs.python.org/3/reference/expressions.html#membership-test-details>`
|
||||
fn perform_membership_test_comparison<'db>(
|
||||
db: &'db dyn Db,
|
||||
left: InstanceType<'db>,
|
||||
right: InstanceType<'db>,
|
||||
op: MembershipTestCompareOperator,
|
||||
) -> Result<Type<'db>, 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};
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue