diff --git a/.changeset/neat-papers-think.md b/.changeset/neat-papers-think.md index f6126b6bc78c..846a7061665d 100644 --- a/.changeset/neat-papers-think.md +++ b/.changeset/neat-papers-think.md @@ -2,12 +2,21 @@ "@biomejs/biome": patch --- -Partially fix [#7583](https://github.com/biomejs/biome/issues/7583). +Fixed [#7583](https://github.com/biomejs/biome/issues/7583): [`organizeImports`](https://biomejs.dev/assist/actions/organize-imports/) now -sorts named specifiers inside bare exports. -This fix doesn't merge adjacent bare exports. +sorts named specifiers inside bare exports and merges bare exports. ```diff - export { b, a }; -+ export { a, b }; +- export { c }; ++ export { a, b, c }; +``` + +Also, `organizeImports` now correctly adds a blank line between an import chunk +and an export chunk. + +```diff + import { A } from "package"; ++ + export { A }; ``` 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 406f948f210b..f8651a05f693 100644 --- a/crates/biome_js_analyze/src/assist/source/organize_imports.rs +++ b/crates/biome_js_analyze/src/assist/source/organize_imports.rs @@ -19,7 +19,7 @@ use biome_rule_options::{organize_imports::OrganizeImportsOptions, sort_order::S use import_key::{ImportInfo, ImportKey}; use rustc_hash::FxHashMap; use specifiers_attributes::{ - JsNamedSpecifiers, are_import_attributes_sorted, merge_export_from_specifiers, + are_import_attributes_sorted, merge_export_from_specifiers, merge_export_specifiers, merge_import_specifiers, sort_attributes, sort_export_from_specifiers, sort_export_specifiers, sort_import_specifiers, }; @@ -825,43 +825,22 @@ impl Rule for OrganizeImports { prev_group = key.group; chunk = Some(ChunkBuilder::new(key)); } - } else { - if chunk.is_some() { - // This is either - // - a bare (side-effect) import - // - an export without `from` clause - // - a buggy import or export - // - a statement - // - // In any case, the chunk ends here - report_unsorted_chunk(chunk.take(), &mut result); - prev_group = 0; - // A statement must be separated of a chunk with a blank line - if let AnyJsModuleItem::AnyJsStatement(statement) = &item - && leading_newlines(statement.syntax()).count() == 1 - { - result.push(Issue::AddLeadingNewline { - slot_index: statement.syntax().index() as u32, - }); - } - } - if let AnyJsModuleItem::JsExport(js_export) = &item - && let Ok(AnyJsExportClause::JsExportNamedClause(clause)) = - js_export.export_clause() + } else if chunk.is_some() { + // This is either + // - a bare (side-effect) import + // - a buggy import or export + // - a statement + // + // In any case, the chunk ends here + report_unsorted_chunk(chunk.take(), &mut result); + prev_group = 0; + // A statement must be separated of a chunk with a blank line + if let AnyJsModuleItem::AnyJsStatement(statement) = &item + && leading_newlines(statement.syntax()).count() == 1 { - let specifiers = - JsNamedSpecifiers::JsExportNamedSpecifierList(clause.specifiers()); - let are_specifiers_unsorted = !specifiers.are_sorted(sort_order); - if are_specifiers_unsorted { - // Report the violation of one of the previous requirement - result.push(Issue::UnorganizedItem { - slot_index: item.syntax().index() as u32, - are_specifiers_unsorted, - // An export without `from` clause has no attributes. - are_attributes_unsorted: false, - newline_issue: NewLineIssue::None, - }); - } + result.push(Issue::AddLeadingNewline { + slot_index: statement.syntax().index() as u32, + }); } } prev_kind = Some(item.syntax().kind()); @@ -1168,33 +1147,45 @@ fn merge( (AnyJsModuleItem::JsExport(item1), AnyJsModuleItem::JsExport(item2)) => { let clause1 = item1.export_clause().ok()?; let clause2 = item2.export_clause().ok()?; - let AnyJsExportClause::JsExportNamedFromClause(clause1) = clause1 else { - return None; + let merged_item = match (clause1, clause2) { + ( + AnyJsExportClause::JsExportNamedFromClause(clause1), + AnyJsExportClause::JsExportNamedFromClause(clause2), + ) => { + let specifiers1 = clause1.specifiers(); + let specifiers2 = clause2.specifiers(); + let merged_specifiers = + merge_export_from_specifiers(&specifiers1, &specifiers2, sort_order)?; + let merged_specifiers = clause1.with_specifiers(merged_specifiers); + item2.clone().with_export_clause(merged_specifiers.into()) + } + ( + AnyJsExportClause::JsExportNamedClause(clause1), + AnyJsExportClause::JsExportNamedClause(clause2), + ) => { + let specifiers1 = clause1.specifiers(); + let specifiers2 = clause2.specifiers(); + let merged_specifiers = + merge_export_specifiers(&specifiers1, &specifiers2, sort_order)?; + let merged_specifiers = clause1.with_specifiers(merged_specifiers); + item2.clone().with_export_clause(merged_specifiers.into()) + } + _ => return None, }; - let clause2 = clause2.as_js_export_named_from_clause()?; - let specifiers1 = clause1.specifiers(); - let specifiers2 = clause2.specifiers(); - if let Some(merged_specifiers) = - merge_export_from_specifiers(&specifiers1, &specifiers2, sort_order) - { - let merged_specifiers = clause1.with_specifiers(merged_specifiers); - let merged_item = item2.clone().with_export_clause(merged_specifiers.into()); - - let item1_leading_trivia = item1.syntax().first_leading_trivia()?; - let merged_item = if item1_leading_trivia.is_empty() { - merged_item - } else { - merged_item - .trim_leading_trivia()? - .prepend_trivia_pieces(item1.syntax().first_leading_trivia()?.pieces())? - }; - return Some(merged_item.into()); - } + let item1_leading_trivia = item1.syntax().first_leading_trivia()?; + let merged_item = if item1_leading_trivia.is_empty() { + merged_item + } else { + merged_item + .trim_leading_trivia()? + .prepend_trivia_pieces(item1_leading_trivia.pieces())? + }; + Some(merged_item.into()) } (AnyJsModuleItem::JsImport(item1), AnyJsModuleItem::JsImport(item2)) => { let clause1 = item1.import_clause().ok()?; let clause2 = item2.import_clause().ok()?; - match (clause1, clause2) { + let merged_item = match (clause1, clause2) { ( AnyJsImportClause::JsImportDefaultClause(clause1), AnyJsImportClause::JsImportNamespaceClause(clause2), @@ -1215,17 +1206,7 @@ fn merge( clause2.source().ok()?, ) .build(); - let merged_item = item2.clone().with_import_clause(merged_clause.into()); - - let item1_leading_trivia = item1.syntax().first_leading_trivia()?; - let merged_item = if item1_leading_trivia.is_empty() { - merged_item - } else { - merged_item.trim_leading_trivia()?.prepend_trivia_pieces( - item1.syntax().first_leading_trivia()?.pieces(), - )? - }; - return Some(merged_item.into()); + item2.clone().with_import_clause(merged_clause.into()) } ( AnyJsImportClause::JsImportCombinedClause(clause1), @@ -1241,22 +1222,10 @@ fn merge( return None; }; let specifiers2 = clause2.named_specifiers().ok()?; - if let Some(merged_specifiers) = - merge_import_specifiers(specifiers1, &specifiers2, sort_order) - { - let merged_clause = clause1.with_specifier(merged_specifiers.into()); - let merged_item = item2.clone().with_import_clause(merged_clause.into()); - - let item1_leading_trivia = item1.syntax().first_leading_trivia()?; - let merged_item = if item1_leading_trivia.is_empty() { - merged_item - } else { - merged_item.trim_leading_trivia()?.prepend_trivia_pieces( - item1.syntax().first_leading_trivia()?.pieces(), - )? - }; - return Some(merged_item.into()); - } + let merged_specifiers = + merge_import_specifiers(specifiers1, &specifiers2, sort_order)?; + let merged_clause = clause1.with_specifier(merged_specifiers.into()); + item2.clone().with_import_clause(merged_clause.into()) } ( AnyJsImportClause::JsImportNamedClause(clause1), @@ -1264,21 +1233,10 @@ fn merge( ) => { let specifiers1 = clause1.named_specifiers().ok()?; let specifiers2 = clause2.named_specifiers().ok()?; - if let Some(merged_specifiers) = - merge_import_specifiers(specifiers1, &specifiers2, sort_order) - { - let merged_clause = clause1.with_named_specifiers(merged_specifiers); - let merged_item = item2.clone().with_import_clause(merged_clause.into()); - let item1_leading_trivia = item1.syntax().first_leading_trivia()?; - let merged_item = if item1_leading_trivia.is_empty() { - merged_item - } else { - merged_item.trim_leading_trivia()?.prepend_trivia_pieces( - item1.syntax().first_leading_trivia()?.pieces(), - )? - }; - return Some(merged_item.into()); - } + let merged_specifiers = + merge_import_specifiers(specifiers1, &specifiers2, sort_order)?; + let merged_clause = clause1.with_named_specifiers(merged_specifiers); + item2.clone().with_import_clause(merged_clause.into()) } ( AnyJsImportClause::JsImportDefaultClause(clause1), @@ -1300,21 +1258,20 @@ fn merge( clause2.source().ok()?, ) .build(); - let merged_item = item2.clone().with_import_clause(merged_clause.into()); - let item1_leading_trivia = item1.syntax().first_leading_trivia()?; - let merged_item = if item1_leading_trivia.is_empty() { - merged_item - } else { - merged_item.trim_leading_trivia()?.prepend_trivia_pieces( - item1.syntax().first_leading_trivia()?.pieces(), - )? - }; - return Some(merged_item.into()); + item2.clone().with_import_clause(merged_clause.into()) } - _ => {} - } + _ => return None, + }; + let item1_leading_trivia = item1.syntax().first_leading_trivia()?; + let merged_item = if item1_leading_trivia.is_empty() { + merged_item + } else { + merged_item + .trim_leading_trivia()? + .prepend_trivia_pieces(item1_leading_trivia.pieces())? + }; + Some(merged_item.into()) } - _ => {} + _ => None, } - None } diff --git a/crates/biome_js_analyze/src/assist/source/organize_imports/import_key.rs b/crates/biome_js_analyze/src/assist/source/organize_imports/import_key.rs index 8e407e6519de..70de2336e582 100644 --- a/crates/biome_js_analyze/src/assist/source/organize_imports/import_key.rs +++ b/crates/biome_js_analyze/src/assist/source/organize_imports/import_key.rs @@ -1,3 +1,5 @@ +use core::cmp::Ordering; + use super::specifiers_attributes::JsNamedSpecifiers; use biome_js_syntax::{ AnyJsCombinedSpecifier, AnyJsExportClause, AnyJsImportClause, AnyJsModuleItem, @@ -11,10 +13,10 @@ use biome_rule_options::organize_imports::import_source::ImportSource; use biome_string_case::comparable_token::ComparableToken; /// Type used to determine the order between imports -#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +#[derive(Debug, Eq, PartialEq)] pub struct ImportKey { pub group: u16, - pub source: ImportSource, + pub source: Option>, pub has_no_attributes: bool, pub kind: ImportStatementKind, /// Slot index of the import in the module. @@ -39,6 +41,37 @@ impl ImportKey { && other.has_no_attributes } } +impl Ord for ImportKey { + fn cmp(&self, other: &Self) -> Ordering { + match self.group.cmp(&other.group) { + Ordering::Equal => {} + ord => return ord, + } + match (&self.source, &other.source) { + (None, None) => {} + (Some(_), None) => return Ordering::Less, + (None, Some(_)) => return Ordering::Greater, + (Some(self_source), Some(other_source)) => match self_source.cmp(other_source) { + Ordering::Equal => {} + ord => return ord, + }, + } + match self.has_no_attributes.cmp(&other.has_no_attributes) { + Ordering::Equal => {} + ord => return ord, + } + match self.kind.cmp(&other.kind) { + Ordering::Equal => {} + ord => return ord, + } + self.slot_index.cmp(&other.slot_index) + } +} +impl PartialOrd for ImportKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[enumflags2::bitflags] @@ -76,7 +109,7 @@ pub struct ImportInfo { /// Slot index of the import in the module. pub slot_index: u32, pub kind: ImportStatementKind, - pub source: ImportSource, + pub source: Option>, pub has_no_attributes: bool, } impl ImportInfo { @@ -147,7 +180,7 @@ impl ImportInfo { }; Some(( Self { - source: ComparableToken::new(source.inner_string_text().ok()?).into(), + source: Some(ComparableToken::new(source.inner_string_text().ok()?).into()), has_no_attributes: attributes.is_none(), kind, slot_index: value.syntax().index() as u32, @@ -160,53 +193,61 @@ impl ImportInfo { fn from_export( value: &JsExport, ) -> Option<(Self, Option, Option)> { - let (kind, _first_local_name, named_specifiers, source, attributes) = - match value.export_clause().ok()? { - AnyJsExportClause::JsExportFromClause(clause) => ( - if clause.type_token().is_some() { - ImportStatementKind::NamespaceType - } else { - ImportStatementKind::Namespace - }, - clause - .export_as() - .and_then(|export_as| export_as.exported_name().ok()), - None, - clause.source(), - clause.assertion(), - ), - AnyJsExportClause::JsExportNamedFromClause(clause) => ( - if clause.type_token().is_some() { - ImportStatementKind::NamedType - } else { - ImportStatementKind::Named - }, - clause - .specifiers() - .into_iter() - .flatten() - .next() - .and_then(|x| x.source_name().ok()), - Some(clause.specifiers()), - clause.source(), - clause.assertion(), - ), - _ => { - return None; - } + let (kind, named_specifiers, source, attributes) = match value.export_clause().ok()? { + AnyJsExportClause::JsExportNamedClause(clause) => ( + if clause.type_token().is_some() { + ImportStatementKind::NamedType + } else { + ImportStatementKind::Named + }, + Some(JsNamedSpecifiers::JsExportNamedSpecifierList( + clause.specifiers(), + )), + None, + None, + ), + AnyJsExportClause::JsExportFromClause(clause) => ( + if clause.type_token().is_some() { + ImportStatementKind::NamespaceType + } else { + ImportStatementKind::Namespace + }, + None, + Some(clause.source()), + clause.assertion(), + ), + AnyJsExportClause::JsExportNamedFromClause(clause) => ( + if clause.type_token().is_some() { + ImportStatementKind::NamedType + } else { + ImportStatementKind::Named + }, + Some(JsNamedSpecifiers::JsExportNamedFromSpecifierList( + clause.specifiers(), + )), + Some(clause.source()), + clause.assertion(), + ), + _ => { + return None; + } + }; + let source = if let Some(source) = source { + let Ok(AnyJsModuleSource::JsModuleSource(source)) = source else { + return None; }; - let Ok(AnyJsModuleSource::JsModuleSource(source)) = source else { - return None; + Some(source.inner_string_text().ok()?) + } else { + None }; - let source = source.inner_string_text().ok()?; Some(( Self { - source: ComparableToken::new(source).into(), + source: source.map(|src| ComparableToken::new(src).into()), has_no_attributes: attributes.is_none(), kind, slot_index: value.syntax().index() as u32, }, - named_specifiers.map(JsNamedSpecifiers::JsExportNamedFromSpecifierList), + named_specifiers, attributes, )) } @@ -215,7 +256,7 @@ impl<'a> From<&'a ImportInfo> for ImportCandidate<'a> { fn from(value: &'a ImportInfo) -> Self { Self { has_type_token: value.kind.has_type_token(), - source: ImportSourceCandidate::new(&value.source), + source: value.source.as_ref().map(ImportSourceCandidate::new), } } } 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 fb8235df136a..c313884df14d 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 @@ -236,6 +236,45 @@ pub fn sort_export_specifiers( Some(new_list) } +pub fn merge_export_specifiers( + specifiers1: &JsExportNamedSpecifierList, + specifiers2: &JsExportNamedSpecifierList, + sort_order: SortOrder, +) -> Option { + let mut nodes = Vec::with_capacity(specifiers1.len() + specifiers2.len()); + let mut separators = Vec::with_capacity(specifiers1.len() + specifiers2.len()); + for AstSeparatedElement { + node, + trailing_separator, + } in specifiers1.elements() + { + let separator = trailing_separator.ok()?; + let mut node = node.ok()?; + if separator.is_none() { + node = node.trim_trailing_trivia()?; + } + let separator = separator.unwrap_or_else(|| { + make::token(T![,]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]) + }); + nodes.push(node); + separators.push(separator); + } + for AstSeparatedElement { + node, + trailing_separator, + } in specifiers2.elements() + { + nodes.push(node.ok()?); + if let Some(separator) = trailing_separator.ok()? { + separators.push(separator); + } + } + sort_export_specifiers( + &make::js_export_named_specifier_list(nodes, separators), + sort_order, + ) +} + pub fn are_import_attributes_sorted( attributes: &JsImportAssertion, sort_order: SortOrder, diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/chunk-with-bare-export.js b/crates/biome_js_analyze/tests/specs/source/organizeImports/chunk-with-bare-export.js new file mode 100644 index 000000000000..aa7f053a20ef --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/chunk-with-bare-export.js @@ -0,0 +1,2 @@ +import { A } from "package"; +export { A }; diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/chunk-with-bare-export.js.snap b/crates/biome_js_analyze/tests/specs/source/organizeImports/chunk-with-bare-export.js.snap new file mode 100644 index 000000000000..f515004003d9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/chunk-with-bare-export.js.snap @@ -0,0 +1,31 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: chunk-with-bare-export.js +--- +# Input +```js +import { A } from "package"; +export { A }; + +``` + +# Diagnostics +``` +chunk-with-bare-export.js:1:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The imports and exports are not sorted. + + > 1 │ import { A } from "package"; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 2 │ export { A }; + 3 │ + + i Safe fix: Organize Imports (Biome) + + 1 1 │ import { A } from "package"; + 2 │ + + 2 3 │ export { A }; + 3 4 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-order-exports.options.json b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-order-exports.options.json new file mode 100644 index 000000000000..db51584efa6f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-order-exports.options.json @@ -0,0 +1,19 @@ +{ + "assist": { + "actions": { + "source": { + "organizeImports": { + "level": "on", + "options": { + "groups": [ + { + "type": true + }, + ":NODE:" + ] + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-order-exports.ts b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-order-exports.ts new file mode 100644 index 000000000000..8bfbc1645fed --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-order-exports.ts @@ -0,0 +1,3 @@ +export { O }; +export * as path from "node:path"; +export type { T }; diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-order-exports.ts.snap b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-order-exports.ts.snap new file mode 100644 index 000000000000..c67e85756346 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/custom-order-exports.ts.snap @@ -0,0 +1,34 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: custom-order-exports.ts +--- +# Input +```ts +export { O }; +export * as path from "node:path"; +export type { T }; + +``` + +# Diagnostics +``` +custom-order-exports.ts:1:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The imports and exports are not sorted. + + > 1 │ export { O }; + │ ^^^^^^^^^^^^^ + 2 │ export * as path from "node:path"; + 3 │ export type { T }; + + i Safe fix: Organize Imports (Biome) + + 1 │ - export·{·O·}; + 1 │ + export·type·{·T·}; + 2 2 │ export * as path from "node:path"; + 3 │ - export·type·{·T·}; + 3 │ + export·{·O·}; + 4 4 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/mergeable-bare-exports.js b/crates/biome_js_analyze/tests/specs/source/organizeImports/mergeable-bare-exports.js new file mode 100644 index 000000000000..3aba0cde8b53 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/mergeable-bare-exports.js @@ -0,0 +1,3 @@ +export { a }; +export { c }; +export { b, d }; diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/mergeable-bare-exports.js.snap b/crates/biome_js_analyze/tests/specs/source/organizeImports/mergeable-bare-exports.js.snap new file mode 100644 index 000000000000..e97a4e5d5067 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/mergeable-bare-exports.js.snap @@ -0,0 +1,33 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: mergeable-bare-exports.js +--- +# Input +```js +export { a }; +export { c }; +export { b, d }; + +``` + +# Diagnostics +``` +mergeable-bare-exports.js:1:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i The imports and exports are not sorted. + + > 1 │ export { a }; + │ ^^^^^^^^^^^^^ + 2 │ export { c }; + 3 │ export { b, d }; + + i Safe fix: Organize Imports (Biome) + + 1 │ - export·{·a·}; + 2 │ - export·{·c·}; + 3 │ - export·{·b,·d·}; + 1 │ + export·{·a,·b,·c,·d·}; + 4 2 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted-from-less-export.js b/crates/biome_js_analyze/tests/specs/source/organizeImports/unorganized-bare-export-specifiers.js similarity index 100% rename from crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted-from-less-export.js rename to crates/biome_js_analyze/tests/specs/source/organizeImports/unorganized-bare-export-specifiers.js diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted-from-less-export.js.snap b/crates/biome_js_analyze/tests/specs/source/organizeImports/unorganized-bare-export-specifiers.js.snap similarity index 63% rename from crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted-from-less-export.js.snap rename to crates/biome_js_analyze/tests/specs/source/organizeImports/unorganized-bare-export-specifiers.js.snap index d2f45d15e6ca..e8dd9ad25250 100644 --- a/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted-from-less-export.js.snap +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/unorganized-bare-export-specifiers.js.snap @@ -1,6 +1,7 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs -expression: unsorted-from-less-export.js +assertion_line: 154 +expression: unorganized-bare-export-specifiers.js --- # Input ```js @@ -10,7 +11,7 @@ export { b, a } # Diagnostics ``` -unsorted-from-less-export.js:1:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━ +unorganized-bare-export-specifiers.js:1:1 assist/source/organizeImports FIXABLE ━━━━━━━━━━━━━━━━━━ i The imports and exports are not sorted. diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted_export_chunk.js b/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted_export_chunk.js index 95945b4960fb..964b4824d30e 100644 --- a/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted_export_chunk.js +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted_export_chunk.js @@ -10,3 +10,4 @@ export * as S from "s"; export { R } from "r"; function f() {} export { A } from "a"; +export { X } diff --git a/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted_export_chunk.js.snap b/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted_export_chunk.js.snap index 8d9602bc499f..c58812028313 100644 --- a/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted_export_chunk.js.snap +++ b/crates/biome_js_analyze/tests/specs/source/organizeImports/unsorted_export_chunk.js.snap @@ -16,6 +16,7 @@ export * as S from "s"; export { R } from "r"; function f() {} export { A } from "a"; +export { X } ``` @@ -56,7 +57,7 @@ unsorted_export_chunk.js:2:1 assist/source/organizeImports FIXABLE ━━━ 11 14 │ function f() {} 15 │ + 12 16 │ export { A } from "a"; - 13 17 │ + 13 17 │ export { X } ``` diff --git a/crates/biome_lsp/src/server.tests.rs b/crates/biome_lsp/src/server.tests.rs index 4c47a18a7e15..0e873cfa4dff 100644 --- a/crates/biome_lsp/src/server.tests.rs +++ b/crates/biome_lsp/src/server.tests.rs @@ -2016,6 +2016,7 @@ async fn pull_code_actions_with_import_sorting() -> Result<()> { import z from "zod"; import { test } from "./test"; import { describe } from "node:test"; + export { describe, test, z }; if(a === -0) {} diff --git a/crates/biome_rule_options/src/organize_imports/import_groups.rs b/crates/biome_rule_options/src/organize_imports/import_groups.rs index 64a91bf47331..2ccea571eb23 100644 --- a/crates/biome_rule_options/src/organize_imports/import_groups.rs +++ b/crates/biome_rule_options/src/organize_imports/import_groups.rs @@ -21,7 +21,6 @@ impl ImportGroups { /// If no group contains `candidate`, then the returned value corresponds to the index of the implicit group. /// The index of the implicit group correspond to the number of groups. pub fn index(&self, candidate: &ImportCandidate<'_>) -> u16 { - candidate.source.as_str(); self.0 .iter() .position(|group| group.contains(candidate)) @@ -47,7 +46,7 @@ impl biome_deserialize::Merge for ImportGroups { pub struct ImportCandidate<'a> { pub has_type_token: bool, - pub source: ImportSourceCandidate<'a>, + pub source: Option>, } impl ImportCandidate<'_> { /// Match against a list of matchers where negated matchers are handled as exceptions. @@ -132,14 +131,20 @@ impl GroupMatcher { pub fn is_match(&self, candidate: &ImportCandidate<'_>) -> bool { match self { Self::Import(matcher) => matcher.is_match(candidate), - Self::Source(matcher) => matcher.is_match(&candidate.source), + Self::Source(matcher) => candidate + .source + .as_ref() + .is_some_and(|src| matcher.is_match(src)), } } pub fn is_raw_match(&self, candidate: &ImportCandidate<'_>) -> bool { match self { Self::Import(matcher) => matcher.is_match(candidate), - Self::Source(matcher) => matcher.is_raw_match(&candidate.source), + Self::Source(matcher) => candidate + .source + .as_ref() + .is_some_and(|src| matcher.is_raw_match(src)), } } } @@ -171,10 +176,12 @@ impl ImportMatcher { .r#type .is_none_or(|r#type| candidate.has_type_token == r#type); matches_type - && self - .source - .as_ref() - .is_none_or(|src| src.is_match(&candidate.source)) + && self.source.as_ref().is_none_or(|src| { + candidate + .source + .as_ref() + .is_some_and(|candidate_source| src.is_match(candidate_source)) + }) } }