diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_30.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_30.py new file mode 100644 index 0000000000000..8c2b8c0e23140 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F811_30.py @@ -0,0 +1,37 @@ +"""Regression test for: https://github.com/astral-sh/ruff/issues/11828""" + + +class A: + """A.""" + + def foo(self) -> None: + """Foo.""" + + bar = foo + + def bar(self) -> None: + """Bar.""" + + +class B: + """B.""" + def baz(self) -> None: + """Baz.""" + + baz = 1 + + +class C: + """C.""" + def foo(self) -> None: + """Foo.""" + + bar = (foo := 1) + + +class D: + """D.""" + foo = 1 + foo = 2 + bar = (foo := 3) + bar = (foo := 4) diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index a700b7ed6c498..f2b1e6d114e1b 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -125,6 +125,7 @@ mod tests { #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_27.py"))] #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_28.py"))] #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_29.pyi"))] + #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_30.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_0.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_1.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_2.py"))] diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_30.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_30.py.snap new file mode 100644 index 0000000000000..e785583128b93 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F811_F811_30.py.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- +F811_30.py:12:9: F811 Redefinition of unused `bar` from line 10 + | +10 | bar = foo +11 | +12 | def bar(self) -> None: + | ^^^ F811 +13 | """Bar.""" + | + = help: Remove definition: `bar` + +F811_30.py:21:5: F811 Redefinition of unused `baz` from line 18 + | +19 | """Baz.""" +20 | +21 | baz = 1 + | ^^^ F811 + | + = help: Remove definition: `baz` + +F811_30.py:29:12: F811 Redefinition of unused `foo` from line 26 + | +27 | """Foo.""" +28 | +29 | bar = (foo := 1) + | ^^^ F811 + | + = help: Remove definition: `foo` diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 22ee07490c826..a4eb2340a4b28 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -177,16 +177,31 @@ impl<'a> Binding<'a> { | BindingKind::Builtin => { return false; } + // Assignment-assignment bindings are not considered redefinitions, as in: + // ```python + // x = 1 + // x = 2 + // ``` + BindingKind::Assignment | BindingKind::NamedExprAssignment => { + if matches!( + existing.kind, + BindingKind::Assignment | BindingKind::NamedExprAssignment + ) { + return false; + } + } _ => {} } - // Otherwise, the shadowed binding must be a class definition, function definition, or - // import to be considered a redefinition. + // Otherwise, the shadowed binding must be a class definition, function definition, + // import, or assignment to be considered a redefinition. matches!( existing.kind, BindingKind::ClassDefinition(_) | BindingKind::FunctionDefinition(_) | BindingKind::Import(_) | BindingKind::FromImport(_) + | BindingKind::Assignment + | BindingKind::NamedExprAssignment ) }