Repository: astral-sh/ruff
File: crates/ty_python_semantic/src/types/infer/builder.rs (Rust)

Code before:

        // Return true (and emit a diagnostic) if this is an invalid assignment to a `Final` attribute.
        // Per PEP 591 and the typing conformance suite, Final instance attributes can be assigned
        // in __init__ methods. Multiple assignments within __init__ are allowed (matching mypy
        // and pyright behavior), as long as the attribute doesn't have a class-level value.
        let invalid_assignment_to_final = |builder: &Self, qualifiers: TypeQualifiers| -> bool {
            // Check if it's a Final attribute
            if !qualifiers.contains(TypeQualifiers::FINAL) {
                return false;
            }

            // Check if we're in an __init__ method (where Final attributes can be initialized).
            let is_in_init = builder
                .current_function_definition()
                .is_some_and(|func| func.name.id == "__init__");

            // Not in __init__ - always disallow
            if !is_in_init {
                if emit_diagnostics {
                    if let Some(builder) = builder.context.report_lint(&INVALID_ASSIGNMENT, target)
                    {
                        builder.into_diagnostic(format_args!(
                            "Cannot assign to final attribute `{attribute}` \
                                             on type `{}`",
                            object_ty.display(db)
                        ));
                    }
                }
                return true;
            }

            // We're in __init__ - check if class-level attribute already has a value
            if let Some(class_ty) = builder.class_context_of_current_method() {
                let class_definition = class_ty.class_literal(db).0;
                let class_scope_id = class_definition.body_scope(db).file_scope_id(db);
                let place_table = builder.index.place_table(class_scope_id);

                if let Some(symbol_id) = place_table.symbol_id(attribute) {
                    let use_def = builder.index.use_def_map(class_scope_id);
                    let declarations = use_def.end_of_scope_symbol_declarations(symbol_id);

                    // Check if any declaration is an annotated assignment with a value
                    for decl_with_constraint in declarations {
                        if let Some(definition) = decl_with_constraint.declaration.definition() {
                            if let DefinitionKind::AnnotatedAssignment(ann_assign) =
                                definition.kind(db)
                            {
                                // Check if the annotated assignment has a value
                                if ann_assign.value(builder.module()).is_some() {
                                    // Class-level Final has a value - disallow reassignment
                                    if emit_diagnostics {

Review comment (suggestion):
I think this logic currently leads to false negatives in cases like these:

```py
from typing import Final

class C:
    x: Final[int] = 100

def _(c: C):
    c.x = 1

def __init__(c: C):
    c.x = 1

class A:
    def __init__(self, c: C):
        c.x = 1
```

We should add these cases to the tests, and expand our checking here to validate that we are in the `__init__` method of the class on which we are trying to assign a final attribute. Even then, there is still this case to consider as well:

```py
from typing import Final, Self

class C:
    x: Final[int]

    def __init__(self, c: Self):
        c.x = 3
```

Ideally this should not be allowed either; `__init__` should only be allowed to mutate the `self` object (the first argument). I think this should be fairly easy to check, I believe we have some other cases where we already check this.

Code after:

        let emit_invalid_final = |builder: &Self| {
            if emit_diagnostics {
                if let Some(builder) = builder.context.report_lint(&INVALID_ASSIGNMENT, target) {
                    builder.into_diagnostic(format_args!(
                        "Cannot assign to final attribute `{attribute}` on type `{}`",
                        object_ty.display(db)
                    ));
                }
            }
        };

        // Return true (and emit a diagnostic) if this is an invalid assignment to a `Final` attribute.
        // Per PEP 591 and the typing conformance suite, Final instance attributes can be assigned
        // in __init__ methods. Multiple assignments within __init__ are allowed (matching mypy
        // and pyright behavior), as long as the attribute doesn't have a class-level value.
        let invalid_assignment_to_final = |builder: &Self, qualifiers: TypeQualifiers| -> bool {
            // Check if it's a Final attribute
            if !qualifiers.contains(TypeQualifiers::FINAL) {
                return false;
            }

            // Check if we're in an __init__ method (where Final attributes can be initialized).
            let is_in_init = builder
                .current_function_definition()
                .is_some_and(|func| func.name.id == "__init__");

            // Not in __init__ - always disallow
            if !is_in_init {
                emit_invalid_final(builder);
                return true;
            }

            // We're in __init__ - verify we're in a method of the class being mutated
            let Some(class_ty) = builder.class_context_of_current_method() else {
                // Not a method (standalone function named __init__)
                emit_invalid_final(builder);
                return true;
            };

            // Check that object_ty is an instance of the class we're in
            if !object_ty.is_subtype_of(builder.db(), Type::instance(builder.db(), class_ty)) {
                // Assigning to a different class's Final attribute
                emit_invalid_final(builder);
                return true;
            }

            // Check if class-level attribute already has a value
            {
                let class_definition = class_ty.class_literal(db).0;
                let class_scope_id = class_definition.body_scope(db).file_scope_id(db);