Skip to content

Commit cadf73d

Browse files
committed
feat(organizeImports): add ignoreBareImports option to control bare import sorting
1 parent 271a437 commit cadf73d

File tree

14 files changed

+295
-52
lines changed

14 files changed

+295
-52
lines changed

.changeset/flat-beers-battle.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@biomejs/biome": minor
3+
---
4+
5+
Added the `ignoreBareImports` option to [`organizeImports`](https://biomejs.dev/assist/actions/organize-imports/),
6+
which allows bare imports to be sorted within other imports when set to `false`.
7+
8+
```json
9+
{
10+
"assist": {
11+
"actions": {
12+
"source": {
13+
"organizeImports": {
14+
"level": "on",
15+
"options": { "ignoreBareImports": false }
16+
}
17+
}
18+
}
19+
}
20+
}
21+
```
22+
23+
```diff
24+
- import "b";
25+
import "a";
26+
+ import "b";
27+
import { A } from "a";
28+
+ import "./file";
29+
import { Local } from "./file";
30+
- import "./file";
31+
```
32+
33+
Note that bare imports are never merged with other imports.

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

Lines changed: 64 additions & 40 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, assist::source::organize_imports::import_key::ImportStatementKind};
16
use biome_analyze::{
27
ActionCategory, Ast, FixKind, Rule, RuleDiagnostic, RuleSource, SourceActionKind,
38
context::RuleContext, declare_source_rule,
@@ -17,20 +22,15 @@ use specifiers_attributes::{
1722
are_import_attributes_sorted, merge_export_specifiers, merge_import_specifiers,
1823
sort_attributes, sort_export_specifiers, sort_import_specifiers,
1924
};
20-
21-
use crate::JsRuleAction;
2225
use util::{attached_trivia, detached_trivia, has_detached_leading_comment, leading_newlines};
2326

24-
pub mod import_key;
25-
pub mod specifiers_attributes;
26-
mod util;
27-
2827
declare_source_rule! {
2928
/// Provides a code action to sort the imports and exports in the file using a built-in or custom order.
3029
///
3130
/// Imports and exports are first separated into chunks, before being sorted.
3231
/// Imports or exports of a chunk are then grouped according to the user-defined groups.
33-
/// Within a group, imports are sorted using a built-in order that depends on the import/export kind, whether the import/export has attributes and the source being imported from.
32+
/// Within a group, imports are sorted using a built-in order that depends on the import/export kind,
33+
/// whether the import/export has attributes and the source being imported from.
3434
/// **source** is also often called **specifier** in the JavaScript ecosystem.
3535
///
3636
/// ```js,ignore
@@ -62,8 +62,9 @@ declare_source_rule! {
6262
/// export { F } from "f";
6363
/// ```
6464
///
65-
/// Chunks also end as soon as a statement or a **side-effect import** (also called _bare import_) is encountered.
66-
/// Every side-effect import forms an independent chunk.
65+
/// Chunks also end as soon as a statement or a **bare import** (also called _side-effect import_) is encountered.
66+
/// Bare imports can be sorted with other imports by setting the `ignoreBareImports` option to `false`.
67+
/// Every bare import forms an independent chunk.
6768
/// The following example contains six chunks:
6869
///
6970
/// ```js,ignore
@@ -84,9 +85,9 @@ declare_source_rule! {
8485
/// export { F } from "f";
8586
/// ```
8687
///
87-
/// 1. The first chunk contains the two first `import` and ends with the appearance of the first side-effect import `import "x"`.
88-
/// 2. The second chunk contains only the side-effect import `import "x"`.
89-
/// 3. The third chunk contains only the side-effect import `import "y"`.
88+
/// 1. The first chunk contains the two first `import` and ends with the appearance of the first bare import `import "x"`.
89+
/// 2. The second chunk contains only the bare import `import "x"`.
90+
/// 3. The third chunk contains only the bare import `import "y"`.
9091
/// 4. The fourth chunk contains a single `import`; The first `export` ends it.
9192
/// 5. The fifth chunk contains the first `export`; The function declaration ends it.
9293
/// 6. The sixth chunk contains the last two `export`.
@@ -111,8 +112,10 @@ declare_source_rule! {
111112
/// The line `import { C } from "c"` forms the second chunk.
112113
/// The blank line between the first two imports is ignored so they form a single chunk.
113114
///
114-
/// The sorter ensures that chunks are separated from each other with blank lines.
115-
/// Only side-effect imports adjacent to a chunk of imports are not separated by a blank line.
115+
/// The sorter ensures that a chunk of imports and a chunk of exports
116+
/// are separated from each other with blank lines.
117+
/// Also, it ensures that a chunk that starts with a detached comment
118+
/// is separated from the previous chunk with a blank line.
116119
/// The following code...
117120
///
118121
/// ```js,ignore
@@ -198,16 +201,18 @@ declare_source_rule! {
198201
/// import sibling from "./file.js";
199202
/// ```
200203
///
201-
/// If two imports or exports share the same source and are in the same chunk, then they are ordered according to their kind as follows:
204+
/// If two imports or exports share the same source and are in the same chunk,
205+
/// then they are ordered according to their kind as follows:
202206
///
203-
/// 1. Namespace type import / Namespace type export
204-
/// 2. Default type import
205-
/// 3. Named type import / Named type export
206-
/// 4. Namespace import / Namespace export
207-
/// 5. Combined default and namespace import
208-
/// 6. Default import
209-
/// 7. Combined default and named import
210-
/// 8. Named import / Named export
207+
/// 1. Bare imports
208+
/// 2. Namespace type import / Namespace type export
209+
/// 3. Default type import
210+
/// 4. Named type import / Named type export
211+
/// 5. Namespace import / Namespace export
212+
/// 6. Combined default and namespace import
213+
/// 7. Default import
214+
/// 8. Combined default and named import
215+
/// 9. Named import / Named export
211216
///
212217
/// Imports and exports with attributes are always placed first.
213218
/// For example, the following code...
@@ -570,6 +575,8 @@ declare_source_rule! {
570575
/// import D2, { A, B } from "package";
571576
/// ```
572577
///
578+
/// Also, note that bare imports are never merged with other imports
579+
/// even if you set `ignoreBareImports` to `false`.
573580
///
574581
/// ## Named imports, named exports and attributes sorting
575582
///
@@ -661,8 +668,11 @@ declare_source_rule! {
661668
/// }
662669
/// ```
663670
///
664-
/// ## Change the sorting of import identifiers to lexicographic sorting
665-
/// This only applies to the named import/exports and not the source itself.
671+
/// ## Change the sorting of import and export identifiers
672+
///
673+
/// By default, attributes, imported and exported names are sorted with a `natural` sort.
674+
/// Yo ucan opt for a `lexicographic` sort (sometimes referred as _binary_ sort) by
675+
/// setting the `identifierOrder` option.
666676
///
667677
/// ```json,options
668678
/// {
@@ -671,31 +681,43 @@ declare_source_rule! {
671681
/// }
672682
/// }
673683
/// ```
684+
///
674685
/// ```js,use_options,expect_diagnostic
675-
/// import { var1, var2, var21, var11, var12, var22 } from 'my-package'
686+
/// import { var1, var2, var21, var11, var12, var22 } from "my-package";
676687
/// ```
677688
///
678-
/// ## Change the sorting of import identifiers to logical sorting
679-
/// This is the default behavior in case you do not override. This only applies to the named import/exports and not the source itself.
689+
/// Note that this order doesn't change how import and export sources are sorted.
690+
///
691+
/// ## Sorting bare imports
692+
///
693+
/// By default, bare imports are not sorted within other imports.
694+
/// Setting `ignoreBareImports` to `false`, allow sorting them with other imports.
695+
/// Note that this can lead to issues because bare imports often signal the presence of side-effects.
696+
/// Thus changing their order can change the behavior of your code.
680697
///
681698
/// ```json,options
682699
/// {
683700
/// "options": {
684-
/// "identifierOrder": "natural"
701+
/// "ignoreBareImports": false
685702
/// }
686703
/// }
687704
/// ```
705+
///
688706
/// ```js,use_options,expect_diagnostic
689-
/// import { var1, var2, var21, var11, var12, var22 } from 'my-package'
707+
/// import "./file";
708+
/// import { A } from "my-package";
690709
/// ```
691-
///
692710
pub OrganizeImports {
693711
version: "1.0.0",
694712
name: "organizeImports",
695713
language: "js",
696714
recommended: true,
697715
fix_kind: FixKind::Safe,
698-
sources: &[RuleSource::Eslint("sort-imports").inspired(), RuleSource::Eslint("no-duplicate-imports").inspired(), RuleSource::EslintImport("order").inspired()],
716+
sources: &[
717+
RuleSource::Eslint("sort-imports").inspired(),
718+
RuleSource::Eslint("no-duplicate-imports").inspired(),
719+
RuleSource::EslintImport("order").inspired()
720+
],
699721
}
700722
}
701723

@@ -757,14 +779,20 @@ impl Rule for OrganizeImports {
757779
let options = ctx.options();
758780
let groups = options.groups.as_ref();
759781
let sort_order = options.identifier_order.unwrap_or_default();
782+
let ignore_bare_imports = options.ignore_bare_imports();
760783
let mut chunk: Option<ChunkBuilder> = None;
761784
let mut prev_kind: Option<JsSyntaxKind> = None;
762785
let mut prev_group = 0;
763786
for item in root.items() {
764787
if let Some((info, specifiers, attributes)) = ImportInfo::from_module_item(&item) {
765788
let prev_is_distinct = prev_kind.is_some_and(|kind| kind != item.syntax().kind());
766-
// A detached comment marks the start of a new chunk
767-
if prev_is_distinct || has_detached_leading_comment(item.syntax()) {
789+
// switching of kind (import/export) marks the start of a new chunk.
790+
if prev_is_distinct
791+
// A detached comment marks the start of a new chunk
792+
|| has_detached_leading_comment(item.syntax())
793+
// bare imports marks the start of a new chunk if they are ignored in the sort.
794+
|| (ignore_bare_imports && info.kind == ImportStatementKind::Bare)
795+
{
768796
// The chunk ends, here
769797
report_unsorted_chunk(chunk.take(), &mut result);
770798
prev_group = 0;
@@ -826,12 +854,8 @@ impl Rule for OrganizeImports {
826854
chunk = Some(ChunkBuilder::new(key));
827855
}
828856
} 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
857+
// This is either a buggy import/export or a statement.
858+
// So, the chunk ends here.
835859
report_unsorted_chunk(chunk.take(), &mut result);
836860
prev_group = 0;
837861
// A statement must be separated of a chunk with a blank line

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

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,17 @@ impl ImportKey {
4242

4343
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
4444
#[enumflags2::bitflags]
45-
#[repr(u8)]
45+
#[repr(u16)]
4646
pub enum ImportStatementKind {
47-
NamespaceType = 1 << 0,
48-
DefaultType = 1 << 1,
49-
NamedType = 1 << 2,
50-
Namespace = 1 << 3,
51-
DefaultNamespace = 1 << 4,
52-
Default = 1 << 5,
53-
DefaultNamed = 1 << 6,
54-
Named = 1 << 7,
47+
Bare = 1 << 0,
48+
NamespaceType = 1 << 1,
49+
DefaultType = 1 << 2,
50+
NamedType = 1 << 3,
51+
Namespace = 1 << 4,
52+
DefaultNamespace = 1 << 5,
53+
Default = 1 << 6,
54+
DefaultNamed = 1 << 7,
55+
Named = 1 << 8,
5556
}
5657
impl ImportStatementKind {
5758
pub fn has_type_token(self) -> bool {
@@ -94,9 +95,12 @@ impl ImportInfo {
9495
value: &JsImport,
9596
) -> Option<(Self, Option<JsNamedSpecifiers>, Option<JsImportAssertion>)> {
9697
let (kind, named_specifiers, source, attributes) = match value.import_clause().ok()? {
97-
AnyJsImportClause::JsImportBareClause(_) => {
98-
return None;
99-
}
98+
AnyJsImportClause::JsImportBareClause(clause) => (
99+
ImportStatementKind::Bare,
100+
None,
101+
clause.source(),
102+
clause.assertion(),
103+
),
100104
AnyJsImportClause::JsImportCombinedClause(clause) => {
101105
let (kind, named_specifiers) = match clause.specifier().ok()? {
102106
AnyJsCombinedSpecifier::JsNamedImportSpecifiers(specifiers) => {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import * as path from "node:path";
2+
import "node:path";
3+
import "./file.js";
4+
import { A } from "./file.js";
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: custom-order-with-bare-imports.js
4+
---
5+
# Input
6+
```js
7+
import * as path from "node:path";
8+
import "node:path";
9+
import "./file.js";
10+
import { A } from "./file.js";
11+
12+
```
13+
14+
# Diagnostics
15+
```
16+
custom-order-with-bare-imports.js:1:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━
17+
18+
i The imports and exports are not sorted.
19+
20+
> 1 │ import * as path from "node:path";
21+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
22+
2 │ import "node:path";
23+
3 │ import "./file.js";
24+
25+
i Safe fix: Organize Imports (Biome)
26+
27+
1 │ - import·*·as·path·from·"node:path";
28+
2 │ - import·"node:path";
29+
3 │ - import·"./file.js";
30+
4 │ - import·{·A·}·from·"./file.js";
31+
1 │ + import·"./file.js";
32+
2 │ + import·{·A·}·from·"./file.js";
33+
3 │ + import·"node:path";
34+
4 │ + import·*·as·path·from·"node:path";
35+
5 5 │
36+
37+
38+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "../../../../../packages/@biomejs/biome/configuration_schema.json",
3+
"assist": {
4+
"actions": {
5+
"source": {
6+
"organizeImports": {
7+
"level": "on",
8+
"options": {
9+
"groups": [
10+
":PATH:",
11+
":NODE:"
12+
],
13+
"ignoreBareImports": false
14+
}
15+
}
16+
}
17+
}
18+
}
19+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import "mod" with { att2: "", att1: "" };
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-bare-import-attributes.js
4+
---
5+
# Input
6+
```js
7+
import "mod" with { att2: "", att1: "" };
8+
9+
```
10+
11+
# Diagnostics
12+
```
13+
unsorted-bare-import-attributes.js:1:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━
14+
15+
i The imports and exports are not sorted.
16+
17+
> 1 │ import "mod" with { att2: "", att1: "" };
18+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
19+
2 │
20+
21+
i Safe fix: Organize Imports (Biome)
22+
23+
1 │ - import·"mod"·with·{·att2""att1""·};
24+
1 │ + import·"mod"·with·{·att1""att2""·};
25+
2 2 │
26+
27+
28+
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import "b";
2+
import "a";
3+
import "c";
4+
import "a" with { att2: "", att1: "" };

0 commit comments

Comments
 (0)