mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-09 10:00:25 +00:00
[red-knot] Improve type inference for except handlers (#14838)
This commit is contained in:
parent
64944f2cf5
commit
ab26d9cf9a
5 changed files with 150 additions and 41 deletions
|
@ -1173,6 +1173,8 @@ impl<'db> Type<'db> {
|
|||
| KnownClass::Set
|
||||
| KnownClass::Dict
|
||||
| KnownClass::Slice
|
||||
| KnownClass::BaseException
|
||||
| KnownClass::BaseExceptionGroup
|
||||
| KnownClass::GenericAlias
|
||||
| KnownClass::ModuleType
|
||||
| KnownClass::FunctionType
|
||||
|
@ -1845,6 +1847,8 @@ pub enum KnownClass {
|
|||
Set,
|
||||
Dict,
|
||||
Slice,
|
||||
BaseException,
|
||||
BaseExceptionGroup,
|
||||
// Types
|
||||
GenericAlias,
|
||||
ModuleType,
|
||||
|
@ -1875,6 +1879,8 @@ impl<'db> KnownClass {
|
|||
Self::List => "list",
|
||||
Self::Type => "type",
|
||||
Self::Slice => "slice",
|
||||
Self::BaseException => "BaseException",
|
||||
Self::BaseExceptionGroup => "BaseExceptionGroup",
|
||||
Self::GenericAlias => "GenericAlias",
|
||||
Self::ModuleType => "ModuleType",
|
||||
Self::FunctionType => "FunctionType",
|
||||
|
@ -1902,6 +1908,12 @@ impl<'db> KnownClass {
|
|||
.unwrap_or(Type::Unknown)
|
||||
}
|
||||
|
||||
pub fn to_subclass_of(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
self.to_class_literal(db)
|
||||
.into_class_literal()
|
||||
.map(|ClassLiteralType { class }| Type::subclass_of(class))
|
||||
}
|
||||
|
||||
/// Return the module in which we should look up the definition for this class
|
||||
pub(crate) fn canonical_module(self, db: &'db dyn Db) -> CoreStdlibModule {
|
||||
match self {
|
||||
|
@ -1916,6 +1928,8 @@ impl<'db> KnownClass {
|
|||
| Self::Tuple
|
||||
| Self::Set
|
||||
| Self::Dict
|
||||
| Self::BaseException
|
||||
| Self::BaseExceptionGroup
|
||||
| Self::Slice => CoreStdlibModule::Builtins,
|
||||
Self::VersionInfo => CoreStdlibModule::Sys,
|
||||
Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types,
|
||||
|
@ -1959,6 +1973,8 @@ impl<'db> KnownClass {
|
|||
| Self::ModuleType
|
||||
| Self::FunctionType
|
||||
| Self::SpecialForm
|
||||
| Self::BaseException
|
||||
| Self::BaseExceptionGroup
|
||||
| Self::TypeVar => false,
|
||||
}
|
||||
}
|
||||
|
@ -1980,6 +1996,8 @@ impl<'db> KnownClass {
|
|||
"dict" => Self::Dict,
|
||||
"list" => Self::List,
|
||||
"slice" => Self::Slice,
|
||||
"BaseException" => Self::BaseException,
|
||||
"BaseExceptionGroup" => Self::BaseExceptionGroup,
|
||||
"GenericAlias" => Self::GenericAlias,
|
||||
"NoneType" => Self::NoneType,
|
||||
"ModuleType" => Self::ModuleType,
|
||||
|
@ -2016,6 +2034,8 @@ impl<'db> KnownClass {
|
|||
| Self::GenericAlias
|
||||
| Self::ModuleType
|
||||
| Self::VersionInfo
|
||||
| Self::BaseException
|
||||
| Self::BaseExceptionGroup
|
||||
| Self::FunctionType => module.name() == self.canonical_module(db).as_str(),
|
||||
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
|
||||
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => {
|
||||
|
|
|
@ -289,6 +289,18 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
|||
);
|
||||
}
|
||||
|
||||
pub(super) fn add_invalid_exception(&mut self, db: &dyn Db, node: &ast::Expr, ty: Type) {
|
||||
self.add(
|
||||
node.into(),
|
||||
"invalid-exception",
|
||||
format_args!(
|
||||
"Cannot catch object of type `{}` in an exception handler \
|
||||
(must be a `BaseException` subclass or a tuple of `BaseException` subclasses)",
|
||||
ty.display(db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds a new diagnostic.
|
||||
///
|
||||
/// The diagnostic does not get added if the rule isn't enabled for this file.
|
||||
|
|
|
@ -1535,40 +1535,56 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
except_handler_definition: &ExceptHandlerDefinitionKind,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
let node_ty = except_handler_definition
|
||||
.handled_exceptions()
|
||||
.map(|ty| self.infer_expression(ty))
|
||||
// If there is no handled exception, it's invalid syntax;
|
||||
// a diagnostic will have already been emitted
|
||||
.unwrap_or(Type::Unknown);
|
||||
let node = except_handler_definition.handled_exceptions();
|
||||
|
||||
// If there is no handled exception, it's invalid syntax;
|
||||
// a diagnostic will have already been emitted
|
||||
let node_ty = node.map_or(Type::Unknown, |ty| self.infer_expression(ty));
|
||||
|
||||
// If it's an `except*` handler, this won't actually be the type of the bound symbol;
|
||||
// it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`.
|
||||
let symbol_ty = if let Type::Tuple(tuple) = node_ty {
|
||||
let type_base_exception = KnownClass::BaseException
|
||||
.to_subclass_of(self.db)
|
||||
.unwrap_or(Type::Unknown);
|
||||
let mut builder = UnionBuilder::new(self.db);
|
||||
for element in tuple.elements(self.db).iter().copied() {
|
||||
builder = builder.add(if element.is_assignable_to(self.db, type_base_exception) {
|
||||
element.to_instance(self.db)
|
||||
} else {
|
||||
if let Some(node) = node {
|
||||
self.diagnostics
|
||||
.add_invalid_exception(self.db, node, element);
|
||||
}
|
||||
Type::Unknown
|
||||
});
|
||||
}
|
||||
builder.build()
|
||||
} else if node_ty.is_subtype_of(self.db, KnownClass::Tuple.to_instance(self.db)) {
|
||||
todo_type!("Homogeneous tuple in exception handler")
|
||||
} else {
|
||||
let type_base_exception = KnownClass::BaseException
|
||||
.to_subclass_of(self.db)
|
||||
.unwrap_or(Type::Unknown);
|
||||
if node_ty.is_assignable_to(self.db, type_base_exception) {
|
||||
node_ty.to_instance(self.db)
|
||||
} else {
|
||||
if let Some(node) = node {
|
||||
self.diagnostics
|
||||
.add_invalid_exception(self.db, node, node_ty);
|
||||
}
|
||||
Type::Unknown
|
||||
}
|
||||
};
|
||||
|
||||
let symbol_ty = if except_handler_definition.is_star() {
|
||||
// TODO should be generic --Alex
|
||||
// TODO: we should infer `ExceptionGroup` if `node_ty` is a subtype of `tuple[type[Exception], ...]`
|
||||
// (needs support for homogeneous tuples).
|
||||
//
|
||||
// TODO should infer `ExceptionGroup` if all caught exceptions
|
||||
// are subclasses of `Exception` --Alex
|
||||
builtins_symbol(self.db, "BaseExceptionGroup")
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::Unknown)
|
||||
.to_instance(self.db)
|
||||
// TODO: should be generic with `symbol_ty` as the generic parameter
|
||||
KnownClass::BaseExceptionGroup.to_instance(self.db)
|
||||
} else {
|
||||
// TODO: anything that's a consistent subtype of
|
||||
// `type[BaseException] | tuple[type[BaseException], ...]` should be valid;
|
||||
// anything else is invalid and should lead to a diagnostic being reported --Alex
|
||||
match node_ty {
|
||||
Type::Any | Type::Unknown => node_ty,
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(class),
|
||||
Type::Tuple(tuple) => UnionType::from_elements(
|
||||
self.db,
|
||||
tuple.elements(self.db).iter().map(|ty| {
|
||||
ty.into_class_literal().map_or(
|
||||
todo_type!("exception type"),
|
||||
|ClassLiteralType { class }| Type::instance(class),
|
||||
)
|
||||
}),
|
||||
),
|
||||
_ => todo_type!("exception type"),
|
||||
}
|
||||
symbol_ty
|
||||
};
|
||||
|
||||
self.add_binding(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue