diff --git a/crates/ruff/resources/test/fixtures/pylint/import_self/__init__.py b/crates/ruff/resources/test/fixtures/pylint/import_self/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/crates/ruff/resources/test/fixtures/pylint/import_self/module.py b/crates/ruff/resources/test/fixtures/pylint/import_self/module.py new file mode 100644 index 0000000000000..5728649cf42d7 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/import_self/module.py @@ -0,0 +1,3 @@ +import import_self.module +from import_self import module +from . import module diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 39f0c70fb246f..14febab297f99 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1000,6 +1000,13 @@ where if self.settings.rules.enabled(Rule::ManualFromImport) { pylint::rules::manual_from_import(self, stmt, alias, names); } + if self.settings.rules.enabled(Rule::ImportSelf) { + if let Some(diagnostic) = + pylint::rules::import_self(alias, self.module_path.as_deref()) + { + self.diagnostics.push(diagnostic); + } + } if let Some(asname) = &alias.node.asname { let name = alias.node.name.split('.').last().unwrap(); @@ -1477,6 +1484,17 @@ where } } + if self.settings.rules.enabled(Rule::ImportSelf) { + if let Some(diagnostic) = pylint::rules::import_from_self( + *level, + module.as_deref(), + names, + self.module_path.as_deref(), + ) { + self.diagnostics.push(diagnostic); + } + } + if self.settings.rules.enabled(Rule::BannedImportFrom) { if let Some(diagnostic) = flake8_import_conventions::rules::banned_import_from( stmt, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 6b4802a9b76b3..cffdf12abe94e 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -204,6 +204,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Pylint, "R5501") => Rule::CollapsibleElseIf, (Pylint, "W0120") => Rule::UselessElseOnLoop, (Pylint, "W0129") => Rule::AssertOnStringLiteral, + (Pylint, "W0406") => Rule::ImportSelf, (Pylint, "W0602") => Rule::GlobalVariableNotAssigned, (Pylint, "W0603") => Rule::GlobalStatement, (Pylint, "W0711") => Rule::BinaryOpException, diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index e4a30ee1a13ae..cc342b9d0eab7 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -156,6 +156,7 @@ ruff_macros::register_rules!( rules::pylint::rules::BadStringFormatType, rules::pylint::rules::BidirectionalUnicode, rules::pylint::rules::BinaryOpException, + rules::pylint::rules::ImportSelf, rules::pylint::rules::InvalidCharacterBackspace, rules::pylint::rules::InvalidCharacterSub, rules::pylint::rules::InvalidCharacterEsc, diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index 2736555b51f30..2cac8fb3d24dc 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -40,6 +40,7 @@ mod tests { #[test_case(Rule::ContinueInFinally, Path::new("continue_in_finally.py"); "PLE0116")] #[test_case(Rule::GlobalStatement, Path::new("global_statement.py"); "PLW0603")] #[test_case(Rule::GlobalVariableNotAssigned, Path::new("global_variable_not_assigned.py"); "PLW0602")] + #[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::InvalidCharacterBackspace, Path::new("invalid_characters.py"); "PLE2510")] diff --git a/crates/ruff/src/rules/pylint/rules/import_self.rs b/crates/ruff/src/rules/pylint/rules/import_self.rs new file mode 100644 index 0000000000000..b3fc53f03793b --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/import_self.rs @@ -0,0 +1,70 @@ +use rustpython_parser::ast::Alias; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::resolve_imported_module_path; + +#[violation] +pub struct ImportSelf { + pub name: String, +} + +impl Violation for ImportSelf { + #[derive_message_formats] + fn message(&self) -> String { + let Self { name } = self; + format!("Module `{name}` imports itself") + } +} + +/// PLW0406 +pub fn import_self(alias: &Alias, module_path: Option<&[String]>) -> Option { + let Some(module_path) = module_path else { + return None; + }; + + if alias.node.name.split('.').eq(module_path) { + return Some(Diagnostic::new( + ImportSelf { + name: alias.node.name.clone(), + }, + alias.range(), + )); + } + + None +} + +/// PLW0406 +pub fn import_from_self( + level: Option, + module: Option<&str>, + names: &[Alias], + module_path: Option<&[String]>, +) -> Option { + let Some(module_path) = module_path else { + return None; + }; + let Some(imported_module_path) = resolve_imported_module_path(level, module, Some(module_path)) else { + return None; + }; + + if imported_module_path + .split('.') + .eq(&module_path[..module_path.len() - 1]) + { + if let Some(alias) = names + .iter() + .find(|alias| alias.node.name == module_path[module_path.len() - 1]) + { + return Some(Diagnostic::new( + ImportSelf { + name: format!("{}.{}", imported_module_path, alias.node.name), + }, + alias.range(), + )); + } + } + + None +} diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index 1cbdd566e41f9..ed332ee4db34f 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -10,6 +10,7 @@ pub use comparison_of_constant::{comparison_of_constant, ComparisonOfConstant}; pub use continue_in_finally::{continue_in_finally, ContinueInFinally}; pub use global_statement::{global_statement, GlobalStatement}; pub use global_variable_not_assigned::GlobalVariableNotAssigned; +pub use import_self::{import_from_self, import_self, ImportSelf}; pub use invalid_all_format::{invalid_all_format, InvalidAllFormat}; pub use invalid_all_object::{invalid_all_object, InvalidAllObject}; pub use invalid_envvar_default::{invalid_envvar_default, InvalidEnvvarDefault}; @@ -57,6 +58,7 @@ mod comparison_of_constant; mod continue_in_finally; mod global_statement; mod global_variable_not_assigned; +mod import_self; mod invalid_all_format; mod invalid_all_object; mod invalid_envvar_default; diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0406_import_self__module.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0406_import_self__module.py.snap new file mode 100644 index 0000000000000..43b8e0a858a15 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0406_import_self__module.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +module.py:1:8: PLW0406 Module `import_self.module` imports itself + | +1 | import import_self.module + | ^^^^^^^^^^^^^^^^^^ PLW0406 +2 | from import_self import module +3 | from . import module + | + +module.py:2:25: PLW0406 Module `import_self.module` imports itself + | +2 | import import_self.module +3 | from import_self import module + | ^^^^^^ PLW0406 +4 | from . import module + | + +module.py:3:15: PLW0406 Module `import_self.module` imports itself + | +3 | import import_self.module +4 | from import_self import module +5 | from . import module + | ^^^^^^ PLW0406 + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 76979c344659a..a39b5d603dcd4 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2033,6 +2033,9 @@ "PLW012", "PLW0120", "PLW0129", + "PLW04", + "PLW040", + "PLW0406", "PLW06", "PLW060", "PLW0602",