[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:
Charlie Marsh
2026-01-13 20:24:05 -05:00
committed by GitHub
parent ea46426157
commit b5814b91c1
3 changed files with 114 additions and 21 deletions

View File

@@ -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) => {