mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-28 18:53:25 +00:00
[ruff] Allow dataclass attribute value instantiation from nested frozen dataclass (RUF009) (#20352)
## Summary Resolves #20266 Definition of the frozen dataclass attribute can be instantiation of a nested frozen dataclass as well as a non-nested one. ### Problem explanation The `function_call_in_dataclass_default` function is invoked during the "defined scope" stage, after all scopes have been processed. At this point, the semantic references the top-level scope. When `SemanticModel::lookup_attribute` executes, it searches for bindings in the top-level module scope rather than the class scope, resulting in an error. To solve this issue, the lookup should be evaluated through the class scope. ## Test Plan - Added test case from issue Co-authored-by: Igor Drokin <drokinii1017@gmail.com>
This commit is contained in:
parent
151ba49b36
commit
c7f6b85fb3
4 changed files with 42 additions and 17 deletions
|
|
@ -126,3 +126,12 @@ class D:
|
|||
@dataclass
|
||||
class E:
|
||||
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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,8 +169,14 @@ 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| {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<BindingId> {
|
||||
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<BindingId> {
|
||||
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue