[`flake8-pie`] Avoid false positive for multiple assignment with `auto()` (`PIE796`) (#17274)

This fix closes #16868 

I noticed the issue is assigned, but the assignee appears to be actively
working on another pull request. I hope that’s okay!

<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

As of Python 3.11.1, `enum.auto()` can be used in multiple assignments.
This pattern should not trigger non-unique-enums check.
Reference: [Python docs on
enum.auto()](https://docs.python.org/3/library/enum.html#enum.auto)

This fix updates the check logic to skip enum variant statements where
the right-hand side is a tuple containing a call to `enum.auto()`.

## Test Plan

<!-- How was it tested? -->

The added test case uses the example from the original issue. It
previously triggered a false positive, but now passes successfully.
This commit is contained in:
Denys Kyslytsyn 2025-04-09 04:53:27 +09:00 committed by GitHub
parent 058439d5d3
commit ed14dbb1a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 22 additions and 9 deletions

View File

@ -78,3 +78,9 @@ class FakeEnum11(enum.Enum):
A = cast(SomeType, ...)
B = cast(SomeType, ...) # PIE796
C = cast(SomeType, ...) # PIE796
class FakeEnum12(enum.Enum):
A = enum.auto(), 0
B = enum.auto(), 1
C = enum.auto(), 0

View File

@ -1,3 +1,4 @@
use ruff_python_semantic::SemanticModel;
use rustc_hash::FxHashSet;
use ruff_diagnostics::Diagnostic;
@ -75,17 +76,15 @@ pub(crate) fn non_unique_enums(checker: &Checker, parent: &Stmt, body: &[Stmt])
continue;
};
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
if checker
.semantic()
.resolve_qualified_name(func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["enum", "auto"]))
{
if is_call_to_enum_auto(semantic, value) {
continue;
} else if let Expr::Tuple(ast::ExprTuple { elts, .. }) = value.as_ref() {
if elts.iter().any(|elt| is_call_to_enum_auto(semantic, elt)) {
continue;
}
}
if checker.source_type.is_stub() && member_has_unknown_value(checker, value) {
if checker.source_type.is_stub() && member_has_unknown_value(semantic, value) {
continue;
}
@ -103,16 +102,24 @@ pub(crate) fn non_unique_enums(checker: &Checker, parent: &Stmt, body: &[Stmt])
}
}
fn is_call_to_enum_auto(semantic: &SemanticModel, expr: &Expr) -> bool {
expr.as_call_expr().is_some_and(|call| {
semantic
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["enum", "auto"]))
})
}
/// Whether the value is a bare ellipsis literal (`A = ...`)
/// or a casted one (`A = cast(SomeType, ...)`).
fn member_has_unknown_value(checker: &Checker, expr: &Expr) -> bool {
fn member_has_unknown_value(semantic: &SemanticModel, expr: &Expr) -> bool {
match expr {
Expr::EllipsisLiteral(_) => true,
Expr::Call(ExprCall {
func, arguments, ..
}) => {
if !checker.semantic().match_typing_expr(func, "cast") {
if !semantic.match_typing_expr(func, "cast") {
return false;
}