Skip to content

Commit 265367e

Browse files
committed
fix(organizeImports): sort specifiers in bare exports
1 parent cb4d7d7 commit 265367e

File tree

6 files changed

+183
-31
lines changed

6 files changed

+183
-31
lines changed

.changeset/neat-papers-think.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Partially fix [#7583](https://github.com/biomejs/biome/issues/7583).
6+
[`organizeImports`](https://biomejs.dev/assist/actions/organize-imports/) now
7+
sorts named specifiers inside bare exports.
8+
9+
```diff
10+
- export { b, a };
11+
+ export { a, b };
12+
```
13+

crates/biome_js_analyze/src/assist/source/organize_imports.rs

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
pub mod import_key;
2+
pub mod specifiers_attributes;
3+
mod util;
4+
5+
use crate::JsRuleAction;
16
use biome_analyze::{
27
ActionCategory, Ast, FixKind, Rule, RuleDiagnostic, RuleSource, SourceActionKind,
38
context::RuleContext, declare_source_rule,
@@ -14,17 +19,12 @@ use biome_rule_options::{organize_imports::OrganizeImportsOptions, sort_order::S
1419
use import_key::{ImportInfo, ImportKey};
1520
use rustc_hash::FxHashMap;
1621
use specifiers_attributes::{
17-
are_import_attributes_sorted, merge_export_specifiers, merge_import_specifiers,
18-
sort_attributes, sort_export_specifiers, sort_import_specifiers,
22+
JsNamedSpecifiers, are_import_attributes_sorted, merge_export_from_specifiers,
23+
merge_import_specifiers, sort_attributes, sort_export_from_specifiers, sort_export_specifiers,
24+
sort_import_specifiers,
1925
};
20-
21-
use crate::JsRuleAction;
2226
use util::{attached_trivia, detached_trivia, has_detached_leading_comment, leading_newlines};
2327

24-
pub mod import_key;
25-
pub mod specifiers_attributes;
26-
mod util;
27-
2828
declare_source_rule! {
2929
/// Provides a code action to sort the imports and exports in the file using a built-in or custom order.
3030
///
@@ -825,22 +825,43 @@ impl Rule for OrganizeImports {
825825
prev_group = key.group;
826826
chunk = Some(ChunkBuilder::new(key));
827827
}
828-
} else if chunk.is_some() {
829-
// This is either
830-
// - a bare (side-effect) import
831-
// - a buggy import or export
832-
// a statement
833-
//
834-
// In any case, the chunk ends here
835-
report_unsorted_chunk(chunk.take(), &mut result);
836-
prev_group = 0;
837-
// A statement must be separated of a chunk with a blank line
838-
if let AnyJsModuleItem::AnyJsStatement(statement) = &item
839-
&& leading_newlines(statement.syntax()).count() == 1
828+
} else {
829+
if chunk.is_some() {
830+
// This is either
831+
// - a bare (side-effect) import
832+
// - an export without `from` clause
833+
// - a buggy import or export
834+
// - a statement
835+
//
836+
// In any case, the chunk ends here
837+
report_unsorted_chunk(chunk.take(), &mut result);
838+
prev_group = 0;
839+
// A statement must be separated of a chunk with a blank line
840+
if let AnyJsModuleItem::AnyJsStatement(statement) = &item
841+
&& leading_newlines(statement.syntax()).count() == 1
842+
{
843+
result.push(Issue::AddLeadingNewline {
844+
slot_index: statement.syntax().index() as u32,
845+
});
846+
}
847+
}
848+
if let AnyJsModuleItem::JsExport(js_export) = &item
849+
&& let Ok(AnyJsExportClause::JsExportNamedClause(clause)) =
850+
js_export.export_clause()
840851
{
841-
result.push(Issue::AddLeadingNewline {
842-
slot_index: statement.syntax().index() as u32,
843-
});
852+
let specifiers =
853+
JsNamedSpecifiers::JsExportNamedSpecifierList(clause.specifiers());
854+
let are_specifiers_unsorted = !specifiers.are_sorted(sort_order);
855+
if are_specifiers_unsorted {
856+
// Report the violation of one of the previous requirement
857+
result.push(Issue::UnorganizedItem {
858+
slot_index: item.syntax().index() as u32,
859+
are_specifiers_unsorted,
860+
// An export without `from` clause has no attributes.
861+
are_attributes_unsorted: true,
862+
newline_issue: NewLineIssue::None,
863+
});
864+
}
844865
}
845866
}
846867
prev_kind = Some(item.syntax().kind());
@@ -902,6 +923,11 @@ impl Rule for OrganizeImports {
902923
if *are_specifiers_unsorted {
903924
// Sort named specifiers
904925
if let AnyJsExportClause::JsExportNamedFromClause(cast) = &clause
926+
&& let Some(sorted_specifiers) =
927+
sort_export_from_specifiers(&cast.specifiers(), sort_order)
928+
{
929+
clause = cast.clone().with_specifiers(sorted_specifiers).into();
930+
} else if let AnyJsExportClause::JsExportNamedClause(cast) = &clause
905931
&& let Some(sorted_specifiers) =
906932
sort_export_specifiers(&cast.specifiers(), sort_order)
907933
{
@@ -1149,7 +1175,7 @@ fn merge(
11491175
let specifiers1 = clause1.specifiers();
11501176
let specifiers2 = clause2.specifiers();
11511177
if let Some(meregd_specifiers) =
1152-
merge_export_specifiers(&specifiers1, &specifiers2, sort_order)
1178+
merge_export_from_specifiers(&specifiers1, &specifiers2, sort_order)
11531179
{
11541180
let meregd_clause = clause1.with_specifiers(meregd_specifiers);
11551181
let merged_item = item2.clone().with_export_clause(meregd_clause.into());

crates/biome_js_analyze/src/assist/source/organize_imports/specifiers_attributes.rs

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use biome_analyze::utils::{is_separated_list_sorted_by, sorted_separated_list_by};
22
use biome_js_factory::make;
33
use biome_js_syntax::{
4-
AnyJsBinding, AnyJsImportAssertionEntry, JsExportNamedFromSpecifierList, JsImportAssertion,
5-
JsNamedImportSpecifiers, T, inner_string_text,
4+
AnyJsBinding, AnyJsImportAssertionEntry, JsExportNamedFromSpecifierList,
5+
JsExportNamedSpecifierList, JsImportAssertion, JsNamedImportSpecifiers, T, inner_string_text,
66
};
77
use biome_rowan::{AstNode, AstSeparatedElement, AstSeparatedList, TriviaPieceKind};
88
use biome_rule_options::organize_imports::SortOrder;
@@ -12,6 +12,7 @@ use std::cmp::Ordering;
1212
pub enum JsNamedSpecifiers {
1313
JsNamedImportSpecifiers(JsNamedImportSpecifiers),
1414
JsExportNamedFromSpecifierList(JsExportNamedFromSpecifierList),
15+
JsExportNamedSpecifierList(JsExportNamedSpecifierList),
1516
}
1617
impl JsNamedSpecifiers {
1718
pub fn are_sorted(&self, sort_order: SortOrder) -> bool {
@@ -20,6 +21,9 @@ impl JsNamedSpecifiers {
2021
are_import_specifiers_sorted(specifeirs, sort_order)
2122
}
2223
Self::JsExportNamedFromSpecifierList(specifeirs) => {
24+
are_export_from_specifiers_sorted(specifeirs, sort_order)
25+
}
26+
Self::JsExportNamedSpecifierList(specifeirs) => {
2327
are_export_specifiers_sorted(specifeirs, sort_order)
2428
}
2529
}
@@ -111,7 +115,7 @@ pub fn merge_import_specifiers(
111115
sort_import_specifiers(named_specifiers1.with_specifiers(new_list), sort_order)
112116
}
113117

114-
pub fn are_export_specifiers_sorted(
118+
pub fn are_export_from_specifiers_sorted(
115119
specifiers: &JsExportNamedFromSpecifierList,
116120
sort_order: SortOrder,
117121
) -> Option<bool> {
@@ -131,7 +135,7 @@ pub fn are_export_specifiers_sorted(
131135
.ok()
132136
}
133137

134-
pub fn sort_export_specifiers(
138+
pub fn sort_export_from_specifiers(
135139
named_specifiers: &JsExportNamedFromSpecifierList,
136140
sort_order: SortOrder,
137141
) -> Option<JsExportNamedFromSpecifierList> {
@@ -152,7 +156,7 @@ pub fn sort_export_specifiers(
152156
Some(new_list)
153157
}
154158

155-
pub fn merge_export_specifiers(
159+
pub fn merge_export_from_specifiers(
156160
specifiers1: &JsExportNamedFromSpecifierList,
157161
specifiers2: &JsExportNamedFromSpecifierList,
158162
sort_order: SortOrder,
@@ -185,12 +189,92 @@ pub fn merge_export_specifiers(
185189
separators.push(separator);
186190
}
187191
}
188-
sort_export_specifiers(
192+
sort_export_from_specifiers(
189193
&make::js_export_named_from_specifier_list(nodes, separators),
190194
sort_order,
191195
)
192196
}
193197

198+
pub fn are_export_specifiers_sorted(
199+
specifiers: &JsExportNamedSpecifierList,
200+
sort_order: SortOrder,
201+
) -> Option<bool> {
202+
let comparator = get_comparator(sort_order);
203+
204+
is_separated_list_sorted_by(
205+
specifiers,
206+
|node| {
207+
node.local_name()
208+
.ok()?
209+
.name()
210+
.ok()
211+
.map(ComparableToken::new)
212+
},
213+
comparator,
214+
)
215+
.ok()
216+
}
217+
218+
pub fn sort_export_specifiers(
219+
named_specifiers: &JsExportNamedSpecifierList,
220+
sort_order: SortOrder,
221+
) -> Option<JsExportNamedSpecifierList> {
222+
let comparator = get_comparator(sort_order);
223+
let new_list = sorted_separated_list_by(
224+
named_specifiers,
225+
|node| {
226+
node.local_name()
227+
.ok()?
228+
.name()
229+
.ok()
230+
.map(ComparableToken::new)
231+
},
232+
|| make::token(T![,]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
233+
comparator,
234+
)
235+
.ok()?;
236+
Some(new_list)
237+
}
238+
239+
pub fn merge_export_specifiers(
240+
specifiers1: &JsExportNamedSpecifierList,
241+
specifiers2: &JsExportNamedSpecifierList,
242+
sort_order: SortOrder,
243+
) -> Option<JsExportNamedSpecifierList> {
244+
let mut nodes = Vec::with_capacity(specifiers1.len() + specifiers2.len());
245+
let mut separators = Vec::with_capacity(specifiers1.len() + specifiers2.len());
246+
for AstSeparatedElement {
247+
node,
248+
trailing_separator,
249+
} in specifiers1.elements()
250+
{
251+
let separator = trailing_separator.ok()?;
252+
let mut node = node.ok()?;
253+
if separator.is_none() {
254+
node = node.trim_trailing_trivia()?;
255+
}
256+
let separator = separator.unwrap_or_else(|| {
257+
make::token(T![,]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")])
258+
});
259+
nodes.push(node);
260+
separators.push(separator);
261+
}
262+
for AstSeparatedElement {
263+
node,
264+
trailing_separator,
265+
} in specifiers2.elements()
266+
{
267+
nodes.push(node.ok()?);
268+
if let Some(separator) = trailing_separator.ok()? {
269+
separators.push(separator);
270+
}
271+
}
272+
sort_export_specifiers(
273+
&make::js_export_named_specifier_list(nodes, separators),
274+
sort_order,
275+
)
276+
}
277+
194278
pub fn are_import_attributes_sorted(
195279
attributes: &JsImportAssertion,
196280
sort_order: SortOrder,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { b, a }
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: unsorted-from-less-export.js
4+
---
5+
# Input
6+
```js
7+
export { b, a }
8+
9+
```
10+
11+
# Diagnostics
12+
```
13+
unsorted-from-less-export.js:1:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━
14+
15+
i The imports and exports are not sorted.
16+
17+
> 1 │ export { b, a }
18+
│ ^^^^^^^^^^^^^^^
19+
2 │
20+
21+
i Safe fix: Organize Imports (Biome)
22+
23+
1 │ - export·{·ba·}
24+
1 │ + export·{·ab·}
25+
2 2 │
26+
27+
28+
```

crates/biome_lsp/src/server.tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2016,7 +2016,7 @@ async fn pull_code_actions_with_import_sorting() -> Result<()> {
20162016
import z from "zod";
20172017
import { test } from "./test";
20182018
import { describe } from "node:test";
2019-
export { z, test, describe };
2019+
export { describe, test, z };
20202020
20212021
if(a === -0) {}
20222022
"#,

0 commit comments

Comments
 (0)