diff --git a/README.md b/README.md index 39d58edf84..fc9aa1bd78 100644 --- a/README.md +++ b/README.md @@ -1141,6 +1141,7 @@ For more, see [flake8-pie](https://pypi.org/project/flake8-pie/0.16.0/) on PyPI. | ---- | ---- | ------- | --- | | PIE790 | NoUnnecessaryPass | Unnecessary `pass` statement | 🛠 | | PIE794 | DupeClassFieldDefinitions | Class field `...` is defined multiple times | 🛠 | +| PIE796 | PreferUniqueEnums | Enum contains duplicate value: `...` | | | PIE807 | PreferListBuiltin | Prefer `list()` over useless lambda | 🛠 | ### flake8-commas (COM) diff --git a/resources/test/fixtures/flake8_pie/PIE796.py b/resources/test/fixtures/flake8_pie/PIE796.py new file mode 100644 index 0000000000..e6c8cb0399 --- /dev/null +++ b/resources/test/fixtures/flake8_pie/PIE796.py @@ -0,0 +1,54 @@ +class FakeEnum(enum.Enum): + A = "A" + B = "B" + C = "B" # PIE796 + + +class FakeEnum2(Enum): + A = 1 + B = 2 + C = 2 # PIE796 + + +class FakeEnum3(str, Enum): + A = "1" + B = "2" + C = "2" # PIE796 + +class FakeEnum4(Enum): + A = 1.0 + B = 2.5 + C = 2.5 # PIE796 + + +class FakeEnum5(Enum): + A = 1.0 + B = True + C = False + D = False # PIE796 + + +class FakeEnum6(Enum): + A = 1 + B = 2 + C = None + D = None # PIE796 + + +@enum.unique +class FakeEnum7(enum.Enum): + A = "A" + B = "B" + C = "C" + +@unique +class FakeEnum8(Enum): + A = 1 + B = 2 + C = 2 # PIE796 + +class FakeEnum9(enum.Enum): + A = "A" + B = "B" + C = "C" + diff --git a/ruff.schema.json b/ruff.schema.json index 65425735ac..821e604156 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1431,6 +1431,7 @@ "PIE79", "PIE790", "PIE794", + "PIE796", "PIE8", "PIE80", "PIE807", diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs index a76d8912c9..21c7d898c3 100644 --- a/src/checkers/ast.rs +++ b/src/checkers/ast.rs @@ -701,6 +701,10 @@ where flake8_pie::rules::dupe_class_field_definitions(self, stmt, body); } + if self.settings.enabled.contains(&RuleCode::PIE796) { + flake8_pie::rules::prefer_unique_enums(self, stmt, body); + } + self.check_builtin_shadowing(name, stmt, false); for expr in bases { diff --git a/src/registry.rs b/src/registry.rs index 8c781dad40..96d9793fb2 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -410,6 +410,7 @@ ruff_macros::define_rule_mapping!( // flake8-pie PIE790 => violations::NoUnnecessaryPass, PIE794 => violations::DupeClassFieldDefinitions, + PIE796 => violations::PreferUniqueEnums, PIE807 => violations::PreferListBuiltin, // flake8-commas COM812 => violations::TrailingCommaMissing, diff --git a/src/rules/flake8_pie/mod.rs b/src/rules/flake8_pie/mod.rs index 754e274a88..ef4cf551c0 100644 --- a/src/rules/flake8_pie/mod.rs +++ b/src/rules/flake8_pie/mod.rs @@ -14,6 +14,7 @@ mod tests { #[test_case(RuleCode::PIE790, Path::new("PIE790.py"); "PIE790")] #[test_case(RuleCode::PIE794, Path::new("PIE794.py"); "PIE794")] + #[test_case(RuleCode::PIE796, Path::new("PIE796.py"); "PIE796")] #[test_case(RuleCode::PIE807, Path::new("PIE807.py"); "PIE807")] fn rules(rule_code: RuleCode, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); diff --git a/src/rules/flake8_pie/rules.rs b/src/rules/flake8_pie/rules.rs index 072b8353c0..cbe2a5f368 100644 --- a/src/rules/flake8_pie/rules.rs +++ b/src/rules/flake8_pie/rules.rs @@ -2,6 +2,8 @@ use log::error; use rustc_hash::FxHashSet; use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind}; +use crate::ast::comparable::ComparableExpr; +use crate::ast::helpers::unparse_expr; use crate::ast::types::{Range, RefEquality}; use crate::autofix::helpers::delete_stmt; use crate::checkers::ast::Checker; @@ -106,6 +108,41 @@ pub fn dupe_class_field_definitions<'a, 'b>( } } +/// PIE796 +pub fn prefer_unique_enums<'a, 'b>(checker: &mut Checker<'a>, parent: &'b Stmt, body: &'b [Stmt]) +where + 'b: 'a, +{ + let StmtKind::ClassDef { bases, .. } = &parent.node else { + return; + }; + + if !bases.iter().any(|expr| { + checker + .resolve_call_path(expr) + .map_or(false, |call_path| call_path == ["enum", "Enum"]) + }) { + return; + } + + let mut seen_targets: FxHashSet = FxHashSet::default(); + for stmt in body { + let StmtKind::Assign { value, .. } = &stmt.node else { + continue; + }; + + if !seen_targets.insert(ComparableExpr::from(value)) { + let diagnostic = Diagnostic::new( + violations::PreferUniqueEnums { + value: unparse_expr(value, checker.stylist), + }, + Range::from_located(stmt), + ); + checker.diagnostics.push(diagnostic); + } + } +} + /// PIE807 pub fn prefer_list_builtin(checker: &mut Checker, expr: &Expr) { let ExprKind::Lambda { args, body } = &expr.node else { diff --git a/src/rules/flake8_pie/snapshots/ruff__rules__flake8_pie__tests__PIE796_PIE796.py.snap b/src/rules/flake8_pie/snapshots/ruff__rules__flake8_pie__tests__PIE796_PIE796.py.snap new file mode 100644 index 0000000000..0079555160 --- /dev/null +++ b/src/rules/flake8_pie/snapshots/ruff__rules__flake8_pie__tests__PIE796_PIE796.py.snap @@ -0,0 +1,75 @@ +--- +source: src/rules/flake8_pie/mod.rs +expression: diagnostics +--- +- kind: + PreferUniqueEnums: "\"B\"" + location: + row: 4 + column: 4 + end_location: + row: 4 + column: 11 + fix: ~ + parent: ~ +- kind: + PreferUniqueEnums: "2" + location: + row: 10 + column: 4 + end_location: + row: 10 + column: 9 + fix: ~ + parent: ~ +- kind: + PreferUniqueEnums: "\"2\"" + location: + row: 16 + column: 4 + end_location: + row: 16 + column: 11 + fix: ~ + parent: ~ +- kind: + PreferUniqueEnums: "2.5" + location: + row: 21 + column: 4 + end_location: + row: 21 + column: 11 + fix: ~ + parent: ~ +- kind: + PreferUniqueEnums: "False" + location: + row: 28 + column: 4 + end_location: + row: 28 + column: 13 + fix: ~ + parent: ~ +- kind: + PreferUniqueEnums: None + location: + row: 35 + column: 4 + end_location: + row: 35 + column: 12 + fix: ~ + parent: ~ +- kind: + PreferUniqueEnums: "2" + location: + row: 48 + column: 4 + end_location: + row: 48 + column: 9 + fix: ~ + parent: ~ + diff --git a/src/violations.rs b/src/violations.rs index 6b9b609905..ca01eaa7f3 100644 --- a/src/violations.rs +++ b/src/violations.rs @@ -5970,6 +5970,24 @@ impl AlwaysAutofixableViolation for DupeClassFieldDefinitions { } } +define_violation!( + pub struct PreferUniqueEnums { + pub value: String, + } +); +impl Violation for PreferUniqueEnums { + fn message(&self) -> String { + let PreferUniqueEnums { value } = self; + format!("Enum contains duplicate value: `{value}`") + } + + fn placeholder() -> Self { + PreferUniqueEnums { + value: "...".to_string(), + } + } +} + define_violation!( pub struct PreferListBuiltin; );