Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
957a9d4
Update utils to accept custom comparator
nazarhussain Jul 2, 2025
afc8b9d
Update string_case crate to have flexible sorting methods
nazarhussain Jul 2, 2025
7138649
Add sortMode to useSortedKeys rule
nazarhussain Jul 2, 2025
5f1e195
Add sortMode to useSortedAttributes rule
nazarhussain Jul 2, 2025
8c06689
Add sortMode to organizeImports rule
nazarhussain Jul 2, 2025
a8b8a7d
Update the configuration schema
nazarhussain Jul 2, 2025
e2d95a8
Rename sort_mode to sort_order
nazarhussain Jul 3, 2025
aa4f143
Update sortOrder to identifierOrder for organizeImports
nazarhussain Jul 3, 2025
fd898bb
Remove files accidently committed
nazarhussain Jul 3, 2025
05d7301
Update the code as per feedback
nazarhussain Jul 7, 2025
2cc1c76
Fix the sorting comaprision
nazarhussain Jul 7, 2025
842ebb9
Update the constraint from Key
nazarhussain Jul 7, 2025
785ce1a
Remove unintended snapshot
nazarhussain Jul 7, 2025
ec37c2a
Update comaparision of None vs Some
nazarhussain Jul 7, 2025
9725c0f
Cleanup the comments
nazarhussain Jul 7, 2025
1cacd43
Clean up some duplicate code
nazarhussain Jul 7, 2025
24399e6
Fix formatting
nazarhussain Jul 8, 2025
3001a47
Fix linting
nazarhussain Jul 8, 2025
b177951
Add a changeset file
nazarhussain Jul 8, 2025
b97422b
Add link for the rules in changeset
nazarhussain Jul 8, 2025
9934529
Apply suggestions from code review
nazarhussain Jul 8, 2025
f6a1102
Update the docs for update options
nazarhussain Jul 8, 2025
ddc4455
Merge branch 'nh/import-sort-config' of github.com:nazarhussain/biome…
nazarhussain Jul 8, 2025
b9d4e0a
Fix codegen and rules check issue
nazarhussain Jul 8, 2025
213ba0d
Fix config schema linting
nazarhussain Jul 8, 2025
55b5730
Update the docs and added exampels for ne rule options
nazarhussain Jul 9, 2025
0ba0eba
Update auto-generated code
nazarhussain Jul 9, 2025
b4fb73d
Merge branch 'next' into nh/import-sort-config
nazarhussain Jul 21, 2025
e2e4556
Update the lint rules
nazarhussain Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/fancy-trains-happen.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 14 additions & 9 deletions crates/biome_analyze/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@ 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.
///
/// 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<Language = L> + 'a,
Key: Ord,
>(
pub fn is_separated_list_sorted_by<'a, L: Language + 'a, N: AstNode<Language = L> + 'a, Key>(
list: &impl AstSeparatedList<Language = L, Node = N>,
get_key: impl Fn(&N) -> Option<Key>,
comparator: impl Fn(&Key, &Key) -> Ordering,
) -> Result<bool, SyntaxError> {
let mut is_sorted = true;

if list.len() > 1 {
let mut previous_key: Option<Key> = None;
for AstSeparatedElement {
Expand All @@ -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;
}
Expand All @@ -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<Key>,
make_separator: fn() -> SyntaxToken<L>,
comparator: impl Fn(&Key, &Key) -> Ordering,
) -> Result<List, SyntaxError>
where
List: AstSeparatedList<Language = L, Node = Node> + AstNode<Language = L> + 'a,
Expand All @@ -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,
Expand Down
64 changes: 53 additions & 11 deletions crates/biome_js_analyze/src/assist/source/organize_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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'
/// ```
///
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, you want to add a snippet that uses the options you defined at line 631. Create a js code block:

```js,use_options,expect_diagnostic
// code here
```

Of course, you want to use a code that shows how the natural order is applied

pub OrganizeImports {
version: "1.0.0",
name: "organizeImports",
Expand Down Expand Up @@ -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<ChunkBuilder> = None;
let mut prev_kind: Option<JsSyntaxKind> = None;
let mut prev_group = 0;
Expand All @@ -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)
Expand Down Expand Up @@ -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<u32, AnyJsModuleItem> = FxHashMap::default();
Expand Down Expand Up @@ -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();
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -1055,6 +1094,7 @@ pub enum NewLineIssue {
fn merge(
item1: Option<&AnyJsModuleItem>,
item2: Option<&AnyJsModuleItem>,
sort_order: SortOrder,
) -> Option<AnyJsModuleItem> {
match (item1?, item2?) {
(AnyJsModuleItem::JsExport(item1), AnyJsModuleItem::JsExport(item2)) => {
Expand All @@ -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());

Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand Down
Loading