diff --git a/crates/ruff/resources/test/fixtures/isort/case_sensitive.py b/crates/ruff/resources/test/fixtures/isort/case_sensitive.py new file mode 100644 index 0000000000..6f500358ee --- /dev/null +++ b/crates/ruff/resources/test/fixtures/isort/case_sensitive.py @@ -0,0 +1,9 @@ +import A +import B +import b +import C +import d +import E +import f +from g import a, B, c +from h import A, b, C diff --git a/crates/ruff/src/rules/isort/mod.rs b/crates/ruff/src/rules/isort/mod.rs index 251b69f6e4..f156451770 100644 --- a/crates/ruff/src/rules/isort/mod.rs +++ b/crates/ruff/src/rules/isort/mod.rs @@ -74,6 +74,7 @@ pub(crate) fn format_imports( combine_as_imports: bool, force_single_line: bool, force_sort_within_sections: bool, + case_sensitive: bool, force_wrap_aliases: bool, force_to_top: &BTreeSet, known_modules: &KnownModules, @@ -114,6 +115,7 @@ pub(crate) fn format_imports( src, package, force_sort_within_sections, + case_sensitive, force_wrap_aliases, force_to_top, known_modules, @@ -171,6 +173,7 @@ fn format_import_block( src: &[PathBuf], package: Option<&Path>, force_sort_within_sections: bool, + case_sensitive: bool, force_wrap_aliases: bool, force_to_top: &BTreeSet, known_modules: &KnownModules, @@ -206,6 +209,7 @@ fn format_import_block( let imports = order_imports( import_block, order_by_type, + case_sensitive, relative_imports_order, classes, constants, @@ -222,7 +226,13 @@ fn format_import_block( .collect::>(); if force_sort_within_sections { imports.sort_by(|import1, import2| { - cmp_either_import(import1, import2, relative_imports_order, force_to_top) + cmp_either_import( + import1, + import2, + relative_imports_order, + force_to_top, + case_sensitive, + ) }); }; imports @@ -449,6 +459,24 @@ mod tests { Ok(()) } + #[test_case(Path::new("case_sensitive.py"))] + fn case_sensitive(path: &Path) -> Result<()> { + let snapshot = format!("case_sensitive_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &Settings { + isort: super::settings::Settings { + case_sensitive: true, + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..Settings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Path::new("force_to_top.py"))] fn force_to_top(path: &Path) -> Result<()> { let snapshot = format!("force_to_top_{}", path.to_string_lossy()); diff --git a/crates/ruff/src/rules/isort/order.rs b/crates/ruff/src/rules/isort/order.rs index 1867ebf0de..66509a797b 100644 --- a/crates/ruff/src/rules/isort/order.rs +++ b/crates/ruff/src/rules/isort/order.rs @@ -9,9 +9,11 @@ use super::settings::RelativeImportsOrder; use super::sorting::{cmp_import_from, cmp_members, cmp_modules}; use super::types::{AliasData, CommentSet, ImportBlock, OrderedImportBlock}; +#[allow(clippy::too_many_arguments)] pub(crate) fn order_imports<'a>( block: ImportBlock<'a>, order_by_type: bool, + case_sensitive: bool, relative_imports_order: RelativeImportsOrder, classes: &'a BTreeSet, constants: &'a BTreeSet, @@ -25,7 +27,9 @@ pub(crate) fn order_imports<'a>( block .import .into_iter() - .sorted_by(|(alias1, _), (alias2, _)| cmp_modules(alias1, alias2, force_to_top)), + .sorted_by(|(alias1, _), (alias2, _)| { + cmp_modules(alias1, alias2, force_to_top, case_sensitive) + }), ); // Sort `Stmt::ImportFrom`. @@ -70,6 +74,7 @@ pub(crate) fn order_imports<'a>( constants, variables, force_to_top, + case_sensitive, ) }) .collect::>(), @@ -83,6 +88,7 @@ pub(crate) fn order_imports<'a>( import_from2, relative_imports_order, force_to_top, + case_sensitive, ) .then_with(|| match (aliases1.first(), aliases2.first()) { (None, None) => Ordering::Equal, @@ -96,6 +102,7 @@ pub(crate) fn order_imports<'a>( constants, variables, force_to_top, + case_sensitive, ), }) }, diff --git a/crates/ruff/src/rules/isort/rules/organize_imports.rs b/crates/ruff/src/rules/isort/rules/organize_imports.rs index d7a3dbe549..89832f9431 100644 --- a/crates/ruff/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff/src/rules/isort/rules/organize_imports.rs @@ -127,6 +127,7 @@ pub(crate) fn organize_imports( settings.isort.combine_as_imports, settings.isort.force_single_line, settings.isort.force_sort_within_sections, + settings.isort.case_sensitive, settings.isort.force_wrap_aliases, &settings.isort.force_to_top, &settings.isort.known_modules, diff --git a/crates/ruff/src/rules/isort/settings.rs b/crates/ruff/src/rules/isort/settings.rs index 262829b73d..f01aa4afd1 100644 --- a/crates/ruff/src/rules/isort/settings.rs +++ b/crates/ruff/src/rules/isort/settings.rs @@ -127,6 +127,15 @@ pub struct Options { /// imports (like `from itertools import groupby`). Instead, sort the /// imports by module, independent of import style. pub force_sort_within_sections: Option, + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + case-sensitive = true + "# + )] + /// Sort imports taking into account case sensitivity. + pub case_sensitive: Option, #[option( default = r#"[]"#, value_type = "list[str]", @@ -303,6 +312,7 @@ pub struct Settings { pub combine_as_imports: bool, pub force_single_line: bool, pub force_sort_within_sections: bool, + pub case_sensitive: bool, pub force_wrap_aliases: bool, pub force_to_top: BTreeSet, pub known_modules: KnownModules, @@ -327,6 +337,7 @@ impl Default for Settings { combine_as_imports: false, force_single_line: false, force_sort_within_sections: false, + case_sensitive: false, force_wrap_aliases: false, force_to_top: BTreeSet::new(), known_modules: KnownModules::default(), @@ -429,6 +440,7 @@ impl From for Settings { combine_as_imports: options.combine_as_imports.unwrap_or(false), force_single_line: options.force_single_line.unwrap_or(false), force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false), + case_sensitive: options.case_sensitive.unwrap_or(false), force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false), force_to_top: BTreeSet::from_iter(options.force_to_top.unwrap_or_default()), known_modules: KnownModules::new( @@ -468,6 +480,7 @@ impl From for Options { ), force_single_line: Some(settings.force_single_line), force_sort_within_sections: Some(settings.force_sort_within_sections), + case_sensitive: Some(settings.case_sensitive), force_wrap_aliases: Some(settings.force_wrap_aliases), force_to_top: Some(settings.force_to_top.into_iter().collect()), known_first_party: Some( diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__case_sensitive_case_sensitive.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__case_sensitive_case_sensitive.py.snap new file mode 100644 index 0000000000..c6fe266802 --- /dev/null +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__case_sensitive_case_sensitive.py.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff/src/rules/isort/mod.rs +--- +case_sensitive.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import A +2 | | import B +3 | | import b +4 | | import C +5 | | import d +6 | | import E +7 | | import f +8 | | from g import a, B, c +9 | | from h import A, b, C + | + = help: Organize imports + +ℹ Fix +1 1 | import A +2 2 | import B + 3 |+import C + 4 |+import E +3 5 | import b +4 |-import C +5 6 | import d +6 |-import E +7 7 | import f +8 |-from g import a, B, c +9 |-from h import A, b, C + 8 |+from g import B, a, c + 9 |+from h import A, C, b + + diff --git a/crates/ruff/src/rules/isort/sorting.rs b/crates/ruff/src/rules/isort/sorting.rs index ec8e15af4e..75bb9a2653 100644 --- a/crates/ruff/src/rules/isort/sorting.rs +++ b/crates/ruff/src/rules/isort/sorting.rs @@ -56,10 +56,17 @@ pub(crate) fn cmp_modules( alias1: &AliasData, alias2: &AliasData, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { cmp_force_to_top(alias1.name, alias2.name, force_to_top) - .then_with(|| natord::compare_ignore_case(alias1.name, alias2.name)) - .then_with(|| natord::compare(alias1.name, alias2.name)) + .then_with(|| { + if case_sensitive { + natord::compare(alias1.name, alias2.name) + } else { + natord::compare_ignore_case(alias1.name, alias2.name) + .then_with(|| natord::compare(alias1.name, alias2.name)) + } + }) .then_with(|| match (alias1.asname, alias2.asname) { (None, None) => Ordering::Equal, (None, Some(_)) => Ordering::Less, @@ -69,6 +76,7 @@ pub(crate) fn cmp_modules( } /// Compare two member imports within `Stmt::ImportFrom` blocks. +#[allow(clippy::too_many_arguments)] pub(crate) fn cmp_members( alias1: &AliasData, alias2: &AliasData, @@ -77,6 +85,7 @@ pub(crate) fn cmp_members( constants: &BTreeSet, variables: &BTreeSet, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { match (alias1.name == "*", alias2.name == "*") { (true, false) => Ordering::Less, @@ -85,9 +94,9 @@ pub(crate) fn cmp_members( if order_by_type { prefix(alias1.name, classes, constants, variables) .cmp(&prefix(alias2.name, classes, constants, variables)) - .then_with(|| cmp_modules(alias1, alias2, force_to_top)) + .then_with(|| cmp_modules(alias1, alias2, force_to_top, case_sensitive)) } else { - cmp_modules(alias1, alias2, force_to_top) + cmp_modules(alias1, alias2, force_to_top, case_sensitive) } } } @@ -116,6 +125,7 @@ pub(crate) fn cmp_import_from( import_from2: &ImportFromData, relative_imports_order: RelativeImportsOrder, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { cmp_levels( import_from1.level, @@ -133,8 +143,13 @@ pub(crate) fn cmp_import_from( (None, None) => Ordering::Equal, (None, Some(_)) => Ordering::Less, (Some(_), None) => Ordering::Greater, - (Some(module1), Some(module2)) => natord::compare_ignore_case(module1, module2) - .then_with(|| natord::compare(module1, module2)), + (Some(module1), Some(module2)) => { + if case_sensitive { + natord::compare(module1, module2) + } else { + natord::compare_ignore_case(module1, module2) + } + } }) } @@ -143,9 +158,14 @@ fn cmp_import_import_from( import: &AliasData, import_from: &ImportFromData, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { cmp_force_to_top(import.name, &import_from.module_name(), force_to_top).then_with(|| { - natord::compare_ignore_case(import.name, import_from.module.unwrap_or_default()) + if case_sensitive { + natord::compare(import.name, import_from.module.unwrap_or_default()) + } else { + natord::compare_ignore_case(import.name, import_from.module.unwrap_or_default()) + } }) } @@ -156,20 +176,24 @@ pub(crate) fn cmp_either_import( b: &EitherImport, relative_imports_order: RelativeImportsOrder, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { match (a, b) { - (Import((alias1, _)), Import((alias2, _))) => cmp_modules(alias1, alias2, force_to_top), + (Import((alias1, _)), Import((alias2, _))) => { + cmp_modules(alias1, alias2, force_to_top, case_sensitive) + } (ImportFrom((import_from, ..)), Import((alias, _))) => { - cmp_import_import_from(alias, import_from, force_to_top).reverse() + cmp_import_import_from(alias, import_from, force_to_top, case_sensitive).reverse() } (Import((alias, _)), ImportFrom((import_from, ..))) => { - cmp_import_import_from(alias, import_from, force_to_top) + cmp_import_import_from(alias, import_from, force_to_top, case_sensitive) } (ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => cmp_import_from( import_from1, import_from2, relative_imports_order, force_to_top, + case_sensitive, ), } } diff --git a/ruff.schema.json b/ruff.schema.json index 13181e83d7..47e0a9fde0 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1135,6 +1135,13 @@ "IsortOptions": { "type": "object", "properties": { + "case-sensitive": { + "description": "Sort imports taking into account case sensitivity.", + "type": [ + "boolean", + "null" + ] + }, "classes": { "description": "An override list of tokens to always recognize as a Class for `order-by-type` regardless of casing.", "type": [