diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF009.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF009.py index b8c76574c2..511d20b8ac 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF009.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF009.py @@ -125,4 +125,13 @@ class D: @dataclass class E: - c: C = C() \ No newline at end of file + c: C = C() + +# https://github.com/astral-sh/ruff/issues/20266 +@dataclass(frozen=True) +class C: + @dataclass(frozen=True) + class D: + foo: int = 1 + + d: D = D() # OK \ No newline at end of file diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs index 2cf6ec15bb..26cf918731 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs @@ -134,7 +134,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) { ); } if checker.is_rule_enabled(Rule::FunctionCallInDataclassDefaultArgument) { - ruff::rules::function_call_in_dataclass_default(checker, class_def); + ruff::rules::function_call_in_dataclass_default(checker, class_def, scope_id); } if checker.is_rule_enabled(Rule::MutableClassDefault) { ruff::rules::mutable_class_default(checker, class_def); diff --git a/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs index 2e1045ba76..26c4c5d657 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -2,10 +2,10 @@ use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::{QualifiedName, UnqualifiedName}; -use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::typing::{ is_immutable_annotation, is_immutable_func, is_immutable_newtype_call, }; +use ruff_python_semantic::{ScopeId, SemanticModel}; use ruff_text_size::Ranged; use crate::Violation; @@ -77,7 +77,11 @@ impl Violation for FunctionCallInDataclassDefaultArgument { } /// RUF009 -pub(crate) fn function_call_in_dataclass_default(checker: &Checker, class_def: &ast::StmtClassDef) { +pub(crate) fn function_call_in_dataclass_default( + checker: &Checker, + class_def: &ast::StmtClassDef, + scope_id: ScopeId, +) { let semantic = checker.semantic(); let Some((dataclass_kind, _)) = dataclass_kind(class_def, semantic) else { @@ -144,7 +148,7 @@ pub(crate) fn function_call_in_dataclass_default(checker: &Checker, class_def: & || func.as_name_expr().is_some_and(|name| { is_immutable_newtype_call(name, checker.semantic(), &extend_immutable_calls) }) - || is_frozen_dataclass_instantiation(func, semantic) + || is_frozen_dataclass_instantiation(func, semantic, scope_id) { continue; } @@ -165,16 +169,22 @@ fn any_annotated(class_body: &[Stmt]) -> bool { /// Checks that the passed function is an instantiation of the class, /// retrieves the ``StmtClassDef`` and verifies that it is a frozen dataclass -fn is_frozen_dataclass_instantiation(func: &Expr, semantic: &SemanticModel) -> bool { - semantic.lookup_attribute(func).is_some_and(|id| { - let binding = &semantic.binding(id); - let Some(Stmt::ClassDef(class_def)) = binding.statement(semantic) else { - return false; - }; +fn is_frozen_dataclass_instantiation( + func: &Expr, + semantic: &SemanticModel, + scope_id: ScopeId, +) -> bool { + semantic + .lookup_attribute_in_scope(func, scope_id) + .is_some_and(|id| { + let binding = &semantic.binding(id); + let Some(Stmt::ClassDef(class_def)) = binding.statement(semantic) else { + return false; + }; - let Some((_, dataclass_decorator)) = dataclass_kind(class_def, semantic) else { - return false; - }; - is_frozen_dataclass(dataclass_decorator, semantic) - }) + let Some((_, dataclass_decorator)) = dataclass_kind(class_def, semantic) else { + return false; + }; + is_frozen_dataclass(dataclass_decorator, semantic) + }) } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 16d57622cc..b8de310a36 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -859,11 +859,17 @@ impl<'a> SemanticModel<'a> { /// associated with `Class`, then the `BindingKind::FunctionDefinition` associated with /// `Class.method`. pub fn lookup_attribute(&self, value: &Expr) -> Option { + self.lookup_attribute_in_scope(value, self.scope_id) + } + + /// Lookup a qualified attribute in the certain scope. + pub fn lookup_attribute_in_scope(&self, value: &Expr, scope_id: ScopeId) -> Option { let unqualified_name = UnqualifiedName::from_expr(value)?; // Find the symbol in the current scope. let (symbol, attribute) = unqualified_name.segments().split_first()?; - let mut binding_id = self.lookup_symbol(symbol)?; + let mut binding_id = + self.lookup_symbol_in_scope(symbol, scope_id, self.in_forward_reference())?; // Recursively resolve class attributes, e.g., `foo.bar.baz` in. let mut tail = attribute;