[ty] Add diagnostics to validate TypeIs and TypeGuard definitions (#22300)
## Summary Closes https://github.com/astral-sh/ty/issues/2267.
This commit is contained in:
@@ -68,8 +68,8 @@ use crate::types::diagnostic::{
|
||||
INVALID_GENERIC_ENUM, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS,
|
||||
INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT,
|
||||
INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM,
|
||||
INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT,
|
||||
IncompatibleBases, NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE,
|
||||
INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_CONSTRAINTS,
|
||||
INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE,
|
||||
POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
|
||||
TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
|
||||
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR,
|
||||
@@ -587,6 +587,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
if self.db().should_check_file(self.file()) {
|
||||
self.check_static_class_definitions();
|
||||
self.check_overloaded_functions(node);
|
||||
self.check_type_guard_definitions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1452,6 +1453,85 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that all type guard function definitions have at least one positional parameter
|
||||
/// (in addition to `self`/`cls` for methods), and for `TypeIs`, that the narrowed type is
|
||||
/// assignable to the declared type of that parameter.
|
||||
fn check_type_guard_definitions(&mut self) {
|
||||
for (definition, ty) in self.declarations.iter() {
|
||||
// Only check actual function definitions, not imports.
|
||||
let DefinitionKind::Function(function_ref) = definition.kind(self.db()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(function) = ty.inner_type().as_function_literal() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for overload in function.iter_overloads_and_implementation(self.db()) {
|
||||
let signature = overload.signature(self.db());
|
||||
let return_ty = signature.return_ty;
|
||||
|
||||
// Check if this is a `TypeIs` or `TypeGuard` return type.
|
||||
let (type_guard_form_name, narrowed_type) = match return_ty {
|
||||
Type::TypeIs(type_is) => ("TypeIs", Some(type_is.return_type(self.db()))),
|
||||
Type::TypeGuard(_) => ("TypeGuard", None),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let function_node = function_ref.node(self.module());
|
||||
|
||||
// The return type annotation must exist since we matched `TypeIs`/`TypeGuard`.
|
||||
let Some(returns_expr) = function_node.returns.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Check if this is a non-static method (first parameter is implicit `self`/`cls`).
|
||||
let is_method = self
|
||||
.index
|
||||
.class_definition_of_method(
|
||||
overload.body_scope(self.db()).file_scope_id(self.db()),
|
||||
)
|
||||
.is_some();
|
||||
let has_implicit_receiver = is_method && !overload.is_staticmethod(self.db());
|
||||
|
||||
// Find the first positional parameter to narrow (skip implicit `self`/`cls`).
|
||||
let positional_params: Vec<_> = signature.parameters().positional().collect();
|
||||
let first_narrowed_param_index = usize::from(has_implicit_receiver);
|
||||
let first_narrowed_param = positional_params.get(first_narrowed_param_index);
|
||||
|
||||
let Some(first_narrowed_param) = first_narrowed_param else {
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_TYPE_GUARD_DEFINITION, returns_expr)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"`{type_guard_form_name}` function must have a parameter to narrow"
|
||||
));
|
||||
}
|
||||
continue;
|
||||
};
|
||||
|
||||
// For `TypeIs`, check that the narrowed type is assignable to the parameter type.
|
||||
if let Some(narrowed_ty) = narrowed_type {
|
||||
let param_ty = first_narrowed_param.annotated_type();
|
||||
if !narrowed_ty.is_assignable_to(self.db(), param_ty) {
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_TYPE_GUARD_DEFINITION, returns_expr)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Narrowed type `{narrowed}` is not assignable \
|
||||
to the declared parameter type `{param}`",
|
||||
narrowed = narrowed_ty.display(self.db()),
|
||||
param = param_ty.display(self.db())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_region_definition(&mut self, definition: Definition<'db>) {
|
||||
match definition.kind(self.db()) {
|
||||
DefinitionKind::Function(function) => {
|
||||
|
||||
Reference in New Issue
Block a user