diff --git a/crates/ruff/resources/test/fixtures/pylint/duplicate_bases.py b/crates/ruff/resources/test/fixtures/pylint/duplicate_bases.py new file mode 100644 index 0000000000..491421ccf5 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/duplicate_bases.py @@ -0,0 +1,24 @@ +### +# Errors. +### +class A: + ... + + +class B(A, A): + ... + + +### +# Non-errors. +### +class C: + ... + + +class D(C): + ... + + +class E(A, C): + ... diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index d37a738bc2..63899c91be 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -761,6 +761,10 @@ where if self.settings.rules.enabled(Rule::BuiltinVariableShadowing) { flake8_builtins::rules::builtin_variable_shadowing(self, name, stmt); } + + if self.settings.rules.enabled(Rule::DuplicateBases) { + pylint::rules::duplicate_bases(self, name, bases); + } } StmtKind::Import(ast::StmtImport { names }) => { if self.settings.rules.enabled(Rule::MultipleImportsOnOneLine) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 3e0e17b352..a712e224b4 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -213,6 +213,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Pylint, "W2901") => Rule::RedefinedLoopName, (Pylint, "E0302") => Rule::UnexpectedSpecialMethodSignature, (Pylint, "W3301") => Rule::NestedMinMax, + (Pylint, "E0241") => Rule::DuplicateBases, // flake8-builtins (Flake8Builtins, "001") => Rule::BuiltinVariableShadowing, diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 7447f1c485..59af691058 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -190,6 +190,7 @@ ruff_macros::register_rules!( rules::pylint::rules::LoggingTooManyArgs, rules::pylint::rules::UnexpectedSpecialMethodSignature, rules::pylint::rules::NestedMinMax, + rules::pylint::rules::DuplicateBases, // flake8-builtins rules::flake8_builtins::rules::BuiltinVariableShadowing, rules::flake8_builtins::rules::BuiltinArgumentShadowing, diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index be74e2b068..eb703e3497 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -46,6 +46,7 @@ mod tests { #[test_case(Rule::ImportSelf, Path::new("import_self/module.py"); "PLW0406")] #[test_case(Rule::InvalidAllFormat, Path::new("invalid_all_format.py"); "PLE0605")] #[test_case(Rule::InvalidAllObject, Path::new("invalid_all_object.py"); "PLE0604")] + #[test_case(Rule::DuplicateBases, Path::new("duplicate_bases.py"); "PLE0241")] #[test_case(Rule::InvalidCharacterBackspace, Path::new("invalid_characters.py"); "PLE2510")] #[test_case(Rule::InvalidCharacterEsc, Path::new("invalid_characters.py"); "PLE2513")] #[test_case(Rule::InvalidCharacterNul, Path::new("invalid_characters.py"); "PLE2514")] diff --git a/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs b/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs new file mode 100644 index 0000000000..5cdd54800d --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs @@ -0,0 +1,42 @@ +use std::hash::BuildHasherDefault; + +use rustc_hash::FxHashSet; +use rustpython_parser::ast::{self, Expr, ExprKind, Identifier}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +#[violation] +pub struct DuplicateBases { + base: String, + class: String, +} + +impl Violation for DuplicateBases { + #[derive_message_formats] + fn message(&self) -> String { + let DuplicateBases { base, class } = self; + format!("Duplicate base `{base}` for class `{class}`") + } +} + +/// PLE0241 +pub(crate) fn duplicate_bases(checker: &mut Checker, name: &str, bases: &[Expr]) { + let mut seen: FxHashSet<&Identifier> = + FxHashSet::with_capacity_and_hasher(bases.len(), BuildHasherDefault::default()); + for base in bases { + if let ExprKind::Name(ast::ExprName { id, .. }) = &base.node { + if !seen.insert(id) { + checker.diagnostics.push(Diagnostic::new( + DuplicateBases { + base: id.to_string(), + class: name.to_string(), + }, + base.range(), + )); + } + } + } +} diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index 19ac6fb52f..04a444ce20 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -8,6 +8,7 @@ pub(crate) use collapsible_else_if::{collapsible_else_if, CollapsibleElseIf}; pub(crate) use compare_to_empty_string::{compare_to_empty_string, CompareToEmptyString}; pub(crate) use comparison_of_constant::{comparison_of_constant, ComparisonOfConstant}; pub(crate) use continue_in_finally::{continue_in_finally, ContinueInFinally}; +pub(crate) use duplicate_bases::{duplicate_bases, DuplicateBases}; pub(crate) use global_statement::{global_statement, GlobalStatement}; pub(crate) use global_variable_not_assigned::GlobalVariableNotAssigned; pub(crate) use import_self::{import_from_self, import_self, ImportSelf}; @@ -57,6 +58,7 @@ mod collapsible_else_if; mod compare_to_empty_string; mod comparison_of_constant; mod continue_in_finally; +mod duplicate_bases; mod global_statement; mod global_variable_not_assigned; mod import_self; diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0241_duplicate_bases.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0241_duplicate_bases.py.snap new file mode 100644 index 0000000000..60b9ce981b --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0241_duplicate_bases.py.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +duplicate_bases.py:8:12: PLE0241 Duplicate base `A` for class `B` + | +8 | class B(A, A): + | ^ PLE0241 +9 | ... + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 70f253b309..caa6e56a73 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1959,6 +1959,9 @@ "PLE0116", "PLE0117", "PLE0118", + "PLE02", + "PLE024", + "PLE0241", "PLE03", "PLE030", "PLE0302",