Skip to content

Commit 81d655f

Browse files
stakeswkyamyreese
andauthored
[pyflakes] suppress false positive in F821 for names used before del in stub files (#23550)
Co-authored-by: stakeswky <[email protected]> Co-authored-by: Amethyst Reese <[email protected]>
1 parent 625b4f5 commit 81d655f

4 files changed

Lines changed: 86 additions & 1 deletion

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# OK: `del` after use in stub file should not trigger F821
2+
class MyClass: ...
3+
def f(cls: MyClass) -> None: ...
4+
del MyClass
5+
6+
# OK: same pattern with a variable
7+
_T = int
8+
x: _T
9+
del _T
10+
11+
# OK: used in base class
12+
class Base: ...
13+
class Child(Base): ...
14+
del Base
15+
16+
# Error: `del` before use should still trigger F821
17+
class EarlyDel: ...
18+
del EarlyDel
19+
def f2(cls: EarlyDel) -> None: ... # F821
20+
21+
# Error: name that was never defined
22+
def g(x: Undefined) -> None: ... # F821

crates/ruff_linter/src/rules/pyflakes/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ mod tests {
168168
#[test_case(Rule::UndefinedName, Path::new("F821_31.py"))]
169169
#[test_case(Rule::UndefinedName, Path::new("F821_32.pyi"))]
170170
#[test_case(Rule::UndefinedName, Path::new("F821_33.py"))]
171+
#[test_case(Rule::UndefinedName, Path::new("F821_34.pyi"))]
171172
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
172173
#[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))]
173174
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
3+
---
4+
F821 Undefined name `EarlyDel`
5+
--> F821_34.pyi:19:13
6+
|
7+
17 | class EarlyDel: ...
8+
18 | del EarlyDel
9+
19 | def f2(cls: EarlyDel) -> None: ... # F821
10+
| ^^^^^^^^
11+
20 |
12+
21 | # Error: name that was never defined
13+
|
14+
15+
F821 Undefined name `Undefined`
16+
--> F821_34.pyi:22:10
17+
|
18+
21 | # Error: name that was never defined
19+
22 | def g(x: Undefined) -> None: ... # F821
20+
| ^^^^^^^^^
21+
|

crates/ruff_python_semantic/src/model.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,48 @@ impl<'a> SemanticModel<'a> {
502502
// print(x)
503503
//
504504
// The `x` in `print(x)` should be treated as unresolved.
505-
BindingKind::Deletion | BindingKind::UnboundException(None) => {
505+
BindingKind::Deletion => {
506+
// In stub files, `del` is used to hide names from re-export
507+
// while the name is still valid for use in type annotations
508+
// within the same file. Since annotations are deferred in
509+
// stubs, the `Deletion` binding is seen at resolve time even
510+
// though the reference textually precedes the `del`. Resolve
511+
// to the shadowed (pre-`del`) binding when available.
512+
if self.in_stub_file() {
513+
if let Some(shadowed_id) =
514+
self.scopes[scope_id].shadowed_binding(binding_id)
515+
{
516+
// Only suppress F821 when the reference textually precedes
517+
// the `del` statement. If `del` comes before the reference,
518+
// the name is genuinely undefined at that point and F821
519+
// should still fire.
520+
let deletion_range = self.bindings[binding_id].range;
521+
if !self.bindings[shadowed_id].is_unbound()
522+
&& name.range.start() < deletion_range.start()
523+
{
524+
let reference_id = self.resolved_references.push(
525+
self.scope_id,
526+
self.node_id,
527+
ExprContext::Load,
528+
self.flags,
529+
name.range,
530+
);
531+
self.bindings[shadowed_id].references.push(reference_id);
532+
self.resolved_names.insert(name.into(), shadowed_id);
533+
return ReadResult::Resolved(shadowed_id);
534+
}
535+
}
536+
}
537+
538+
self.unresolved_references.push(
539+
name.range,
540+
self.exceptions(),
541+
UnresolvedReferenceFlags::empty(),
542+
);
543+
return ReadResult::UnboundLocal(binding_id);
544+
}
545+
546+
BindingKind::UnboundException(None) => {
506547
self.unresolved_references.push(
507548
name.range,
508549
self.exceptions(),

0 commit comments

Comments
 (0)