mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:16 +00:00
[ty] Support dataclasses.InitVar
(#19527)
## Summary I saw that this creates a lot of false positives in the ecosystem, and it seemed to be relatively easy to add basic support for this. Some preliminary work on this was done by @InSyncWithFoo — thank you. part of https://github.com/astral-sh/ty/issues/111 ## Ecosystem analysis The results look good. ## Test Plan New Markdown tests --------- Co-authored-by: InSync <insyncwithfoo@gmail.com> Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
1079975b35
commit
dc6be457b5
5 changed files with 336 additions and 57 deletions
|
@ -0,0 +1,150 @@
|
|||
# `dataclasses.InitVar`
|
||||
|
||||
From the Python documentation on [`dataclasses.InitVar`]:
|
||||
|
||||
If a field is an `InitVar`, it is considered a pseudo-field called an init-only field. As it is not
|
||||
a true field, it is not returned by the module-level `fields()` function. Init-only fields are added
|
||||
as parameters to the generated `__init__()` method, and are passed to the optional `__post_init__()`
|
||||
method. They are not otherwise used by dataclasses.
|
||||
|
||||
## Basic
|
||||
|
||||
Consider the following dataclass example where the `db` attribute is annotated with `InitVar`:
|
||||
|
||||
```py
|
||||
from dataclasses import InitVar, dataclass
|
||||
|
||||
class Database: ...
|
||||
|
||||
@dataclass(order=True)
|
||||
class Person:
|
||||
db: InitVar[Database]
|
||||
|
||||
name: str
|
||||
age: int
|
||||
```
|
||||
|
||||
We can see in the signature of `__init__` that `db` is included as an argument:
|
||||
|
||||
```py
|
||||
reveal_type(Person.__init__) # revealed: (self: Person, db: Database, name: str, age: int) -> None
|
||||
```
|
||||
|
||||
However, when we create an instance of this dataclass, the `db` attribute is not accessible:
|
||||
|
||||
```py
|
||||
db = Database()
|
||||
alice = Person(db, "Alice", 30)
|
||||
|
||||
alice.db # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
The `db` attribute is also not accessible on the class itself:
|
||||
|
||||
```py
|
||||
Person.db # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
Other fields can still be accessed normally:
|
||||
|
||||
```py
|
||||
reveal_type(alice.name) # revealed: str
|
||||
reveal_type(alice.age) # revealed: int
|
||||
```
|
||||
|
||||
## `InitVar` with default value
|
||||
|
||||
An `InitVar` can also have a default value. In this case, the attribute *is* accessible on the class
|
||||
and on instances:
|
||||
|
||||
```py
|
||||
from dataclasses import InitVar, dataclass
|
||||
|
||||
@dataclass
|
||||
class Person:
|
||||
name: str
|
||||
age: int
|
||||
|
||||
metadata: InitVar[str] = "default"
|
||||
|
||||
reveal_type(Person.__init__) # revealed: (self: Person, name: str, age: int, metadata: str = Literal["default"]) -> None
|
||||
|
||||
alice = Person("Alice", 30)
|
||||
bob = Person("Bob", 25, "custom metadata")
|
||||
|
||||
reveal_type(bob.metadata) # revealed: str
|
||||
|
||||
reveal_type(Person.metadata) # revealed: str
|
||||
```
|
||||
|
||||
## Overwritten `InitVar`
|
||||
|
||||
We do not emit an error if an `InitVar` attribute is later overwritten on the instance. In that
|
||||
case, we also allow the attribute to be accessed:
|
||||
|
||||
```py
|
||||
from dataclasses import InitVar, dataclass
|
||||
|
||||
@dataclass
|
||||
class Person:
|
||||
name: str
|
||||
metadata: InitVar[str]
|
||||
|
||||
def __post_init__(self, metadata: str) -> None:
|
||||
self.metadata = f"Person with name {self.name}"
|
||||
|
||||
alice = Person("Alice", "metadata that will be overwritten")
|
||||
|
||||
reveal_type(alice.metadata) # revealed: str
|
||||
```
|
||||
|
||||
## Error cases
|
||||
|
||||
### Syntax
|
||||
|
||||
`InitVar` can only be used with a single argument:
|
||||
|
||||
```py
|
||||
from dataclasses import InitVar, dataclass
|
||||
|
||||
@dataclass
|
||||
class Wrong:
|
||||
x: InitVar[int, str] # error: [invalid-type-form] "Type qualifier `InitVar` expected exactly 1 argument, got 2"
|
||||
```
|
||||
|
||||
A bare `InitVar` is not allowed according to the [type annotation grammar]:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class AlsoWrong:
|
||||
x: InitVar # error: [invalid-type-form] "`InitVar` may not be used without a type argument"
|
||||
```
|
||||
|
||||
### Outside of dataclasses
|
||||
|
||||
`InitVar` annotations are not allowed outside of dataclass attribute annotations:
|
||||
|
||||
```py
|
||||
from dataclasses import InitVar, dataclass
|
||||
|
||||
# error: [invalid-type-form] "`InitVar` annotations are only allowed in class-body scopes"
|
||||
x: InitVar[int] = 1
|
||||
|
||||
def f(x: InitVar[int]) -> None: # error: [invalid-type-form] "`InitVar` is not allowed in function parameter annotations"
|
||||
pass
|
||||
|
||||
def g() -> InitVar[int]: # error: [invalid-type-form] "`InitVar` is not allowed in function return type annotations"
|
||||
return 1
|
||||
|
||||
class C:
|
||||
# TODO: this would ideally be an error
|
||||
x: InitVar[int]
|
||||
|
||||
@dataclass
|
||||
class D:
|
||||
def __init__(self) -> None:
|
||||
self.x: InitVar[int] = 1 # error: [invalid-type-form] "`InitVar` annotations are not allowed for non-name targets"
|
||||
```
|
||||
|
||||
[type annotation grammar]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
|
||||
[`dataclasses.initvar`]: https://docs.python.org/3/library/dataclasses.html#dataclasses.InitVar
|
|
@ -244,7 +244,7 @@ pub(crate) fn class_symbol<'db>(
|
|||
ConsideredDefinitions::EndOfScope,
|
||||
);
|
||||
|
||||
if !place_and_quals.place.is_unbound() {
|
||||
if !place_and_quals.place.is_unbound() && !place_and_quals.is_init_var() {
|
||||
// Trust the declared type if we see a class-level declaration
|
||||
return place_and_quals;
|
||||
}
|
||||
|
@ -524,6 +524,11 @@ impl<'db> PlaceAndQualifiers<'db> {
|
|||
self.qualifiers.contains(TypeQualifiers::CLASS_VAR)
|
||||
}
|
||||
|
||||
/// Returns `true` if the place has a `InitVar` type qualifier.
|
||||
pub(crate) fn is_init_var(&self) -> bool {
|
||||
self.qualifiers.contains(TypeQualifiers::INIT_VAR)
|
||||
}
|
||||
|
||||
/// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type.
|
||||
pub(crate) fn is_bare_final(&self) -> Option<TypeQualifiers> {
|
||||
match self {
|
||||
|
|
|
@ -6137,11 +6137,29 @@ bitflags! {
|
|||
const CLASS_VAR = 1 << 0;
|
||||
/// `typing.Final`
|
||||
const FINAL = 1 << 1;
|
||||
/// `dataclasses.InitVar`
|
||||
const INIT_VAR = 1 << 2;
|
||||
}
|
||||
}
|
||||
|
||||
impl get_size2::GetSize for TypeQualifiers {}
|
||||
|
||||
impl TypeQualifiers {
|
||||
/// Get the name of a qualifier. Note that this only works
|
||||
///
|
||||
/// Panics if more than a single bit is set.
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::CLASS_VAR => "ClassVar",
|
||||
Self::FINAL => "Final",
|
||||
Self::INIT_VAR => "InitVar",
|
||||
_ => {
|
||||
unreachable!("Only a single bit should be set when calling `TypeQualifiers::name`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When inferring the type of an annotation expression, we can also encounter type qualifiers
|
||||
/// such as `ClassVar` or `Final`. These do not affect the inferred type itself, but rather
|
||||
/// control how a particular place can be accessed or modified. This struct holds a type and
|
||||
|
|
|
@ -881,6 +881,20 @@ impl MethodDecorator {
|
|||
}
|
||||
}
|
||||
|
||||
/// Metadata regarding a dataclass field/attribute.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct DataclassField<'db> {
|
||||
/// The declared type of the field
|
||||
pub(crate) field_ty: Type<'db>,
|
||||
|
||||
/// The type of the default value for this field
|
||||
pub(crate) default_ty: Option<Type<'db>>,
|
||||
|
||||
/// Whether or not this field is "init-only". If this is true, it only appears in the
|
||||
/// `__init__` signature, but is not accessible as a real field
|
||||
pub(crate) init_only: bool,
|
||||
}
|
||||
|
||||
/// Representation of a class definition statement in the AST: either a non-generic class, or a
|
||||
/// generic class that has not been specialized.
|
||||
///
|
||||
|
@ -1580,10 +1594,16 @@ impl<'db> ClassLiteral<'db> {
|
|||
|
||||
let signature_from_fields = |mut parameters: Vec<_>| {
|
||||
let mut kw_only_field_seen = false;
|
||||
for (name, (mut attr_ty, mut default_ty)) in
|
||||
self.fields(db, specialization, field_policy)
|
||||
for (
|
||||
field_name,
|
||||
DataclassField {
|
||||
mut field_ty,
|
||||
mut default_ty,
|
||||
init_only: _,
|
||||
},
|
||||
) in self.fields(db, specialization, field_policy)
|
||||
{
|
||||
if attr_ty
|
||||
if field_ty
|
||||
.into_nominal_instance()
|
||||
.is_some_and(|instance| instance.class.is_known(db, KnownClass::KwOnly))
|
||||
{
|
||||
|
@ -1594,7 +1614,7 @@ impl<'db> ClassLiteral<'db> {
|
|||
continue;
|
||||
}
|
||||
|
||||
let dunder_set = attr_ty.class_member(db, "__set__".into());
|
||||
let dunder_set = field_ty.class_member(db, "__set__".into());
|
||||
if let Place::Type(dunder_set, Boundness::Bound) = dunder_set.place {
|
||||
// The descriptor handling below is guarded by this not-dynamic check, because
|
||||
// dynamic types like `Any` are valid (data) descriptors: since they have all
|
||||
|
@ -1623,7 +1643,7 @@ impl<'db> ClassLiteral<'db> {
|
|||
}
|
||||
}
|
||||
}
|
||||
attr_ty = value_types.build();
|
||||
field_ty = value_types.build();
|
||||
|
||||
// The default value of the attribute is *not* determined by the right hand side
|
||||
// of the class-body assignment. Instead, the runtime invokes `__get__` on the
|
||||
|
@ -1640,11 +1660,11 @@ impl<'db> ClassLiteral<'db> {
|
|||
}
|
||||
|
||||
let mut parameter = if kw_only_field_seen {
|
||||
Parameter::keyword_only(name)
|
||||
Parameter::keyword_only(field_name)
|
||||
} else {
|
||||
Parameter::positional_or_keyword(name)
|
||||
Parameter::positional_or_keyword(field_name)
|
||||
}
|
||||
.with_annotated_type(attr_ty);
|
||||
.with_annotated_type(field_ty);
|
||||
|
||||
if let Some(default_ty) = default_ty {
|
||||
parameter = parameter.with_default_type(default_ty);
|
||||
|
@ -1746,7 +1766,7 @@ impl<'db> ClassLiteral<'db> {
|
|||
db: &'db dyn Db,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
field_policy: CodeGeneratorKind,
|
||||
) -> FxOrderMap<Name, (Type<'db>, Option<Type<'db>>)> {
|
||||
) -> FxOrderMap<Name, DataclassField<'db>> {
|
||||
if field_policy == CodeGeneratorKind::NamedTuple {
|
||||
// NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the
|
||||
// fields of this class only.
|
||||
|
@ -1793,7 +1813,7 @@ impl<'db> ClassLiteral<'db> {
|
|||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> FxOrderMap<Name, (Type<'db>, Option<Type<'db>>)> {
|
||||
) -> FxOrderMap<Name, DataclassField<'db>> {
|
||||
let mut attributes = FxOrderMap::default();
|
||||
|
||||
let class_body_scope = self.body_scope(db);
|
||||
|
@ -1835,11 +1855,12 @@ impl<'db> ClassLiteral<'db> {
|
|||
|
||||
attributes.insert(
|
||||
place_expr.expect_name().clone(),
|
||||
(
|
||||
attr_ty.apply_optional_specialization(db, specialization),
|
||||
default_ty
|
||||
DataclassField {
|
||||
field_ty: attr_ty.apply_optional_specialization(db, specialization),
|
||||
default_ty: default_ty
|
||||
.map(|ty| ty.apply_optional_specialization(db, specialization)),
|
||||
),
|
||||
init_only: attr.is_init_var(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2254,6 +2275,17 @@ impl<'db> ClassLiteral<'db> {
|
|||
declared = Place::Unbound;
|
||||
}
|
||||
|
||||
if qualifiers.contains(TypeQualifiers::INIT_VAR) {
|
||||
// We ignore `InitVar` declarations on the class body, unless that attribute is overwritten
|
||||
// by an implicit assignment in a method
|
||||
if Self::implicit_attribute(db, body_scope, name, MethodDecorator::None)
|
||||
.place
|
||||
.is_unbound()
|
||||
{
|
||||
return Place::Unbound.into();
|
||||
}
|
||||
}
|
||||
|
||||
// The attribute is declared in the class body.
|
||||
|
||||
let bindings = use_def.end_of_scope_bindings(place_id);
|
||||
|
@ -2592,6 +2624,7 @@ pub enum KnownClass {
|
|||
// dataclasses
|
||||
Field,
|
||||
KwOnly,
|
||||
InitVar,
|
||||
// _typeshed._type_checker_internals
|
||||
NamedTupleFallback,
|
||||
}
|
||||
|
@ -2686,6 +2719,7 @@ impl KnownClass {
|
|||
| Self::Deprecated
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback => Truthiness::Ambiguous,
|
||||
}
|
||||
}
|
||||
|
@ -2744,6 +2778,7 @@ impl KnownClass {
|
|||
| Self::EllipsisType
|
||||
| Self::NotImplementedType
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::VersionInfo
|
||||
| Self::Bool
|
||||
| Self::NoneType => false,
|
||||
|
@ -2843,6 +2878,7 @@ impl KnownClass {
|
|||
| KnownClass::NotImplementedType
|
||||
| KnownClass::Field
|
||||
| KnownClass::KwOnly
|
||||
| KnownClass::InitVar
|
||||
| KnownClass::NamedTupleFallback => false,
|
||||
}
|
||||
}
|
||||
|
@ -2925,6 +2961,7 @@ impl KnownClass {
|
|||
| Self::UnionType
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback => false,
|
||||
}
|
||||
}
|
||||
|
@ -3016,6 +3053,7 @@ impl KnownClass {
|
|||
Self::NotImplementedType => "_NotImplementedType",
|
||||
Self::Field => "Field",
|
||||
Self::KwOnly => "KW_ONLY",
|
||||
Self::InitVar => "InitVar",
|
||||
Self::NamedTupleFallback => "NamedTupleFallback",
|
||||
}
|
||||
}
|
||||
|
@ -3269,8 +3307,7 @@ impl KnownClass {
|
|||
| Self::DefaultDict
|
||||
| Self::Deque
|
||||
| Self::OrderedDict => KnownModule::Collections,
|
||||
Self::Field => KnownModule::Dataclasses,
|
||||
Self::KwOnly => KnownModule::Dataclasses,
|
||||
Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses,
|
||||
Self::NamedTupleFallback => KnownModule::TypeCheckerInternals,
|
||||
}
|
||||
}
|
||||
|
@ -3342,6 +3379,7 @@ impl KnownClass {
|
|||
| Self::NewType
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::NamedTupleFallback => false,
|
||||
|
@ -3417,6 +3455,7 @@ impl KnownClass {
|
|||
| Self::NewType
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::NamedTupleFallback => false,
|
||||
|
@ -3504,6 +3543,7 @@ impl KnownClass {
|
|||
"_NotImplementedType" => Self::NotImplementedType,
|
||||
"Field" => Self::Field,
|
||||
"KW_ONLY" => Self::KwOnly,
|
||||
"InitVar" => Self::InitVar,
|
||||
"NamedTupleFallback" => Self::NamedTupleFallback,
|
||||
_ => return None,
|
||||
};
|
||||
|
@ -3566,6 +3606,7 @@ impl KnownClass {
|
|||
| Self::WrapperDescriptorType
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback => module == self.canonical_module(db),
|
||||
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
|
||||
Self::SpecialForm
|
||||
|
|
|
@ -88,7 +88,7 @@ use crate::semantic_index::{
|
|||
ApplicableConstraints, EagerSnapshotResult, SemanticIndex, place_table, semantic_index,
|
||||
};
|
||||
use crate::types::call::{Binding, Bindings, CallArguments, CallError};
|
||||
use crate::types::class::{CodeGeneratorKind, MetaclassErrorKind, SliceLiteral};
|
||||
use crate::types::class::{CodeGeneratorKind, DataclassField, MetaclassErrorKind, SliceLiteral};
|
||||
use crate::types::diagnostic::{
|
||||
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
|
||||
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
|
||||
|
@ -1365,8 +1365,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
let specialization = None;
|
||||
let mut kw_only_field_names = vec![];
|
||||
|
||||
for (name, (attr_ty, _)) in class.fields(self.db(), specialization, field_policy) {
|
||||
let Some(instance) = attr_ty.into_nominal_instance() else {
|
||||
for (name, DataclassField { field_ty, .. }) in
|
||||
class.fields(self.db(), specialization, field_policy)
|
||||
{
|
||||
let Some(instance) = field_ty.into_nominal_instance() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
|
@ -2651,18 +2653,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
if let Some(returns) = returns {
|
||||
let annotated = self.infer_annotation_expression(returns, deferred_expression_state);
|
||||
|
||||
if annotated.qualifiers.contains(TypeQualifiers::FINAL) {
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, returns) {
|
||||
builder.into_diagnostic(
|
||||
"`Final` is not allowed in function return type annotations",
|
||||
);
|
||||
}
|
||||
}
|
||||
if annotated.qualifiers.contains(TypeQualifiers::CLASS_VAR) {
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, returns) {
|
||||
builder.into_diagnostic(
|
||||
"`ClassVar` is not allowed in function return type annotations",
|
||||
);
|
||||
if !annotated.qualifiers.is_empty() {
|
||||
for qualifier in [
|
||||
TypeQualifiers::FINAL,
|
||||
TypeQualifiers::CLASS_VAR,
|
||||
TypeQualifiers::INIT_VAR,
|
||||
] {
|
||||
if annotated.qualifiers.contains(qualifier) {
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, returns)
|
||||
{
|
||||
builder.into_diagnostic(format!(
|
||||
"`{name}` is not allowed in function return type annotations",
|
||||
name = qualifier.name()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2704,18 +2709,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
);
|
||||
|
||||
if let Some(qualifiers) = annotated.map(|annotated| annotated.qualifiers) {
|
||||
if qualifiers.contains(TypeQualifiers::FINAL) {
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, parameter) {
|
||||
builder.into_diagnostic(
|
||||
"`Final` is not allowed in function parameter annotations",
|
||||
);
|
||||
}
|
||||
}
|
||||
if qualifiers.contains(TypeQualifiers::CLASS_VAR) {
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, parameter) {
|
||||
builder.into_diagnostic(
|
||||
"`ClassVar` is not allowed in function parameter annotations",
|
||||
);
|
||||
if !qualifiers.is_empty() {
|
||||
for qualifier in [
|
||||
TypeQualifiers::FINAL,
|
||||
TypeQualifiers::CLASS_VAR,
|
||||
TypeQualifiers::INIT_VAR,
|
||||
] {
|
||||
if qualifiers.contains(qualifier) {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_TYPE_FORM, parameter)
|
||||
{
|
||||
builder.into_diagnostic(format!(
|
||||
"`{name}` is not allowed in function parameter annotations",
|
||||
name = qualifier.name()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4264,14 +4273,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
let annotated =
|
||||
self.infer_annotation_expression(annotation, DeferredExpressionState::None);
|
||||
|
||||
if annotated.qualifiers.contains(TypeQualifiers::CLASS_VAR) {
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_TYPE_FORM, annotation.as_ref())
|
||||
{
|
||||
builder.into_diagnostic(
|
||||
"`ClassVar` annotations are not allowed for non-name targets",
|
||||
);
|
||||
if !annotated.qualifiers.is_empty() {
|
||||
for qualifier in [TypeQualifiers::CLASS_VAR, TypeQualifiers::INIT_VAR] {
|
||||
if annotated.qualifiers.contains(qualifier) {
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_TYPE_FORM, annotation.as_ref())
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"`{name}` annotations are not allowed for non-name targets",
|
||||
name = qualifier.name()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4306,14 +4320,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
DeferredExpressionState::from(self.defer_annotations()),
|
||||
);
|
||||
|
||||
if declared.qualifiers.contains(TypeQualifiers::CLASS_VAR) {
|
||||
if !declared.qualifiers.is_empty() {
|
||||
let current_scope_id = self.scope().file_scope_id(self.db());
|
||||
let current_scope = self.index.scope(current_scope_id);
|
||||
if current_scope.kind() != ScopeKind::Class {
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, annotation) {
|
||||
builder.into_diagnostic(
|
||||
"`ClassVar` annotations are only allowed in class-body scopes",
|
||||
);
|
||||
for qualifier in [TypeQualifiers::CLASS_VAR, TypeQualifiers::INIT_VAR] {
|
||||
if declared.qualifiers.contains(qualifier) {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_TYPE_FORM, annotation)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"`{name}` annotations are only allowed in class-body scopes",
|
||||
name = qualifier.name()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9064,6 +9085,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
|||
Type::SpecialForm(SpecialFormType::Final) => {
|
||||
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL)
|
||||
}
|
||||
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())
|
||||
.unwrap_or_else(|error| {
|
||||
|
@ -9134,6 +9167,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
|||
let type_and_qualifiers = if num_arguments == 1 {
|
||||
let mut type_and_qualifiers =
|
||||
self.infer_annotation_expression_impl(slice);
|
||||
|
||||
match type_qualifier {
|
||||
SpecialFormType::ClassVar => {
|
||||
type_and_qualifiers.add_qualifier(TypeQualifiers::CLASS_VAR);
|
||||
|
@ -9163,6 +9197,37 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
|||
}
|
||||
type_and_qualifiers
|
||||
}
|
||||
Type::ClassLiteral(class) if class.is_known(self.db(), KnownClass::InitVar) => {
|
||||
let arguments = if let ast::Expr::Tuple(tuple) = slice {
|
||||
&*tuple.elts
|
||||
} else {
|
||||
std::slice::from_ref(slice)
|
||||
};
|
||||
let num_arguments = arguments.len();
|
||||
let type_and_qualifiers = if num_arguments == 1 {
|
||||
let mut type_and_qualifiers =
|
||||
self.infer_annotation_expression_impl(slice);
|
||||
type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR);
|
||||
type_and_qualifiers
|
||||
} else {
|
||||
for element in arguments {
|
||||
self.infer_annotation_expression_impl(element);
|
||||
}
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Type qualifier `InitVar` expected exactly 1 argument, \
|
||||
got {num_arguments}",
|
||||
));
|
||||
}
|
||||
Type::unknown().into()
|
||||
};
|
||||
if slice.is_tuple_expr() {
|
||||
self.store_expression_type(slice, type_and_qualifiers.inner_type());
|
||||
}
|
||||
type_and_qualifiers
|
||||
}
|
||||
_ => self
|
||||
.infer_subscript_type_expression_no_store(subscript, slice, value_ty)
|
||||
.into(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue