diff --git a/crates/ty_project/resources/test/corpus/except_handler_with_Any_bound_typevar.py b/crates/ty_project/resources/test/corpus/except_handler_with_Any_bound_typevar.py new file mode 100644 index 0000000000..a951d5034f --- /dev/null +++ b/crates/ty_project/resources/test/corpus/except_handler_with_Any_bound_typevar.py @@ -0,0 +1,14 @@ +def name_1[name_0: name_0](name_2: name_0): + try: + pass + except name_2: + pass + +from typing import Any + +def name_2[T: Any](x: T): + try: + pass + except x: + pass + diff --git a/crates/ty_python_semantic/resources/mdtest/exception/basic.md b/crates/ty_python_semantic/resources/mdtest/exception/basic.md index 6097625eda..7e4ebcd4b4 100644 --- a/crates/ty_python_semantic/resources/mdtest/exception/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/exception/basic.md @@ -76,6 +76,44 @@ except Error as err: ... ``` +## Exception with no captured type + +```py +try: + {}.get("foo") +except TypeError: + pass +``` + +## Exception which catches typevar + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable + +def silence[T: type[BaseException]]( + func: Callable[[], None], + exception_type: T, +): + try: + func() + except exception_type as e: + reveal_type(e) # revealed: T'instance + +def silence2[T: ( + type[ValueError], + type[TypeError], +)](func: Callable[[], None], exception_type: T,): + try: + func() + except exception_type as e: + reveal_type(e) # revealed: T'instance +``` + ## Invalid exception handlers ```py @@ -108,6 +146,12 @@ def foo( # error: [invalid-exception-caught] except z as g: reveal_type(g) # revealed: Unknown + +try: + {}.get("foo") +# error: [invalid-exception-caught] +except int: + pass ``` ## Object raised is not an exception diff --git a/crates/ty_python_semantic/resources/mdtest/exception/except_star.md b/crates/ty_python_semantic/resources/mdtest/exception/except_star.md index 8f862e747e..36ccd7d82a 100644 --- a/crates/ty_python_semantic/resources/mdtest/exception/except_star.md +++ b/crates/ty_python_semantic/resources/mdtest/exception/except_star.md @@ -43,9 +43,23 @@ except* (KeyboardInterrupt, AttributeError) as e: reveal_type(e) # revealed: BaseExceptionGroup[KeyboardInterrupt | AttributeError] ``` -## Invalid `except*` handlers +## `except*` with no captured exception type ```py +try: + help() +except* TypeError: + pass +``` + +## Invalid `except*` handlers with or without a captured exception type + +```py +try: + help() +except* int: # error: [invalid-exception-caught] + pass + try: help() except* 3 as e: # error: [invalid-exception-caught] diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 7c64993ae8..efe9eaa9d0 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4745,6 +4745,32 @@ impl<'db> Type<'db> { } Some(builder.build()) } + // If there is no bound or constraints on a typevar `T`, `T: object` implicitly, which + // has no instance type. Otherwise, synthesize a typevar with bound or constraints + // mapped through `to_instance`. + Type::TypeVar(typevar) => { + let bound_or_constraints = match typevar.bound_or_constraints(db)? { + TypeVarBoundOrConstraints::UpperBound(upper_bound) => { + TypeVarBoundOrConstraints::UpperBound(upper_bound.to_instance(db)?) + } + TypeVarBoundOrConstraints::Constraints(constraints) => { + let mut builder = UnionBuilder::new(db); + for constraint in constraints.elements(db) { + builder = builder.add(constraint.to_instance(db)?); + } + TypeVarBoundOrConstraints::Constraints(builder.build().into_union()?) + } + }; + Some(Type::TypeVar(TypeVarInstance::new( + db, + Name::new(format!("{}'instance", typevar.name(db))), + None, + Some(bound_or_constraints), + typevar.variance(db), + None, + typevar.kind(db), + ))) + } Type::Intersection(_) => Some(todo_type!("Type::Intersection.to_instance")), Type::BooleanLiteral(_) | Type::BytesLiteral(_) @@ -4763,7 +4789,6 @@ impl<'db> Type<'db> { | Type::IntLiteral(_) | Type::StringLiteral(_) | Type::Tuple(_) - | Type::TypeVar(_) | Type::LiteralString | Type::BoundSuper(_) | Type::AlwaysTruthy @@ -4890,7 +4915,7 @@ impl<'db> Type<'db> { Ok(Type::TypeVar(TypeVarInstance::new( db, ast::name::Name::new("Self"), - class_def, + Some(class_def), Some(TypeVarBoundOrConstraints::UpperBound(instance)), TypeVarVariance::Invariant, None, @@ -5431,7 +5456,7 @@ impl<'db> Type<'db> { } Self::KnownInstance(instance) => match instance { KnownInstanceType::TypeVar(var) => { - Some(TypeDefinition::TypeVar(var.definition(db))) + Some(TypeDefinition::TypeVar(var.definition(db)?)) } KnownInstanceType::TypeAliasType(type_alias) => { type_alias.definition(db).map(TypeDefinition::TypeAlias) @@ -5457,7 +5482,7 @@ impl<'db> Type<'db> { | Self::BoundSuper(_) | Self::Tuple(_) => self.to_meta_type(db).definition(db), - Self::TypeVar(var) => Some(TypeDefinition::TypeVar(var.definition(db))), + Self::TypeVar(var) => Some(TypeDefinition::TypeVar(var.definition(db)?)), Self::ProtocolInstance(protocol) => match protocol.inner { Protocol::FromClass(class) => Some(TypeDefinition::Class(class.definition(db))), @@ -5806,8 +5831,8 @@ pub struct TypeVarInstance<'db> { #[returns(ref)] name: ast::name::Name, - /// The type var's definition - pub definition: Definition<'db>, + /// The type var's definition (None if synthesized) + pub definition: Option>, /// The upper bound or constraint on the type of this TypeVar bound_or_constraints: Option>, diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index be6641e510..9750bd5304 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1820,10 +1820,13 @@ impl<'db> BindingError<'db> { typevar.name(context.db()), )); - let typevar_range = typevar.definition(context.db()).full_range(context.db()); - let mut sub = SubDiagnostic::new(Severity::Info, "Type variable defined here"); - sub.annotate(Annotation::primary(typevar_range.into())); - diag.sub(sub); + if let Some(typevar_definition) = typevar.definition(context.db()) { + let typevar_range = typevar_definition.full_range(context.db()); + let mut sub = SubDiagnostic::new(Severity::Info, "Type variable defined here"); + sub.annotate(Annotation::primary(typevar_range.into())); + diag.sub(sub); + } + if let Some(union_diag) = union_diag { union_diag.add_union_context(context.db(), &mut diag); } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index f95e926d3a..beb7fb260b 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -2452,7 +2452,7 @@ impl<'db> TypeInferenceBuilder<'db> { if symbol_name.is_some() { self.infer_definition(handler); } else { - self.infer_optional_expression(handled_exceptions.as_deref()); + self.infer_exception(handled_exceptions.as_deref(), try_statement.is_star); } self.infer_body(body); @@ -2555,11 +2555,7 @@ impl<'db> TypeInferenceBuilder<'db> { }) } - fn infer_except_handler_definition( - &mut self, - except_handler_definition: &ExceptHandlerDefinitionKind, - definition: Definition<'db>, - ) { + fn infer_exception(&mut self, node: Option<&ast::Expr>, is_star: bool) -> Type<'db> { fn extract_tuple_specialization<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { let class = ty.into_nominal_instance()?.class; if !class.is_known(db, KnownClass::Tuple) { @@ -2576,8 +2572,6 @@ impl<'db> TypeInferenceBuilder<'db> { .then_some(specialization_instance) } - 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)); @@ -2632,7 +2626,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::unknown() }; - let symbol_ty = if except_handler_definition.is_star() { + if is_star { let class = if symbol_ty .is_subtype_of(self.db(), KnownClass::Exception.to_instance(self.db())) { @@ -2643,7 +2637,18 @@ impl<'db> TypeInferenceBuilder<'db> { class.to_specialized_instance(self.db(), [symbol_ty]) } else { symbol_ty - }; + } + } + + fn infer_except_handler_definition( + &mut self, + except_handler_definition: &ExceptHandlerDefinitionKind, + definition: Definition<'db>, + ) { + let symbol_ty = self.infer_exception( + except_handler_definition.handled_exceptions(), + except_handler_definition.is_star(), + ); self.add_binding( except_handler_definition.node().into(), @@ -2706,7 +2711,7 @@ impl<'db> TypeInferenceBuilder<'db> { let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( self.db(), name.id.clone(), - definition, + Some(definition), bound_or_constraint, TypeVarVariance::Invariant, // TODO: infer this default_ty, @@ -5361,7 +5366,7 @@ impl<'db> TypeInferenceBuilder<'db> { KnownInstanceType::TypeVar(TypeVarInstance::new( self.db(), target.id.clone(), - containing_assignment, + Some(containing_assignment), bound_or_constraint, variance, *default,