diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/import_aliasing_2/__init__.py b/crates/ruff_linter/resources/test/fixtures/pylint/import_aliasing_2/__init__.py new file mode 100644 index 00000000000000..c6a3d8d27eefbe --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/import_aliasing_2/__init__.py @@ -0,0 +1,4 @@ +import collections as collections +from collections import OrderedDict as OrderedDict +from . import foo as foo +from .foo import bar as bar diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index e67efdec3d2740..8640eac1a18125 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -77,3 +77,10 @@ pub(crate) const fn is_multiple_with_statements_fix_safe_enabled( ) -> bool { settings.preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/18400 +pub(crate) const fn is_ignore_init_files_in_useless_alias_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 6a52c3eb82c24b..dd4c11f67c166e 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -168,6 +168,7 @@ mod tests { )] #[test_case(Rule::UselessElseOnLoop, Path::new("useless_else_on_loop.py"))] #[test_case(Rule::UselessImportAlias, Path::new("import_aliasing.py"))] + #[test_case(Rule::UselessImportAlias, Path::new("import_aliasing_2/__init__.py"))] #[test_case(Rule::UselessReturn, Path::new("useless_return.py"))] #[test_case(Rule::UselessWithLock, Path::new("useless_with_lock.py"))] #[test_case(Rule::UnreachableCode, Path::new("unreachable.py"))] @@ -418,6 +419,19 @@ mod tests { Ok(()) } + #[test] + fn preview_useless_import_alias() -> Result<()> { + let diagnostics = test_path( + Path::new("pylint/import_aliasing_2/__init__.py"), + &LinterSettings { + preview: PreviewMode::Enabled, + ..LinterSettings::for_rule(Rule::UselessImportAlias) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + #[test] fn import_outside_top_level_with_banned() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs index 5816f8faf555bf..31d01656d97be5 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs @@ -4,11 +4,14 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::preview::is_ignore_init_files_in_useless_alias_enabled; use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for import aliases that do not rename the original package. /// +/// In [preview] this rule does not apply in `__init__.py` files. +/// /// ## Why is this bad? /// The import alias is redundant and should be removed to avoid confusion. /// @@ -32,6 +35,8 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ```python /// import numpy /// ``` +/// +/// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] pub(crate) struct UselessImportAlias { required_import_conflict: bool, @@ -67,6 +72,14 @@ pub(crate) fn useless_import_alias(checker: &Checker, alias: &Alias) { if alias.name.as_str() != asname.as_str() { return; } + + // A re-export in __init__.py is probably intentional. + if checker.path().ends_with("__init__.py") + && is_ignore_init_files_in_useless_alias_enabled(checker.settings) + { + return; + } + // A required import with a useless alias causes an infinite loop. // See https://github.com/astral-sh/ruff/issues/14283 let required_import_conflict = checker @@ -100,6 +113,12 @@ pub(crate) fn useless_import_from_alias( if alias.name.as_str() != asname.as_str() { return; } + + // A re-export in __init__.py is probably intentional. + if checker.path().ends_with("__init__.py") { + return; + } + // A required import with a useless alias causes an infinite loop. // See https://github.com/astral-sh/ruff/issues/14283 let required_import_conflict = checker.settings.isort.requires_member_import( diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0414_import_aliasing_2____init__.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0414_import_aliasing_2____init__.py.snap new file mode 100644 index 00000000000000..101902f31102b9 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLC0414_import_aliasing_2____init__.py.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +__init__.py:1:8: PLC0414 [*] Import alias does not rename original package + | +1 | import collections as collections + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0414 +2 | from collections import OrderedDict as OrderedDict +3 | from . import foo as foo + | + = help: Remove import alias + +ℹ Unsafe fix +1 |-import collections as collections + 1 |+import collections +2 2 | from collections import OrderedDict as OrderedDict +3 3 | from . import foo as foo +4 4 | from .foo import bar as bar diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview_useless_import_alias.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview_useless_import_alias.snap new file mode 100644 index 00000000000000..6c123427ab2b8b --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview_useless_import_alias.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +