@@ -3077,6 +3077,20 @@ impl<'db> TypeInferenceBuilder<'db> {
3077
3077
| Type :: TypeVar ( ..)
3078
3078
| Type :: AlwaysTruthy
3079
3079
| Type :: AlwaysFalsy => {
3080
+ let is_read_only = || {
3081
+ let dataclass_params = match object_ty {
3082
+ Type :: NominalInstance ( instance) => match instance. class {
3083
+ ClassType :: NonGeneric ( cls) => cls. dataclass_params ( self . db ( ) ) ,
3084
+ ClassType :: Generic ( cls) => {
3085
+ cls. origin ( self . db ( ) ) . dataclass_params ( self . db ( ) )
3086
+ }
3087
+ } ,
3088
+ _ => None ,
3089
+ } ;
3090
+
3091
+ dataclass_params. is_some_and ( |params| params. contains ( DataclassParams :: FROZEN ) )
3092
+ } ;
3093
+
3080
3094
match object_ty. class_member ( db, attribute. into ( ) ) {
3081
3095
meta_attr @ SymbolAndQualifiers { .. } if meta_attr. is_class_var ( ) => {
3082
3096
if emit_diagnostics {
@@ -3096,68 +3110,83 @@ impl<'db> TypeInferenceBuilder<'db> {
3096
3110
symbol : Symbol :: Type ( meta_attr_ty, meta_attr_boundness) ,
3097
3111
qualifiers : _,
3098
3112
} => {
3099
- let assignable_to_meta_attr = if let Symbol :: Type ( meta_dunder_set, _) =
3100
- meta_attr_ty. class_member ( db, "__set__" . into ( ) ) . symbol
3101
- {
3102
- let successful_call = meta_dunder_set
3103
- . try_call (
3104
- db,
3105
- & CallArgumentTypes :: positional ( [
3106
- meta_attr_ty,
3107
- object_ty,
3108
- value_ty,
3109
- ] ) ,
3110
- )
3111
- . is_ok ( ) ;
3112
-
3113
- if !successful_call && emit_diagnostics {
3113
+ if is_read_only ( ) {
3114
+ if emit_diagnostics {
3114
3115
if let Some ( builder) =
3115
3116
self . context . report_lint ( & INVALID_ASSIGNMENT , target)
3116
3117
{
3117
- // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
3118
3118
builder. into_diagnostic ( format_args ! (
3119
- "Invalid assignment to data descriptor attribute \
3120
- `{attribute}` on type `{}` with custom `__set__` method",
3121
- object_ty. display( db)
3119
+ "Property `{attribute}` defined in `{ty}` is read-only" ,
3120
+ ty = object_ty. display( self . db( ) ) ,
3122
3121
) ) ;
3123
3122
}
3124
3123
}
3125
-
3126
- successful_call
3124
+ false
3127
3125
} else {
3128
- ensure_assignable_to ( meta_attr_ty)
3129
- } ;
3126
+ let assignable_to_meta_attr = if let Symbol :: Type ( meta_dunder_set, _) =
3127
+ meta_attr_ty. class_member ( db, "__set__" . into ( ) ) . symbol
3128
+ {
3129
+ let successful_call = meta_dunder_set
3130
+ . try_call (
3131
+ db,
3132
+ & CallArgumentTypes :: positional ( [
3133
+ meta_attr_ty,
3134
+ object_ty,
3135
+ value_ty,
3136
+ ] ) ,
3137
+ )
3138
+ . is_ok ( ) ;
3130
3139
3131
- let assignable_to_instance_attribute = if meta_attr_boundness
3132
- == Boundness :: PossiblyUnbound
3133
- {
3134
- let ( assignable, boundness) =
3135
- if let Symbol :: Type ( instance_attr_ty, instance_attr_boundness) =
3136
- object_ty. instance_member ( db, attribute) . symbol
3137
- {
3138
- (
3139
- ensure_assignable_to ( instance_attr_ty) ,
3140
+ if !successful_call && emit_diagnostics {
3141
+ if let Some ( builder) =
3142
+ self . context . report_lint ( & INVALID_ASSIGNMENT , target)
3143
+ {
3144
+ // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
3145
+ builder. into_diagnostic ( format_args ! (
3146
+ "Invalid assignment to data descriptor attribute \
3147
+ `{attribute}` on type `{}` with custom `__set__` method",
3148
+ object_ty. display( db)
3149
+ ) ) ;
3150
+ }
3151
+ }
3152
+
3153
+ successful_call
3154
+ } else {
3155
+ ensure_assignable_to ( meta_attr_ty)
3156
+ } ;
3157
+
3158
+ let assignable_to_instance_attribute =
3159
+ if meta_attr_boundness == Boundness :: PossiblyUnbound {
3160
+ let ( assignable, boundness) = if let Symbol :: Type (
3161
+ instance_attr_ty,
3140
3162
instance_attr_boundness,
3141
- )
3142
- } else {
3143
- ( true , Boundness :: PossiblyUnbound )
3144
- } ;
3163
+ ) =
3164
+ object_ty. instance_member ( db, attribute) . symbol
3165
+ {
3166
+ (
3167
+ ensure_assignable_to ( instance_attr_ty) ,
3168
+ instance_attr_boundness,
3169
+ )
3170
+ } else {
3171
+ ( true , Boundness :: PossiblyUnbound )
3172
+ } ;
3145
3173
3146
- if boundness == Boundness :: PossiblyUnbound {
3147
- report_possibly_unbound_attribute (
3148
- & self . context ,
3149
- target,
3150
- attribute,
3151
- object_ty,
3152
- ) ;
3153
- }
3174
+ if boundness == Boundness :: PossiblyUnbound {
3175
+ report_possibly_unbound_attribute (
3176
+ & self . context ,
3177
+ target,
3178
+ attribute,
3179
+ object_ty,
3180
+ ) ;
3181
+ }
3154
3182
3155
- assignable
3156
- } else {
3157
- true
3158
- } ;
3183
+ assignable
3184
+ } else {
3185
+ true
3186
+ } ;
3159
3187
3160
- assignable_to_meta_attr && assignable_to_instance_attribute
3188
+ assignable_to_meta_attr && assignable_to_instance_attribute
3189
+ }
3161
3190
}
3162
3191
3163
3192
SymbolAndQualifiers {
@@ -3176,7 +3205,21 @@ impl<'db> TypeInferenceBuilder<'db> {
3176
3205
) ;
3177
3206
}
3178
3207
3179
- ensure_assignable_to ( instance_attr_ty)
3208
+ if is_read_only ( ) {
3209
+ if emit_diagnostics {
3210
+ if let Some ( builder) =
3211
+ self . context . report_lint ( & INVALID_ASSIGNMENT , target)
3212
+ {
3213
+ builder. into_diagnostic ( format_args ! (
3214
+ "Property `{attribute}` defined in `{ty}` is read-only" ,
3215
+ ty = object_ty. display( self . db( ) ) ,
3216
+ ) ) ;
3217
+ }
3218
+ }
3219
+ false
3220
+ } else {
3221
+ ensure_assignable_to ( instance_attr_ty)
3222
+ }
3180
3223
} else {
3181
3224
let result = object_ty. try_call_dunder_with_policy (
3182
3225
db,
0 commit comments