Skip to content

Commit 5ff50f8

Browse files
authored
perf: optimise noImportCycles (#6884)
1 parent 3402976 commit 5ff50f8

14 files changed

+114
-108
lines changed

.changeset/cyclic-checks-charge.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Improved the performance of `noImportCycles` by ~30%.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_analyze/src/lint/nursery/no_import_cycles.rs

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::collections::HashSet;
2-
31
use biome_analyze::{
42
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
53
};
@@ -10,6 +8,7 @@ use biome_module_graph::{JsModuleInfo, ResolvedPath};
108
use biome_rowan::AstNode;
119
use biome_rule_options::no_import_cycles::NoImportCyclesOptions;
1210
use camino::{Utf8Path, Utf8PathBuf};
11+
use rustc_hash::FxHashSet;
1312

1413
use crate::services::module_graph::ResolvedImports;
1514

@@ -166,36 +165,32 @@ fn find_cycle(
166165
start_path: &Utf8Path,
167166
mut module_info: JsModuleInfo,
168167
) -> Option<Box<[Box<str>]>> {
169-
let mut seen = HashSet::new();
168+
let mut seen = FxHashSet::default();
170169
let mut stack: Vec<(Box<str>, JsModuleInfo)> = Vec::new();
171170

172171
'outer: loop {
173172
for resolved_path in module_info.all_import_paths() {
174-
let Some(resolved_path) = resolved_path.as_path() else {
173+
let Some(path) = resolved_path.as_path() else {
175174
continue;
176175
};
177176

178-
if resolved_path == ctx.file_path() {
177+
if !seen.insert(resolved_path.clone()) {
178+
continue;
179+
}
180+
181+
if path == ctx.file_path() {
179182
// Return all the paths from `start_path` to `resolved_path`:
180183
let paths = Some(start_path.as_str())
181184
.into_iter()
182185
.map(Box::from)
183186
.chain(stack.into_iter().map(|(path, _)| path))
184-
.chain(Some(Box::from(resolved_path.as_str())))
187+
.chain(Some(Box::from(path.as_str())))
185188
.collect();
186189
return Some(paths);
187190
}
188191

189-
// FIXME: Use `get_or_insert_with()` once it's stabilized.
190-
// See: https://github.com/rust-lang/rust/issues/60896
191-
if seen.contains(resolved_path.as_str()) {
192-
continue;
193-
}
194-
195-
seen.insert(resolved_path.to_string());
196-
197-
if let Some(next_module_info) = ctx.module_info_for_path(resolved_path) {
198-
stack.push((resolved_path.as_str().into(), module_info));
192+
if let Some(next_module_info) = ctx.module_info_for_path(path) {
193+
stack.push((path.as_str().into(), module_info));
199194
module_info = next_module_info;
200195
continue 'outer;
201196
}

crates/biome_module_graph/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ biome_resolver = { workspace = true }
2626
biome_rowan = { workspace = true }
2727
camino = { workspace = true }
2828
cfg-if = { workspace = true }
29+
indexmap = { workspace = true }
2930
once_cell = "1.21.3" # Use `std::sync::OnceLock::get_or_try_init` when it is stable.
3031
papaya = { workspace = true }
3132
rust-lapper = { workspace = true }

crates/biome_module_graph/src/js_module_info.rs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ mod module_resolver;
44
mod scope;
55
mod visitor;
66

7-
use std::{collections::BTreeMap, ops::Deref, sync::Arc};
7+
use std::{ops::Deref, sync::Arc};
88

99
use biome_js_syntax::AnyJsImportLike;
1010
use biome_js_type_info::{BindingId, ImportSymbol, ResolvedTypeId, ScopeId, TypeData};
1111
use biome_jsdoc_comment::JsdocComment;
1212
use biome_resolver::ResolvedPath;
1313
use biome_rowan::{Text, TextRange};
14+
use indexmap::IndexMap;
1415
use rust_lapper::Lapper;
1516
use rustc_hash::FxHashMap;
1617

@@ -38,10 +39,9 @@ impl JsModuleInfo {
3839
/// Returns an iterator over all the static and dynamic imports in this
3940
/// module.
4041
pub fn all_import_paths(&self) -> impl Iterator<Item = ResolvedPath> + use<> {
41-
let module_info = self.0.as_ref();
4242
ImportPathIterator {
43-
static_import_paths: module_info.static_import_paths.clone(),
44-
dynamic_import_paths: module_info.dynamic_import_paths.clone(),
43+
module_info: self.clone(),
44+
index: 0,
4545
}
4646
}
4747

@@ -105,7 +105,7 @@ pub struct JsModuleInfoInner {
105105
/// absolute path it resolves to. The resolved path may be looked up as key
106106
/// in the [ModuleGraph::data] map, although it is not required to exist
107107
/// (for instance, if the path is outside the project's scope).
108-
pub static_import_paths: BTreeMap<Text, ResolvedPath>,
108+
pub static_import_paths: IndexMap<Text, ResolvedPath>,
109109

110110
/// Map of all dynamic import paths found in the module for which the import
111111
/// specifier could be statically determined.
@@ -121,7 +121,7 @@ pub struct JsModuleInfoInner {
121121
///
122122
/// Paths found in `require()` expressions in CommonJS sources are also
123123
/// included with the dynamic import paths.
124-
pub dynamic_import_paths: BTreeMap<Text, ResolvedPath>,
124+
pub dynamic_import_paths: IndexMap<Text, ResolvedPath>,
125125

126126
/// Map of exports from the module.
127127
///
@@ -156,21 +156,22 @@ pub struct JsModuleInfoInner {
156156
}
157157

158158
#[derive(Debug, Default)]
159-
pub struct Exports(pub(crate) BTreeMap<Text, JsExport>);
159+
pub struct Exports(pub(crate) IndexMap<Text, JsExport>);
160160

161161
impl Deref for Exports {
162-
type Target = BTreeMap<Text, JsExport>;
162+
type Target = IndexMap<Text, JsExport>;
163163

164164
fn deref(&self) -> &Self::Target {
165165
&self.0
166166
}
167167
}
168168

169169
#[derive(Debug, Default)]
170-
pub struct Imports(pub(crate) BTreeMap<Text, JsImport>);
170+
pub struct Imports(pub(crate) IndexMap<Text, JsImport>);
171171

172172
impl Deref for Imports {
173-
type Target = BTreeMap<Text, JsImport>;
173+
type Target = IndexMap<Text, JsImport>;
174+
174175
fn deref(&self) -> &Self::Target {
175176
&self.0
176177
}
@@ -307,23 +308,29 @@ pub struct JsReexport {
307308
}
308309

309310
struct ImportPathIterator {
310-
static_import_paths: BTreeMap<Text, ResolvedPath>,
311-
dynamic_import_paths: BTreeMap<Text, ResolvedPath>,
311+
module_info: JsModuleInfo,
312+
index: usize,
312313
}
313314

314315
impl Iterator for ImportPathIterator {
315316
type Item = ResolvedPath;
316317

317318
fn next(&mut self) -> Option<Self::Item> {
318-
if self.static_import_paths.is_empty() {
319-
self.dynamic_import_paths
320-
.pop_first()
321-
.map(|(_source, path)| path)
319+
let num_static_imports = self.module_info.static_import_paths.len();
320+
let resolved_path = if self.index < num_static_imports {
321+
let resolved_path = &self.module_info.static_import_paths[self.index];
322+
self.index += 1;
323+
resolved_path
324+
} else if self.index < self.module_info.dynamic_import_paths.len() + num_static_imports {
325+
let resolved_path =
326+
&self.module_info.dynamic_import_paths[self.index - num_static_imports];
327+
self.index += 1;
328+
resolved_path
322329
} else {
323-
self.static_import_paths
324-
.pop_first()
325-
.map(|(_identifier, path)| path)
326-
}
330+
return None;
331+
};
332+
333+
Some(resolved_path.clone())
327334
}
328335
}
329336

crates/biome_module_graph/src/js_module_info/collector.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
use std::{
2-
borrow::Cow,
3-
collections::{BTreeMap, BTreeSet},
4-
sync::Arc,
5-
};
1+
use std::{borrow::Cow, collections::BTreeSet, sync::Arc};
62

73
use biome_js_semantic::{SemanticEvent, SemanticEventExtractor};
84
use biome_js_syntax::{
@@ -19,6 +15,7 @@ use biome_js_type_info::{
1915
};
2016
use biome_jsdoc_comment::JsdocComment;
2117
use biome_rowan::{AstNode, Text, TextRange, TextSize, TokenText};
18+
use indexmap::IndexMap;
2219
use rust_lapper::{Interval, Lapper};
2320
use rustc_hash::FxHashMap;
2421

@@ -77,11 +74,11 @@ pub(super) struct JsModuleInfoCollector {
7774

7875
/// Map with all static import paths, from the source specifier to the
7976
/// resolved path.
80-
static_import_paths: BTreeMap<Text, ResolvedPath>,
77+
static_import_paths: IndexMap<Text, ResolvedPath>,
8178

8279
/// Map with all dynamic import paths, from the import source to the
8380
/// resolved path.
84-
dynamic_import_paths: BTreeMap<Text, ResolvedPath>,
81+
dynamic_import_paths: IndexMap<Text, ResolvedPath>,
8582

8683
/// All collected exports.
8784
///
@@ -96,7 +93,7 @@ pub(super) struct JsModuleInfoCollector {
9693
types: TypeStore,
9794

9895
/// Static imports mapped from the local name of the binding being imported.
99-
static_imports: BTreeMap<Text, JsImport>,
96+
static_imports: IndexMap<Text, JsImport>,
10097
}
10198

10299
/// Intermediary representation for an exported symbol.
@@ -519,7 +516,7 @@ impl JsModuleInfoCollector {
519516
}
520517
}
521518

522-
fn finalise(&mut self) -> (BTreeMap<Text, JsExport>, Lapper<u32, ScopeId>) {
519+
fn finalise(&mut self) -> (IndexMap<Text, JsExport>, Lapper<u32, ScopeId>) {
523520
let scope_by_range = Lapper::new(
524521
self.scope_range_by_start
525522
.iter()
@@ -767,8 +764,8 @@ impl JsModuleInfoCollector {
767764
}
768765
}
769766

770-
fn collect_exports(&mut self) -> BTreeMap<Text, JsExport> {
771-
let mut finalised_exports = BTreeMap::new();
767+
fn collect_exports(&mut self) -> IndexMap<Text, JsExport> {
768+
let mut finalised_exports = IndexMap::new();
772769

773770
let exports = std::mem::take(&mut self.exports);
774771
for export in exports {

crates/biome_module_graph/tests/snapshots/test_export_const_type_declaration_with_namespace.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ export = shared;
2222

2323
```
2424
Exports {
25-
"default" => {
26-
ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(3))
27-
}
2825
"foo" => {
2926
ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(1))
3027
}
28+
"default" => {
29+
ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(3))
30+
}
3131
}
3232
Imports {
3333
No imports

crates/biome_module_graph/tests/snapshots/test_resolve_export_types.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ export const superComputer = new DeepThought();
4242

4343
```
4444
Exports {
45-
"superComputer" => {
46-
ExportOwnExport => JsOwnExport::Binding(3)
47-
}
4845
"theAnswer" => {
4946
ExportOwnExport => JsOwnExport::Binding(0)
5047
}
48+
"superComputer" => {
49+
ExportOwnExport => JsOwnExport::Binding(3)
50+
}
5151
}
5252
Imports {
5353
No imports

crates/biome_module_graph/tests/snapshots/test_resolve_exports.snap

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,29 +60,35 @@ export * as renamed2 from "./renamed-reexports";
6060
6161
```
6262
Exports {
63-
"a" => {
64-
ExportOwnExport => JsOwnExport::Binding(5)
63+
"foo" => {
64+
ExportOwnExport => JsOwnExport::Binding(0)
6565
}
66-
"b" => {
67-
ExportOwnExport => JsOwnExport::Binding(6)
66+
"qux" => {
67+
ExportOwnExport => JsOwnExport::Binding(4)
6868
}
6969
"bar" => {
7070
ExportOwnExport => JsOwnExport::Binding(1)
7171
}
72+
"quz" => {
73+
ExportOwnExport => JsOwnExport::Binding(2)
74+
}
7275
"baz" => {
7376
ExportOwnExport => JsOwnExport::Binding(3)
7477
}
78+
"a" => {
79+
ExportOwnExport => JsOwnExport::Binding(5)
80+
}
81+
"b" => {
82+
ExportOwnExport => JsOwnExport::Binding(6)
83+
}
7584
"d" => {
7685
ExportOwnExport => JsOwnExport::Binding(7)
7786
}
78-
"default" => {
79-
ExportOwnExport => JsOwnExport::Binding(11)
80-
}
8187
"e" => {
8288
ExportOwnExport => JsOwnExport::Binding(8)
8389
}
84-
"foo" => {
85-
ExportOwnExport => JsOwnExport::Binding(0)
90+
"default" => {
91+
ExportOwnExport => JsOwnExport::Binding(11)
8692
}
8793
"oh\nno" => {
8894
ExportReexport => Reexport(
@@ -91,12 +97,6 @@ Exports {
9197
Import Symbol: ohNo
9298
)
9399
}
94-
"qux" => {
95-
ExportOwnExport => JsOwnExport::Binding(4)
96-
}
97-
"quz" => {
98-
ExportOwnExport => JsOwnExport::Binding(2)
99-
}
100100
"renamed2" => {
101101
ExportReexport => Reexport(
102102
Specifier: "./renamed-reexports"

crates/biome_module_graph/tests/snapshots/test_resolve_merged_types.snap

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,18 @@ export { A, B };
2525

2626
```
2727
Exports {
28-
"A" => {
29-
ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(2))
30-
}
31-
"B" => {
32-
ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(10))
33-
}
3428
"Union" => {
3529
ExportOwnExport => JsOwnExport::Binding(3)
3630
}
3731
"Union2" => {
3832
ExportOwnExport => JsOwnExport::Binding(7)
3933
}
34+
"A" => {
35+
ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(2))
36+
}
37+
"B" => {
38+
ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(10))
39+
}
4040
}
4141
Imports {
4242
No imports

0 commit comments

Comments
 (0)