mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] Allow annotation expressions to be ast::Attribute
nodes (#20413)
Fixes https://github.com/astral-sh/ty/issues/1187
This commit is contained in:
parent
1f1365a0fa
commit
8341da7f63
2 changed files with 65 additions and 44 deletions
|
@ -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
|
||||
|
|
|
@ -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 => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue