From 8341da7f637f9eb9caf12b9b47ff99b7b95afc27 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 15 Sep 2025 12:06:48 +0100 Subject: [PATCH] [ty] Allow annotation expressions to be `ast::Attribute` nodes (#20413) Fixes https://github.com/astral-sh/ty/issues/1187 --- .../mdtest/type_qualifiers/classvar.md | 5 + .../infer/builder/annotation_expression.rs | 104 ++++++++++-------- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md index 51d76ac524..95aeb24a11 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md @@ -9,6 +9,7 @@ For more details on the semantics of pure class variables, see [this test](../at ## Basic ```py +import typing from typing import ClassVar, Annotated class C: @@ -17,12 +18,14 @@ class C: c: ClassVar[Annotated[int, "the annotation for c"]] = 1 d: ClassVar = 1 e: "ClassVar[int]" = 1 + f: typing.ClassVar = 1 reveal_type(C.a) # revealed: int reveal_type(C.b) # revealed: int reveal_type(C.c) # revealed: int reveal_type(C.d) # revealed: Unknown | Literal[1] reveal_type(C.e) # revealed: int +reveal_type(C.f) # revealed: Unknown | Literal[1] c = C() @@ -36,6 +39,8 @@ c.c = 2 c.d = 2 # error: [invalid-attribute-access] c.e = 2 +# error: [invalid-attribute-access] +c.f = 3 ``` ## From stubs diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 3316808579..e03f5ef8be 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -42,6 +42,53 @@ impl<'db> TypeInferenceBuilder<'db, '_> { &mut self, annotation: &ast::Expr, ) -> TypeAndQualifiers<'db> { + fn infer_name_or_attribute<'db>( + ty: Type<'db>, + annotation: &ast::Expr, + builder: &TypeInferenceBuilder<'db, '_>, + ) -> TypeAndQualifiers<'db> { + match ty { + Type::SpecialForm(SpecialFormType::ClassVar) => { + TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::CLASS_VAR) + } + Type::SpecialForm(SpecialFormType::Final) => { + TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL) + } + Type::SpecialForm(SpecialFormType::Required) => { + TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::REQUIRED) + } + Type::SpecialForm(SpecialFormType::NotRequired) => { + TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::NOT_REQUIRED) + } + Type::SpecialForm(SpecialFormType::ReadOnly) => { + TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::READ_ONLY) + } + Type::ClassLiteral(class) if class.is_known(builder.db(), KnownClass::InitVar) => { + if let Some(builder) = + builder.context.report_lint(&INVALID_TYPE_FORM, annotation) + { + builder + .into_diagnostic("`InitVar` may not be used without a type argument"); + } + TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::INIT_VAR) + } + _ => ty + .in_type_expression( + builder.db(), + builder.scope(), + builder.typevar_binding_context, + ) + .unwrap_or_else(|error| { + error.into_fallback_type( + &builder.context, + annotation, + builder.is_reachable(annotation), + ) + }) + .into(), + } + } + // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression let annotation_ty = match annotation { // String annotations: https://typing.python.org/en/latest/spec/annotations.html#string-annotations @@ -68,52 +115,21 @@ impl<'db> TypeInferenceBuilder<'db, '_> { TypeAndQualifiers::unknown() } + ast::Expr::Attribute(attribute) => match attribute.ctx { + ast::ExprContext::Load => infer_name_or_attribute( + self.infer_attribute_expression(attribute), + annotation, + self, + ), + ast::ExprContext::Invalid => TypeAndQualifiers::unknown(), + ast::ExprContext::Store | ast::ExprContext::Del => { + todo_type!("Attribute expression annotation in Store/Del context").into() + } + }, + ast::Expr::Name(name) => match name.ctx { ast::ExprContext::Load => { - let name_expr_ty = self.infer_name_expression(name); - match name_expr_ty { - Type::SpecialForm(SpecialFormType::ClassVar) => { - TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::CLASS_VAR) - } - Type::SpecialForm(SpecialFormType::Final) => { - TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL) - } - Type::SpecialForm(SpecialFormType::Required) => { - TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::REQUIRED) - } - Type::SpecialForm(SpecialFormType::NotRequired) => { - TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::NOT_REQUIRED) - } - Type::SpecialForm(SpecialFormType::ReadOnly) => { - TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::READ_ONLY) - } - Type::ClassLiteral(class) - if class.is_known(self.db(), KnownClass::InitVar) => - { - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, annotation) - { - builder.into_diagnostic( - "`InitVar` may not be used without a type argument", - ); - } - TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::INIT_VAR) - } - _ => name_expr_ty - .in_type_expression( - self.db(), - self.scope(), - self.typevar_binding_context, - ) - .unwrap_or_else(|error| { - error.into_fallback_type( - &self.context, - annotation, - self.is_reachable(annotation), - ) - }) - .into(), - } + infer_name_or_attribute(self.infer_name_expression(name), annotation, self) } ast::ExprContext::Invalid => TypeAndQualifiers::unknown(), ast::ExprContext::Store | ast::ExprContext::Del => {