Skip to content

Commit 20a58fc

Browse files
committed
[ty] Refactor frozen dataclass diagnostic
1 parent d3a8d73 commit 20a58fc

2 files changed

Lines changed: 34 additions & 20 deletions

File tree

crates/ty_python_semantic/resources/mdtest/dataclasses.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,14 +369,14 @@ To do
369369

370370
### `frozen`
371371

372-
If true (the default is False), assigning to fields will generate a diagnostic. If `__setattr__()` or
373-
`__delattr__()` is defined in the class, we should emit a diagnostic.
372+
If true (the default is False), assigning to fields will generate a diagnostic. If `__setattr__()`
373+
or `__delattr__()` is defined in the class, we should emit a diagnostic.
374374

375375
```py
376376
from dataclasses import dataclass
377377

378378
@dataclass(frozen=True)
379-
class Frozen:
379+
class MyFrozenClass:
380380
x: int
381381

382382
# TODO: Emit a diagnostic here
@@ -385,10 +385,23 @@ class Frozen:
385385
# TODO: Emit a diagnostic here
386386
def __delattr__(self, name: str) -> None: ...
387387

388-
frozen = Frozen(1)
388+
frozen = MyFrozenClass(1)
389389
frozen.x = 2 # error: [invalid-assignment]
390390
```
391391

392+
When attempting to mutate an unresolved attribute on a frozen dataclass, only `unresolved-attribute`
393+
is emitted:
394+
395+
```py
396+
from dataclasses import dataclass
397+
398+
@dataclass(frozen=True)
399+
class MyFrozenClass: ...
400+
401+
frozen = MyFrozenClass()
402+
frozen.x = 2 # error: [unresolved-attribute]
403+
```
404+
392405
### `match_args`
393406

394407
To do

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2924,24 +2924,16 @@ impl<'db> TypeInferenceBuilder<'db> {
29242924
| Type::TypeVar(..)
29252925
| Type::AlwaysTruthy
29262926
| Type::AlwaysFalsy => {
2927-
if let Type::NominalInstance(instance) = object_ty {
2928-
let dataclass_params = match instance.class() {
2927+
let dataclass_params = match object_ty {
2928+
Type::NominalInstance(instance) => match instance.class() {
29292929
ClassType::NonGeneric(cls) => cls.dataclass_params(self.db()),
29302930
ClassType::Generic(_) => None,
2931-
};
2932-
let frozen = dataclass_params
2933-
.is_some_and(|params| params.contains(DataclassParams::FROZEN));
2934-
if frozen && emit_diagnostics {
2935-
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target)
2936-
{
2937-
builder.into_diagnostic(format_args!(
2938-
"Property `{attribute}` defined in `{ty}` is read-only",
2939-
ty = object_ty.display(self.db()),
2940-
));
2941-
}
2942-
}
2943-
}
2944-
match object_ty.class_member(db, attribute.into()) {
2931+
},
2932+
_ => None,
2933+
};
2934+
let ro =
2935+
dataclass_params.is_some_and(|params| params.contains(DataclassParams::FROZEN));
2936+
let assignable_if_rw = match object_ty.class_member(db, attribute.into()) {
29452937
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
29462938
if emit_diagnostics {
29472939
if let Some(builder) =
@@ -3092,7 +3084,16 @@ impl<'db> TypeInferenceBuilder<'db> {
30923084
}
30933085
}
30943086
}
3087+
};
3088+
if (assignable_if_rw && ro) && emit_diagnostics {
3089+
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) {
3090+
builder.into_diagnostic(format_args!(
3091+
"Property `{attribute}` defined in `{ty}` is read-only",
3092+
ty = object_ty.display(self.db()),
3093+
));
3094+
}
30953095
}
3096+
assignable_if_rw && !ro
30963097
}
30973098

30983099
Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => {

0 commit comments

Comments
 (0)