diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index ebd202b052..30b4ad9957 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -3652,3 +3652,33 @@ fn supported_file_extensions_preview_enabled() -> Result<()> { "); Ok(()) } + +// Regression test for https://github.com/astral-sh/ruff/issues/20891 +#[test] +fn required_import_skips_pyi025() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .arg("--isolated") + .arg("check") + .args(["--select", "F401,I002,PYI025"]) + .arg("--config") + .arg(r#"lint.isort.required-imports=["from collections.abc import Set"]"#) + .arg("-") + .arg("--unsafe-fixes") + .arg("--no-cache") + .pass_stdin("1"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + I002 [*] Missing required import: `from collections.abc import Set` + --> -:1:1 + help: Insert required import: `from collections.abc import Set` + + Found 1 error. + [*] 1 fixable with the --fix option. + + ----- stderr ----- + " + ); +} diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs index 47b111f97e..dac7e5c158 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs @@ -1,10 +1,12 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{Stmt, StmtImportFrom}; use ruff_python_semantic::Imported; use ruff_python_semantic::{Binding, BindingKind, Scope}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::renamer::Renamer; +use crate::rules::pyupgrade::rules::is_import_required_by_isort; use crate::{Applicability, Fix, FixAvailability, Violation}; /// ## What it does @@ -73,6 +75,20 @@ pub(crate) fn unaliased_collections_abc_set_import(checker: &Checker, binding: & return; } + // Skip if this import is required by isort to prevent infinite loops with I002 and F401 + if let Some(stmt @ Stmt::ImportFrom(StmtImportFrom { names, .. })) = + binding.statement(checker.semantic()) + && names.iter().any(|alias| { + is_import_required_by_isort( + &checker.settings().isort.required_imports, + stmt.into(), + alias, + ) + }) + { + return; + } + let mut diagnostic = checker.report_diagnostic(UnaliasedCollectionsAbcSetImport, binding.range()); if checker.semantic().is_available("AbstractSet") {