diff --git a/.changeset/fancy-trains-happen.md b/.changeset/fancy-trains-happen.md new file mode 100644 index 000000000000..bbaa74ea1179 --- /dev/null +++ b/.changeset/fancy-trains-happen.md @@ -0,0 +1,14 @@ +--- +"@biomejs/biome": minor +--- + +Allow customization of the sort order for different sorting actions. These actions now support a sort option: + +- [`assist/source/useSortedKeys`](https://biomejs.dev/assist/actions/use-sorted-keys/) now has a `sortOrder` option +- [`assist/source/useSortedAttributes`](https://biomejs.dev/assist/actions/use-sorted-attributes/) now has a `sortOrder` option +- [`assist/source/organizeImports`](https://biomejs.dev/assist/actions/organize-imports/) now has an `identifierOrder` option + +For each of these options, the supported values are the same: + +1. **`natural`**. Compares two strings using a natural ASCII order. Uppercase letters come first (e.g. `A` < `a` < `B` < `b`) and number are compared in a human way (e.g. `9` < `10`). This is the default value. +2. **`lexicographic`**. Strings are ordered lexicographically by their byte values. This orders Unicode code points based on their positions in the code charts. This is not necessarily the same as “alphabetical” order, which varies by language and locale. diff --git a/crates/biome_analyze/src/utils.rs b/crates/biome_analyze/src/utils.rs index f982f3457fec..becffe35dccf 100644 --- a/crates/biome_analyze/src/utils.rs +++ b/crates/biome_analyze/src/utils.rs @@ -2,6 +2,7 @@ use biome_rowan::{ AstNode, AstSeparatedElement, AstSeparatedList, Language, SyntaxError, SyntaxNode, SyntaxToken, chain_trivia_pieces, }; +use std::cmp::Ordering; /// Returns `true` if `list` is sorted by `get_key`. /// The function returns an error if we encounter a buggy node or separator. @@ -9,16 +10,13 @@ use biome_rowan::{ /// The list is divided into chunks of nodes with keys. /// Thus, a node without key acts as a chuck delimiter. /// Chunks are sorted separately. -pub fn is_separated_list_sorted_by< - 'a, - L: Language + 'a, - N: AstNode + 'a, - Key: Ord, ->( +pub fn is_separated_list_sorted_by<'a, L: Language + 'a, N: AstNode + 'a, Key>( list: &impl AstSeparatedList, get_key: impl Fn(&N) -> Option, + comparator: impl Fn(&Key, &Key) -> Ordering, ) -> Result { let mut is_sorted = true; + if list.len() > 1 { let mut previous_key: Option = None; for AstSeparatedElement { @@ -29,7 +27,8 @@ pub fn is_separated_list_sorted_by< // We have to check if the separator is not buggy. let _separator = trailing_separator?; previous_key = if let Some(key) = get_key(&node?) { - if previous_key.is_some_and(|previous_key| previous_key > key) { + if previous_key.is_some_and(|previous_key| comparator(&previous_key, &key).is_gt()) + { // We don't return early because we want to return the error if we met one. is_sorted = false; } @@ -51,10 +50,11 @@ pub fn is_separated_list_sorted_by< /// Chunks are sorted separately. /// /// This sort is stable (i.e., does not reorder equal elements). -pub fn sorted_separated_list_by<'a, L: Language + 'a, List, Node, Key: Ord>( +pub fn sorted_separated_list_by<'a, L: Language + 'a, List, Node, Key>( list: &List, get_key: impl Fn(&Node) -> Option, make_separator: fn() -> SyntaxToken, + comparator: impl Fn(&Key, &Key) -> Ordering, ) -> Result where List: AstSeparatedList + AstNode + 'a, @@ -74,7 +74,12 @@ where // Iterate over chunks of node with a key for slice in elements.split_mut(|(key, _, _)| key.is_none()) { let last_has_separator = slice.last().is_some_and(|(_, _, sep)| sep.is_some()); - slice.sort_by(|(key1, _, _), (key2, _, _)| key1.cmp(key2)); + slice.sort_by(|(key1, _, _), (key2, _, _)| match (key1, key2) { + (Some(k1), Some(k2)) => comparator(k1, k2), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }); fix_separators( slice.iter_mut().map(|(_, node, sep)| (node, sep)), last_has_separator, diff --git a/crates/biome_js_analyze/src/assist/source/organize_imports.rs b/crates/biome_js_analyze/src/assist/source/organize_imports.rs index 62a25da8382f..cd84ebb726a8 100644 --- a/crates/biome_js_analyze/src/assist/source/organize_imports.rs +++ b/crates/biome_js_analyze/src/assist/source/organize_imports.rs @@ -10,7 +10,7 @@ use biome_js_syntax::{ JsSyntaxKind, T, }; use biome_rowan::{AstNode, BatchMutationExt, TextRange, TriviaPieceKind, chain_trivia_pieces}; -use biome_rule_options::organize_imports::OrganizeImportsOptions; +use biome_rule_options::{organize_imports::OrganizeImportsOptions, sort_order::SortOrder}; use import_key::{ImportInfo, ImportKey}; use rustc_hash::FxHashMap; use specifiers_attributes::{ @@ -618,6 +618,34 @@ declare_source_rule! { /// } /// ``` /// + /// ## Change the sorting of import identifiers to lexicographic sorting + /// This only applies to the named import/exports and not the source itself. + /// + /// ```json,options + /// { + /// "options": { + /// "identifierOrder": "lexicographic" + /// } + /// } + /// ``` + /// ```js,use_options,expect_diagnostic + /// import { var1, var2, var21, var11, var12, var22 } from 'my-package' + /// ``` + /// + /// ## Change the sorting of import identifiers to logical sorting + /// This is the default behavior incase you do not override. This only applies to the named import/exports and not the source itself. + /// + /// ```json,options + /// { + /// "options": { + /// "identifierOrder": "natural" + /// } + /// } + /// ``` + /// ```js,use_options,expect_diagnostic + /// import { var1, var2, var21, var11, var12, var22 } from 'my-package' + /// ``` + /// pub OrganizeImports { version: "1.0.0", name: "organizeImports", @@ -683,6 +711,7 @@ impl Rule for OrganizeImports { let root = ctx.query(); let mut result = Vec::new(); let options = ctx.options(); + let sort_order = options.identifier_order; let mut chunk: Option = None; let mut prev_kind: Option = None; let mut prev_group = 0; @@ -702,10 +731,10 @@ impl Rule for OrganizeImports { let starts_chunk = chunk.is_none(); let leading_newline_count = leading_newlines(item.syntax()).count(); let are_specifiers_unsorted = - specifiers.is_some_and(|specifiers| !specifiers.are_sorted()); + specifiers.is_some_and(|specifiers| !specifiers.are_sorted(sort_order)); let are_attributes_unsorted = attributes.is_some_and(|attributes| { // Assume the attributes are sorted if there are any bogus nodes. - !(are_import_attributes_sorted(&attributes).unwrap_or(true)) + !(are_import_attributes_sorted(&attributes, sort_order).unwrap_or(true)) }); let newline_issue = if leading_newline_count == 1 // A chunk must start with a blank line (two newlines) @@ -792,6 +821,7 @@ impl Rule for OrganizeImports { } let options = ctx.options(); + let sort_order = options.identifier_order; let root = ctx.query(); let items = root.items().into_syntax(); let mut organized_items: FxHashMap = FxHashMap::default(); @@ -828,7 +858,7 @@ impl Rule for OrganizeImports { // Sort named specifiers if let AnyJsExportClause::JsExportNamedFromClause(cast) = &clause { if let Some(sorted_specifiers) = - sort_export_specifiers(&cast.specifiers()) + sort_export_specifiers(&cast.specifiers(), sort_order) { clause = cast.clone().with_specifiers(sorted_specifiers).into(); @@ -837,7 +867,9 @@ impl Rule for OrganizeImports { } if *are_attributes_unsorted { // Sort import attributes - let sorted_attrs = clause.attribute().and_then(sort_attributes); + let sorted_attrs = clause + .attribute() + .and_then(|attrs| sort_attributes(attrs, sort_order)); clause = clause.with_attribute(sorted_attrs); } export.with_export_clause(clause).into() @@ -847,14 +879,18 @@ impl Rule for OrganizeImports { if *are_specifiers_unsorted { // Sort named specifiers if let Some(sorted_specifiers) = - clause.named_specifiers().and_then(sort_import_specifiers) + clause.named_specifiers().and_then(|specifiers| { + sort_import_specifiers(specifiers, sort_order) + }) { clause = clause.with_named_specifiers(sorted_specifiers) } } if *are_attributes_unsorted { // Sort import attributes - let sorted_attrs = clause.attribute().and_then(sort_attributes); + let sorted_attrs = clause + .attribute() + .and_then(|attrs| sort_attributes(attrs, sort_order)); clause = clause.with_attribute(sorted_attrs); } import.with_import_clause(clause).into() @@ -905,6 +941,7 @@ impl Rule for OrganizeImports { import_keys.sort_unstable_by( |KeyedItem { key: k1, .. }, KeyedItem { key: k2, .. }| k1.cmp(k2), ); + // Merge imports/exports // We use `while` and indexing to allow both iteration and mutation of `import_keys`. let mut i = import_keys.len() - 1; @@ -916,7 +953,9 @@ impl Rule for OrganizeImports { } = &import_keys[i - 1]; let KeyedItem { key, item, .. } = &import_keys[i]; if prev_key.is_mergeable(key) { - if let Some(merged) = merge(prev_item.as_ref(), item.as_ref()) { + if let Some(merged) = + merge(prev_item.as_ref(), item.as_ref(), sort_order) + { import_keys[i - 1].was_merged = true; import_keys[i - 1].item = Some(merged); import_keys[i].item = None; @@ -1055,6 +1094,7 @@ pub enum NewLineIssue { fn merge( item1: Option<&AnyJsModuleItem>, item2: Option<&AnyJsModuleItem>, + sort_order: SortOrder, ) -> Option { match (item1?, item2?) { (AnyJsModuleItem::JsExport(item1), AnyJsModuleItem::JsExport(item2)) => { @@ -1066,7 +1106,9 @@ fn merge( let clause2 = clause2.as_js_export_named_from_clause()?; let specifiers1 = clause1.specifiers(); let specifiers2 = clause2.specifiers(); - if let Some(meregd_specifiers) = merge_export_specifiers(&specifiers1, &specifiers2) { + if let Some(meregd_specifiers) = + merge_export_specifiers(&specifiers1, &specifiers2, sort_order) + { let meregd_clause = clause1.with_specifiers(meregd_specifiers); let merged_item = item2.clone().with_export_clause(meregd_clause.into()); @@ -1132,7 +1174,7 @@ fn merge( }; let specifiers2 = clause2.named_specifiers().ok()?; if let Some(meregd_specifiers) = - merge_import_specifiers(specifiers1, &specifiers2) + merge_import_specifiers(specifiers1, &specifiers2, sort_order) { let merged_clause = clause1.with_specifier(meregd_specifiers.into()); let merged_item = item2.clone().with_import_clause(merged_clause.into()); @@ -1155,7 +1197,7 @@ fn merge( let specifiers1 = clause1.named_specifiers().ok()?; let specifiers2 = clause2.named_specifiers().ok()?; if let Some(meregd_specifiers) = - merge_import_specifiers(specifiers1, &specifiers2) + merge_import_specifiers(specifiers1, &specifiers2, sort_order) { let merged_clause = clause1.with_named_specifiers(meregd_specifiers); let merged_item = item2.clone().with_import_clause(merged_clause.into()); diff --git a/crates/biome_js_analyze/src/assist/source/organize_imports/specifiers_attributes.rs b/crates/biome_js_analyze/src/assist/source/organize_imports/specifiers_attributes.rs index 679740950bed..67100e53d01f 100644 --- a/crates/biome_js_analyze/src/assist/source/organize_imports/specifiers_attributes.rs +++ b/crates/biome_js_analyze/src/assist/source/organize_imports/specifiers_attributes.rs @@ -5,18 +5,22 @@ use biome_js_syntax::{ JsNamedImportSpecifiers, T, inner_string_text, }; use biome_rowan::{AstNode, AstSeparatedElement, AstSeparatedList, TriviaPieceKind}; +use biome_rule_options::organize_imports::SortOrder; use biome_string_case::comparable_token::ComparableToken; +use std::cmp::Ordering; pub enum JsNamedSpecifiers { JsNamedImportSpecifiers(JsNamedImportSpecifiers), JsExportNamedFromSpecifierList(JsExportNamedFromSpecifierList), } impl JsNamedSpecifiers { - pub fn are_sorted(&self) -> bool { + pub fn are_sorted(&self, sort_order: SortOrder) -> bool { match self { - Self::JsNamedImportSpecifiers(specifeirs) => are_import_specifiers_sorted(specifeirs), + Self::JsNamedImportSpecifiers(specifeirs) => { + are_import_specifiers_sorted(specifeirs, sort_order) + } Self::JsExportNamedFromSpecifierList(specifeirs) => { - are_export_specifiers_sorted(specifeirs) + are_export_specifiers_sorted(specifeirs, sort_order) } } // Assume the import is already sorted if there are any bogus nodes, otherwise the `--write` @@ -25,21 +29,32 @@ impl JsNamedSpecifiers { } } -pub fn are_import_specifiers_sorted(named_specifiers: &JsNamedImportSpecifiers) -> Option { - is_separated_list_sorted_by(&named_specifiers.specifiers(), |node| { - let AnyJsBinding::JsIdentifierBinding(name) = node.local_name()? else { - return None; - }; - Some(ComparableToken::new( - name.name_token().ok()?.token_text_trimmed(), - )) - }) +pub fn are_import_specifiers_sorted( + named_specifiers: &JsNamedImportSpecifiers, + sort_order: SortOrder, +) -> Option { + let comparator = get_comparator(sort_order); + + is_separated_list_sorted_by( + &named_specifiers.specifiers(), + |node| { + let AnyJsBinding::JsIdentifierBinding(name) = node.local_name()? else { + return None; + }; + Some(ComparableToken::new( + name.name_token().ok()?.token_text_trimmed(), + )) + }, + comparator, + ) .ok() } pub fn sort_import_specifiers( named_specifiers: JsNamedImportSpecifiers, + sort_order: SortOrder, ) -> Option { + let comparator = get_comparator(sort_order); let new_list = sorted_separated_list_by( &named_specifiers.specifiers(), |node| { @@ -51,6 +66,7 @@ pub fn sort_import_specifiers( )) }, || make::token(T![,]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), + comparator, ) .ok()?; Some(named_specifiers.with_specifiers(new_list)) @@ -59,6 +75,7 @@ pub fn sort_import_specifiers( pub fn merge_import_specifiers( named_specifiers1: JsNamedImportSpecifiers, named_specifiers2: &JsNamedImportSpecifiers, + sort_order: SortOrder, ) -> Option { let specifiers1 = named_specifiers1.specifiers(); let specifiers2 = named_specifiers2.specifiers(); @@ -91,23 +108,34 @@ pub fn merge_import_specifiers( } } let new_list = make::js_named_import_specifier_list(nodes, separators); - sort_import_specifiers(named_specifiers1.with_specifiers(new_list)) + sort_import_specifiers(named_specifiers1.with_specifiers(new_list), sort_order) } -pub fn are_export_specifiers_sorted(specifiers: &JsExportNamedFromSpecifierList) -> Option { - is_separated_list_sorted_by(specifiers, |node| { - node.source_name() - .ok()? - .inner_string_text() - .ok() - .map(ComparableToken::new) - }) +pub fn are_export_specifiers_sorted( + specifiers: &JsExportNamedFromSpecifierList, + sort_order: SortOrder, +) -> Option { + let comparator = get_comparator(sort_order); + + is_separated_list_sorted_by( + specifiers, + |node| { + node.source_name() + .ok()? + .inner_string_text() + .ok() + .map(ComparableToken::new) + }, + comparator, + ) .ok() } pub fn sort_export_specifiers( named_specifiers: &JsExportNamedFromSpecifierList, + sort_order: SortOrder, ) -> Option { + let comparator = get_comparator(sort_order); let new_list = sorted_separated_list_by( named_specifiers, |node| { @@ -118,6 +146,7 @@ pub fn sort_export_specifiers( .map(ComparableToken::new) }, || make::token(T![,]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), + comparator, ) .ok()?; Some(new_list) @@ -126,6 +155,7 @@ pub fn sort_export_specifiers( pub fn merge_export_specifiers( specifiers1: &JsExportNamedFromSpecifierList, specifiers2: &JsExportNamedFromSpecifierList, + sort_order: SortOrder, ) -> Option { let mut nodes = Vec::with_capacity(specifiers1.len() + specifiers2.len()); let mut separators = Vec::with_capacity(specifiers1.len() + specifiers2.len()); @@ -155,22 +185,36 @@ pub fn merge_export_specifiers( separators.push(separator); } } - sort_export_specifiers(&make::js_export_named_from_specifier_list( - nodes, separators, - )) + sort_export_specifiers( + &make::js_export_named_from_specifier_list(nodes, separators), + sort_order, + ) } -pub fn are_import_attributes_sorted(attributes: &JsImportAssertion) -> Option { - is_separated_list_sorted_by(&attributes.assertions(), |node| { - let AnyJsImportAssertionEntry::JsImportAssertionEntry(node) = node else { - return None; - }; - Some(ComparableToken::new(inner_string_text(&node.key().ok()?))) - }) +pub fn are_import_attributes_sorted( + attributes: &JsImportAssertion, + sort_order: SortOrder, +) -> Option { + let comparator = get_comparator(sort_order); + is_separated_list_sorted_by( + &attributes.assertions(), + |node| { + let AnyJsImportAssertionEntry::JsImportAssertionEntry(node) = node else { + return None; + }; + Some(ComparableToken::new(inner_string_text(&node.key().ok()?))) + }, + comparator, + ) .ok() } -pub fn sort_attributes(attributes: JsImportAssertion) -> Option { +pub fn sort_attributes( + attributes: JsImportAssertion, + sort_order: SortOrder, +) -> Option { + let comparator = get_comparator(sort_order); + let new_list = sorted_separated_list_by( &attributes.assertions(), |node| { @@ -180,7 +224,15 @@ pub fn sort_attributes(attributes: JsImportAssertion) -> Option fn(&ComparableToken, &ComparableToken) -> Ordering { + match sort_order { + SortOrder::Lexicographic => ComparableToken::lexicographic_cmp, + SortOrder::Natural => ComparableToken::ascii_nat_cmp, + } +} diff --git a/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs b/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs index 294ee2530d5e..5b406e8bf631 100644 --- a/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs +++ b/crates/biome_js_analyze/src/assist/source/use_sorted_attributes.rs @@ -11,7 +11,7 @@ use biome_js_syntax::{ AnyJsxAttribute, JsxAttribute, JsxAttributeList, JsxOpeningElement, JsxSelfClosingElement, }; use biome_rowan::{AstNode, BatchMutationExt}; -use biome_rule_options::use_sorted_attributes::UseSortedAttributesOptions; +use biome_rule_options::use_sorted_attributes::{SortOrder, UseSortedAttributesOptions}; use biome_string_case::StrLikeExtension; use crate::JsRuleAction; @@ -38,6 +38,38 @@ declare_source_rule! { /// ; /// ``` /// + /// ## Options + /// This actions accepts following options + /// + /// ### `sortOrder` + /// This options supports `natural` and `lexicographic` values. Where as `natural` is the default. + /// + /// Following will apply the natural sort order. + /// + /// ```json,options + /// { + /// "options": { + /// "sortOrder": "natural" + /// } + /// } + /// ``` + /// ```jsx,use_options,expect_diagnostic + /// ; + /// ``` + /// + /// Following will apply the lexicographic sort order. + /// + /// ```json,options + /// { + /// "options": { + /// "sortOrder": "lexicographic" + /// } + /// } + /// ``` + /// ```jsx,use_options,expect_diagnostic + /// ; + /// ``` + /// pub UseSortedAttributes { version: "2.0.0", name: "useSortedAttributes", @@ -58,6 +90,18 @@ impl Rule for UseSortedAttributes { let props = ctx.query(); let mut current_prop_group = PropGroup::default(); let mut prop_groups = Vec::new(); + let options = ctx.options(); + let sort_by = options.sort_order; + + let comparator = match sort_by { + SortOrder::Natural => PropElement::ascii_nat_cmp, + SortOrder::Lexicographic => PropElement::lexicographic_cmp, + }; + + // Convert to boolean-based comparator for is_sorted_by + let boolean_comparator = + |a: &PropElement, b: &PropElement| comparator(a, b) != Ordering::Greater; + for prop in props { match prop { AnyJsxAttribute::JsxAttribute(attr) => { @@ -65,7 +109,9 @@ impl Rule for UseSortedAttributes { } // spread prop reset sort order AnyJsxAttribute::JsxSpreadAttribute(_) => { - if !current_prop_group.is_empty() && !current_prop_group.is_sorted() { + if !current_prop_group.is_empty() + && !current_prop_group.is_sorted(boolean_comparator) + { prop_groups.push(current_prop_group); current_prop_group = PropGroup::default(); } else { @@ -76,7 +122,7 @@ impl Rule for UseSortedAttributes { AnyJsxAttribute::JsMetavariable(_) => {} } } - if !current_prop_group.is_empty() && !current_prop_group.is_sorted() { + if !current_prop_group.is_empty() && !current_prop_group.is_sorted(boolean_comparator) { prop_groups.push(current_prop_group); } prop_groups.into_boxed_slice() @@ -102,9 +148,16 @@ impl Rule for UseSortedAttributes { fn action(ctx: &RuleContext, state: &Self::State) -> Option { let mut mutation = ctx.root().begin(); + let options = ctx.options(); + let sort_by = options.sort_order; + + let comparator = match sort_by { + SortOrder::Natural => PropElement::ascii_nat_cmp, + SortOrder::Lexicographic => PropElement::lexicographic_cmp, + }; for (PropElement { prop }, PropElement { prop: sorted_prop }) in - zip(state.props.iter(), state.get_sorted_props()) + zip(state.props.iter(), state.get_sorted_props(comparator)) { mutation.replace_node(prop.clone(), sorted_prop); } @@ -123,8 +176,8 @@ pub struct PropElement { prop: JsxAttribute, } -impl Ord for PropElement { - fn cmp(&self, other: &Self) -> Ordering { +impl PropElement { + pub fn ascii_nat_cmp(&self, other: &Self) -> Ordering { let (Ok(self_name), Ok(other_name)) = (self.prop.name(), other.prop.name()) else { return Ordering::Equal; }; @@ -136,11 +189,18 @@ impl Ord for PropElement { .text_trimmed() .ascii_nat_cmp(other_name.text_trimmed()) } -} -impl PartialOrd for PropElement { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + pub fn lexicographic_cmp(&self, other: &Self) -> Ordering { + let (Ok(self_name), Ok(other_name)) = (self.prop.name(), other.prop.name()) else { + return Ordering::Equal; + }; + let (Ok(self_name), Ok(other_name)) = (self_name.name(), other_name.name()) else { + return Ordering::Equal; + }; + + self_name + .text_trimmed() + .lexicographic_cmp(other_name.text_trimmed()) } } @@ -154,13 +214,19 @@ impl PropGroup { self.props.is_empty() } - fn is_sorted(&self) -> bool { - self.props.is_sorted() + fn is_sorted(&self, comparator: F) -> bool + where + F: Fn(&PropElement, &PropElement) -> bool, + { + self.props.is_sorted_by(comparator) } - fn get_sorted_props(&self) -> Vec { + fn get_sorted_props(&self, comparator: F) -> Vec + where + F: FnMut(&PropElement, &PropElement) -> Ordering, + { let mut new_props = self.props.clone(); - new_props.sort_unstable(); + new_props.sort_by(comparator); new_props } diff --git a/crates/biome_js_analyze/src/assist/source/use_sorted_keys.rs b/crates/biome_js_analyze/src/assist/source/use_sorted_keys.rs index 1b02d2f36383..7a75e3c4e2d5 100644 --- a/crates/biome_js_analyze/src/assist/source/use_sorted_keys.rs +++ b/crates/biome_js_analyze/src/assist/source/use_sorted_keys.rs @@ -12,7 +12,7 @@ use biome_diagnostics::{Applicability, category}; use biome_js_factory::make; use biome_js_syntax::{JsObjectExpression, JsObjectMemberList, T}; use biome_rowan::{AstNode, BatchMutationExt, TriviaPieceKind}; -use biome_rule_options::use_sorted_keys::UseSortedKeysOptions; +use biome_rule_options::use_sorted_keys::{SortOrder, UseSortedKeysOptions}; use biome_string_case::comparable_token::ComparableToken; use crate::JsRuleAction; @@ -72,6 +72,51 @@ declare_source_rule! { /// q: 1, /// } /// ``` + /// + /// ## Options + /// This actions accepts following options + /// + /// ### `sortOrder` + /// This options supports `natural` and `lexicographic` values. Where as `natural` is the default. + /// + /// Following will apply the natural sort order. + /// + /// ```json,options + /// { + /// "options": { + /// "sortOrder": "natural" + /// } + /// } + /// ``` + /// ```js,use_options,expect_diagnostic + /// const obj = { + /// val13: 1, + /// val1: 1, + /// val2: 1, + /// val21: 1, + /// val11: 1, + /// }; + /// ``` + /// + /// Following will apply the lexicographic sort order. + /// + /// ```json,options + /// { + /// "options": { + /// "sortOrder": "lexicographic" + /// } + /// } + /// ``` + /// ```js,use_options,expect_diagnostic + /// const obj = { + /// val13: 1, + /// val1: 1, + /// val2: 1, + /// val21: 1, + /// val11: 1, + /// }; + /// ``` + /// pub UseSortedKeys { version: "2.0.0", name: "useSortedKeys", @@ -89,10 +134,21 @@ impl Rule for UseSortedKeys { type Options = UseSortedKeysOptions; fn run(ctx: &RuleContext) -> Self::Signals { - is_separated_list_sorted_by(ctx.query(), |node| node.name().map(ComparableToken::new)) - .ok()? - .not() - .then_some(()) + let options = ctx.options(); + let sort_order = options.sort_order; + let comparator = match sort_order { + SortOrder::Natural => ComparableToken::ascii_nat_cmp, + SortOrder::Lexicographic => ComparableToken::lexicographic_cmp, + }; + + is_separated_list_sorted_by( + ctx.query(), + |node| node.name().map(ComparableToken::new), + comparator, + ) + .ok()? + .not() + .then_some(()) } fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { @@ -115,11 +171,18 @@ impl Rule for UseSortedKeys { fn action(ctx: &RuleContext, _: &Self::State) -> Option { let list = ctx.query(); + let options = ctx.options(); + let sort_order = options.sort_order; + let comparator = match sort_order { + SortOrder::Natural => ComparableToken::ascii_nat_cmp, + SortOrder::Lexicographic => ComparableToken::lexicographic_cmp, + }; let new_list = sorted_separated_list_by( list, |node| node.name().map(ComparableToken::new), || make::token(T![,]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), + comparator, ) .ok()?; diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-lexicographic.js b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-lexicographic.js new file mode 100644 index 000000000000..78fce545f4f9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-lexicographic.js @@ -0,0 +1,20 @@ +import { Module, module, Module1, Module2, module2, module1, module21, module1, module2, module11 } from "@scopeX/special" +import { var1, var2, var21, var11, var12, var22 } from 'custom-package' +import { + BlindedBeaconBlock, + Attestation, + Epoch, + ProducedBlockSource, + Slot, + UintBn64, + ValidatorIndex, + altair, + BLSSignature, + phase0, + ssz, + Root, + sszTypesFor, + stringType, + CommitteeIndex, + BeaconBlockOrContents, +} from "@lodestar/types"; diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-lexicographic.js.snap b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-lexicographic.js.snap new file mode 100644 index 000000000000..b046ea7265ee --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-lexicographic.js.snap @@ -0,0 +1,87 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 134 +expression: custom-sort-lexicographic.js +--- +# Input +```js +import { Module, module, Module1, Module2, module2, module1, module21, module1, module2, module11 } from "@scopeX/special" +import { var1, var2, var21, var11, var12, var22 } from 'custom-package' +import { + BlindedBeaconBlock, + Attestation, + Epoch, + ProducedBlockSource, + Slot, + UintBn64, + ValidatorIndex, + altair, + BLSSignature, + phase0, + ssz, + Root, + sszTypesFor, + stringType, + CommitteeIndex, + BeaconBlockOrContents, +} from "@lodestar/types"; + +``` + +# Diagnostics +``` +custom-sort-lexicographic.js:1:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The imports and exports are not sorted. + + > 1 │ import { Module, module, Module1, Module2, module2, module1, module21, module1, module2, module11 } from "@scopeX/special" + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 2 │ import { var1, var2, var21, var11, var12, var22 } from 'custom-package' + 3 │ import { + + i Safe fix: Organize Imports (Biome) + + 1 │ - import·{·Module,·module,·Module1,·Module2,·module2,·module1,·module21,·module1,·module2,·module11··}·from·"@scopeX/special" + 2 │ - import·{·var1,·var2,·var21,·var11,·var12,·var22·}·from·'custom-package' + 3 │ - import·{ + 4 │ - ··BlindedBeaconBlock,·· + 5 │ - ··Attestation,·· + 6 │ - ··Epoch, + 7 │ - ··ProducedBlockSource, + 8 │ - ··Slot, + 9 │ - ··UintBn64, + 10 │ - ··ValidatorIndex, + 11 │ - ··altair, + 12 │ - ··BLSSignature, + 13 │ - ··phase0, + 14 │ - ··ssz, + 15 │ - ··Root, + 16 │ - ··sszTypesFor, + 17 │ - ··stringType, + 18 │ - ··CommitteeIndex, + 19 │ - ··BeaconBlockOrContents, + 20 │ - }·from·"@lodestar/types"; + 1 │ + import·{ + 2 │ + ··Attestation,·· + 3 │ + ··BLSSignature, + 4 │ + ··BeaconBlockOrContents, + 5 │ + ··BlindedBeaconBlock,·· + 6 │ + ··CommitteeIndex, + 7 │ + ··Epoch, + 8 │ + ··ProducedBlockSource, + 9 │ + ··Root, + 10 │ + ··Slot, + 11 │ + ··UintBn64, + 12 │ + ··ValidatorIndex, + 13 │ + ··altair, + 14 │ + ··phase0, + 15 │ + ··ssz, + 16 │ + ··sszTypesFor, + 17 │ + ··stringType, + 18 │ + }·from·"@lodestar/types"; + 19 │ + import·{·Module,·Module1,·Module2,·module,·module1,·module2,·module21,·module1,·module2,·module11··}·from·"@scopeX/special" + 20 │ + import·{·var1,·var11,·var12,·var2,·var21,·var22·}·from·'custom-package' + 21 21 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-lexicographic.options.json b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-lexicographic.options.json new file mode 100644 index 000000000000..b23868303464 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-lexicographic.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "organizeImports": { + "level": "on", + "options": { + "identifierOrder": "lexicographic" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-natural.js b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-natural.js new file mode 100644 index 000000000000..78fce545f4f9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-natural.js @@ -0,0 +1,20 @@ +import { Module, module, Module1, Module2, module2, module1, module21, module1, module2, module11 } from "@scopeX/special" +import { var1, var2, var21, var11, var12, var22 } from 'custom-package' +import { + BlindedBeaconBlock, + Attestation, + Epoch, + ProducedBlockSource, + Slot, + UintBn64, + ValidatorIndex, + altair, + BLSSignature, + phase0, + ssz, + Root, + sszTypesFor, + stringType, + CommitteeIndex, + BeaconBlockOrContents, +} from "@lodestar/types"; diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-natural.js.snap b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-natural.js.snap new file mode 100644 index 000000000000..157f650d4220 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-natural.js.snap @@ -0,0 +1,87 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 134 +expression: custom-sort-natural.js +--- +# Input +```js +import { Module, module, Module1, Module2, module2, module1, module21, module1, module2, module11 } from "@scopeX/special" +import { var1, var2, var21, var11, var12, var22 } from 'custom-package' +import { + BlindedBeaconBlock, + Attestation, + Epoch, + ProducedBlockSource, + Slot, + UintBn64, + ValidatorIndex, + altair, + BLSSignature, + phase0, + ssz, + Root, + sszTypesFor, + stringType, + CommitteeIndex, + BeaconBlockOrContents, +} from "@lodestar/types"; + +``` + +# Diagnostics +``` +custom-sort-natural.js:1:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The imports and exports are not sorted. + + > 1 │ import { Module, module, Module1, Module2, module2, module1, module21, module1, module2, module11 } from "@scopeX/special" + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 2 │ import { var1, var2, var21, var11, var12, var22 } from 'custom-package' + 3 │ import { + + i Safe fix: Organize Imports (Biome) + + 1 │ - import·{·Module,·module,·Module1,·Module2,·module2,·module1,·module21,·module1,·module2,·module11··}·from·"@scopeX/special" + 2 │ - import·{·var1,·var2,·var21,·var11,·var12,·var22·}·from·'custom-package' + 3 │ - import·{ + 4 │ - ··BlindedBeaconBlock,·· + 5 │ - ··Attestation,·· + 6 │ - ··Epoch, + 7 │ - ··ProducedBlockSource, + 8 │ - ··Slot, + 9 │ - ··UintBn64, + 10 │ - ··ValidatorIndex, + 11 │ - ··altair, + 12 │ - ··BLSSignature, + 13 │ - ··phase0, + 14 │ - ··ssz, + 15 │ - ··Root, + 16 │ - ··sszTypesFor, + 17 │ - ··stringType, + 18 │ - ··CommitteeIndex, + 19 │ - ··BeaconBlockOrContents, + 20 │ - }·from·"@lodestar/types"; + 1 │ + import·{ + 2 │ + ··Attestation,·· + 3 │ + ··altair, + 4 │ + ··BeaconBlockOrContents, + 5 │ + ··BLSSignature, + 6 │ + ··BlindedBeaconBlock,·· + 7 │ + ··CommitteeIndex, + 8 │ + ··Epoch, + 9 │ + ··ProducedBlockSource, + 10 │ + ··phase0, + 11 │ + ··Root, + 12 │ + ··Slot, + 13 │ + ··ssz, + 14 │ + ··sszTypesFor, + 15 │ + ··stringType, + 16 │ + ··UintBn64, + 17 │ + ··ValidatorIndex, + 18 │ + }·from·"@lodestar/types"; + 19 │ + import·{·Module,·Module1,·Module2,·module,·module1,·module2,·module21,·module1,·module2,·module11··}·from·"@scopeX/special" + 20 │ + import·{·var1,·var2,·var11,·var12,·var21,·var22·}·from·'custom-package' + 21 21 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-natural.options.json b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-natural.options.json new file mode 100644 index 000000000000..f8ac89214ee8 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-sort-natural.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "organizeImports": { + "level": "on", + "options": { + "identifierOrder": "natural" + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-lexicographic.jsx b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-lexicographic.jsx new file mode 100644 index 000000000000..2c482de01d99 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-lexicographic.jsx @@ -0,0 +1,3 @@ +; +; + diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-lexicographic.jsx.snap b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-lexicographic.jsx.snap new file mode 100644 index 000000000000..3540a39dec5d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-lexicographic.jsx.snap @@ -0,0 +1,35 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 134 +expression: sorted-lexicographic.jsx +--- +# Input +```jsx +; +; + + +``` + +# Diagnostics +``` +sorted-lexicographic.jsx:2:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The attributes are not sorted. + + 1 │ ; + > 2 │ ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 │ + 4 │ + + i Safe fix: Sort the JSX props. + + 1 1 │ ; + 2 │ - ; + 2 │ + ; + 3 3 │ + 4 4 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-lexicographic.options.json b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-lexicographic.options.json new file mode 100644 index 000000000000..0f678f1fbbf3 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-lexicographic.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "useSortedAttributes": { + "level": "on", + "options": { + "sortOrder": "lexicographic" + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-natural.jsx b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-natural.jsx new file mode 100644 index 000000000000..2c482de01d99 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-natural.jsx @@ -0,0 +1,3 @@ +; +; + diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-natural.jsx.snap b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-natural.jsx.snap new file mode 100644 index 000000000000..6e4007c2a719 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-natural.jsx.snap @@ -0,0 +1,35 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 134 +expression: sorted-natural.jsx +--- +# Input +```jsx +; +; + + +``` + +# Diagnostics +``` +sorted-natural.jsx:2:1 assist/source/useSortedAttributes FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The attributes are not sorted. + + 1 │ ; + > 2 │ ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 │ + 4 │ + + i Safe fix: Sort the JSX props. + + 1 1 │ ; + 2 │ - ; + 2 │ + ; + 3 3 │ + 4 4 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-natural.options.json b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-natural.options.json new file mode 100644 index 000000000000..083c09e37c77 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedAttributes/sorted-natural.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "useSortedAttributes": { + "level": "on", + "options": { + "sortOrder": "natural" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.js b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.js new file mode 100644 index 000000000000..738bbd017fa5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.js @@ -0,0 +1,7 @@ +const obj = { + val13: 1, + val1: 1, + val2: 1, + val21: 1, + val11: 1, +}; diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.js.snap b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.js.snap new file mode 100644 index 000000000000..e343786e34d7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.js.snap @@ -0,0 +1,52 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 134 +expression: sorted-lexicographic.js +--- +# Input +```js +const obj = { + val13: 1, + val1: 1, + val2: 1, + val21: 1, + val11: 1, +}; + +``` + +# Diagnostics +``` +sorted-lexicographic.js:2:2 assist/source/useSortedKeys FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The object properties are not sorted by key. + + 1 │ const obj = { + > 2 │ val13: 1, + │ ^^^^^^^^^ + > 3 │ val1: 1, + > 4 │ val2: 1, + > 5 │ val21: 1, + > 6 │ val11: 1, + │ ^^^^^^^^^ + 7 │ }; + 8 │ + + i Safe fix: Sort the object properties by key. + + 1 1 │ const obj = { + 2 │ - → val13:·1, + 3 │ - → val1:·1, + 4 │ - → val2:·1, + 5 │ - → val21:·1, + 6 │ - → val11:·1, + 2 │ + → val1:·1, + 3 │ + → val11:·1, + 4 │ + → val13:·1, + 5 │ + → val2:·1, + 6 │ + → val21:·1, + 7 7 │ }; + 8 8 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.options.json b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.options.json new file mode 100644 index 000000000000..a7bb7afbacad --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "useSortedKeys": { + "level": "on", + "options": { + "sortOrder": "lexicographic" + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-natural.js b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-natural.js new file mode 100644 index 000000000000..738bbd017fa5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-natural.js @@ -0,0 +1,7 @@ +const obj = { + val13: 1, + val1: 1, + val2: 1, + val21: 1, + val11: 1, +}; diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-natural.js.snap b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-natural.js.snap new file mode 100644 index 000000000000..5e7ff6ff488c --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-natural.js.snap @@ -0,0 +1,52 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 134 +expression: sorted-natural.js +--- +# Input +```js +const obj = { + val13: 1, + val1: 1, + val2: 1, + val21: 1, + val11: 1, +}; + +``` + +# Diagnostics +``` +sorted-natural.js:2:2 assist/source/useSortedKeys FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The object properties are not sorted by key. + + 1 │ const obj = { + > 2 │ val13: 1, + │ ^^^^^^^^^ + > 3 │ val1: 1, + > 4 │ val2: 1, + > 5 │ val21: 1, + > 6 │ val11: 1, + │ ^^^^^^^^^ + 7 │ }; + 8 │ + + i Safe fix: Sort the object properties by key. + + 1 1 │ const obj = { + 2 │ - → val13:·1, + 3 │ - → val1:·1, + 4 │ - → val2:·1, + 5 │ - → val21:·1, + 6 │ - → val11:·1, + 2 │ + → val1:·1, + 3 │ + → val2:·1, + 4 │ + → val11:·1, + 5 │ + → val13:·1, + 6 │ + → val21:·1, + 7 7 │ }; + 8 8 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-natural.options.json b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-natural.options.json new file mode 100644 index 000000000000..afb39daa0929 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/useSortedKeys/sorted-natural.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "useSortedKeys": { + "level": "on", + "options": { + "sortOrder": "natural" + } + } + } + } + } +} diff --git a/crates/biome_json_analyze/src/assist/source/use_sorted_keys.rs b/crates/biome_json_analyze/src/assist/source/use_sorted_keys.rs index fa8bc6fa8fe5..6a34d0385d03 100644 --- a/crates/biome_json_analyze/src/assist/source/use_sorted_keys.rs +++ b/crates/biome_json_analyze/src/assist/source/use_sorted_keys.rs @@ -8,7 +8,7 @@ use biome_diagnostics::category; use biome_json_factory::make; use biome_json_syntax::{JsonMemberList, JsonObjectValue, T, TextRange}; use biome_rowan::{AstNode, BatchMutationExt}; -use biome_rule_options::use_sorted_keys::UseSortedKeysOptions; +use biome_rule_options::use_sorted_keys::{SortOrder, UseSortedKeysOptions}; use biome_string_case::comparable_token::ComparableToken; use std::ops::Not; @@ -26,6 +26,51 @@ declare_source_rule! { /// } /// } /// ``` + /// + /// ## Options + /// This actions accepts following options + /// + /// ### `sortOrder` + /// This options supports `natural` and `lexicographic` values. Where as `natural` is the default. + /// + /// Following will apply the natural sort order. + /// + /// ```json,options + /// { + /// "options": { + /// "sortOrder": "natural" + /// } + /// } + /// ``` + /// ```json,use_options,expect_diagnostic + /// { + /// "val13": 1, + /// "val1": 1, + /// "val2": 1, + /// "val21": 1, + /// "val11": 1, + /// } + /// ``` + /// + /// Following will apply the lexicographic sort order. + /// + /// ```json,options + /// { + /// "options": { + /// "sortOrder": "lexicographic" + /// } + /// } + /// ``` + /// ```json,use_options,expect_diagnostic + /// { + /// "val13": 1, + /// "val1": 1, + /// "val2": 1, + /// "val21": 1, + /// "val11": 1, + /// } + /// ``` + /// pub UseSortedKeys { version: "1.9.0", name: "useSortedKeys", @@ -41,13 +86,24 @@ impl Rule for UseSortedKeys { type Options = UseSortedKeysOptions; fn run(ctx: &RuleContext) -> Option { - is_separated_list_sorted_by(ctx.query(), |node| { - node.name() - .ok()? - .inner_string_text() - .ok() - .map(ComparableToken::new) - }) + let options = ctx.options(); + let sort_order = options.sort_order; + let comparator = match sort_order { + SortOrder::Natural => ComparableToken::ascii_nat_cmp, + SortOrder::Lexicographic => ComparableToken::lexicographic_cmp, + }; + + is_separated_list_sorted_by( + ctx.query(), + |node| { + node.name() + .ok()? + .inner_string_text() + .ok() + .map(ComparableToken::new) + }, + comparator, + ) .ok()? .not() .then_some(()) @@ -73,6 +129,12 @@ impl Rule for UseSortedKeys { fn action(ctx: &RuleContext, _state: &Self::State) -> Option { let list = ctx.query(); + let options = ctx.options(); + let sort_order = options.sort_order; + let comparator = match sort_order { + SortOrder::Natural => ComparableToken::ascii_nat_cmp, + SortOrder::Lexicographic => ComparableToken::lexicographic_cmp, + }; let new_list = sorted_separated_list_by( list, @@ -84,6 +146,7 @@ impl Rule for UseSortedKeys { .map(ComparableToken::new) }, || make::token(T![,]), + comparator, ) .ok()?; diff --git a/crates/biome_json_analyze/tests/spec_tests.rs b/crates/biome_json_analyze/tests/spec_tests.rs index 5a460c38f2cb..2cf1f42b3170 100644 --- a/crates/biome_json_analyze/tests/spec_tests.rs +++ b/crates/biome_json_analyze/tests/spec_tests.rs @@ -22,6 +22,11 @@ fn run_test(input: &'static str, _: &str, _: &str, _: &str) { let input_file = Utf8Path::new(input); let file_name = input_file.file_name().unwrap(); + // We should skip running test for .options.json as input_file + if file_name.ends_with(".options.json") || file_name.ends_with(".options.jsonc") { + return; + } + let parser_options = match input_file.extension() { Some("json") => JsonParserOptions::default(), Some("jsonc") => JsonParserOptions::default() diff --git a/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.json b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.json new file mode 100644 index 000000000000..6e11f9deb010 --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.json @@ -0,0 +1,7 @@ +{ + "val13": 1, + "val1": 1, + "val2": 1, + "val21": 1, + "val11": 1 +} diff --git a/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.json.snap b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.json.snap new file mode 100644 index 000000000000..98e1baf06c08 --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.json.snap @@ -0,0 +1,52 @@ +--- +source: crates/biome_json_analyze/tests/spec_tests.rs +assertion_line: 87 +expression: sorted-lexicographic.json +--- +# Input +```json +{ + "val13": 1, + "val1": 1, + "val2": 1, + "val21": 1, + "val11": 1 +} + +``` + +# Diagnostics +``` +sorted-lexicographic.json:1:1 assist/source/useSortedKeys FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The members are not sorted by key. + + > 1 │ { + │ ^ + > 2 │ "val13": 1, + > 3 │ "val1": 1, + > 4 │ "val2": 1, + > 5 │ "val21": 1, + > 6 │ "val11": 1 + > 7 │ } + │ ^ + 8 │ + + i Safe fix: Sort the members by key. + + 1 1 │ { + 2 │ - → "val13":·1, + 3 │ - → "val1":·1, + 4 │ - → "val2":·1, + 5 │ - → "val21":·1, + 6 │ - → "val11":·1 + 2 │ + → "val1":·1, + 3 │ + → "val11":·1, + 4 │ + → "val13":·1, + 5 │ + → "val2":·1, + 6 │ + → "val21":·1 + 7 7 │ } + 8 8 │ + + +``` diff --git a/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.options.json b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.options.json new file mode 100644 index 000000000000..a7bb7afbacad --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-lexicographic.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "useSortedKeys": { + "level": "on", + "options": { + "sortOrder": "lexicographic" + } + } + } + } + } +} diff --git a/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-natural.json b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-natural.json new file mode 100644 index 000000000000..6e11f9deb010 --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-natural.json @@ -0,0 +1,7 @@ +{ + "val13": 1, + "val1": 1, + "val2": 1, + "val21": 1, + "val11": 1 +} diff --git a/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-natural.json.snap b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-natural.json.snap new file mode 100644 index 000000000000..0e21aab920d1 --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-natural.json.snap @@ -0,0 +1,52 @@ +--- +source: crates/biome_json_analyze/tests/spec_tests.rs +assertion_line: 87 +expression: sorted-natural.json +--- +# Input +```json +{ + "val13": 1, + "val1": 1, + "val2": 1, + "val21": 1, + "val11": 1 +} + +``` + +# Diagnostics +``` +sorted-natural.json:1:1 assist/source/useSortedKeys FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The members are not sorted by key. + + > 1 │ { + │ ^ + > 2 │ "val13": 1, + > 3 │ "val1": 1, + > 4 │ "val2": 1, + > 5 │ "val21": 1, + > 6 │ "val11": 1 + > 7 │ } + │ ^ + 8 │ + + i Safe fix: Sort the members by key. + + 1 1 │ { + 2 │ - → "val13":·1, + 3 │ - → "val1":·1, + 4 │ - → "val2":·1, + 5 │ - → "val21":·1, + 6 │ - → "val11":·1 + 2 │ + → "val1":·1, + 3 │ + → "val2":·1, + 4 │ + → "val11":·1, + 5 │ + → "val13":·1, + 6 │ + → "val21":·1 + 7 7 │ } + 8 8 │ + + +``` diff --git a/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-natural.options.json b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-natural.options.json new file mode 100644 index 000000000000..b57696e649df --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/source/useSortedKeys/sorted-natural.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "useSortedKeys": { + "level": "on", + "options": { + "sortOrder": "natural" + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/biome_rule_options/src/organize_imports.rs b/crates/biome_rule_options/src/organize_imports.rs index 6f4ceddf0866..a44229597887 100644 --- a/crates/biome_rule_options/src/organize_imports.rs +++ b/crates/biome_rule_options/src/organize_imports.rs @@ -2,6 +2,7 @@ pub mod import_groups; pub mod import_source; use crate::organize_imports::import_groups::ImportGroups; +pub use crate::shared::sort_order::SortOrder; use biome_deserialize_macros::Deserializable; use serde::{Deserialize, Serialize}; @@ -10,4 +11,5 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase", deny_unknown_fields, default)] pub struct OrganizeImportsOptions { pub groups: ImportGroups, + pub identifier_order: SortOrder, } diff --git a/crates/biome_rule_options/src/shared/mod.rs b/crates/biome_rule_options/src/shared/mod.rs index e744bd2e321b..a9965fbb4866 100644 --- a/crates/biome_rule_options/src/shared/mod.rs +++ b/crates/biome_rule_options/src/shared/mod.rs @@ -1 +1,2 @@ pub mod restricted_regex; +pub mod sort_order; diff --git a/crates/biome_rule_options/src/shared/sort_order.rs b/crates/biome_rule_options/src/shared/sort_order.rs new file mode 100644 index 000000000000..058be1c11118 --- /dev/null +++ b/crates/biome_rule_options/src/shared/sort_order.rs @@ -0,0 +1,18 @@ +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + biome_deserialize_macros::Deserializable, +)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub enum SortOrder { + #[default] + Natural, + Lexicographic, +} diff --git a/crates/biome_rule_options/src/use_sorted_attributes.rs b/crates/biome_rule_options/src/use_sorted_attributes.rs index fee965f6015a..a28f54f5e624 100644 --- a/crates/biome_rule_options/src/use_sorted_attributes.rs +++ b/crates/biome_rule_options/src/use_sorted_attributes.rs @@ -1,6 +1,10 @@ +pub use crate::shared::sort_order::SortOrder; use biome_deserialize_macros::Deserializable; use serde::{Deserialize, Serialize}; + #[derive(Default, Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields, default)] -pub struct UseSortedAttributesOptions {} +pub struct UseSortedAttributesOptions { + pub sort_order: SortOrder, +} diff --git a/crates/biome_rule_options/src/use_sorted_keys.rs b/crates/biome_rule_options/src/use_sorted_keys.rs index 97aedbd23e26..87fd6a69fecb 100644 --- a/crates/biome_rule_options/src/use_sorted_keys.rs +++ b/crates/biome_rule_options/src/use_sorted_keys.rs @@ -1,6 +1,10 @@ +pub use crate::shared::sort_order::SortOrder; use biome_deserialize_macros::Deserializable; use serde::{Deserialize, Serialize}; + #[derive(Default, Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields, default)] -pub struct UseSortedKeysOptions {} +pub struct UseSortedKeysOptions { + pub sort_order: SortOrder, +} diff --git a/crates/biome_string_case/src/comparable_token.rs b/crates/biome_string_case/src/comparable_token.rs index 569efcfe0121..f16596747210 100644 --- a/crates/biome_string_case/src/comparable_token.rs +++ b/crates/biome_string_case/src/comparable_token.rs @@ -13,6 +13,14 @@ impl ComparableToken { pub const fn new(token: TokenText) -> Self { Self { token } } + + pub fn ascii_nat_cmp(&self, other: &Self) -> Ordering { + self.token.text().ascii_nat_cmp(other.token.text()) + } + + pub fn lexicographic_cmp(&self, other: &Self) -> Ordering { + self.token.text().lexicographic_cmp(other.token.text()) + } } impl From for ComparableToken { fn from(value: TokenText) -> Self { diff --git a/crates/biome_string_case/src/lib.rs b/crates/biome_string_case/src/lib.rs index f138f77ae63d..601c24f464a8 100644 --- a/crates/biome_string_case/src/lib.rs +++ b/crates/biome_string_case/src/lib.rs @@ -649,6 +649,11 @@ pub trait StrLikeExtension: ToOwned { /// Uppercase letters come first (e.g. `A` < `a` < `B` < `b`) /// and number are compared in a human way (e.g. `9` < `10`). fn ascii_nat_cmp(&self, other: &Self) -> Ordering; + + /// Compare two strings using lexicographically by their byte values. + /// + /// This orders Unicode code points based on their positions in the code charts. + fn lexicographic_cmp(&self, other: &Self) -> Ordering; } pub trait StrOnlyExtension: ToOwned { @@ -672,6 +677,10 @@ impl StrLikeExtension for str { fn ascii_nat_cmp(&self, other: &Self) -> Ordering { self.as_bytes().ascii_nat_cmp(other.as_bytes()) } + + fn lexicographic_cmp(&self, other: &Self) -> Ordering { + self.as_bytes().lexicographic_cmp(other.as_bytes()) + } } impl StrOnlyExtension for str { @@ -704,6 +713,10 @@ impl StrLikeExtension for std::ffi::OsStr { self.as_encoded_bytes() .ascii_nat_cmp(other.as_encoded_bytes()) } + + fn lexicographic_cmp(&self, other: &Self) -> Ordering { + self.as_encoded_bytes().cmp(other.as_encoded_bytes()) + } } impl StrLikeExtension for [u8] { @@ -719,6 +732,10 @@ impl StrLikeExtension for [u8] { fn ascii_nat_cmp(&self, other: &Self) -> Ordering { CldrAsciiCollator.cmp(self.iter().copied(), other.iter().copied()) } + + fn lexicographic_cmp(&self, other: &Self) -> Ordering { + self.iter().copied().cmp(other.iter().copied()) + } } // TODO: Once trait-alias are stabilized it would be enough to `use` this trait instead of individual ones. diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index ddfa34a3406e..970e73dae18e 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -3455,9 +3455,14 @@ export type RuleFixConfiguration_for_UseStrictModeOptions = | RuleWithFixOptions_for_UseStrictModeOptions; export interface OrganizeImportsOptions { groups?: ImportGroups; + identifierOrder?: SortOrder; +} +export interface UseSortedAttributesOptions { + sortOrder?: SortOrder; +} +export interface UseSortedKeysOptions { + sortOrder?: SortOrder; } -export interface UseSortedAttributesOptions {} -export interface UseSortedKeysOptions {} export interface UseSortedPropertiesOptions {} export type RulePlainConfiguration = "off" | "on" | "info" | "warn" | "error"; export interface RuleWithFixOptions_for_NoAccessKeyOptions { @@ -7405,6 +7410,7 @@ export interface RuleWithFixOptions_for_UseStrictModeOptions { options: UseStrictModeOptions; } export type ImportGroups = ImportGroup[]; +export type SortOrder = "natural" | "lexicographic"; /** * Used to identify the kind of code action emitted by a rule */ diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 895eee7d7496..7f789cdae9c7 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -5064,6 +5064,10 @@ "groups": { "default": [], "allOf": [{ "$ref": "#/definitions/ImportGroups" }] + }, + "identifierOrder": { + "default": "natural", + "allOf": [{ "$ref": "#/definitions/SortOrder" }] } }, "additionalProperties": false @@ -11422,6 +11426,7 @@ { "$ref": "#/definitions/Suspicious" } ] }, + "SortOrder": { "type": "string", "enum": ["natural", "lexicographic"] }, "Source": { "description": "A list of rules that belong to this group", "type": "object", @@ -13565,6 +13570,12 @@ }, "UseSortedAttributesOptions": { "type": "object", + "properties": { + "sortOrder": { + "default": "natural", + "allOf": [{ "$ref": "#/definitions/SortOrder" }] + } + }, "additionalProperties": false }, "UseSortedClassesConfiguration": { @@ -13589,7 +13600,16 @@ }, "additionalProperties": false }, - "UseSortedKeysOptions": { "type": "object", "additionalProperties": false }, + "UseSortedKeysOptions": { + "type": "object", + "properties": { + "sortOrder": { + "default": "natural", + "allOf": [{ "$ref": "#/definitions/SortOrder" }] + } + }, + "additionalProperties": false + }, "UseSortedPropertiesOptions": { "type": "object", "additionalProperties": false