From 0b1741d8425c7d9a78d64ee5e29e1322e2564622 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Mon, 9 Feb 2026 00:28:14 +0000 Subject: [PATCH 01/26] test: add test cases for #6371 fixes #6371 --- .../test/fixtures/isort/import_heading.py | 11 ++++++ .../isort/import_heading_already_correct.py | 16 +++++++++ .../isort/import_heading_already_present.py | 15 ++++++++ ...port_heading_force_sort_within_sections.py | 5 +++ .../fixtures/isort/import_heading_partial.py | 5 +++ .../isort/import_heading_single_section.py | 2 ++ .../fixtures/isort/import_heading_unsorted.py | 7 ++++ .../import_heading_with_no_lines_before.py | 11 ++++++ .../isort/import_heading_wrong_heading.py | 9 +++++ ...ect_import_heading_already_correct.py.snap | 4 +++ ...ent_import_heading_already_present.py.snap | 36 +++++++++++++++++++ ...heading_force_sort_within_sections.py.snap | 24 +++++++++++++ ...sts__import_heading_import_heading.py.snap | 36 +++++++++++++++++++ ...ing_partial_import_heading_partial.py.snap | 22 ++++++++++++ ...tion_import_heading_single_section.py.snap | 15 ++++++++ ...g_unsorted_import_heading_unsorted.py.snap | 32 +++++++++++++++++ ...mport_heading_with_no_lines_before.py.snap | 36 +++++++++++++++++++ ...ading_import_heading_wrong_heading.py.snap | 31 ++++++++++++++++ 18 files changed, 317 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/import_heading.py create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/import_heading_already_correct.py create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/import_heading_already_present.py create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/import_heading_force_sort_within_sections.py create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/import_heading_partial.py create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/import_heading_single_section.py create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/import_heading_unsorted.py create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/import_heading_with_no_lines_before.py create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/import_heading_wrong_heading.py create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_correct_import_heading_already_correct.py.snap create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_force_sort_within_sections_import_heading_force_sort_within_sections.py.snap create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_partial_import_heading_partial.py.snap create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_single_section_import_heading_single_section.py.snap create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading.py new file mode 100644 index 00000000000000..ece0b4b99cb659 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import os +import sys + +import requests +import pandas + +from my_first_party import my_first_party_object + +from . import my_local_folder_object diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_already_correct.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_already_correct.py new file mode 100644 index 00000000000000..27cba5e0a4a361 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_already_correct.py @@ -0,0 +1,16 @@ +# Future imports +from __future__ import annotations + +# Standard library imports +import os +import sys + +# Third party imports +import pandas +import requests + +# First party imports +from my_first_party import my_first_party_object + +# Local folder imports +from . import my_local_folder_object diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_already_present.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_already_present.py new file mode 100644 index 00000000000000..faa8b87be73190 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_already_present.py @@ -0,0 +1,15 @@ +# Future imports +from __future__ import annotations + +# Standard library imports +import os +import sys + +# Third party imports +import requests +import pandas + +# First party imports +from my_first_party import my_first_party_object + +from . import my_local_folder_object diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_force_sort_within_sections.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_force_sort_within_sections.py new file mode 100644 index 00000000000000..9887350b1720b7 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_force_sort_within_sections.py @@ -0,0 +1,5 @@ +from __future__ import annotations +from typing import Any + +import requests +import pandas diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_partial.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_partial.py new file mode 100644 index 00000000000000..8866a627dd65f9 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_partial.py @@ -0,0 +1,5 @@ +import os +import sys + +import requests +import pandas diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_single_section.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_single_section.py new file mode 100644 index 00000000000000..149b10ef5f13ba --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_single_section.py @@ -0,0 +1,2 @@ +import requests +import pandas diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_unsorted.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_unsorted.py new file mode 100644 index 00000000000000..49ad885bd6d9c1 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_unsorted.py @@ -0,0 +1,7 @@ +import pandas +import os +from __future__ import annotations +import sys +import requests +from my_first_party import my_first_party_object +from . import my_local_folder_object diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_with_no_lines_before.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_with_no_lines_before.py new file mode 100644 index 00000000000000..ece0b4b99cb659 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_with_no_lines_before.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import os +import sys + +import requests +import pandas + +from my_first_party import my_first_party_object + +from . import my_local_folder_object diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_wrong_heading.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_wrong_heading.py new file mode 100644 index 00000000000000..a5ba3554c6d081 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_wrong_heading.py @@ -0,0 +1,9 @@ +# Wrong heading +import os +import sys + +# Also wrong heading +import requests +import pandas + +from my_first_party import my_first_party_object diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_correct_import_heading_already_correct.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_correct_import_heading_already_correct.py.snap new file mode 100644 index 00000000000000..ed369f0fd61f02 --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_correct_import_heading_already_correct.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap new file mode 100644 index 00000000000000..409cbb7dfbc407 --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> import_heading_already_present.py:2:1 + | + 1 | # Future imports + 2 | / from __future__ import annotations + 3 | | + 4 | | # Standard library imports + 5 | | import os + 6 | | import sys + 7 | | + 8 | | # Third party imports + 9 | | import requests +10 | | import pandas +11 | | +12 | | # First party imports +13 | | from my_first_party import my_first_party_object +14 | | +15 | | from . import my_local_folder_object + | |____________________________________^ + | +help: Organize imports +6 | import sys +7 | +8 | # Third party imports + - import requests +9 | import pandas + - + - # First party imports +10 + import requests +11 | from my_first_party import my_first_party_object +12 | +13 + # Local folder imports +14 | from . import my_local_folder_object diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_force_sort_within_sections_import_heading_force_sort_within_sections.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_force_sort_within_sections_import_heading_force_sort_within_sections.py.snap new file mode 100644 index 00000000000000..bb67a64156be30 --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_force_sort_within_sections_import_heading_force_sort_within_sections.py.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> import_heading_force_sort_within_sections.py:1:1 + | +1 | / from __future__ import annotations +2 | | from typing import Any +3 | | +4 | | import requests +5 | | import pandas + | |_____________^ + | +help: Organize imports +1 + # Future imports +2 | from __future__ import annotations +3 + +4 + # Standard library imports +5 | from typing import Any +6 | + - import requests +7 + # Third party imports +8 | import pandas +9 + import requests diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap new file mode 100644 index 00000000000000..f824376fbd7efb --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> import_heading.py:1:1 + | + 1 | / from __future__ import annotations + 2 | | + 3 | | import os + 4 | | import sys + 5 | | + 6 | | import requests + 7 | | import pandas + 8 | | + 9 | | from my_first_party import my_first_party_object +10 | | +11 | | from . import my_local_folder_object + | |____________________________________^ + | +help: Organize imports +1 + # Future imports +2 | from __future__ import annotations +3 | +4 + # Standard library imports +5 | import os +6 | import sys +7 | + - import requests +8 + # Third party imports +9 | import pandas + - +10 + import requests +11 | from my_first_party import my_first_party_object +12 | +13 + # Local folder imports +14 | from . import my_local_folder_object diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_partial_import_heading_partial.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_partial_import_heading_partial.py.snap new file mode 100644 index 00000000000000..a04652f972fea5 --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_partial_import_heading_partial.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> import_heading_partial.py:1:1 + | +1 | / import os +2 | | import sys +3 | | +4 | | import requests +5 | | import pandas + | |_____________^ + | +help: Organize imports +1 + # Standard library imports +2 | import os +3 | import sys +4 | +5 + # Third party imports +6 + import pandas +7 | import requests + - import pandas diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_single_section_import_heading_single_section.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_single_section_import_heading_single_section.py.snap new file mode 100644 index 00000000000000..6710dcdd4efe7b --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_single_section_import_heading_single_section.py.snap @@ -0,0 +1,15 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> import_heading_single_section.py:1:1 + | +1 | / import requests +2 | | import pandas + | |_____________^ + | +help: Organize imports + - import requests +1 + # Third party imports +2 | import pandas +3 + import requests diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap new file mode 100644 index 00000000000000..f8eb9d3e7f925b --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap @@ -0,0 +1,32 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> import_heading_unsorted.py:1:1 + | +1 | / import pandas +2 | | import os +3 | | from __future__ import annotations +4 | | import sys +5 | | import requests +6 | | from my_first_party import my_first_party_object +7 | | from . import my_local_folder_object + | |____________________________________^ + | +help: Organize imports + - import pandas +1 + # Future imports +2 + from __future__ import annotations +3 + +4 + # Standard library imports +5 | import os + - from __future__ import annotations +6 | import sys +7 + +8 + # Third party imports +9 + import pandas +10 | import requests +11 | from my_first_party import my_first_party_object +12 + +13 + # Local folder imports +14 | from . import my_local_folder_object diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap new file mode 100644 index 00000000000000..99e0d7ae028336 --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> import_heading_with_no_lines_before.py:1:1 + | + 1 | / from __future__ import annotations + 2 | | + 3 | | import os + 4 | | import sys + 5 | | + 6 | | import requests + 7 | | import pandas + 8 | | + 9 | | from my_first_party import my_first_party_object +10 | | +11 | | from . import my_local_folder_object + | |____________________________________^ + | +help: Organize imports +1 + # Future imports +2 | from __future__ import annotations +3 | +4 + # Standard library imports +5 | import os +6 | import sys +7 | + - import requests +8 + # Third party imports +9 | import pandas + - +10 + import requests +11 | from my_first_party import my_first_party_object +12 | +13 + # Local folder imports +14 | from . import my_local_folder_object diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap new file mode 100644 index 00000000000000..43732681219812 --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> import_heading_wrong_heading.py:2:1 + | +1 | # Wrong heading +2 | / import os +3 | | import sys +4 | | +5 | | # Also wrong heading +6 | | import requests +7 | | import pandas +8 | | +9 | | from my_first_party import my_first_party_object + | |________________________________________________^ + | +help: Organize imports +1 | # Wrong heading +2 + # Standard library imports +3 | import os +4 | import sys +5 | +6 + # Third party imports +7 + import pandas +8 + +9 | # Also wrong heading +10 | import requests + - import pandas + - +11 | from my_first_party import my_first_party_object From 9c642c3d12a8cc9b983859648b355f256f8c41ba Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Mon, 9 Feb 2026 01:33:04 +0000 Subject: [PATCH 02/26] feat(isort): implement import_heading option fixes #6371 --- crates/ruff_linter/src/rules/isort/mod.rs | 268 +++++++++++++++++- .../src/rules/isort/rules/organize_imports.rs | 32 ++- .../ruff_linter/src/rules/isort/settings.rs | 5 +- crates/ruff_workspace/src/options.rs | 33 +++ 4 files changed, 333 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index abbff742d43972..30c01013ed2cda 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -183,6 +183,13 @@ fn format_import_block( let mut output = String::new(); + // Collect all configured heading values (as "# {heading}") for stripping. + let heading_comments: Vec = settings + .import_headings + .values() + .map(|heading| format!("# {heading}")) + .collect(); + // Generate replacement source code. let mut is_first_block = true; let mut pending_lines_before = false; @@ -196,7 +203,28 @@ fn format_import_block( continue; }; - let imports = order_imports(import_block, import_section, settings); + let mut imports = order_imports(import_block, import_section, settings); + + // Strip any heading comments from all imports in the section, + // as they will be re-added in the correct position. + // Heading comments may be on any import (not just the first) since + // sorting may have reordered them. + if !heading_comments.is_empty() { + for import in &mut imports { + match import { + Import((_, comments)) => { + comments + .atop + .retain(|c| !heading_comments.iter().any(|h| c.as_ref() == h)); + } + ImportFrom((_, comments, _, _)) => { + comments + .atop + .retain(|c| !heading_comments.iter().any(|h| c.as_ref() == h)); + } + } + } + } // Add a blank line between every section. if is_first_block { @@ -207,6 +235,12 @@ fn format_import_block( pending_lines_before = false; } + // Insert heading comment for this section, if configured. + if let Some(heading) = settings.import_headings.get(import_section) { + output.push_str(&format!("# {heading}")); + output.push_str(&stylist.line_ending()); + } + let mut line_insertion = None; let mut is_first_statement = true; let lines_between_types = settings.lines_between_types; @@ -1200,6 +1234,238 @@ mod tests { Ok(()) } + /// Helper to create a standard import_headings map for all sections. + fn all_section_headings() -> FxHashMap { + FxHashMap::from_iter([ + ( + ImportSection::Known(ImportType::Future), + "Future imports".to_string(), + ), + ( + ImportSection::Known(ImportType::StandardLibrary), + "Standard library imports".to_string(), + ), + ( + ImportSection::Known(ImportType::ThirdParty), + "Third party imports".to_string(), + ), + ( + ImportSection::Known(ImportType::FirstParty), + "First party imports".to_string(), + ), + ( + ImportSection::Known(ImportType::LocalFolder), + "Local folder imports".to_string(), + ), + ]) + } + + #[test_case(Path::new("import_heading.py"))] + fn import_heading(path: &Path) -> Result<()> { + let snapshot = format!("import_heading_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &LinterSettings { + isort: super::settings::Settings { + import_headings: all_section_headings(), + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..LinterSettings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("import_heading_already_present.py"))] + fn import_heading_already_present(path: &Path) -> Result<()> { + let snapshot = format!("import_heading_already_present_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &LinterSettings { + isort: super::settings::Settings { + import_headings: all_section_headings(), + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..LinterSettings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("import_heading_unsorted.py"))] + fn import_heading_unsorted(path: &Path) -> Result<()> { + let snapshot = format!("import_heading_unsorted_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &LinterSettings { + isort: super::settings::Settings { + import_headings: all_section_headings(), + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..LinterSettings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("import_heading_with_no_lines_before.py"))] + fn import_heading_with_no_lines_before(path: &Path) -> Result<()> { + let snapshot = format!( + "import_heading_with_no_lines_before_{}", + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &LinterSettings { + isort: super::settings::Settings { + import_headings: all_section_headings(), + no_lines_before: FxHashSet::from_iter([ImportSection::Known( + ImportType::LocalFolder, + )]), + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..LinterSettings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("import_heading_partial.py"))] + fn import_heading_partial(path: &Path) -> Result<()> { + let snapshot = format!("import_heading_partial_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &LinterSettings { + isort: super::settings::Settings { + import_headings: FxHashMap::from_iter([ + ( + ImportSection::Known(ImportType::StandardLibrary), + "Standard library imports".to_string(), + ), + ( + ImportSection::Known(ImportType::ThirdParty), + "Third party imports".to_string(), + ), + ]), + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..LinterSettings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("import_heading_wrong_heading.py"))] + fn import_heading_wrong_heading(path: &Path) -> Result<()> { + let snapshot = format!("import_heading_wrong_heading_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &LinterSettings { + isort: super::settings::Settings { + import_headings: FxHashMap::from_iter([ + ( + ImportSection::Known(ImportType::StandardLibrary), + "Standard library imports".to_string(), + ), + ( + ImportSection::Known(ImportType::ThirdParty), + "Third party imports".to_string(), + ), + ( + ImportSection::Known(ImportType::FirstParty), + "First party imports".to_string(), + ), + ]), + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..LinterSettings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("import_heading_single_section.py"))] + fn import_heading_single_section(path: &Path) -> Result<()> { + let snapshot = format!("import_heading_single_section_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &LinterSettings { + isort: super::settings::Settings { + import_headings: FxHashMap::from_iter([( + ImportSection::Known(ImportType::ThirdParty), + "Third party imports".to_string(), + )]), + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..LinterSettings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + /// Test that already-correct imports with headings produce no diagnostic. + #[test_case(Path::new("import_heading_already_correct.py"))] + fn import_heading_already_correct(path: &Path) -> Result<()> { + let snapshot = format!("import_heading_already_correct_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &LinterSettings { + isort: super::settings::Settings { + import_headings: all_section_headings(), + known_modules: KnownModules::new( + vec![pattern("my_first_party")], + vec![], + vec![], + vec![], + FxHashMap::default(), + ), + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..LinterSettings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + + /// Test heading with force_sort_within_sections. + #[test_case(Path::new("import_heading_force_sort_within_sections.py"))] + fn import_heading_force_sort_within_sections(path: &Path) -> Result<()> { + let snapshot = format!( + "import_heading_force_sort_within_sections_{}", + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &LinterSettings { + isort: super::settings::Settings { + import_headings: all_section_headings(), + force_sort_within_sections: true, + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..LinterSettings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Path::new("lines_after_imports_nothing_after.py"))] #[test_case(Path::new("lines_after_imports.pyi"))] #[test_case(Path::new("lines_after_imports_func_after.py"))] diff --git a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs index 2071555c8b58fe..ea7456fbcffb42 100644 --- a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs @@ -8,7 +8,7 @@ use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_trivia::{PythonWhitespace, leading_indentation, textwrap::indent}; use ruff_source_file::{LineRanges, UniversalNewlines}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::Locator; use crate::checkers::ast::LintContext; @@ -110,8 +110,34 @@ pub(crate) fn organize_imports( } // Extract comments. Take care to grab any inline comments from the last line. + // Also extend the start backward to include any import heading comments above + // the first import, so they're collected and can be stripped/re-added correctly. + let import_headings = &settings.isort.import_headings; + let comment_range_start = if import_headings.is_empty() { + locator.line_start(range.start()) + } else { + let heading_comments: Vec = + import_headings.values().map(|h| format!("# {h}")).collect(); + + // start at the first import's line, walk backward while we see heading comments + let mut earliest = locator.line_start(range.start()); + while earliest > TextSize::from(0) { + let prev_line_start = locator.line_start(earliest - TextSize::from(1)); + let prev_line = locator + .slice(TextRange::new(prev_line_start, earliest)) + .trim(); + + if heading_comments.iter().any(|h| prev_line == h) { + earliest = prev_line_start; + } else { + break; + } + } + earliest + }; + let comments = comments::collect_comments( - TextRange::new(range.start(), locator.full_line_end(range.end())), + TextRange::new(comment_range_start, locator.full_line_end(range.end())), locator, indexer.comment_ranges(), ); @@ -139,7 +165,7 @@ pub(crate) fn organize_imports( ); // Expand the span the entire range, including leading and trailing space. - let fix_range = TextRange::new(locator.line_start(range.start()), trailing_line_end); + let fix_range = TextRange::new(comment_range_start, trailing_line_end); let actual = locator.slice(fix_range); if matches_ignoring_indentation(actual, &expected) { return; diff --git a/crates/ruff_linter/src/rules/isort/settings.rs b/crates/ruff_linter/src/rules/isort/settings.rs index cab9ab35ed166e..ced2dfdcb7b34c 100644 --- a/crates/ruff_linter/src/rules/isort/settings.rs +++ b/crates/ruff_linter/src/rules/isort/settings.rs @@ -5,7 +5,7 @@ use std::error::Error; use std::fmt; use std::fmt::{Display, Formatter}; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; @@ -60,6 +60,7 @@ pub struct Settings { pub constants: FxHashSet, pub variables: FxHashSet, pub no_lines_before: FxHashSet, + pub import_headings: FxHashMap, pub lines_after_imports: isize, pub lines_between_types: usize, pub forced_separate: Vec, @@ -114,6 +115,7 @@ impl Default for Settings { constants: FxHashSet::default(), variables: FxHashSet::default(), no_lines_before: FxHashSet::default(), + import_headings: FxHashMap::default(), lines_after_imports: -1, lines_between_types: 0, forced_separate: Vec::new(), @@ -150,6 +152,7 @@ impl Display for Settings { self.constants | set, self.variables | set, self.no_lines_before | set, + self.import_headings | map, self.lines_after_imports, self.lines_between_types, self.forced_separate | array, diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 472b0e66f4cfa4..0f7db2f2699a44 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -2482,6 +2482,27 @@ pub struct IsortOptions { )] pub no_lines_before: Option>, + /// A mapping from import section names to their heading comments. + /// + /// When set, a comment with the specified text will be added above imports + /// in the corresponding section. If a heading comment already exists, it + /// will be replaced. + /// + /// Compatible with isort's `import_heading_{section_name}` settings. + #[option( + default = r#"{}"#, + value_type = r#"dict["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str, str]"#, + example = r#" + [tool.ruff.lint.isort.import-heading] + future = "Future imports" + standard-library = "Standard library imports" + third-party = "Third party imports" + first-party = "First party imports" + local-folder = "Local folder imports" + "# + )] + pub import_heading: Option>, + /// The number of blank lines to place after imports. /// Use `-1` for automatic determination. /// @@ -2828,6 +2849,17 @@ impl IsortOptions { } } + let import_heading = self.import_heading.unwrap_or_default(); + + // Verify that all sections listed in `import_heading` are defined in `sections`. + for section in import_heading.keys() { + if let ImportSection::UserDefined(section_name) = section { + if !sections.contains_key(section_name) { + warn_user_once!("`import-heading` contains unknown section: `{:?}`", section,); + } + } + } + // Verify that `default_section` is in `section_order`. if !section_order.contains(&default_section) { warn_user_once!( @@ -2868,6 +2900,7 @@ impl IsortOptions { constants: FxHashSet::from_iter(self.constants.unwrap_or_default()), variables: FxHashSet::from_iter(self.variables.unwrap_or_default()), no_lines_before: FxHashSet::from_iter(no_lines_before), + import_headings: import_heading, lines_after_imports: self.lines_after_imports.unwrap_or(-1), lines_between_types, forced_separate: Vec::from_iter(self.forced_separate.unwrap_or_default()), From cb982e4fc78ee8b291a1b85e7c6f6db8d8879720 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Mon, 9 Feb 2026 19:44:30 +0000 Subject: [PATCH 03/26] fix: clippy warnings --- crates/ruff_linter/src/rules/isort/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 30c01013ed2cda..1ca22a17726635 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -1,5 +1,6 @@ //! Rules from [isort](https://pypi.org/project/isort/). +use std::fmt::Write; use std::path::PathBuf; use annotate::annotate_imports; @@ -237,7 +238,7 @@ fn format_import_block( // Insert heading comment for this section, if configured. if let Some(heading) = settings.import_headings.get(import_section) { - output.push_str(&format!("# {heading}")); + write!(output, "# {heading}").unwrap(); output.push_str(&stylist.line_ending()); } @@ -1234,7 +1235,7 @@ mod tests { Ok(()) } - /// Helper to create a standard import_headings map for all sections. + /// Helper to create a standard `import_headings` map for all sections. fn all_section_headings() -> FxHashMap { FxHashMap::from_iter([ ( @@ -1443,7 +1444,7 @@ mod tests { Ok(()) } - /// Test heading with force_sort_within_sections. + /// Test heading with `force_sort_within_sections`. #[test_case(Path::new("import_heading_force_sort_within_sections.py"))] fn import_heading_force_sort_within_sections(path: &Path) -> Result<()> { let snapshot = format!( From e9c5c31aae0a6643edd6ba0a719907a2b611d726 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Mon, 9 Feb 2026 20:12:08 +0000 Subject: [PATCH 04/26] fix: run cargo dev generate-all --- ruff.schema.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ruff.schema.json b/ruff.schema.json index 5af883777953bc..d809dd15f9add9 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1758,6 +1758,16 @@ "null" ] }, + "import-heading": { + "description": "A mapping from import section names to their heading comments.\n\nWhen set, a comment with the specified text will be added above imports\nin the corresponding section. If a heading comment already exists, it\nwill be replaced.\n\nCompatible with isort's `import_heading_{section_name}` settings.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, "known-first-party": { "description": "A list of modules to consider first-party, regardless of whether they\ncan be identified as such via introspection of the local filesystem.\n\nSupports glob patterns. For more information on the glob syntax, refer\nto the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ From 6ffebbf9900f60db6201f874110f4a62eee3eef9 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Mon, 9 Feb 2026 20:12:57 +0000 Subject: [PATCH 05/26] fix: update snapshots --- .../cli__lint__requires_python_extend_from_shared_config.snap | 1 + .../tests/cli/snapshots/cli__lint__requires_python_no_tool.snap | 1 + .../cli__lint__requires_python_no_tool_preview_enabled.snap | 1 + ...__lint__requires_python_no_tool_target_version_override.snap | 1 + .../cli__lint__requires_python_pyproject_toml_above.snap | 1 + ...i__lint__requires_python_pyproject_toml_above_with_tool.snap | 1 + .../snapshots/cli__lint__requires_python_ruff_toml_above-2.snap | 1 + .../snapshots/cli__lint__requires_python_ruff_toml_above.snap | 1 + ...cli__lint__requires_python_ruff_toml_no_target_fallback.snap | 1 + .../snapshots/show_settings__display_default_settings.snap | 1 + .../tests/e2e/snapshots/e2e__commands__debug_command.snap | 2 +- .../snapshots/e2e__signature_help__works_in_function_name.snap | 1 + 12 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index ce6ae89c1a1fb1..4ba408531f5328 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -208,6 +208,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index a1236947bd8cd3..6744979c522dff 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -210,6 +210,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap index 5868ceb04fa7d9..906e0f14e06eea 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap @@ -212,6 +212,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index 726abc733e294b..dcdf9366dfbe70 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -212,6 +212,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index db8a2890049c8a..a648dc5ae90a34 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -209,6 +209,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index 193aac85f3d46e..16fdb52d8763ad 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -209,6 +209,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index e7914052c391fc..f35b8f815d9748 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -208,6 +208,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index 76a57bae281c01..69ea5dc1428e62 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -208,6 +208,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index ecdc9bfc622c3e..3a61f249c06248 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -208,6 +208,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = [] diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index 94d6cbc6036dfe..b23fac18ff9a52 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -321,6 +321,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = [] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index 1d2ad41fe3d5fa..7fd410da4e6506 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -161,7 +161,7 @@ Memory report: =======SALSA STRUCTS======= `Program` metadata=[X.XXMB] fields=[X.XXMB] count=1 `Project` metadata=[X.XXMB] fields=[X.XXMB] count=1 -`FileRoot` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`FileRoot` metadata=[X.XXMB] fields=[X.XXMB] count=2 =======SALSA QUERIES======= =======SALSA SUMMARY======= TOTAL MEMORY USAGE: [X.XXMB] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__signature_help__works_in_function_name.snap b/crates/ty_server/tests/e2e/snapshots/e2e__signature_help__works_in_function_name.snap index 624472d0584a9b..e30af5f75cb91a 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__signature_help__works_in_function_name.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__signature_help__works_in_function_name.snap @@ -21,6 +21,7 @@ expression: signature_help }, { "label": "(pattern: bytes | Pattern[bytes], string: Buffer, flags: int = 0) -> Match[bytes] | None", + "documentation": "Try to apply the pattern at the start of the string, returning/na Match object, or None if no match was found.\n", "parameters": [ { "label": "pattern: bytes | Pattern[bytes]" From 9b0167cdaaab5937ccdeb604949165a005890ea8 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Mon, 9 Feb 2026 20:16:30 +0000 Subject: [PATCH 06/26] fix: address PR comment https://github.com/astral-sh/ruff/pull/23151#discussion_r2784268940 --- crates/ruff_linter/src/rules/isort/mod.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 1ca22a17726635..6fb6805b716cac 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -213,15 +213,12 @@ fn format_import_block( if !heading_comments.is_empty() { for import in &mut imports { match import { - Import((_, comments)) => { - comments - .atop - .retain(|c| !heading_comments.iter().any(|h| c.as_ref() == h)); - } - ImportFrom((_, comments, _, _)) => { - comments - .atop - .retain(|c| !heading_comments.iter().any(|h| c.as_ref() == h)); + Import((_, comments)) | ImportFrom((_, comments, _, _)) => { + comments.atop.retain(|comment| { + !heading_comments + .iter() + .any(|heading| comment.as_ref() == heading.as_str()) + }); } } } From fe12aa385f2a0f8199f7487576f78ead9017fe53 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Mon, 9 Feb 2026 20:35:02 +0000 Subject: [PATCH 07/26] fix: address PR comment https://github.com/astral-sh/ruff/pull/23151#discussion_r2784255545 --- crates/ruff_linter/src/rules/isort/mod.rs | 44 ++++++++++++++++++- ...ent_import_heading_already_present.py.snap | 18 ++++---- ...sts__import_heading_import_heading.py.snap | 11 ++--- ...g_unsorted_import_heading_unsorted.py.snap | 16 ++++--- ...mport_heading_with_no_lines_before.py.snap | 15 ++++--- ...ading_import_heading_wrong_heading.py.snap | 5 ++- 6 files changed, 78 insertions(+), 31 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 6fb6805b716cac..236cd4c079954e 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -213,7 +213,14 @@ fn format_import_block( if !heading_comments.is_empty() { for import in &mut imports { match import { - Import((_, comments)) | ImportFrom((_, comments, _, _)) => { + Import((_, comments)) => { + comments.atop.retain(|comment| { + !heading_comments + .iter() + .any(|heading| comment.as_ref() == heading.as_str()) + }); + } + ImportFrom((_, comments, _, _)) => { comments.atop.retain(|comment| { !heading_comments .iter() @@ -1266,6 +1273,13 @@ mod tests { &LinterSettings { isort: super::settings::Settings { import_headings: all_section_headings(), + known_modules: KnownModules::new( + vec![pattern("my_first_party")], + vec![], + vec![], + vec![], + FxHashMap::default(), + ), ..super::settings::Settings::default() }, src: vec![test_resource_path("fixtures/isort")], @@ -1284,6 +1298,13 @@ mod tests { &LinterSettings { isort: super::settings::Settings { import_headings: all_section_headings(), + known_modules: KnownModules::new( + vec![pattern("my_first_party")], + vec![], + vec![], + vec![], + FxHashMap::default(), + ), ..super::settings::Settings::default() }, src: vec![test_resource_path("fixtures/isort")], @@ -1302,6 +1323,13 @@ mod tests { &LinterSettings { isort: super::settings::Settings { import_headings: all_section_headings(), + known_modules: KnownModules::new( + vec![pattern("my_first_party")], + vec![], + vec![], + vec![], + FxHashMap::default(), + ), ..super::settings::Settings::default() }, src: vec![test_resource_path("fixtures/isort")], @@ -1323,6 +1351,13 @@ mod tests { &LinterSettings { isort: super::settings::Settings { import_headings: all_section_headings(), + known_modules: KnownModules::new( + vec![pattern("my_first_party")], + vec![], + vec![], + vec![], + FxHashMap::default(), + ), no_lines_before: FxHashSet::from_iter([ImportSection::Known( ImportType::LocalFolder, )]), @@ -1384,6 +1419,13 @@ mod tests { "First party imports".to_string(), ), ]), + known_modules: KnownModules::new( + vec![pattern("my_first_party")], + vec![], + vec![], + vec![], + FxHashMap::default(), + ), ..super::settings::Settings::default() }, src: vec![test_resource_path("fixtures/isort")], diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap index 409cbb7dfbc407..35f157372d6806 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap @@ -25,12 +25,12 @@ help: Organize imports 6 | import sys 7 | 8 | # Third party imports - - import requests -9 | import pandas - - - - # First party imports -10 + import requests -11 | from my_first_party import my_first_party_object -12 | -13 + # Local folder imports -14 | from . import my_local_folder_object +9 + import pandas +10 | import requests + - import pandas +11 | +12 | # First party imports +13 | from my_first_party import my_first_party_object +14 | +15 + # Local folder imports +16 | from . import my_local_folder_object diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap index f824376fbd7efb..5df6b80e58d746 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading.py.snap @@ -28,9 +28,10 @@ help: Organize imports - import requests 8 + # Third party imports 9 | import pandas - - 10 + import requests -11 | from my_first_party import my_first_party_object -12 | -13 + # Local folder imports -14 | from . import my_local_folder_object +11 | +12 + # First party imports +13 | from my_first_party import my_first_party_object +14 | +15 + # Local folder imports +16 | from . import my_local_folder_object diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap index f8eb9d3e7f925b..fd3b649b1a3b0f 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap @@ -15,18 +15,20 @@ I001 [*] Import block is un-sorted or un-formatted | help: Organize imports - import pandas + - import os 1 + # Future imports -2 + from __future__ import annotations +2 | from __future__ import annotations 3 + 4 + # Standard library imports -5 | import os - - from __future__ import annotations +5 + import os 6 | import sys 7 + 8 + # Third party imports 9 + import pandas 10 | import requests -11 | from my_first_party import my_first_party_object -12 + -13 + # Local folder imports -14 | from . import my_local_folder_object +11 + +12 + # First party imports +13 | from my_first_party import my_first_party_object +14 + +15 + # Local folder imports +16 | from . import my_local_folder_object diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap index 99e0d7ae028336..297319792a2b6d 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_with_no_lines_before_import_heading_with_no_lines_before.py.snap @@ -25,12 +25,13 @@ help: Organize imports 5 | import os 6 | import sys 7 | - - import requests 8 + # Third party imports -9 | import pandas +9 + import pandas +10 | import requests + - import pandas +11 | +12 + # First party imports +13 | from my_first_party import my_first_party_object - -10 + import requests -11 | from my_first_party import my_first_party_object -12 | -13 + # Local folder imports -14 | from . import my_local_folder_object +14 + # Local folder imports +15 | from . import my_local_folder_object diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap index 43732681219812..81ad723f728240 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap @@ -27,5 +27,6 @@ help: Organize imports 9 | # Also wrong heading 10 | import requests - import pandas - - -11 | from my_first_party import my_first_party_object +11 | +12 + # First party imports +13 | from my_first_party import my_first_party_object From e05212c3280bdab414c857974b4b2bf1f34bc981 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Mon, 9 Feb 2026 20:57:05 +0000 Subject: [PATCH 08/26] fix: update test to better match the name of the test and update snapshot --- .../isort/import_heading_wrong_heading.py | 2 +- ...eading_import_heading_wrong_heading.py.snap | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_wrong_heading.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_wrong_heading.py index a5ba3554c6d081..59c81407b2158e 100644 --- a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_wrong_heading.py +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_wrong_heading.py @@ -3,7 +3,7 @@ import sys # Also wrong heading -import requests import pandas +import requests from my_first_party import my_first_party_object diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap index 81ad723f728240..86dc06b3e61a8b 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_wrong_heading_import_heading_wrong_heading.py.snap @@ -9,8 +9,8 @@ I001 [*] Import block is un-sorted or un-formatted 3 | | import sys 4 | | 5 | | # Also wrong heading -6 | | import requests -7 | | import pandas +6 | | import pandas +7 | | import requests 8 | | 9 | | from my_first_party import my_first_party_object | |________________________________________________^ @@ -22,11 +22,9 @@ help: Organize imports 4 | import sys 5 | 6 + # Third party imports -7 + import pandas -8 + -9 | # Also wrong heading -10 | import requests - - import pandas -11 | -12 + # First party imports -13 | from my_first_party import my_first_party_object +7 | # Also wrong heading +8 | import pandas +9 | import requests +10 | +11 + # First party imports +12 | from my_first_party import my_first_party_object From e433060b89caf7e23596266a05701793ff0796bf Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Tue, 10 Feb 2026 10:04:39 +0000 Subject: [PATCH 09/26] fix: address PR review comments https://github.com/astral-sh/ruff/pull/23151#pullrequestreview-3776290773 --- crates/ruff_linter/src/rules/isort/mod.rs | 132 ++++++------------ .../src/rules/isort/rules/organize_imports.rs | 9 +- crates/ruff_text_size/src/size.rs | 2 + crates/ruff_workspace/src/options.rs | 5 +- 4 files changed, 56 insertions(+), 92 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 236cd4c079954e..2e67b1c86d4baa 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -184,11 +184,11 @@ fn format_import_block( let mut output = String::new(); - // Collect all configured heading values (as "# {heading}") for stripping. - let heading_comments: Vec = settings + // Heading comments are already formatted as "# {heading}" in settings. + let heading_comments: Vec<&str> = settings .import_headings .values() - .map(|heading| format!("# {heading}")) + .map(String::as_str) .collect(); // Generate replacement source code. @@ -212,22 +212,15 @@ fn format_import_block( // sorting may have reordered them. if !heading_comments.is_empty() { for import in &mut imports { - match import { - Import((_, comments)) => { - comments.atop.retain(|comment| { - !heading_comments - .iter() - .any(|heading| comment.as_ref() == heading.as_str()) - }); - } - ImportFrom((_, comments, _, _)) => { - comments.atop.retain(|comment| { - !heading_comments - .iter() - .any(|heading| comment.as_ref() == heading.as_str()) - }); - } - } + let atop = match import { + Import((_, comments)) => &mut comments.atop, + ImportFrom((_, comments, _, _)) => &mut comments.atop, + }; + atop.retain(|comment| { + !heading_comments + .iter() + .any(|&heading| comment.as_ref() == heading) + }); } } @@ -242,7 +235,7 @@ fn format_import_block( // Insert heading comment for this section, if configured. if let Some(heading) = settings.import_headings.get(import_section) { - write!(output, "# {heading}").unwrap(); + write!(output, "{heading}").unwrap(); output.push_str(&stylist.line_ending()); } @@ -1244,44 +1237,50 @@ mod tests { FxHashMap::from_iter([ ( ImportSection::Known(ImportType::Future), - "Future imports".to_string(), + "# Future imports".to_string(), ), ( ImportSection::Known(ImportType::StandardLibrary), - "Standard library imports".to_string(), + "# Standard library imports".to_string(), ), ( ImportSection::Known(ImportType::ThirdParty), - "Third party imports".to_string(), + "# Third party imports".to_string(), ), ( ImportSection::Known(ImportType::FirstParty), - "First party imports".to_string(), + "# First party imports".to_string(), ), ( ImportSection::Known(ImportType::LocalFolder), - "Local folder imports".to_string(), + "# Local folder imports".to_string(), ), ]) } + /// Helper to create a standard isort Settings with all section headings + /// and `known_modules` configured for `my_first_party`. + fn isort_settings_with_all_headings() -> super::settings::Settings { + super::settings::Settings { + import_headings: all_section_headings(), + known_modules: KnownModules::new( + vec![pattern("my_first_party")], + vec![], + vec![], + vec![], + FxHashMap::default(), + ), + ..super::settings::Settings::default() + } + } + #[test_case(Path::new("import_heading.py"))] fn import_heading(path: &Path) -> Result<()> { let snapshot = format!("import_heading_{}", path.to_string_lossy()); let diagnostics = test_path( Path::new("isort").join(path).as_path(), &LinterSettings { - isort: super::settings::Settings { - import_headings: all_section_headings(), - known_modules: KnownModules::new( - vec![pattern("my_first_party")], - vec![], - vec![], - vec![], - FxHashMap::default(), - ), - ..super::settings::Settings::default() - }, + isort: isort_settings_with_all_headings(), src: vec![test_resource_path("fixtures/isort")], ..LinterSettings::for_rule(Rule::UnsortedImports) }, @@ -1296,17 +1295,7 @@ mod tests { let diagnostics = test_path( Path::new("isort").join(path).as_path(), &LinterSettings { - isort: super::settings::Settings { - import_headings: all_section_headings(), - known_modules: KnownModules::new( - vec![pattern("my_first_party")], - vec![], - vec![], - vec![], - FxHashMap::default(), - ), - ..super::settings::Settings::default() - }, + isort: isort_settings_with_all_headings(), src: vec![test_resource_path("fixtures/isort")], ..LinterSettings::for_rule(Rule::UnsortedImports) }, @@ -1321,17 +1310,7 @@ mod tests { let diagnostics = test_path( Path::new("isort").join(path).as_path(), &LinterSettings { - isort: super::settings::Settings { - import_headings: all_section_headings(), - known_modules: KnownModules::new( - vec![pattern("my_first_party")], - vec![], - vec![], - vec![], - FxHashMap::default(), - ), - ..super::settings::Settings::default() - }, + isort: isort_settings_with_all_headings(), src: vec![test_resource_path("fixtures/isort")], ..LinterSettings::for_rule(Rule::UnsortedImports) }, @@ -1350,18 +1329,10 @@ mod tests { Path::new("isort").join(path).as_path(), &LinterSettings { isort: super::settings::Settings { - import_headings: all_section_headings(), - known_modules: KnownModules::new( - vec![pattern("my_first_party")], - vec![], - vec![], - vec![], - FxHashMap::default(), - ), no_lines_before: FxHashSet::from_iter([ImportSection::Known( ImportType::LocalFolder, )]), - ..super::settings::Settings::default() + ..isort_settings_with_all_headings() }, src: vec![test_resource_path("fixtures/isort")], ..LinterSettings::for_rule(Rule::UnsortedImports) @@ -1381,11 +1352,11 @@ mod tests { import_headings: FxHashMap::from_iter([ ( ImportSection::Known(ImportType::StandardLibrary), - "Standard library imports".to_string(), + "# Standard library imports".to_string(), ), ( ImportSection::Known(ImportType::ThirdParty), - "Third party imports".to_string(), + "# Third party imports".to_string(), ), ]), ..super::settings::Settings::default() @@ -1408,15 +1379,15 @@ mod tests { import_headings: FxHashMap::from_iter([ ( ImportSection::Known(ImportType::StandardLibrary), - "Standard library imports".to_string(), + "# Standard library imports".to_string(), ), ( ImportSection::Known(ImportType::ThirdParty), - "Third party imports".to_string(), + "# Third party imports".to_string(), ), ( ImportSection::Known(ImportType::FirstParty), - "First party imports".to_string(), + "# First party imports".to_string(), ), ]), known_modules: KnownModules::new( @@ -1445,7 +1416,7 @@ mod tests { isort: super::settings::Settings { import_headings: FxHashMap::from_iter([( ImportSection::Known(ImportType::ThirdParty), - "Third party imports".to_string(), + "# Third party imports".to_string(), )]), ..super::settings::Settings::default() }, @@ -1464,17 +1435,7 @@ mod tests { let diagnostics = test_path( Path::new("isort").join(path).as_path(), &LinterSettings { - isort: super::settings::Settings { - import_headings: all_section_headings(), - known_modules: KnownModules::new( - vec![pattern("my_first_party")], - vec![], - vec![], - vec![], - FxHashMap::default(), - ), - ..super::settings::Settings::default() - }, + isort: isort_settings_with_all_headings(), src: vec![test_resource_path("fixtures/isort")], ..LinterSettings::for_rule(Rule::UnsortedImports) }, @@ -1494,9 +1455,8 @@ mod tests { Path::new("isort").join(path).as_path(), &LinterSettings { isort: super::settings::Settings { - import_headings: all_section_headings(), force_sort_within_sections: true, - ..super::settings::Settings::default() + ..isort_settings_with_all_headings() }, src: vec![test_resource_path("fixtures/isort")], ..LinterSettings::for_rule(Rule::UnsortedImports) diff --git a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs index ea7456fbcffb42..590a4a839fa41b 100644 --- a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs @@ -116,18 +116,17 @@ pub(crate) fn organize_imports( let comment_range_start = if import_headings.is_empty() { locator.line_start(range.start()) } else { - let heading_comments: Vec = - import_headings.values().map(|h| format!("# {h}")).collect(); + // Heading comments are already formatted as "# {heading}" in settings // start at the first import's line, walk backward while we see heading comments let mut earliest = locator.line_start(range.start()); - while earliest > TextSize::from(0) { - let prev_line_start = locator.line_start(earliest - TextSize::from(1)); + while earliest > TextSize::ZERO { + let prev_line_start = locator.line_start(earliest - TextSize::ONE); let prev_line = locator .slice(TextRange::new(prev_line_start, earliest)) .trim(); - if heading_comments.iter().any(|h| prev_line == h) { + if import_headings.values().any(|h| prev_line == h) { earliest = prev_line_start; } else { break; diff --git a/crates/ruff_text_size/src/size.rs b/crates/ruff_text_size/src/size.rs index aa4a8571e31f3e..866c057047cb13 100644 --- a/crates/ruff_text_size/src/size.rs +++ b/crates/ruff_text_size/src/size.rs @@ -35,6 +35,8 @@ impl fmt::Debug for TextSize { impl TextSize { /// A `TextSize` of zero. pub const ZERO: TextSize = TextSize::new(0); + /// A `TextSize` of one. + pub const ONE: TextSize = TextSize::new(1); /// Creates a new `TextSize` at the given `offset`. /// diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 0f7db2f2699a44..0890ebe3d5b8c1 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -2900,7 +2900,10 @@ impl IsortOptions { constants: FxHashSet::from_iter(self.constants.unwrap_or_default()), variables: FxHashSet::from_iter(self.variables.unwrap_or_default()), no_lines_before: FxHashSet::from_iter(no_lines_before), - import_headings: import_heading, + import_headings: import_heading + .into_iter() + .map(|(section, heading)| (section, format!("# {heading}"))) + .collect(), lines_after_imports: self.lines_after_imports.unwrap_or(-1), lines_between_types, forced_separate: Vec::from_iter(self.forced_separate.unwrap_or_default()), From de184816c868ce23cbc805d0d5ef43a12e2d0af8 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Tue, 10 Feb 2026 13:38:32 +0000 Subject: [PATCH 10/26] fix: update (rever) snapshots after running tests outside of conda in a fresh venv see https://github.com/astral-sh/ruff/pull/23151#issuecomment-3877657682 --- .../tests/e2e/snapshots/e2e__commands__debug_command.snap | 2 +- .../snapshots/e2e__signature_help__works_in_function_name.snap | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index 7fd410da4e6506..1d2ad41fe3d5fa 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -161,7 +161,7 @@ Memory report: =======SALSA STRUCTS======= `Program` metadata=[X.XXMB] fields=[X.XXMB] count=1 `Project` metadata=[X.XXMB] fields=[X.XXMB] count=1 -`FileRoot` metadata=[X.XXMB] fields=[X.XXMB] count=2 +`FileRoot` metadata=[X.XXMB] fields=[X.XXMB] count=1 =======SALSA QUERIES======= =======SALSA SUMMARY======= TOTAL MEMORY USAGE: [X.XXMB] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__signature_help__works_in_function_name.snap b/crates/ty_server/tests/e2e/snapshots/e2e__signature_help__works_in_function_name.snap index e30af5f75cb91a..624472d0584a9b 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__signature_help__works_in_function_name.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__signature_help__works_in_function_name.snap @@ -21,7 +21,6 @@ expression: signature_help }, { "label": "(pattern: bytes | Pattern[bytes], string: Buffer, flags: int = 0) -> Match[bytes] | None", - "documentation": "Try to apply the pattern at the start of the string, returning/na Match object, or None if no match was found.\n", "parameters": [ { "label": "pattern: bytes | Pattern[bytes]" From b5140c9e674529b54cff0c2b88fa7260da04fd22 Mon Sep 17 00:00:00 2001 From: Alexander Ley <94057608+Alex-ley-scrub@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:00:54 +0000 Subject: [PATCH 11/26] fix: adopt PR suggestion https://github.com/astral-sh/ruff/pull/23151#discussion_r2789720130 Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- crates/ruff_linter/src/rules/isort/mod.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 2e67b1c86d4baa..9f1c90e29d57db 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -185,11 +185,7 @@ fn format_import_block( let mut output = String::new(); // Heading comments are already formatted as "# {heading}" in settings. - let heading_comments: Vec<&str> = settings - .import_headings - .values() - .map(String::as_str) - .collect(); + let heading_comments = settings.import_headings; // Generate replacement source code. let mut is_first_block = true; From 1e89052650643f6f6649cdd6a99d3ef8101d8bb1 Mon Sep 17 00:00:00 2001 From: Alexander Ley <94057608+Alex-ley-scrub@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:02:33 +0000 Subject: [PATCH 12/26] fix: adopt PR suggestion https://github.com/astral-sh/ruff/pull/23151#discussion_r2789917038 Co-authored-by: Amethyst Reese --- crates/ruff_linter/src/rules/isort/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 9f1c90e29d57db..b4552506c2ca1a 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -206,7 +206,7 @@ fn format_import_block( // as they will be re-added in the correct position. // Heading comments may be on any import (not just the first) since // sorting may have reordered them. - if !heading_comments.is_empty() { + if !settings.import_headings.is_empty() { for import in &mut imports { let atop = match import { Import((_, comments)) => &mut comments.atop, From e18a7529e85b91d820d159c0af38375e509438a0 Mon Sep 17 00:00:00 2001 From: Alexander Ley <94057608+Alex-ley-scrub@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:03:49 +0000 Subject: [PATCH 13/26] fix: adopt PR suggestion https://github.com/astral-sh/ruff/pull/23151#discussion_r2789832508 Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- crates/ruff_workspace/src/options.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 0890ebe3d5b8c1..7d69f4d299b5d2 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -2493,7 +2493,6 @@ pub struct IsortOptions { default = r#"{}"#, value_type = r#"dict["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str, str]"#, example = r#" - [tool.ruff.lint.isort.import-heading] future = "Future imports" standard-library = "Standard library imports" third-party = "Third party imports" From 83400cc65c9cfd0889c4f450fc27cf0dcfb6d392 Mon Sep 17 00:00:00 2001 From: Alexander Ley <94057608+Alex-ley-scrub@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:10:03 +0000 Subject: [PATCH 14/26] fix: adopt PR suggestion https://github.com/astral-sh/ruff/pull/23151#discussion_r2789779630 Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- crates/ruff_linter/src/rules/isort/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index b4552506c2ca1a..def5537c5e2cd5 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -231,7 +231,7 @@ fn format_import_block( // Insert heading comment for this section, if configured. if let Some(heading) = settings.import_headings.get(import_section) { - write!(output, "{heading}").unwrap(); + output.push_str(&heading); output.push_str(&stylist.line_ending()); } From ca0f60854a579078df12ae562c5c956c04c3de6e Mon Sep 17 00:00:00 2001 From: Alexander Ley <94057608+Alex-ley-scrub@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:17:57 +0000 Subject: [PATCH 15/26] fix: adopt PR suggestion https://github.com/astral-sh/ruff/pull/23151#discussion_r2789811803 Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- crates/ruff_linter/src/rules/isort/mod.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index def5537c5e2cd5..7f5f9a1d3a101b 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -1271,6 +1271,7 @@ mod tests { } #[test_case(Path::new("import_heading.py"))] + #[test_case(Path::new("import_heading_already_present.py"))] fn import_heading(path: &Path) -> Result<()> { let snapshot = format!("import_heading_{}", path.to_string_lossy()); let diagnostics = test_path( @@ -1285,21 +1286,6 @@ mod tests { Ok(()) } - #[test_case(Path::new("import_heading_already_present.py"))] - fn import_heading_already_present(path: &Path) -> Result<()> { - let snapshot = format!("import_heading_already_present_{}", path.to_string_lossy()); - let diagnostics = test_path( - Path::new("isort").join(path).as_path(), - &LinterSettings { - isort: isort_settings_with_all_headings(), - src: vec![test_resource_path("fixtures/isort")], - ..LinterSettings::for_rule(Rule::UnsortedImports) - }, - )?; - assert_diagnostics!(snapshot, diagnostics); - Ok(()) - } - #[test_case(Path::new("import_heading_unsorted.py"))] fn import_heading_unsorted(path: &Path) -> Result<()> { let snapshot = format!("import_heading_unsorted_{}", path.to_string_lossy()); From 9f79fb7dc8d33bb2386fda34d9c9373f7b8d772b Mon Sep 17 00:00:00 2001 From: Alexander Ley <94057608+Alex-ley-scrub@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:19:35 +0000 Subject: [PATCH 16/26] fix: adopt PR suggestion https://github.com/astral-sh/ruff/pull/23151#discussion_r2789913240 (might need to add a & ref to make this compile, will check locally) Co-authored-by: Amethyst Reese --- crates/ruff_linter/src/rules/isort/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 7f5f9a1d3a101b..e7228bc6e61f17 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -215,7 +215,9 @@ fn format_import_block( atop.retain(|comment| { !heading_comments .iter() - .any(|&heading| comment.as_ref() == heading) + !settings.import_headings + .values() + .any(|heading| comment == heading) }); } } From ab3d7459cfe64ee432d900ea305a841490b1cb70 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Wed, 11 Feb 2026 00:26:19 +0000 Subject: [PATCH 17/26] fix: clippy/compiler warnings/errors --- crates/ruff_linter/src/rules/isort/mod.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index e7228bc6e61f17..7fa1ecefdc9980 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -1,6 +1,5 @@ //! Rules from [isort](https://pypi.org/project/isort/). -use std::fmt::Write; use std::path::PathBuf; use annotate::annotate_imports; @@ -184,9 +183,6 @@ fn format_import_block( let mut output = String::new(); - // Heading comments are already formatted as "# {heading}" in settings. - let heading_comments = settings.import_headings; - // Generate replacement source code. let mut is_first_block = true; let mut pending_lines_before = false; @@ -213,9 +209,8 @@ fn format_import_block( ImportFrom((_, comments, _, _)) => &mut comments.atop, }; atop.retain(|comment| { - !heading_comments - .iter() - !settings.import_headings + !settings + .import_headings .values() .any(|heading| comment == heading) }); @@ -233,7 +228,7 @@ fn format_import_block( // Insert heading comment for this section, if configured. if let Some(heading) = settings.import_headings.get(import_section) { - output.push_str(&heading); + output.push_str(heading); output.push_str(&stylist.line_ending()); } From c35e5364b2e971ad069f8a7d3cf670fbeb3924a0 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Wed, 11 Feb 2026 01:21:06 +0000 Subject: [PATCH 18/26] fix: rename snapshots --- ...tests__import_heading_import_heading_already_correct.py.snap} | 0 ...tests__import_heading_import_heading_already_present.py.snap} | 1 + ...isort__tests__import_heading_import_heading_unsorted.py.snap} | 0 3 files changed, 1 insertion(+) rename crates/ruff_linter/src/rules/isort/snapshots/{ruff_linter__rules__isort__tests__import_heading_already_correct_import_heading_already_correct.py.snap => ruff_linter__rules__isort__tests__import_heading_import_heading_already_correct.py.snap} (100%) rename crates/ruff_linter/src/rules/isort/snapshots/{ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap => ruff_linter__rules__isort__tests__import_heading_import_heading_already_present.py.snap} (97%) rename crates/ruff_linter/src/rules/isort/snapshots/{ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap => ruff_linter__rules__isort__tests__import_heading_import_heading_unsorted.py.snap} (100%) diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_correct_import_heading_already_correct.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_already_correct.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_correct_import_heading_already_correct.py.snap rename to crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_already_correct.py.snap diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_already_present.py.snap similarity index 97% rename from crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap rename to crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_already_present.py.snap index 35f157372d6806..19d135acb65659 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_already_present_import_heading_already_present.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_already_present.py.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_linter/src/rules/isort/mod.rs +assertion_line: 1284 --- I001 [*] Import block is un-sorted or un-formatted --> import_heading_already_present.py:2:1 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_unsorted.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_unsorted_import_heading_unsorted.py.snap rename to crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_unsorted.py.snap From 9e52b333729f0ef5ef0ec30352eec124acc1d7a4 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Wed, 11 Feb 2026 01:24:54 +0000 Subject: [PATCH 19/26] fix: address some more PR feedback --- crates/ruff_linter/src/rules/isort/mod.rs | 33 +------------- .../src/rules/isort/rules/organize_imports.rs | 43 +++++++++++-------- crates/ruff_text_size/src/size.rs | 2 - 3 files changed, 27 insertions(+), 51 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 7fa1ecefdc9980..b809ed2173eefa 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -1269,6 +1269,8 @@ mod tests { #[test_case(Path::new("import_heading.py"))] #[test_case(Path::new("import_heading_already_present.py"))] + #[test_case(Path::new("import_heading_unsorted.py"))] + #[test_case(Path::new("import_heading_already_correct.py"))] fn import_heading(path: &Path) -> Result<()> { let snapshot = format!("import_heading_{}", path.to_string_lossy()); let diagnostics = test_path( @@ -1283,21 +1285,6 @@ mod tests { Ok(()) } - #[test_case(Path::new("import_heading_unsorted.py"))] - fn import_heading_unsorted(path: &Path) -> Result<()> { - let snapshot = format!("import_heading_unsorted_{}", path.to_string_lossy()); - let diagnostics = test_path( - Path::new("isort").join(path).as_path(), - &LinterSettings { - isort: isort_settings_with_all_headings(), - src: vec![test_resource_path("fixtures/isort")], - ..LinterSettings::for_rule(Rule::UnsortedImports) - }, - )?; - assert_diagnostics!(snapshot, diagnostics); - Ok(()) - } - #[test_case(Path::new("import_heading_with_no_lines_before.py"))] fn import_heading_with_no_lines_before(path: &Path) -> Result<()> { let snapshot = format!( @@ -1407,22 +1394,6 @@ mod tests { Ok(()) } - /// Test that already-correct imports with headings produce no diagnostic. - #[test_case(Path::new("import_heading_already_correct.py"))] - fn import_heading_already_correct(path: &Path) -> Result<()> { - let snapshot = format!("import_heading_already_correct_{}", path.to_string_lossy()); - let diagnostics = test_path( - Path::new("isort").join(path).as_path(), - &LinterSettings { - isort: isort_settings_with_all_headings(), - src: vec![test_resource_path("fixtures/isort")], - ..LinterSettings::for_rule(Rule::UnsortedImports) - }, - )?; - assert_diagnostics!(snapshot, diagnostics); - Ok(()) - } - /// Test heading with `force_sort_within_sections`. #[test_case(Path::new("import_heading_force_sort_within_sections.py"))] fn import_heading_force_sort_within_sections(path: &Path) -> Result<()> { diff --git a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs index 590a4a839fa41b..f5dedf7d00ecaa 100644 --- a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs @@ -8,7 +8,7 @@ use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_trivia::{PythonWhitespace, leading_indentation, textwrap::indent}; use ruff_source_file::{LineRanges, UniversalNewlines}; -use ruff_text_size::{Ranged, TextRange, TextSize}; +use ruff_text_size::{Ranged, TextRange}; use crate::Locator; use crate::checkers::ast::LintContext; @@ -113,30 +113,37 @@ pub(crate) fn organize_imports( // Also extend the start backward to include any import heading comments above // the first import, so they're collected and can be stripped/re-added correctly. let import_headings = &settings.isort.import_headings; - let comment_range_start = if import_headings.is_empty() { - locator.line_start(range.start()) + let (comment_start, fix_start) = if import_headings.is_empty() { + // Preserve original behavior: comments from import start, + // fix range from line start. + (range.start(), locator.line_start(range.start())) } else { - // Heading comments are already formatted as "# {heading}" in settings - - // start at the first import's line, walk backward while we see heading comments - let mut earliest = locator.line_start(range.start()); - while earliest > TextSize::ZERO { - let prev_line_start = locator.line_start(earliest - TextSize::ONE); - let prev_line = locator - .slice(TextRange::new(prev_line_start, earliest)) - .trim(); - - if import_headings.values().any(|h| prev_line == h) { - earliest = prev_line_start; + // Heading comments are already formatted as "# {heading}" in settings. + // Walk backward through comment ranges to find adjacent heading comments + // above the first import. + let comment_ranges: &[TextRange] = indexer.comment_ranges(); + let import_line_start = locator.line_start(range.start()); + let partition = comment_ranges.partition_point(|c| c.start() < import_line_start); + + let mut earliest = import_line_start; + for comment_range in comment_ranges[..partition].iter().rev() { + // The comment's line must end right where 'earliest' starts (adjacent). + if locator.full_line_end(comment_range.end()) != earliest { + break; + } + + let comment_text = locator.slice(*comment_range); + if import_headings.values().any(|h| comment_text == h.as_str()) { + earliest = locator.line_start(comment_range.start()); } else { break; } } - earliest + (earliest, earliest) }; let comments = comments::collect_comments( - TextRange::new(comment_range_start, locator.full_line_end(range.end())), + TextRange::new(comment_start, locator.full_line_end(range.end())), locator, indexer.comment_ranges(), ); @@ -164,7 +171,7 @@ pub(crate) fn organize_imports( ); // Expand the span the entire range, including leading and trailing space. - let fix_range = TextRange::new(comment_range_start, trailing_line_end); + let fix_range = TextRange::new(fix_start, trailing_line_end); let actual = locator.slice(fix_range); if matches_ignoring_indentation(actual, &expected) { return; diff --git a/crates/ruff_text_size/src/size.rs b/crates/ruff_text_size/src/size.rs index 866c057047cb13..aa4a8571e31f3e 100644 --- a/crates/ruff_text_size/src/size.rs +++ b/crates/ruff_text_size/src/size.rs @@ -35,8 +35,6 @@ impl fmt::Debug for TextSize { impl TextSize { /// A `TextSize` of zero. pub const ZERO: TextSize = TextSize::new(0); - /// A `TextSize` of one. - pub const ONE: TextSize = TextSize::new(1); /// Creates a new `TextSize` at the given `offset`. /// From 1fb2633b8e9f464666a9e0e32d069a120af1b95b Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Wed, 11 Feb 2026 01:27:17 +0000 Subject: [PATCH 20/26] fix: address some more PR feedback, and use better variable names again --- .../src/rules/isort/rules/organize_imports.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs index f5dedf7d00ecaa..9543c494ef0395 100644 --- a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs @@ -119,11 +119,11 @@ pub(crate) fn organize_imports( (range.start(), locator.line_start(range.start())) } else { // Heading comments are already formatted as "# {heading}" in settings. - // Walk backward through comment ranges to find adjacent heading comments - // above the first import. + // Walk backward through comment ranges to find adjacent heading comments above the first import. let comment_ranges: &[TextRange] = indexer.comment_ranges(); let import_line_start = locator.line_start(range.start()); - let partition = comment_ranges.partition_point(|c| c.start() < import_line_start); + let partition = + comment_ranges.partition_point(|comment| comment.start() < import_line_start); let mut earliest = import_line_start; for comment_range in comment_ranges[..partition].iter().rev() { @@ -133,7 +133,10 @@ pub(crate) fn organize_imports( } let comment_text = locator.slice(*comment_range); - if import_headings.values().any(|h| comment_text == h.as_str()) { + if import_headings + .values() + .any(|header| comment_text == header.as_str()) + { earliest = locator.line_start(comment_range.start()); } else { break; From 90e2229e64114814412a0fa809f1bf559d6163ed Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Wed, 11 Feb 2026 01:34:27 +0000 Subject: [PATCH 21/26] fix: minor comment formatting --- crates/ruff_linter/src/rules/isort/rules/organize_imports.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs index 9543c494ef0395..43c0ed9b9014a3 100644 --- a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs @@ -114,8 +114,7 @@ pub(crate) fn organize_imports( // the first import, so they're collected and can be stripped/re-added correctly. let import_headings = &settings.isort.import_headings; let (comment_start, fix_start) = if import_headings.is_empty() { - // Preserve original behavior: comments from import start, - // fix range from line start. + // Preserve original behavior: comments from import start, fix range from line start. (range.start(), locator.line_start(range.start())) } else { // Heading comments are already formatted as "# {heading}" in settings. From 5d4a50420f01d95368b8c02a8ca2481a748b6f86 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Wed, 11 Feb 2026 07:50:52 +0000 Subject: [PATCH 22/26] fix: manually update ruff.schema.json because was missing the properties field when running locally with `cargo dev generate-all` --- ruff.schema.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ruff.schema.json b/ruff.schema.json index d809dd15f9add9..9622a5c991b277 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1764,6 +1764,23 @@ "object", "null" ], + "properties": { + "first-party": { + "type": "string" + }, + "future": { + "type": "string" + }, + "local-folder": { + "type": "string" + }, + "standard-library": { + "type": "string" + }, + "third-party": { + "type": "string" + } + }, "additionalProperties": { "type": "string" } From e3794e7f69d9b19e0cfdffece1c4997b9a6541b3 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Wed, 11 Feb 2026 12:25:57 +0000 Subject: [PATCH 23/26] fix: add missing `schemars(extend("properties" ...))` to ensure properties are not missing when we run `cargo dev generate-all` --- crates/ruff_workspace/src/options.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 7d69f4d299b5d2..461af16df742ae 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -2500,6 +2500,13 @@ pub struct IsortOptions { local-folder = "Local folder imports" "# )] + #[cfg_attr(feature = "schemars", schemars(extend("properties" = { + "future": {"type": "string"}, + "standard-library": {"type": "string"}, + "third-party": {"type": "string"}, + "first-party": {"type": "string"}, + "local-folder": {"type": "string"} + })))] pub import_heading: Option>, /// The number of blank lines to place after imports. From 3c1fbf0acd7aaf1b9f90c2f9c6e3f4fe43884e26 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Wed, 11 Feb 2026 16:44:53 +0000 Subject: [PATCH 24/26] fix: add test case to clarify isort vs ruff.isort behaviour with duplicate header comments see: https://github.com/astral-sh/ruff/pull/23151#discussion_r2789775302 --- .../isort/import_heading_duplicate.py | 7 ++++++ crates/ruff_linter/src/rules/isort/mod.rs | 1 + ...t_heading_import_heading_duplicate.py.snap | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/isort/import_heading_duplicate.py create mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_duplicate.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/isort/import_heading_duplicate.py b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_duplicate.py new file mode 100644 index 00000000000000..c870e8d3c17d22 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/isort/import_heading_duplicate.py @@ -0,0 +1,7 @@ +# Standard library imports +# Standard library imports +import os +import sys + +import requests +import pandas diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index b809ed2173eefa..76cb8314004f6d 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -1271,6 +1271,7 @@ mod tests { #[test_case(Path::new("import_heading_already_present.py"))] #[test_case(Path::new("import_heading_unsorted.py"))] #[test_case(Path::new("import_heading_already_correct.py"))] + #[test_case(Path::new("import_heading_duplicate.py"))] fn import_heading(path: &Path) -> Result<()> { let snapshot = format!("import_heading_{}", path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_duplicate.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_duplicate.py.snap new file mode 100644 index 00000000000000..50247617162c71 --- /dev/null +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__import_heading_import_heading_duplicate.py.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_linter/src/rules/isort/mod.rs +--- +I001 [*] Import block is un-sorted or un-formatted + --> import_heading_duplicate.py:3:1 + | +1 | # Standard library imports +2 | # Standard library imports +3 | / import os +4 | | import sys +5 | | +6 | | import requests +7 | | import pandas + | |_____________^ + | +help: Organize imports +1 | # Standard library imports + - # Standard library imports +2 | import os +3 | import sys +4 | +5 + # Third party imports +6 + import pandas +7 | import requests + - import pandas From a3037c2ef9843da3f9e860c27fb125bd430a9e5b Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Wed, 11 Feb 2026 20:05:07 +0000 Subject: [PATCH 25/26] Revert "fix: add missing `schemars(extend("properties" ...))` to ensure properties are not missing when we run `cargo dev generate-all`" This reverts commit e3794e7f69d9b19e0cfdffece1c4997b9a6541b3. --- crates/ruff_workspace/src/options.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index c3f8abbda8438a..07011ad66026c4 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -2515,13 +2515,6 @@ pub struct IsortOptions { local-folder = "Local folder imports" "# )] - #[cfg_attr(feature = "schemars", schemars(extend("properties" = { - "future": {"type": "string"}, - "standard-library": {"type": "string"}, - "third-party": {"type": "string"}, - "first-party": {"type": "string"}, - "local-folder": {"type": "string"} - })))] pub import_heading: Option>, /// The number of blank lines to place after imports. From 41dfd96556d2c42ea79020e7e4a2d4a25462f0e8 Mon Sep 17 00:00:00 2001 From: Alex Ley Date: Wed, 11 Feb 2026 20:50:41 +0000 Subject: [PATCH 26/26] fix: update settings snapshot after updating branch with upstream --- .../show_settings__display_settings_from_nested_directory.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ruff/tests/snapshots/show_settings__display_settings_from_nested_directory.snap b/crates/ruff/tests/snapshots/show_settings__display_settings_from_nested_directory.snap index 1141adb331863d..2c6025ccaea525 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_settings_from_nested_directory.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_settings_from_nested_directory.snap @@ -329,6 +329,7 @@ linter.isort.classes = [] linter.isort.constants = [] linter.isort.variables = [] linter.isort.no_lines_before = [] +linter.isort.import_headings = {} linter.isort.lines_after_imports = -1 linter.isort.lines_between_types = 0 linter.isort.forced_separate = []