Skip to content

Commit 3272ab1

Browse files
committed
[ty] diagnostic on overridden __setattr__ and __delattr__ in frozen dataclasses
astral-sh/ty#111
1 parent 69ace00 commit 3272ab1

4 files changed

Lines changed: 94 additions & 5 deletions

File tree

crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,7 @@ from dataclasses import dataclass
452452
class MyFrozenClass:
453453
x: int
454454

455-
# TODO: Emit a diagnostic here
456-
def __setattr__(self, name: str, value: object) -> None: ...
455+
def __setattr__(self, name: str, value: object) -> None: ... # error: [cannot-overwrite-attribute]
457456

458457
# TODO: Emit a diagnostic here
459458
def __delattr__(self, name: str) -> None: ...

crates/ty_python_semantic/src/types/class.rs

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::semantic_index::{
1919
use crate::types::bound_super::BoundSuperError;
2020
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
2121
use crate::types::context::InferContext;
22-
use crate::types::diagnostic::INVALID_TYPE_ALIAS_TYPE;
22+
use crate::types::diagnostic::{CANNOT_OVERWRITE_ATTRIBUTE, INVALID_TYPE_ALIAS_TYPE};
2323
use crate::types::enums::enum_metadata;
2424
use crate::types::function::{DataclassTransformerParams, KnownFunction};
2525
use crate::types::generics::{
@@ -2285,9 +2285,8 @@ impl<'db> ClassLiteral<'db> {
22852285
inherited_generic_context: Option<GenericContext<'db>>,
22862286
name: &str,
22872287
) -> Option<Type<'db>> {
2288-
let dataclass_params = self.dataclass_params(db);
2289-
22902288
let field_policy = CodeGeneratorKind::from_class(db, self, specialization)?;
2289+
let dataclass_params = self.dataclass_params(db);
22912290

22922291
let mut transformer_params =
22932292
if let CodeGeneratorKind::DataclassLike(Some(transformer_params)) = field_policy {
@@ -2989,6 +2988,85 @@ impl<'db> ClassLiteral<'db> {
29892988
.collect()
29902989
}
29912990

2991+
pub(crate) fn validate_members(self, context: &InferContext<'db, '_>) {
2992+
let db = context.db();
2993+
let Some(field_policy) = CodeGeneratorKind::from_class(db, self, None) else {
2994+
return;
2995+
};
2996+
let dataclass_params = self.dataclass_params(db);
2997+
let mut transformer_params =
2998+
if let CodeGeneratorKind::DataclassLike(Some(transformer_params)) = field_policy {
2999+
Some(DataclassParams::from_transformer_params(
3000+
db,
3001+
transformer_params,
3002+
))
3003+
} else {
3004+
None
3005+
};
3006+
3007+
// Dataclass transformer flags can be overwritten using class arguments.
3008+
if let Some(transformer_params) = transformer_params.as_mut() {
3009+
if let Some(class_def) = self.definition(db).kind(db).as_class() {
3010+
let module = parsed_module(db, self.file(db)).load(db);
3011+
3012+
if let Some(arguments) = &class_def.node(&module).arguments {
3013+
let mut flags = transformer_params.flags(db);
3014+
3015+
for keyword in &arguments.keywords {
3016+
if let Some(arg_name) = &keyword.arg {
3017+
if let Some(is_set) =
3018+
keyword.value.as_boolean_literal_expr().map(|b| b.value)
3019+
{
3020+
for (flag_name, flag) in DATACLASS_FLAGS {
3021+
if arg_name.as_str() == *flag_name {
3022+
flags.set(*flag, is_set);
3023+
}
3024+
}
3025+
}
3026+
}
3027+
}
3028+
3029+
*transformer_params =
3030+
DataclassParams::new(db, flags, transformer_params.field_specifiers(db));
3031+
}
3032+
}
3033+
}
3034+
3035+
let has_dataclass_param = |param| {
3036+
dataclass_params.is_some_and(|params| params.flags(db).contains(param))
3037+
|| transformer_params.is_some_and(|params| params.flags(db).contains(param))
3038+
};
3039+
let db = context.db();
3040+
let class_body_scope = self.body_scope(db);
3041+
let table = place_table(db, class_body_scope);
3042+
let use_def = use_def_map(db, class_body_scope);
3043+
for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() {
3044+
let result = place_from_declarations(db, declarations.clone());
3045+
let attr = result.ignore_conflicting_declarations();
3046+
let symbol = table.symbol(symbol_id);
3047+
let name = symbol.name();
3048+
if let Some(Type::FunctionLiteral(literal)) = attr.place.ignore_possibly_undefined()
3049+
&& name == "__setattr__"
3050+
{
3051+
if let Some(CodeGeneratorKind::DataclassLike(_)) =
3052+
CodeGeneratorKind::from_class(db, self, None)
3053+
&& has_dataclass_param(DataclassFlags::FROZEN)
3054+
{
3055+
if let Some(builder) = context.report_lint(
3056+
&CANNOT_OVERWRITE_ATTRIBUTE,
3057+
literal.node(db, context.file(), context.module()),
3058+
) {
3059+
let mut diagnostic = builder.into_diagnostic(format_args!(
3060+
"Cannot overwrite attribute __setattr__ in class {}",
3061+
self.name(db)
3062+
));
3063+
diagnostic.info("__setattr__");
3064+
}
3065+
}
3066+
}
3067+
}
3068+
}
3069+
29923070
/// Returns a list of all annotated attributes defined in the body of this class. This is similar
29933071
/// to the `__annotations__` attribute at runtime, but also contains default values.
29943072
///

crates/ty_python_semantic/src/types/diagnostic.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
5151
registry.register_lint(&AMBIGUOUS_PROTOCOL_MEMBER);
5252
registry.register_lint(&CALL_NON_CALLABLE);
5353
registry.register_lint(&POSSIBLY_MISSING_IMPLICIT_CALL);
54+
registry.register_lint(&CANNOT_OVERWRITE_ATTRIBUTE);
5455
registry.register_lint(&CONFLICTING_ARGUMENT_FORMS);
5556
registry.register_lint(&CONFLICTING_DECLARATIONS);
5657
registry.register_lint(&CONFLICTING_METACLASS);
@@ -392,6 +393,15 @@ declare_lint! {
392393
}
393394
}
394395

396+
declare_lint! {
397+
/// TODO
398+
pub(crate) static CANNOT_OVERWRITE_ATTRIBUTE = {
399+
summary: "TODO",
400+
status: LintStatus::preview("1.0.0"),
401+
default_level: Level::Error,
402+
}
403+
}
404+
395405
declare_lint! {
396406
/// ## What it does
397407
/// Checks for classes definitions which will fail at runtime due to

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
979979
if let Some(protocol) = class.into_protocol_class(self.db()) {
980980
protocol.validate_members(&self.context);
981981
}
982+
983+
class.validate_members(&self.context);
982984
}
983985
}
984986

0 commit comments

Comments
 (0)