Skip to content

Commit ef45185

Browse files
Allow users to provide custom diagnostic messages when unwrapping calls (#13597)
## Summary You can now call `return_ty_result` to operate on a `Result` directly thereby using your own diagnostics, as in: ```rust return dunder_getitem_method .call(self.db, &[slice_ty]) .return_ty_result(self.db, value.as_ref().into(), self) .unwrap_or_else(|err| { self.add_diagnostic( (&**value).into(), "call-non-callable", format_args!( "Method `__getitem__` is not callable on object of type '{}'.", value_ty.display(self.db), ), ); err.return_ty() }); ```
1 parent 961fc98 commit ef45185

File tree

2 files changed

+156
-47
lines changed

2 files changed

+156
-47
lines changed

crates/red_knot_python_semantic/src/types.rs

+128-43
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,9 @@ fn declarations_ty<'db>(
221221
first
222222
};
223223
if conflicting.is_empty() {
224-
DeclaredTypeResult::Ok(declared_ty)
224+
Ok(declared_ty)
225225
} else {
226-
DeclaredTypeResult::Err((
226+
Err((
227227
declared_ty,
228228
[first].into_iter().chain(conflicting).collect(),
229229
))
@@ -900,37 +900,88 @@ impl<'db> CallOutcome<'db> {
900900
}
901901
}
902902

903-
/// Get the return type of the call, emitting diagnostics if needed.
903+
/// Get the return type of the call, emitting default diagnostics if needed.
904904
fn unwrap_with_diagnostic<'a>(
905905
&self,
906906
db: &'db dyn Db,
907907
node: ast::AnyNodeRef,
908908
builder: &'a mut TypeInferenceBuilder<'db>,
909909
) -> Type<'db> {
910-
match self {
911-
Self::Callable { return_ty } => *return_ty,
912-
Self::RevealType {
910+
match self.return_ty_result(db, node, builder) {
911+
Ok(return_ty) => return_ty,
912+
Err(NotCallableError::Type {
913+
not_callable_ty,
913914
return_ty,
914-
revealed_ty,
915-
} => {
915+
}) => {
916916
builder.add_diagnostic(
917917
node,
918-
"revealed-type",
919-
format_args!("Revealed type is '{}'.", revealed_ty.display(db)),
918+
"call-non-callable",
919+
format_args!(
920+
"Object of type '{}' is not callable.",
921+
not_callable_ty.display(db)
922+
),
920923
);
921-
*return_ty
924+
return_ty
922925
}
923-
Self::NotCallable { not_callable_ty } => {
926+
Err(NotCallableError::UnionElement {
927+
not_callable_ty,
928+
called_ty,
929+
return_ty,
930+
}) => {
924931
builder.add_diagnostic(
925932
node,
926933
"call-non-callable",
927934
format_args!(
928-
"Object of type '{}' is not callable.",
929-
not_callable_ty.display(db)
935+
"Object of type '{}' is not callable (due to union element '{}').",
936+
called_ty.display(db),
937+
not_callable_ty.display(db),
930938
),
931939
);
932-
Type::Unknown
940+
return_ty
933941
}
942+
Err(NotCallableError::UnionElements {
943+
not_callable_tys,
944+
called_ty,
945+
return_ty,
946+
}) => {
947+
builder.add_diagnostic(
948+
node,
949+
"call-non-callable",
950+
format_args!(
951+
"Object of type '{}' is not callable (due to union elements {}).",
952+
called_ty.display(db),
953+
not_callable_tys.display(db),
954+
),
955+
);
956+
return_ty
957+
}
958+
}
959+
}
960+
961+
/// Get the return type of the call as a result.
962+
fn return_ty_result<'a>(
963+
&self,
964+
db: &'db dyn Db,
965+
node: ast::AnyNodeRef,
966+
builder: &'a mut TypeInferenceBuilder<'db>,
967+
) -> Result<Type<'db>, NotCallableError<'db>> {
968+
match self {
969+
Self::Callable { return_ty } => Ok(*return_ty),
970+
Self::RevealType {
971+
return_ty,
972+
revealed_ty,
973+
} => {
974+
builder.add_diagnostic(
975+
node,
976+
"revealed-type",
977+
format_args!("Revealed type is '{}'.", revealed_ty.display(db)),
978+
);
979+
Ok(*return_ty)
980+
}
981+
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
982+
not_callable_ty: *not_callable_ty,
983+
return_ty: Type::Unknown,
984+
}),
934985
Self::Union {
935986
outcomes,
936987
called_ty,
@@ -959,41 +1010,75 @@ impl<'db> CallOutcome<'db> {
9591010
};
9601011
union_builder = union_builder.add(return_ty);
9611012
}
1013+
let return_ty = union_builder.build();
9621014
match not_callable[..] {
963-
[] => {}
964-
[elem] => builder.add_diagnostic(
965-
node,
966-
"call-non-callable",
967-
format_args!(
968-
"Object of type '{}' is not callable (due to union element '{}').",
969-
called_ty.display(db),
970-
elem.display(db),
971-
),
972-
),
973-
_ if not_callable.len() == outcomes.len() => builder.add_diagnostic(
974-
node,
975-
"call-non-callable",
976-
format_args!(
977-
"Object of type '{}' is not callable.",
978-
called_ty.display(db)
979-
),
980-
),
981-
_ => builder.add_diagnostic(
982-
node,
983-
"call-non-callable",
984-
format_args!(
985-
"Object of type '{}' is not callable (due to union elements {}).",
986-
called_ty.display(db),
987-
not_callable.display(db),
988-
),
989-
),
1015+
[] => Ok(return_ty),
1016+
[elem] => Err(NotCallableError::UnionElement {
1017+
not_callable_ty: elem,
1018+
called_ty: *called_ty,
1019+
return_ty,
1020+
}),
1021+
_ if not_callable.len() == outcomes.len() => Err(NotCallableError::Type {
1022+
not_callable_ty: *called_ty,
1023+
return_ty,
1024+
}),
1025+
_ => Err(NotCallableError::UnionElements {
1026+
not_callable_tys: not_callable.into_boxed_slice(),
1027+
called_ty: *called_ty,
1028+
return_ty,
1029+
}),
9901030
}
991-
union_builder.build()
9921031
}
9931032
}
9941033
}
9951034
}
9961035

1036+
#[derive(Debug, Clone, PartialEq, Eq)]
1037+
enum NotCallableError<'db> {
1038+
/// The type is not callable.
1039+
Type {
1040+
not_callable_ty: Type<'db>,
1041+
return_ty: Type<'db>,
1042+
},
1043+
/// A single union element is not callable.
1044+
UnionElement {
1045+
not_callable_ty: Type<'db>,
1046+
called_ty: Type<'db>,
1047+
return_ty: Type<'db>,
1048+
},
1049+
/// Multiple (but not all) union elements are not callable.
1050+
UnionElements {
1051+
not_callable_tys: Box<[Type<'db>]>,
1052+
called_ty: Type<'db>,
1053+
return_ty: Type<'db>,
1054+
},
1055+
}
1056+
1057+
impl<'db> NotCallableError<'db> {
1058+
/// The return type that should be used when a call is not callable.
1059+
fn return_ty(&self) -> Type<'db> {
1060+
match self {
1061+
Self::Type { return_ty, .. } => *return_ty,
1062+
Self::UnionElement { return_ty, .. } => *return_ty,
1063+
Self::UnionElements { return_ty, .. } => *return_ty,
1064+
}
1065+
}
1066+
1067+
/// The resolved type that was not callable.
1068+
///
1069+
/// For unions, returns the union type itself, which may contain a mix of callable and
1070+
/// non-callable types.
1071+
fn called_ty(&self) -> Type<'db> {
1072+
match self {
1073+
Self::Type {
1074+
not_callable_ty, ..
1075+
} => *not_callable_ty,
1076+
Self::UnionElement { called_ty, .. } => *called_ty,
1077+
Self::UnionElements { called_ty, .. } => *called_ty,
1078+
}
1079+
}
1080+
}
1081+
9971082
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9981083
enum IterationOutcome<'db> {
9991084
Iterable { element_ty: Type<'db> },

crates/red_knot_python_semantic/src/types/infer.rs

+28-4
Original file line numberDiff line numberDiff line change
@@ -2616,7 +2616,19 @@ impl<'db> TypeInferenceBuilder<'db> {
26162616
if !dunder_getitem_method.is_unbound() {
26172617
return dunder_getitem_method
26182618
.call(self.db, &[slice_ty])
2619-
.unwrap_with_diagnostic(self.db, value.as_ref().into(), self);
2619+
.return_ty_result(self.db, value.as_ref().into(), self)
2620+
.unwrap_or_else(|err| {
2621+
self.add_diagnostic(
2622+
(&**value).into(),
2623+
"call-non-callable",
2624+
format_args!(
2625+
"Method `__getitem__` of type '{}' is not callable on object of type '{}'.",
2626+
err.called_ty().display(self.db),
2627+
value_ty.display(self.db),
2628+
),
2629+
);
2630+
err.return_ty()
2631+
});
26202632
}
26212633

26222634
// Otherwise, if the value is itself a class and defines `__class_getitem__`,
@@ -2626,7 +2638,19 @@ impl<'db> TypeInferenceBuilder<'db> {
26262638
if !dunder_class_getitem_method.is_unbound() {
26272639
return dunder_class_getitem_method
26282640
.call(self.db, &[slice_ty])
2629-
.unwrap_with_diagnostic(self.db, value.as_ref().into(), self);
2641+
.return_ty_result(self.db, value.as_ref().into(), self)
2642+
.unwrap_or_else(|err| {
2643+
self.add_diagnostic(
2644+
(&**value).into(),
2645+
"call-non-callable",
2646+
format_args!(
2647+
"Method `__class_getitem__` of type '{}' is not callable on object of type '{}'.",
2648+
err.called_ty().display(self.db),
2649+
value_ty.display(self.db),
2650+
),
2651+
);
2652+
err.return_ty()
2653+
});
26302654
}
26312655

26322656
self.non_subscriptable_diagnostic(
@@ -6840,7 +6864,7 @@ mod tests {
68406864
assert_file_diagnostics(
68416865
&db,
68426866
"/src/a.py",
6843-
&["Object of type 'None' is not callable."],
6867+
&["Method `__getitem__` of type 'None' is not callable on object of type 'NotSubscriptable'."],
68446868
);
68456869

68466870
Ok(())
@@ -7015,7 +7039,7 @@ mod tests {
70157039
assert_file_diagnostics(
70167040
&db,
70177041
"/src/a.py",
7018-
&["Object of type 'Literal[__class_getitem__] | Unbound' is not callable (due to union element 'Unbound')."],
7042+
&["Method `__class_getitem__` of type 'Literal[__class_getitem__] | Unbound' is not callable on object of type 'Literal[Identity, Identity]'."],
70197043
);
70207044

70217045
Ok(())

0 commit comments

Comments
 (0)