Repository: astral-sh/ruff
File: crates/ruff_python_semantic/src/model.rs (Rust)

Code before:
                    // ```
                    //
                    // The `x` in `print(x)` should be treated as unresolved.
                    //
                    // Similarly, given:
                    //
                    // ```python
                    // try:
                    //     pass
                    // except ValueError as x:
                    //     pass
                    //
                    // print(x)
                    //
                    // The `x` in `print(x)` should be treated as unresolved.
                    BindingKind::Deletion => {
                        // In stub files, `del` is used to hide names from re-export
                        // while the name is still valid for use in type annotations
                        // within the same file. Since annotations are deferred in
                        // stubs, the `Deletion` binding is seen at resolve time even
                        // though the reference textually precedes the `del`. Resolve
                        // to the shadowed (pre-`del`) binding when available.
                        if self.in_stub_file() {
                            if let Some(shadowed_id) =
                                self.scopes[scope_id].shadowed_binding(binding_id)
                            {
                                if !self.bindings[shadowed_id].is_unbound() {
                                    let reference_id = self.resolved_references.push(
                                        self.scope_id,
                                        self.node_id,
                                        ExprContext::Load,
                                        self.flags,
                                        name.range,
                                    );
                                    self.bindings[shadowed_id].references.push(reference_id);
                                    self.resolved_names.insert(name.into(), shadowed_id);
                                    return ReadResult::Resolved(shadowed_id);
                                }
                            }
                        }

                        self.unresolved_references.push(
                            name.range,
                            self.exceptions(),
                            UnresolvedReferenceFlags::empty(),
                        );
                        return ReadResult::UnboundLocal(binding_id);
                    }

                    BindingKind::UnboundException(None) => {
                        self.unresolved_references.push(

Review comment (suggestion):
Could you add a test case like this:

```py
class MyClass: ...
del MyClass
def f(cls: MyClass) -> None: ...
```

This properly gives an F821 diagnostic in a .py file in the [playground](https://play.ruff.rs/1af9170d-143c-42db-9d81-0e677a6c6fe0), but if I'm reading this code correctly, I think it would give a false negative in .pyi files on this PR branch.

I think it may end up being easier to do this check in the rule itself where we can inspect the relative ranges of all the bindings rather than modifying the semantic model, but I haven't played with the code myself.

Code after:
                    // ```
                    //
                    // The `x` in `print(x)` should be treated as unresolved.
                    //
                    // Similarly, given:
                    //
                    // ```python
                    // try:
                    //     pass
                    // except ValueError as x:
                    //     pass
                    //
                    // print(x)
                    //
                    // The `x` in `print(x)` should be treated as unresolved.
                    BindingKind::Deletion => {
                        // In stub files, `del` is used to hide names from re-export
                        // while the name is still valid for use in type annotations
                        // within the same file. Since annotations are deferred in
                        // stubs, the `Deletion` binding is seen at resolve time even
                        // though the reference textually precedes the `del`. Resolve
                        // to the shadowed (pre-`del`) binding when available.
                        if self.in_stub_file() {
                            if let Some(shadowed_id) =
                                self.scopes[scope_id].shadowed_binding(binding_id)
                            {
                                // Only suppress F821 when the reference textually precedes
                                // the `del` statement. If `del` comes before the reference,
                                // the name is genuinely undefined at that point and F821
                                // should still fire.
                                let deletion_range = self.bindings[binding_id].range;
                                if !self.bindings[shadowed_id].is_unbound()
                                    && name.range.start() < deletion_range.start()
                                {
                                    let reference_id = self.resolved_references.push(
                                        self.scope_id,
                                        self.node_id,
                                        ExprContext::Load,
                                        self.flags,
                                        name.range,
                                    );
                                    self.bindings[shadowed_id].references.push(reference_id);
                                    self.resolved_names.insert(name.into(), shadowed_id);
                                    return ReadResult::Resolved(shadowed_id);
                                }
                            }
                        }

                        self.unresolved_references.push(
                            name.range,
                            self.exceptions(),