diff --git a/README.md b/README.md index 3ef4d6e04a..6117d40038 100644 --- a/README.md +++ b/README.md @@ -2994,6 +2994,24 @@ combine-as-imports = true --- +#### [`constants`](#constants) + +An override list of tokens to always recognize as a CONSTANT +for `order-by-type` regardless of casing. + +**Default value**: `[]` + +**Type**: `Vec` + +**Example usage**: + +```toml +[tool.ruff.isort] +constants = ["constant"] +``` + +--- + #### [`extra-standard-library`](#extra-standard-library) A list of modules to consider standard-library, in addition to those @@ -3213,6 +3231,24 @@ split-on-trailing-comma = false --- +#### [`variables`](#variables) + +An override list of tokens to always recognize as a var +for `order-by-type` regardless of casing. + +**Default value**: `[]` + +**Type**: `Vec` + +**Example usage**: + +```toml +[tool.ruff.isort] +variables = ["VAR"] +``` + +--- + ### `mccabe` #### [`max-complexity`](#max-complexity) diff --git a/resources/test/fixtures/isort/order_by_type_with_custom_constants.py b/resources/test/fixtures/isort/order_by_type_with_custom_constants.py new file mode 100644 index 0000000000..2bda31d7e9 --- /dev/null +++ b/resources/test/fixtures/isort/order_by_type_with_custom_constants.py @@ -0,0 +1,2 @@ +from sklearn.svm import XYZ, func, variable, Const, Klass, constant +from subprocess import First, var, func, Class, konst, A_constant, Last, STDOUT diff --git a/resources/test/fixtures/isort/order_by_type_with_custom_variables.py b/resources/test/fixtures/isort/order_by_type_with_custom_variables.py new file mode 100644 index 0000000000..ec97760dd3 --- /dev/null +++ b/resources/test/fixtures/isort/order_by_type_with_custom_variables.py @@ -0,0 +1,2 @@ +from sklearn.svm import VAR, Class, MyVar, CONST, abc +from subprocess import utils, var_ABC, Variable, Klass, CONSTANT, exe diff --git a/ruff.schema.json b/ruff.schema.json index 655114b43c..fd45526b3d 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -782,6 +782,16 @@ "null" ] }, + "constants": { + "description": "An override list of tokens to always recognize as a CONSTANT for `order-by-type` regardless of casing.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "extra-standard-library": { "description": "A list of modules to consider standard-library, in addition to those known to Ruff in advance.", "type": [ @@ -877,6 +887,16 @@ "boolean", "null" ] + }, + "variables": { + "description": "An override list of tokens to always recognize as a var for `order-by-type` regardless of casing.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } } }, "additionalProperties": false diff --git a/src/rules/isort/mod.rs b/src/rules/isort/mod.rs index 3944aa6629..60b80a0064 100644 --- a/src/rules/isort/mod.rs +++ b/src/rules/isort/mod.rs @@ -389,6 +389,8 @@ fn order_imports<'a>( order_by_type: bool, relative_imports_order: RelatveImportsOrder, classes: &'a BTreeSet, + constants: &'a BTreeSet, + variables: &'a BTreeSet, ) -> OrderedImportBlock<'a> { let mut ordered = OrderedImportBlock::default(); @@ -468,7 +470,14 @@ fn order_imports<'a>( aliases .into_iter() .sorted_by(|(alias1, _), (alias2, _)| { - cmp_members(alias1, alias2, order_by_type, classes) + cmp_members( + alias1, + alias2, + order_by_type, + classes, + constants, + variables, + ) }) .collect::>(), ) @@ -480,9 +489,14 @@ fn order_imports<'a>( (None, None) => Ordering::Equal, (None, Some(_)) => Ordering::Less, (Some(_), None) => Ordering::Greater, - (Some((alias1, _)), Some((alias2, _))) => { - cmp_members(alias1, alias2, order_by_type, classes) - } + (Some((alias1, _)), Some((alias2, _))) => cmp_members( + alias1, + alias2, + order_by_type, + classes, + constants, + variables, + ), }, ) }, @@ -559,6 +573,8 @@ pub fn format_imports( single_line_exclusions: &BTreeSet, split_on_trailing_comma: bool, classes: &BTreeSet, + constants: &BTreeSet, + variables: &BTreeSet, ) -> String { let trailer = &block.trailer; let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma); @@ -581,8 +597,14 @@ pub fn format_imports( // Generate replacement source code. let mut is_first_block = true; for import_block in block_by_type.into_values() { - let mut imports = - order_imports(import_block, order_by_type, relative_imports_order, classes); + let mut imports = order_imports( + import_block, + order_by_type, + relative_imports_order, + classes, + constants, + variables, + ); if force_single_line { imports = force_single_line_imports(imports, single_line_exclusions); @@ -702,6 +724,8 @@ mod tests { #[test_case(Path::new("trailing_suffix.py"))] #[test_case(Path::new("type_comments.py"))] #[test_case(Path::new("order_by_type_with_custom_classes.py"))] + #[test_case(Path::new("order_by_type_with_custom_constants.py"))] + #[test_case(Path::new("order_by_type_with_custom_variables.py"))] fn default(path: &Path) -> Result<()> { let snapshot = format!("{}", path.to_string_lossy()); let diagnostics = test_path( @@ -852,6 +876,68 @@ mod tests { Ok(()) } + #[test_case(Path::new("order_by_type_with_custom_constants.py"))] + fn order_by_type_with_custom_constants(path: &Path) -> Result<()> { + let snapshot = format!( + "order_by_type_with_custom_constants_{}", + path.to_string_lossy() + ); + let mut diagnostics = test_path( + Path::new("./resources/test/fixtures/isort") + .join(path) + .as_path(), + &Settings { + isort: super::settings::Settings { + order_by_type: true, + constants: BTreeSet::from([ + "Const".to_string(), + "constant".to_string(), + "First".to_string(), + "Last".to_string(), + "A_constant".to_string(), + "konst".to_string(), + ]), + ..super::settings::Settings::default() + }, + src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()], + ..Settings::for_rule(RuleCode::I001) + }, + )?; + diagnostics.sort_by_key(|diagnostic| diagnostic.location); + insta::assert_yaml_snapshot!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("order_by_type_with_custom_variables.py"))] + fn order_by_type_with_custom_variables(path: &Path) -> Result<()> { + let snapshot = format!( + "order_by_type_with_custom_variables_{}", + path.to_string_lossy() + ); + let mut diagnostics = test_path( + Path::new("./resources/test/fixtures/isort") + .join(path) + .as_path(), + &Settings { + isort: super::settings::Settings { + order_by_type: true, + variables: BTreeSet::from([ + "VAR".to_string(), + "Variable".to_string(), + "MyVar".to_string(), + "var_ABC".to_string(), + ]), + ..super::settings::Settings::default() + }, + src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()], + ..Settings::for_rule(RuleCode::I001) + }, + )?; + diagnostics.sort_by_key(|diagnostic| diagnostic.location); + insta::assert_yaml_snapshot!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Path::new("force_sort_within_sections.py"))] fn force_sort_within_sections(path: &Path) -> Result<()> { let snapshot = format!("force_sort_within_sections_{}", path.to_string_lossy()); diff --git a/src/rules/isort/rules/organize_imports.rs b/src/rules/isort/rules/organize_imports.rs index 7cea1ab2d6..167d168e24 100644 --- a/src/rules/isort/rules/organize_imports.rs +++ b/src/rules/isort/rules/organize_imports.rs @@ -86,6 +86,8 @@ pub fn organize_imports( &settings.isort.single_line_exclusions, settings.isort.split_on_trailing_comma, &settings.isort.classes, + &settings.isort.constants, + &settings.isort.variables, ); // Expand the span the entire range, including leading and trailing space. diff --git a/src/rules/isort/settings.rs b/src/rules/isort/settings.rs index c1711b316a..5c48d952c4 100644 --- a/src/rules/isort/settings.rs +++ b/src/rules/isort/settings.rs @@ -181,6 +181,26 @@ pub struct Options { /// An override list of tokens to always recognize as a Class for /// `order-by-type` regardless of casing. pub classes: Option>, + #[option( + default = r#"[]"#, + value_type = "Vec", + example = r#" + constants = ["constant"] + "# + )] + /// An override list of tokens to always recognize as a CONSTANT + /// for `order-by-type` regardless of casing. + pub constants: Option>, + #[option( + default = r#"[]"#, + value_type = "Vec", + example = r#" + variables = ["VAR"] + "# + )] + /// An override list of tokens to always recognize as a var + /// for `order-by-type` regardless of casing. + pub variables: Option>, } #[derive(Debug, Hash)] @@ -199,6 +219,8 @@ pub struct Settings { pub single_line_exclusions: BTreeSet, pub split_on_trailing_comma: bool, pub classes: BTreeSet, + pub constants: BTreeSet, + pub variables: BTreeSet, } impl Default for Settings { @@ -217,6 +239,8 @@ impl Default for Settings { single_line_exclusions: BTreeSet::new(), split_on_trailing_comma: true, classes: BTreeSet::new(), + constants: BTreeSet::new(), + variables: BTreeSet::new(), } } } @@ -241,6 +265,8 @@ impl From for Settings { ), split_on_trailing_comma: options.split_on_trailing_comma.unwrap_or(true), classes: BTreeSet::from_iter(options.classes.unwrap_or_default()), + constants: BTreeSet::from_iter(options.constants.unwrap_or_default()), + variables: BTreeSet::from_iter(options.variables.unwrap_or_default()), } } } @@ -261,6 +287,8 @@ impl From for Options { single_line_exclusions: Some(settings.single_line_exclusions.into_iter().collect()), split_on_trailing_comma: Some(settings.split_on_trailing_comma), classes: Some(settings.classes.into_iter().collect()), + constants: Some(settings.constants.into_iter().collect()), + variables: Some(settings.variables.into_iter().collect()), } } } diff --git a/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_constants.py.snap b/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_constants.py.snap new file mode 100644 index 0000000000..fa275b13c3 --- /dev/null +++ b/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_constants.py.snap @@ -0,0 +1,22 @@ +--- +source: src/rules/isort/mod.rs +expression: diagnostics +--- +- kind: + UnsortedImports: ~ + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 0 + fix: + content: "from subprocess import STDOUT, A_constant, Class, First, Last, func, konst, var\n\nfrom sklearn.svm import XYZ, Const, Klass, constant, func, variable\n" + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 0 + parent: ~ + diff --git a/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_constants_order_by_type_with_custom_constants.py.snap b/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_constants_order_by_type_with_custom_constants.py.snap new file mode 100644 index 0000000000..cc8b14db81 --- /dev/null +++ b/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_constants_order_by_type_with_custom_constants.py.snap @@ -0,0 +1,22 @@ +--- +source: src/rules/isort/mod.rs +expression: diagnostics +--- +- kind: + UnsortedImports: ~ + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 0 + fix: + content: "from subprocess import A_constant, First, konst, Last, STDOUT, Class, func, var\n\nfrom sklearn.svm import Const, constant, XYZ, Klass, func, variable\n" + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 0 + parent: ~ + diff --git a/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_variables.py.snap b/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_variables.py.snap new file mode 100644 index 0000000000..b6d7756b35 --- /dev/null +++ b/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_variables.py.snap @@ -0,0 +1,22 @@ +--- +source: src/rules/isort/mod.rs +expression: diagnostics +--- +- kind: + UnsortedImports: ~ + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 0 + fix: + content: "from subprocess import CONSTANT, Klass, Variable, exe, utils, var_ABC\n\nfrom sklearn.svm import CONST, VAR, Class, MyVar, abc\n" + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 0 + parent: ~ + diff --git a/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_variables_order_by_type_with_custom_variables.py.snap b/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_variables_order_by_type_with_custom_variables.py.snap new file mode 100644 index 0000000000..a27dcc3e3a --- /dev/null +++ b/src/rules/isort/snapshots/ruff__rules__isort__tests__order_by_type_with_custom_variables_order_by_type_with_custom_variables.py.snap @@ -0,0 +1,22 @@ +--- +source: src/rules/isort/mod.rs +expression: diagnostics +--- +- kind: + UnsortedImports: ~ + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 0 + fix: + content: "from subprocess import CONSTANT, Klass, exe, utils, var_ABC, Variable\n\nfrom sklearn.svm import CONST, Class, abc, MyVar, VAR\n" + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 0 + parent: ~ + diff --git a/src/rules/isort/sorting.rs b/src/rules/isort/sorting.rs index a58045df8e..0095df6527 100644 --- a/src/rules/isort/sorting.rs +++ b/src/rules/isort/sorting.rs @@ -14,10 +14,21 @@ pub enum Prefix { Variables, } -fn prefix(name: &str, classes: &BTreeSet) -> Prefix { - if classes.contains(name) { +fn prefix( + name: &str, + classes: &BTreeSet, + constants: &BTreeSet, + variables: &BTreeSet, +) -> Prefix { + if constants.contains(name) { + // Ex) `CONSTANT` + Prefix::Constants + } else if classes.contains(name) { // Ex) `CLASS` Prefix::Classes + } else if variables.contains(name) { + // Ex) `variable` + Prefix::Variables } else if name.len() > 1 && string::is_upper(name) { // Ex) `CONSTANT` Prefix::Constants @@ -48,10 +59,12 @@ pub fn cmp_members( alias2: &AliasData, order_by_type: bool, classes: &BTreeSet, + constants: &BTreeSet, + variables: &BTreeSet, ) -> Ordering { if order_by_type { - prefix(alias1.name, classes) - .cmp(&prefix(alias2.name, classes)) + prefix(alias1.name, classes, constants, variables) + .cmp(&prefix(alias2.name, classes, constants, variables)) .then_with(|| cmp_modules(alias1, alias2)) } else { cmp_modules(alias1, alias2)