Skip to content

Commit 091d124

Browse files
committed
feat(assist): import organizer revamping
1 parent c7703a4 commit 091d124

File tree

66 files changed

+1754
-1925
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1754
-1925
lines changed

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

+283-56
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use std::cmp::Ordering;
2+
3+
use biome_rowan::TokenText;
4+
use biome_string_case::StrLikeExtension;
5+
6+
/// A [JsSyntaxToken] that is ordered according to the ASCII natural order.
7+
#[derive(Clone, Debug)]
8+
pub struct ComparableToken(pub TokenText);
9+
impl From<TokenText> for ComparableToken {
10+
fn from(value: TokenText) -> Self {
11+
Self(value)
12+
}
13+
}
14+
impl AsRef<str> for ComparableToken {
15+
fn as_ref(&self) -> &str {
16+
self.0.text()
17+
}
18+
}
19+
impl Eq for ComparableToken {}
20+
impl PartialEq for ComparableToken {
21+
fn eq(&self, other: &Self) -> bool {
22+
self.0.text() == other.0.text()
23+
}
24+
}
25+
impl std::hash::Hash for ComparableToken {
26+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
27+
self.0.text().hash(state);
28+
}
29+
}
30+
impl Ord for ComparableToken {
31+
fn cmp(&self, other: &Self) -> Ordering {
32+
self.0.text().ascii_nat_cmp(other.0.text())
33+
}
34+
}
35+
impl PartialOrd for ComparableToken {
36+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
37+
Some(self.cmp(other))
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use biome_deserialize::{Deserializable, DeserializationContext, Text};
2+
use biome_deserialize_macros::Deserializable;
3+
use biome_glob::{CandidatePath, Glob};
4+
5+
use crate::globals::is_node_builtin_module;
6+
7+
#[derive(
8+
Clone, Debug, Default, Deserializable, Eq, PartialEq, serde::Deserialize, serde::Serialize,
9+
)]
10+
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
11+
pub struct ImportGroups(Box<[ImportGroup]>);
12+
impl ImportGroups {
13+
/// Returns the index of the first group containing `candidate`.
14+
/// If no group contains `candidate`, then the returned value corresponds to the index of the implicit group.
15+
/// The index of the implicit group correspond to the number of groups.
16+
pub fn index(&self, candidate: &ImportSourceCandidate) -> usize {
17+
self.0
18+
.iter()
19+
.position(|group| group.contains(candidate))
20+
.unwrap_or(self.0.len())
21+
}
22+
}
23+
24+
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
25+
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26+
#[serde(untagged)]
27+
pub enum ImportGroup {
28+
Predefined(PredefinedImportGroup),
29+
Glob(Box<ImportSourceGlob>),
30+
GlobList(Box<[ImportSourceGlob]>),
31+
}
32+
impl ImportGroup {
33+
pub fn contains(&self, candidate: &ImportSourceCandidate) -> bool {
34+
match self {
35+
ImportGroup::Predefined(predefined) => predefined.contains(candidate),
36+
ImportGroup::Glob(glob) => {
37+
// TODO: use `is_match_candidate` instead?
38+
glob.is_match(candidate)
39+
}
40+
ImportGroup::GlobList(globs) => candidate
41+
.path_candidate
42+
.matches_with_exceptions(globs.iter().map(|glob| &glob.0)),
43+
}
44+
}
45+
}
46+
impl Deserializable for ImportGroup {
47+
fn deserialize(
48+
ctx: &mut impl DeserializationContext,
49+
value: &impl biome_deserialize::DeserializableValue,
50+
name: &str,
51+
) -> Option<Self> {
52+
if value.visitable_type() == Some(biome_deserialize::DeserializableType::Str) {
53+
let value_text = Text::deserialize(ctx, value, name)?;
54+
if value_text.starts_with(':') && value_text.ends_with(':') {
55+
Deserializable::deserialize(ctx, value, name).map(ImportGroup::Predefined)
56+
} else {
57+
Deserializable::deserialize(ctx, value, name).map(ImportGroup::Glob)
58+
}
59+
} else {
60+
Deserializable::deserialize(ctx, value, name).map(ImportGroup::GlobList)
61+
}
62+
}
63+
}
64+
65+
#[derive(Clone, Debug, Deserializable, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
66+
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67+
pub enum PredefinedImportGroup {
68+
#[serde(rename = ":BUN:")]
69+
Bun,
70+
#[serde(rename = ":NODE:")]
71+
Node,
72+
}
73+
impl PredefinedImportGroup {
74+
fn contains(&self, candidate: &ImportSourceCandidate) -> bool {
75+
let import_source = candidate.as_str();
76+
match self {
77+
Self::Bun => import_source == "bun" || import_source.starts_with("bun:"),
78+
Self::Node => {
79+
import_source.starts_with("node:") || is_node_builtin_module(import_source)
80+
}
81+
}
82+
}
83+
}
84+
85+
/// Glob to match against import sources.
86+
#[derive(Clone, Debug, Deserializable, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
87+
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
88+
pub struct ImportSourceGlob(Glob);
89+
impl ImportSourceGlob {
90+
/// Tests whether the given import source matches this pattern or not.
91+
pub fn is_match(&self, import_source: &ImportSourceCandidate) -> bool {
92+
import_source.path_candidate.matches(&self.0)
93+
}
94+
}
95+
96+
/// A candidate import source for matching.
97+
///
98+
/// Constructing candidates has a very small cost associated with it.
99+
/// The cost is amortized by matching against several import source globs.
100+
pub struct ImportSourceCandidate<'a> {
101+
import_source: &'a str,
102+
path_candidate: CandidatePath<'a>,
103+
}
104+
impl<'a> ImportSourceCandidate<'a> {
105+
/// Create a new candidate for matching from the given path.
106+
pub fn new(import_source: &'a str) -> Self {
107+
Self {
108+
import_source,
109+
path_candidate: CandidatePath::new(import_source),
110+
}
111+
}
112+
113+
/// Returns the original string of this import source.
114+
pub fn as_str(&self) -> &str {
115+
self.import_source
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
use biome_js_syntax::{
2+
AnyJsCombinedSpecifier, AnyJsExportClause, AnyJsImportClause, AnyJsModuleItem,
3+
AnyJsModuleSource, JsExport, JsImport, JsImportAssertion,
4+
};
5+
use biome_rowan::AstNode;
6+
7+
use super::{
8+
comparable_token::ComparableToken, import_groups, import_source,
9+
specifiers_attributes::JsNamedSpecifiers,
10+
};
11+
12+
/// Type used to determine the order between imports
13+
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
14+
pub struct ImportKey {
15+
pub group: u16,
16+
pub source: import_source::ImportSource<ComparableToken>,
17+
pub has_no_attributes: bool,
18+
pub kind: ImportStatementKind,
19+
/// Slot index of the import in the module.
20+
/// This is used as a last resort for ensuring a strict total order between imports.
21+
pub slot_index: u32,
22+
}
23+
impl ImportKey {
24+
pub fn new(info: ImportInfo, groups: &import_groups::ImportGroups) -> Self {
25+
let candidate = import_groups::ImportSourceCandidate::new(info.source.inner().0.text());
26+
Self {
27+
group: groups.index(&candidate) as u16,
28+
source: info.source,
29+
has_no_attributes: info.has_no_attributes,
30+
kind: info.kind,
31+
slot_index: info.slot_index,
32+
}
33+
}
34+
}
35+
36+
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37+
#[enumflags2::bitflags]
38+
#[repr(u8)]
39+
pub enum ImportStatementKind {
40+
DefaultType = 1 << 0,
41+
Default = 1 << 1,
42+
DefaultNamespace = 1 << 2,
43+
DefaultNamed = 1 << 3,
44+
NamespaceType = 1 << 4,
45+
Namespace = 1 << 5,
46+
NamedType = 1 << 6,
47+
Named = 1 << 7,
48+
}
49+
impl ImportStatementKind {
50+
pub fn has_type_token(self) -> bool {
51+
(ImportStatementKind::DefaultType
52+
| ImportStatementKind::NamespaceType
53+
| ImportStatementKind::NamedType)
54+
.contains(self)
55+
}
56+
57+
pub fn is_mergeable(self, kinds: ImportStatementKinds) -> bool {
58+
match self {
59+
ImportStatementKind::DefaultNamed => kinds.contains(ImportStatementKind::Named),
60+
ImportStatementKind::Named => kinds
61+
.0
62+
.intersects(ImportStatementKind::DefaultNamed | ImportStatementKind::Named),
63+
ImportStatementKind::NamedType => kinds.contains(ImportStatementKind::NamedType),
64+
_ => false,
65+
}
66+
}
67+
}
68+
69+
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
70+
pub struct ImportStatementKinds(enumflags2::BitFlags<ImportStatementKind>);
71+
impl ImportStatementKinds {
72+
pub fn contains(self, kind: ImportStatementKind) -> bool {
73+
self.0.contains(kind)
74+
}
75+
76+
pub fn insert(&mut self, kind: ImportStatementKind) {
77+
self.0 |= kind;
78+
}
79+
80+
pub fn clear(&mut self) {
81+
self.0 = Default::default();
82+
}
83+
}
84+
85+
/// Type that gathers information extracted from an import or an export.
86+
#[derive(Debug)]
87+
pub struct ImportInfo {
88+
/// Slot index of the import in the module.
89+
pub slot_index: u32,
90+
pub kind: ImportStatementKind,
91+
pub source: import_source::ImportSource<ComparableToken>,
92+
pub has_no_attributes: bool,
93+
}
94+
impl ImportInfo {
95+
pub fn from_module_item(
96+
item: &AnyJsModuleItem,
97+
) -> Option<(Self, Option<JsNamedSpecifiers>, Option<JsImportAssertion>)> {
98+
match item {
99+
AnyJsModuleItem::AnyJsStatement(_) => None,
100+
AnyJsModuleItem::JsExport(export) => Self::from_export(export),
101+
AnyJsModuleItem::JsImport(import) => Self::from_import(import),
102+
}
103+
}
104+
105+
fn from_import(
106+
value: &JsImport,
107+
) -> Option<(Self, Option<JsNamedSpecifiers>, Option<JsImportAssertion>)> {
108+
let (kind, named_specifiers, source, attributes) = match value.import_clause().ok()? {
109+
AnyJsImportClause::JsImportBareClause(_) => {
110+
return None;
111+
}
112+
AnyJsImportClause::JsImportCombinedClause(clause) => {
113+
let (kind, named_specifiers) = match clause.specifier().ok()? {
114+
AnyJsCombinedSpecifier::JsNamedImportSpecifiers(specifiers) => {
115+
(ImportStatementKind::DefaultNamed, Some(specifiers))
116+
}
117+
AnyJsCombinedSpecifier::JsNamespaceImportSpecifier(_) => {
118+
(ImportStatementKind::DefaultNamespace, None)
119+
}
120+
};
121+
(kind, named_specifiers, clause.source(), clause.assertion())
122+
}
123+
AnyJsImportClause::JsImportDefaultClause(clause) => (
124+
if clause.type_token().is_some() {
125+
ImportStatementKind::DefaultType
126+
} else {
127+
ImportStatementKind::Default
128+
},
129+
None,
130+
clause.source(),
131+
clause.assertion(),
132+
),
133+
AnyJsImportClause::JsImportNamedClause(clause) => {
134+
let named_specifiers = clause.named_specifiers().ok();
135+
(
136+
if clause.type_token().is_some() {
137+
ImportStatementKind::NamedType
138+
} else {
139+
ImportStatementKind::Named
140+
},
141+
named_specifiers,
142+
clause.source(),
143+
clause.assertion(),
144+
)
145+
}
146+
AnyJsImportClause::JsImportNamespaceClause(clause) => (
147+
if clause.type_token().is_some() {
148+
ImportStatementKind::NamespaceType
149+
} else {
150+
ImportStatementKind::Namespace
151+
},
152+
None,
153+
clause.source(),
154+
clause.assertion(),
155+
),
156+
};
157+
let Ok(AnyJsModuleSource::JsModuleSource(source)) = source else {
158+
return None;
159+
};
160+
Some((
161+
Self {
162+
source: ComparableToken(source.inner_string_text().ok()?).into(),
163+
has_no_attributes: attributes.is_none(),
164+
kind,
165+
slot_index: value.syntax().index() as u32,
166+
},
167+
named_specifiers.map(JsNamedSpecifiers::JsNamedImportSpecifiers),
168+
attributes,
169+
))
170+
}
171+
172+
fn from_export(
173+
value: &JsExport,
174+
) -> Option<(Self, Option<JsNamedSpecifiers>, Option<JsImportAssertion>)> {
175+
let (kind, _first_local_name, named_specifiers, source, attributes) =
176+
match value.export_clause().ok()? {
177+
AnyJsExportClause::JsExportFromClause(clause) => (
178+
if clause.type_token().is_some() {
179+
ImportStatementKind::NamespaceType
180+
} else {
181+
ImportStatementKind::Namespace
182+
},
183+
clause
184+
.export_as()
185+
.and_then(|export_as| export_as.exported_name().ok()),
186+
None,
187+
clause.source(),
188+
clause.assertion(),
189+
),
190+
AnyJsExportClause::JsExportNamedFromClause(clause) => (
191+
if clause.type_token().is_some() {
192+
ImportStatementKind::NamedType
193+
} else {
194+
ImportStatementKind::Named
195+
},
196+
clause
197+
.specifiers()
198+
.into_iter()
199+
.flatten()
200+
.next()
201+
.and_then(|x| x.source_name().ok()),
202+
Some(clause.specifiers()),
203+
clause.source(),
204+
clause.assertion(),
205+
),
206+
_ => {
207+
return None;
208+
}
209+
};
210+
let Ok(AnyJsModuleSource::JsModuleSource(source)) = source else {
211+
return None;
212+
};
213+
let source = source.inner_string_text().ok()?;
214+
Some((
215+
Self {
216+
source: ComparableToken(source).into(),
217+
has_no_attributes: attributes.is_none(),
218+
kind,
219+
slot_index: value.syntax().index() as u32,
220+
},
221+
named_specifiers.map(JsNamedSpecifiers::JsExportNamedFromSpecifierList),
222+
attributes,
223+
))
224+
}
225+
}

0 commit comments

Comments
 (0)