Repository: biomejs/biome (language: rust)

Issue:
📎 Make `DependencyGraph` available to lint rules
### Description

See: https://github.com/biomejs/biome/pull/4929


Discussion / hints:

Fix patch:
diff --git a/.changeset/add_the_new_rule_noimportcycles.md b/.changeset/add_the_new_rule_noimportcycles.md
new file mode 100644
index 000000000000..a12a6c92caf5
--- /dev/null
+++ b/.changeset/add_the_new_rule_noimportcycles.md
@@ -0,0 +1,5 @@
+---
+cli: minor
+---
+
+# Add the new rule [`noImportCycles`](https://biomejs.dev/linter/rules/no-import-cycles)
diff --git a/Cargo.lock b/Cargo.lock
index f6eee0d6e321..57367a7b31fd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -811,6 +811,7 @@ dependencies = [
  "biome_aria_metadata",
  "biome_console",
  "biome_control_flow",
+ "biome_dependency_graph",
  "biome_deserialize",
  "biome_deserialize_macros",
  "biome_diagnostics",
@@ -1343,9 +1344,12 @@ dependencies = [
  "biome_analyze",
  "biome_configuration",
  "biome_console",
+ "biome_dependency_graph",
  "biome_deserialize",
  "biome_diagnostics",
  "biome_formatter",
+ "biome_fs",
+ "biome_js_parser",
  "biome_json_parser",
  "biome_package",
  "biome_project_layout",
diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
index 0c841e127fff..0fd2eda9e9e2 100644
--- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
+++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
@@ -681,6 +681,17 @@ pub(crate) fn migrate_eslint_any_rule(
                 .get_or_insert(Default::default());
             rule.set_level(rule.level().max(rule_severity.into()));
         }
+        "import/no-cycle" => {
+            if !options.include_nursery {
+                return false;
+            }
+            let group = rules.nursery.get_or_insert_with(Default::default);
+            let rule = group
+                .unwrap_group_as_mut()
+                .no_import_cycles
+                .get_or_insert(Default::default());
+            rule.set_level(rule.level().max(rule_severity.into()));
+        }
         "import/no-default-export" => {
             let group = rules.style.get_or_insert_with(Default::default);
             let rule = group
diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs
index 43e6fed01a65..9bcf2c01a92c 100644
--- a/crates/biome_configuration/src/analyzer/linter/rules.rs
+++ b/crates/biome_configuration/src/analyzer/linter/rules.rs
@@ -3086,6 +3086,9 @@ pub struct Nursery {
     #[doc = "Prevent usage of \\<img> element in a Next.js project."]
     #[serde(skip_serializing_if = "Option::is_none")]
     pub no_img_element: Option<RuleConfiguration<biome_js_analyze::options::NoImgElement>>,
+    #[doc = "Prevent import cycles."]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub no_import_cycles: Option<RuleConfiguration<biome_js_analyze::options::NoImportCycles>>,
     #[doc = "Disallows the use of irregular whitespace characters."]
     #[serde(skip_serializing_if = "Option::is_none")]
     pub no_irregular_whitespace:
@@ -3267,6 +3270,7 @@ impl Nursery {
         "noHeadElement",
         "noHeadImportInDocument",
         "noImgElement",
+        "noImportCycles",
         "noIrregularWhitespace",
         "noMissingVarFunction",
         "noNestedTernary",
@@ -3318,19 +3322,19 @@ impl Nursery {
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58]),
     ];
     const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]),
@@ -3393,6 +3397,7 @@ impl Nursery {
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60]),
     ];
 }
 impl RuleGroupExt for Nursery {
@@ -3484,226 +3489,231 @@ impl RuleGroupExt for Nursery {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]));
             }
         }
-        if let Some(rule) = self.no_irregular_whitespace.as_ref() {
+        if let Some(rule) = self.no_import_cycles.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]));
             }
         }
-        if let Some(rule) = self.no_missing_var_function.as_ref() {
+        if let Some(rule) = self.no_irregular_whitespace.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]));
             }
         }
-        if let Some(rule) = self.no_nested_ternary.as_ref() {
+        if let Some(rule) = self.no_missing_var_function.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]));
             }
         }
-        if let Some(rule) = self.no_noninteractive_element_interactions.as_ref() {
+        if let Some(rule) = self.no_nested_ternary.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]));
             }
         }
-        if let Some(rule) = self.no_octal_escape.as_ref() {
+        if let Some(rule) = self.no_noninteractive_element_interactions.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]));
             }
         }
-        if let Some(rule) = self.no_package_private_imports.as_ref() {
+        if let Some(rule) = self.no_octal_escape.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]));
             }
         }
-        if let Some(rule) = self.no_process_env.as_ref() {
+        if let Some(rule) = self.no_package_private_imports.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]));
             }
         }
-        if let Some(rule) = self.no_process_global.as_ref() {
+        if let Some(rule) = self.no_process_env.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]));
             }
         }
-        if let Some(rule) = self.no_restricted_imports.as_ref() {
+        if let Some(rule) = self.no_process_global.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]));
             }
         }
-        if let Some(rule) = self.no_restricted_types.as_ref() {
+        if let Some(rule) = self.no_restricted_imports.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]));
             }
         }
-        if let Some(rule) = self.no_secrets.as_ref() {
+        if let Some(rule) = self.no_restricted_types.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]));
             }
         }
-        if let Some(rule) = self.no_static_element_interactions.as_ref() {
+        if let Some(rule) = self.no_secrets.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]));
             }
         }
-        if let Some(rule) = self.no_substr.as_ref() {
+        if let Some(rule) = self.no_static_element_interactions.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]));
             }
         }
-        if let Some(rule) = self.no_template_curly_in_string.as_ref() {
+        if let Some(rule) = self.no_substr.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]));
             }
         }
-        if let Some(rule) = self.no_ts_ignore.as_ref() {
+        if let Some(rule) = self.no_template_curly_in_string.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]));
             }
         }
-        if let Some(rule) = self.no_unknown_at_rule.as_ref() {
+        if let Some(rule) = self.no_ts_ignore.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]));
             }
         }
-        if let Some(rule) = self.no_unknown_pseudo_class.as_ref() {
+        if let Some(rule) = self.no_unknown_at_rule.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]));
             }
         }
-        if let Some(rule) = self.no_unknown_pseudo_element.as_ref() {
+        if let Some(rule) = self.no_unknown_pseudo_class.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]));
             }
         }
-        if let Some(rule) = self.no_unknown_type_selector.as_ref() {
+        if let Some(rule) = self.no_unknown_pseudo_element.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]));
             }
         }
-        if let Some(rule) = self.no_unwanted_polyfillio.as_ref() {
+        if let Some(rule) = self.no_unknown_type_selector.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]));
             }
         }
-        if let Some(rule) = self.no_useless_escape_in_regex.as_ref() {
+        if let Some(rule) = self.no_unwanted_polyfillio.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]));
             }
         }
-        if let Some(rule) = self.no_useless_string_raw.as_ref() {
+        if let Some(rule) = self.no_useless_escape_in_regex.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]));
             }
         }
-        if let Some(rule) = self.no_useless_undefined.as_ref() {
+        if let Some(rule) = self.no_useless_string_raw.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]));
             }
         }
-        if let Some(rule) = self.no_value_at_rule.as_ref() {
+        if let Some(rule) = self.no_useless_undefined.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]));
             }
         }
-        if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
+        if let Some(rule) = self.no_value_at_rule.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]));
             }
         }
-        if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
+        if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]));
             }
         }
-        if let Some(rule) = self.use_at_index.as_ref() {
+        if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]));
             }
         }
-        if let Some(rule) = self.use_collapsed_if.as_ref() {
+        if let Some(rule) = self.use_at_index.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]));
             }
         }
-        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
+        if let Some(rule) = self.use_collapsed_if.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]));
             }
         }
-        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
+        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]));
             }
         }
-        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
+        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]));
             }
         }
-        if let Some(rule) = self.use_deprecated_reason.as_ref() {
+        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]));
             }
         }
-        if let Some(rule) = self.use_explicit_type.as_ref() {
+        if let Some(rule) = self.use_deprecated_reason.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]));
             }
         }
-        if let Some(rule) = self.use_exports_last.as_ref() {
+        if let Some(rule) = self.use_explicit_type.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49]));
             }
         }
-        if let Some(rule) = self.use_google_font_display.as_ref() {
+        if let Some(rule) = self.use_exports_last.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]));
             }
         }
-        if let Some(rule) = self.use_google_font_preconnect.as_ref() {
+        if let Some(rule) = self.use_google_font_display.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]));
             }
         }
-        if let Some(rule) = self.use_guard_for_in.as_ref() {
+        if let Some(rule) = self.use_google_font_preconnect.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]));
             }
         }
-        if let Some(rule) = self.use_named_operation.as_ref() {
+        if let Some(rule) = self.use_guard_for_in.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53]));
             }
         }
-        if let Some(rule) = self.use_naming_convention.as_ref() {
+        if let Some(rule) = self.use_named_operation.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54]));
             }
         }
-        if let Some(rule) = self.use_parse_int_radix.as_ref() {
+        if let Some(rule) = self.use_naming_convention.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55]));
             }
         }
-        if let Some(rule) = self.use_sorted_classes.as_ref() {
+        if let Some(rule) = self.use_parse_int_radix.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56]));
             }
         }
-        if let Some(rule) = self.use_strict_mode.as_ref() {
+        if let Some(rule) = self.use_sorted_classes.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]));
             }
         }
-        if let Some(rule) = self.use_trim_start_end.as_ref() {
+        if let Some(rule) = self.use_strict_mode.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58]));
             }
         }
-        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+        if let Some(rule) = self.use_trim_start_end.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59]));
             }
         }
+        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+            if rule.is_enabled() {
+                index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60]));
+            }
+        }
         index_set
     }
     fn get_disabled_rules(&self) -> FxHashSet<RuleFilter<'static>> {
@@ -3788,226 +3798,231 @@ impl RuleGroupExt for Nursery {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]));
             }
         }
-        if let Some(rule) = self.no_irregular_whitespace.as_ref() {
+        if let Some(rule) = self.no_import_cycles.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]));
             }
         }
-        if let Some(rule) = self.no_missing_var_function.as_ref() {
+        if let Some(rule) = self.no_irregular_whitespace.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]));
             }
         }
-        if let Some(rule) = self.no_nested_ternary.as_ref() {
+        if let Some(rule) = self.no_missing_var_function.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]));
             }
         }
-        if let Some(rule) = self.no_noninteractive_element_interactions.as_ref() {
+        if let Some(rule) = self.no_nested_ternary.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]));
             }
         }
-        if let Some(rule) = self.no_octal_escape.as_ref() {
+        if let Some(rule) = self.no_noninteractive_element_interactions.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]));
             }
         }
-        if let Some(rule) = self.no_package_private_imports.as_ref() {
+        if let Some(rule) = self.no_octal_escape.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]));
             }
         }
-        if let Some(rule) = self.no_process_env.as_ref() {
+        if let Some(rule) = self.no_package_private_imports.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]));
             }
         }
-        if let Some(rule) = self.no_process_global.as_ref() {
+        if let Some(rule) = self.no_process_env.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]));
             }
         }
-        if let Some(rule) = self.no_restricted_imports.as_ref() {
+        if let Some(rule) = self.no_process_global.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]));
             }
         }
-        if let Some(rule) = self.no_restricted_types.as_ref() {
+        if let Some(rule) = self.no_restricted_imports.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]));
             }
         }
-        if let Some(rule) = self.no_secrets.as_ref() {
+        if let Some(rule) = self.no_restricted_types.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]));
             }
         }
-        if let Some(rule) = self.no_static_element_interactions.as_ref() {
+        if let Some(rule) = self.no_secrets.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]));
             }
         }
-        if let Some(rule) = self.no_substr.as_ref() {
+        if let Some(rule) = self.no_static_element_interactions.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]));
             }
         }
-        if let Some(rule) = self.no_template_curly_in_string.as_ref() {
+        if let Some(rule) = self.no_substr.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]));
             }
         }
-        if let Some(rule) = self.no_ts_ignore.as_ref() {
+        if let Some(rule) = self.no_template_curly_in_string.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]));
             }
         }
-        if let Some(rule) = self.no_unknown_at_rule.as_ref() {
+        if let Some(rule) = self.no_ts_ignore.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]));
             }
         }
-        if let Some(rule) = self.no_unknown_pseudo_class.as_ref() {
+        if let Some(rule) = self.no_unknown_at_rule.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]));
             }
         }
-        if let Some(rule) = self.no_unknown_pseudo_element.as_ref() {
+        if let Some(rule) = self.no_unknown_pseudo_class.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]));
             }
         }
-        if let Some(rule) = self.no_unknown_type_selector.as_ref() {
+        if let Some(rule) = self.no_unknown_pseudo_element.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]));
             }
         }
-        if let Some(rule) = self.no_unwanted_polyfillio.as_ref() {
+        if let Some(rule) = self.no_unknown_type_selector.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]));
             }
         }
-        if let Some(rule) = self.no_useless_escape_in_regex.as_ref() {
+        if let Some(rule) = self.no_unwanted_polyfillio.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]));
             }
         }
-        if let Some(rule) = self.no_useless_string_raw.as_ref() {
+        if let Some(rule) = self.no_useless_escape_in_regex.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]));
             }
         }
-        if let Some(rule) = self.no_useless_undefined.as_ref() {
+        if let Some(rule) = self.no_useless_string_raw.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]));
             }
         }
-        if let Some(rule) = self.no_value_at_rule.as_ref() {
+        if let Some(rule) = self.no_useless_undefined.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]));
             }
         }
-        if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
+        if let Some(rule) = self.no_value_at_rule.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]));
             }
         }
-        if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
+        if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]));
             }
         }
-        if let Some(rule) = self.use_at_index.as_ref() {
+        if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]));
             }
         }
-        if let Some(rule) = self.use_collapsed_if.as_ref() {
+        if let Some(rule) = self.use_at_index.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]));
             }
         }
-        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
+        if let Some(rule) = self.use_collapsed_if.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]));
             }
         }
-        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
+        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]));
             }
         }
-        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
+        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]));
             }
         }
-        if let Some(rule) = self.use_deprecated_reason.as_ref() {
+        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]));
             }
         }
-        if let Some(rule) = self.use_explicit_type.as_ref() {
+        if let Some(rule) = self.use_deprecated_reason.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]));
             }
         }
-        if let Some(rule) = self.use_exports_last.as_ref() {
+        if let Some(rule) = self.use_explicit_type.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49]));
             }
         }
-        if let Some(rule) = self.use_google_font_display.as_ref() {
+        if let Some(rule) = self.use_exports_last.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]));
             }
         }
-        if let Some(rule) = self.use_google_font_preconnect.as_ref() {
+        if let Some(rule) = self.use_google_font_display.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]));
             }
         }
-        if let Some(rule) = self.use_guard_for_in.as_ref() {
+        if let Some(rule) = self.use_google_font_preconnect.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]));
             }
         }
-        if let Some(rule) = self.use_named_operation.as_ref() {
+        if let Some(rule) = self.use_guard_for_in.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53]));
             }
         }
-        if let Some(rule) = self.use_naming_convention.as_ref() {
+        if let Some(rule) = self.use_named_operation.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54]));
             }
         }
-        if let Some(rule) = self.use_parse_int_radix.as_ref() {
+        if let Some(rule) = self.use_naming_convention.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55]));
             }
         }
-        if let Some(rule) = self.use_sorted_classes.as_ref() {
+        if let Some(rule) = self.use_parse_int_radix.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56]));
             }
         }
-        if let Some(rule) = self.use_strict_mode.as_ref() {
+        if let Some(rule) = self.use_sorted_classes.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]));
             }
         }
-        if let Some(rule) = self.use_trim_start_end.as_ref() {
+        if let Some(rule) = self.use_strict_mode.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58]));
             }
         }
-        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+        if let Some(rule) = self.use_trim_start_end.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59]));
             }
         }
+        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+            if rule.is_disabled() {
+                index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60]));
+            }
+        }
         index_set
     }
     #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"]
@@ -4102,6 +4117,10 @@ impl RuleGroupExt for Nursery {
                 .no_img_element
                 .as_ref()
                 .map(|conf| (conf.level(), conf.get_options())),
+            "noImportCycles" => self
+                .no_import_cycles
+                .as_ref()
+                .map(|conf| (conf.level(), conf.get_options())),
             "noIrregularWhitespace" => self
                 .no_irregular_whitespace
                 .as_ref()
diff --git a/crates/biome_console/src/markup.rs b/crates/biome_console/src/markup.rs
index 2d4f6b49c196..42a520f29e18 100644
--- a/crates/biome_console/src/markup.rs
+++ b/crates/biome_console/src/markup.rs
@@ -182,6 +182,19 @@ impl Markup<'_> {
 pub struct MarkupBuf(pub Vec<MarkupNodeBuf>);
 
 impl MarkupBuf {
+    /// Extends the buffer with additional markup.
+    ///
+    /// ## Example
+    ///
+    /// ```rs
+    /// let mut markup = markup!(<Info>"Hello"</Info>).to_owned();
+    /// markup.extend_with(markup!(<Info>"world"</Info>));
+    /// ```
+    pub fn extend_with(&mut self, markup: Markup) {
+        // SAFETY: The implementation of Write for MarkupBuf below always returns Ok
+        Formatter::new(self).write_markup(markup).unwrap();
+    }
+
     pub fn is_empty(&self) -> bool {
         self.0.iter().all(|node| node.content.is_empty())
     }
diff --git a/crates/biome_dependency_graph/src/dependency_graph.rs b/crates/biome_dependency_graph/src/dependency_graph.rs
index 5c31848fc3e9..a0b9587006f6 100644
--- a/crates/biome_dependency_graph/src/dependency_graph.rs
+++ b/crates/biome_dependency_graph/src/dependency_graph.rs
@@ -66,6 +66,24 @@ pub struct ModuleImports {
     pub dynamic_imports: BTreeMap<String, Import>,
 }
 
+impl ModuleImports {
+    /// Allows draining a single entry from the imports.
+    ///
+    /// Returns a `(specifier, import)` pair from either the static or dynamic
+    /// imports, whichever is non-empty. Returns `None` if both are empty.
+    ///
+    /// Using this method allows for consuming the struct while iterating over
+    /// it, without necessarily turning the entire struct into an iterator at
+    /// once.
+    pub fn drain_one(&mut self) -> Option<(String, Import)> {
+        if self.static_imports.is_empty() {
+            self.dynamic_imports.pop_first()
+        } else {
+            self.static_imports.pop_first()
+        }
+    }
+}
+
 #[derive(Clone, Debug, PartialEq)]
 pub struct Import {
     /// Absolute path of the resource being imported, if it can be resolved.
diff --git a/crates/biome_dependency_graph/src/lib.rs b/crates/biome_dependency_graph/src/lib.rs
index d7257a0d9a1c..fd9311947de3 100644
--- a/crates/biome_dependency_graph/src/lib.rs
+++ b/crates/biome_dependency_graph/src/lib.rs
@@ -2,4 +2,4 @@ mod dependency_graph;
 mod import_visitor;
 mod resolver_cache;
 
-pub use dependency_graph::DependencyGraph;
+pub use dependency_graph::{DependencyGraph, ModuleImports};
diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs
index e97ca84b3c92..d32aec8359fe 100644
--- a/crates/biome_diagnostics_categories/src/categories.rs
+++ b/crates/biome_diagnostics_categories/src/categories.rs
@@ -154,6 +154,7 @@ define_categories! {
     "lint/nursery/noHeadElement": "https://biomejs.dev/linter/rules/no-head-element",
     "lint/nursery/noHeadImportInDocument": "https://biomejs.dev/linter/rules/no-head-import-in-document",
     "lint/nursery/noImgElement": "https://biomejs.dev/linter/rules/no-img-element",
+    "lint/nursery/noImportCycles": "https://biomejs.dev/linter/rules/no-import-cycles",
     "lint/nursery/noImportantInKeyframe": "https://biomejs.dev/linter/rules/no-important-in-keyframe",
     "lint/nursery/noInvalidDirectionInLinearGradient": "https://biomejs.dev/linter/rules/no-invalid-direction-in-linear-gradient",
     "lint/nursery/noInvalidGridAreas": "https://biomejs.dev/linter/rules/use-consistent-grid-areas",
diff --git a/crates/biome_js_analyze/Cargo.toml b/crates/biome_js_analyze/Cargo.toml
index 037f094b6f44..0567058264d8 100644
--- a/crates/biome_js_analyze/Cargo.toml
+++ b/crates/biome_js_analyze/Cargo.toml
@@ -16,6 +16,7 @@ biome_aria               = { workspace = true }
 biome_aria_metadata      = { workspace = true }
 biome_console            = { workspace = true }
 biome_control_flow       = { workspace = true }
+biome_dependency_graph   = { workspace = true }
 biome_deserialize        = { workspace = true, features = ["smallvec"] }
 biome_deserialize_macros = { workspace = true }
 biome_diagnostics        = { workspace = true }
diff --git a/crates/biome_js_analyze/src/lib.rs b/crates/biome_js_analyze/src/lib.rs
index d49e447c70c6..799dfafb790e 100644
--- a/crates/biome_js_analyze/src/lib.rs
+++ b/crates/biome_js_analyze/src/lib.rs
@@ -7,6 +7,7 @@ use biome_analyze::{
     MatchQueryParams, MetadataRegistry, RuleAction, RuleRegistry,
 };
 use biome_aria::AriaRoles;
+use biome_dependency_graph::DependencyGraph;
 use biome_diagnostics::Error as DiagnosticError;
 use biome_js_syntax::{JsFileSource, JsLanguage};
 use biome_project_layout::ProjectLayout;
@@ -39,21 +40,42 @@ pub static METADATA: LazyLock<MetadataRegistry> = LazyLock::new(|| {
     metadata
 });
 
+#[derive(Default)]
+pub struct JsAnalyzerServices {
+    dependency_graph: Arc<DependencyGraph>,
+    project_layout: Arc<ProjectLayout>,
+    source_type: JsFileSource,
+}
+
+impl From<(Arc<DependencyGraph>, Arc<ProjectLayout>, JsFileSource)> for JsAnalyzerServices {
+    fn from(
+        (dependency_graph, project_layout, source_type): (
+            Arc<DependencyGraph>,
+            Arc<ProjectLayout>,
+            JsFileSource,
+        ),
+    ) -> Self {
+        Self {
+            dependency_graph,
+            project_layout,
+            source_type,
+        }
+    }
+}
+
 /// Run the analyzer on the provided `root`: this process will use the given `filter`
 /// to selectively restrict analysis to specific rules / a specific source range,
 /// then call `emit_signal` when an analysis rule emits a diagnostic or action.
 /// Additionally, this function takes a `inspect_matcher` function that can be
 /// used to inspect the "query matches" emitted by the analyzer before they are
 /// processed by the lint rules registry
-#[expect(clippy::too_many_arguments)]
 pub fn analyze_with_inspect_matcher<'a, V, F, B>(
     root: &LanguageRoot<JsLanguage>,
     filter: AnalysisFilter,
     inspect_matcher: V,
     options: &'a AnalyzerOptions,
     plugins: Vec<Box<dyn AnalyzerPlugin>>,
-    source_type: JsFileSource,
-    project_layout: Arc<ProjectLayout>,
+    services: JsAnalyzerServices,
     mut emit_signal: F,
 ) -> (Option<B>, Vec<DiagnosticError>)
 where
@@ -90,6 +112,12 @@ where
     let mut registry = RuleRegistry::builder(&filter, root);
     visit_registry(&mut registry);
 
+    let JsAnalyzerServices {
+        dependency_graph,
+        project_layout,
+        source_type,
+    } = services;
+
     let (registry, mut services, diagnostics, visitors) = registry.build();
 
     // Bail if we can't parse a rule option
@@ -117,7 +145,7 @@ where
 
     services.insert_service(Arc::new(AriaRoles));
     services.insert_service(source_type);
-
+    services.insert_service(dependency_graph);
     services.insert_service(project_layout.get_node_manifest_for_path(&options.file_path));
     services.insert_service(project_layout);
 
@@ -140,8 +168,7 @@ pub fn analyze<'a, F, B>(
     filter: AnalysisFilter,
     options: &'a AnalyzerOptions,
     plugins: Vec<Box<dyn AnalyzerPlugin>>,
-    source_type: JsFileSource,
-    project_layout: Arc<ProjectLayout>,
+    services: JsAnalyzerServices,
     emit_signal: F,
 ) -> (Option<B>, Vec<DiagnosticError>)
 where
@@ -154,8 +181,7 @@ where
         |_| {},
         options,
         plugins,
-        source_type,
-        project_layout,
+        services,
         emit_signal,
     )
 }
@@ -195,6 +221,12 @@ let bar = 33;
         let mut dependencies = Dependencies::default();
         dependencies.add("buffer", "latest");
 
+        let services = JsAnalyzerServices::from((
+            Default::default(),
+            project_layout_with_top_level_dependencies(dependencies),
+            JsFileSource::tsx(),
+        ));
+
         analyze(
             &parsed.tree(),
             AnalysisFilter {
@@ -203,8 +235,7 @@ let bar = 33;
             },
             &options,
             Vec::new(),
-            JsFileSource::tsx(),
-            project_layout_with_top_level_dependencies(dependencies),
+            services,
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
                     error_ranges.push(diag.location().span.unwrap());
@@ -252,7 +283,6 @@ let bar = 33;
             AnalysisFilter::default(),
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -338,7 +368,6 @@ let bar = 33;
             AnalysisFilter::default(),
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -410,7 +439,6 @@ let bar = 33;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -455,7 +483,6 @@ let bar = 33;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -507,7 +534,6 @@ debugger;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -554,7 +580,6 @@ debugger;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -603,7 +628,6 @@ debugger;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -653,7 +677,6 @@ let bar = 33;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -701,7 +724,6 @@ let bar = 33;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -752,7 +774,6 @@ let c;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -804,7 +825,6 @@ debugger;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -857,7 +877,6 @@ let d;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
@@ -896,26 +915,22 @@ const foo0 = function (bar: string) {
         };
         let options = AnalyzerOptions::default();
         let root = parsed.tree();
-        analyze(
-            &root,
-            filter,
-            &options,
-            Vec::new(),
-            JsFileSource::ts(),
-            Default::default(),
-            |signal| {
-                if let Some(diag) = signal.diagnostic() {
-                    let error = diag
-                        .with_file_path("dummyFile")
-                        .with_file_source_code(SOURCE);
-                    let text = print_diagnostic_to_string(&error);
-                    eprintln!("{text}");
-                    panic!("Unexpected diagnostic");
-                }
 
-                ControlFlow::<Never>::Continue(())
-            },
-        );
+        let services =
+            JsAnalyzerServices::from((Default::default(), Default::default(), JsFileSource::ts()));
+
+        analyze(&root, filter, &options, Vec::new(), services, |signal| {
+            if let Some(diag) = signal.diagnostic() {
+                let error = diag
+                    .with_file_path("dummyFile")
+                    .with_file_source_code(SOURCE);
+                let text = print_diagnostic_to_string(&error);
+                eprintln!("{text}");
+                panic!("Unexpected diagnostic");
+            }
+
+            ControlFlow::<Never>::Continue(())
+        });
     }
 
     #[test]
@@ -949,7 +964,6 @@ a == b;
             filter,
             &options,
             Vec::new(),
-            JsFileSource::js_module(),
             Default::default(),
             |signal| {
                 if let Some(diag) = signal.diagnostic() {
diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs
index 7b72aa066636..60e4206ec607 100644
--- a/crates/biome_js_analyze/src/lint/nursery.rs
+++ b/crates/biome_js_analyze/src/lint/nursery.rs
@@ -14,6 +14,7 @@ pub mod no_global_dirname_filename;
 pub mod no_head_element;
 pub mod no_head_import_in_document;
 pub mod no_img_element;
+pub mod no_import_cycles;
 pub mod no_irregular_whitespace;
 pub mod no_nested_ternary;
 pub mod no_noninteractive_element_interactions;
@@ -66,6 +67,7 @@ declare_lint_group! {
             self :: no_head_element :: NoHeadElement ,
             self :: no_head_import_in_document :: NoHeadImportInDocument ,
             self :: no_img_element :: NoImgElement ,
+            self :: no_import_cycles :: NoImportCycles ,
             self :: no_irregular_whitespace :: NoIrregularWhitespace ,
             self :: no_nested_ternary :: NoNestedTernary ,
             self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions ,
diff --git a/crates/biome_js_analyze/src/lint/nursery/no_import_cycles.rs b/crates/biome_js_analyze/src/lint/nursery/no_import_cycles.rs
new file mode 100644
index 000000000000..8a51a8029478
--- /dev/null
+++ b/crates/biome_js_analyze/src/lint/nursery/no_import_cycles.rs
@@ -0,0 +1,212 @@
+use std::collections::HashSet;
+
+use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource};
+use biome_console::markup;
+use biome_dependency_graph::ModuleImports;
+use biome_diagnostics::Severity;
+use biome_js_syntax::{inner_string_text, AnyJsImportLike};
+use biome_rowan::AstNode;
+use camino::{Utf8Path, Utf8PathBuf};
+
+use crate::services::dependency_graph::ResolvedImports;
+
+declare_lint_rule! {
+    /// Prevent import cycles.
+    ///
+    /// This rule warns when a file imports another file that, either directly
+    /// or indirectly, imports the original file again.
+    ///
+    /// Cycles can lead to symbols that are unexpectedly `undefined` and are
+    /// generally considered poor code hygiene.
+    ///
+    /// If a cycle is detected, it is advised to move code such that imports
+    /// only go in a single direction, i.e. they don't point "back" to the
+    /// importing file.
+    ///
+    /// ## Examples
+    ///
+    /// ### Invalid
+    ///
+    /// **`foobar.js`**
+    /// ```js
+    /// import { baz } from "./baz.js";
+    ///
+    /// export function foo() {
+    ///     baz();
+    /// }
+    ///
+    /// export function bar() {
+    ///     console.log("foobar");
+    /// }
+    /// ```
+    ///
+    /// **`baz.js`**
+    /// ```js
+    /// import { bar } from "./foobar.js";
+    ///
+    /// export function baz() {
+    ///     bar();
+    /// }
+    /// ```
+    ///
+    /// ### Valid
+    ///
+    /// **`foo.js`**
+    /// ```js
+    /// import { baz } from "./baz.js";
+    ///
+    /// export function foo() {
+    ///     baz();
+    /// }
+    /// ```
+    ///
+    /// **`bar.js`**
+    /// ```js
+    /// export function bar() {
+    ///     console.log("foobar");
+    /// }
+    /// ```
+    ///
+    /// **`baz.js`**
+    /// ```js
+    /// import { bar } from "./bar.js";
+    ///
+    /// export function baz() {
+    ///     bar();
+    /// }
+    /// ```
+    ///
+    pub NoImportCycles {
+        version: "next",
+        name: "noImportCycles",
+        language: "js",
+        sources: &[
+            RuleSource::EslintImport("no-cycle"),
+        ],
+        severity: Severity::Warning,
+        recommended: false,
+    }
+}
+
+impl Rule for NoImportCycles {
+    type Query = ResolvedImports;
+    type State = Vec<String>;
+    type Signals = Option<Self::State>;
+    type Options = ();
+
+    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+        let file_imports = ctx.imports_for_path(ctx.file_path())?;
+
+        let node = ctx.query();
+        let name_token = node.module_name_token()?;
+        let specifier_text = inner_string_text(&name_token);
+        let specifier = specifier_text.text();
+        let import = if is_static_import(node) {
+            file_imports.static_imports.get(specifier)
+        } else {
+            file_imports.dynamic_imports.get(specifier)
+        }?;
+        let resolved_path = import.resolved_path.as_ref().ok()?;
+
+        let imports = ctx.imports_for_path(resolved_path)?;
+        find_cycle(ctx, resolved_path, imports)
+    }
+
+    fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+        let node = ctx.query();
+
+        let cwd = Utf8PathBuf::from(
+            std::env::current_dir()
+                .map(|cwd| cwd.to_string_lossy().to_string())
+                .unwrap_or_default(),
+        );
+
+        let mut note = markup!("This import resolves to ").to_owned();
+        for (i, path) in state.iter().enumerate() {
+            if i > 0 {
+                note.extend_with(markup!("\n    ... which imports "));
+            }
+
+            match Utf8Path::new(path).strip_prefix(&cwd) {
+                Ok(relative_path) => {
+                    note.extend_with(markup!(<Info>{relative_path.as_str()}</Info>))
+                }
+                Err(_) => note.extend_with(markup!(<Info>{path}</Info>)),
+            }
+        }
+        note.extend_with(markup!("\n    ... which is the file we're importing from."));
+
+        Some(
+            RuleDiagnostic::new(
+                rule_category!(),
+                node.range(),
+                markup!("This import is part of a cycle."),
+            )
+            .note(note),
+        )
+    }
+}
+
+/// Attempts to find a cycle by traversing all imports from `start_path` and
+/// finding those that lead back to `ctx.file_path()`.
+///
+/// Cycles that don't lead back to `ctx.file_path()` are not reported, since
+/// they should be reported for the respective files instead.
+///
+/// `imports` are the imports found in `start_path`.
+///
+/// If a cycle is found, this returns a vector with all the paths involved in
+/// the cycle, starting with `start_path` and ending with `ctx.file_path()`.
+fn find_cycle(
+    ctx: &RuleContext<NoImportCycles>,
+    start_path: &Utf8Path,
+    mut imports: ModuleImports,
+) -> Option<Vec<String>> {
+    let mut seen = HashSet::new();
+    let mut stack = Vec::new();
+
+    'outer: loop {
+        while let Some((_specifier, import)) = imports.drain_one() {
+            let Ok(resolved_path) = import.resolved_path else {
+                continue;
+            };
+
+            if resolved_path == ctx.file_path() {
+                // Return all the paths from `start_path` to `resolved_path`:
+                let paths = Some(start_path.to_string())
+                    .into_iter()
+                    .chain(stack.into_iter().map(|(path, _)| path))
+                    .chain(Some(resolved_path.into()))
+                    .collect();
+                return Some(paths);
+            }
+
+            // FIXME: Use `get_or_insert_with()` once it's stabilized.
+            //        See: https://github.com/rust-lang/rust/issues/60896
+            if seen.contains(resolved_path.as_str()) {
+                continue;
+            }
+
+            seen.insert(resolved_path.to_string());
+
+            if let Some(resolved_imports) = ctx.imports_for_path(&resolved_path) {
+                stack.push((resolved_path.into(), imports));
+                imports = resolved_imports;
+                continue 'outer;
+            }
+        }
+
+        match stack.pop() {
+            Some((_previous_path, previous_imports)) => {
+                imports = previous_imports;
+            }
+            None => break,
+        }
+    }
+
+    None
+}
+
+fn is_static_import(node: &AnyJsImportLike) -> bool {
+    matches!(node, AnyJsImportLike::JsModuleSource(_))
+}
diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs
index 2c76ac356702..910947962c39 100644
--- a/crates/biome_js_analyze/src/options.rs
+++ b/crates/biome_js_analyze/src/options.rs
@@ -128,6 +128,8 @@ pub type NoImplicitBoolean =
     <lint::style::no_implicit_boolean::NoImplicitBoolean as biome_analyze::Rule>::Options;
 pub type NoImportAssign =
     <lint::suspicious::no_import_assign::NoImportAssign as biome_analyze::Rule>::Options;
+pub type NoImportCycles =
+    <lint::nursery::no_import_cycles::NoImportCycles as biome_analyze::Rule>::Options;
 pub type NoInferrableTypes =
     <lint::style::no_inferrable_types::NoInferrableTypes as biome_analyze::Rule>::Options;
 pub type NoInnerDeclarations =
diff --git a/crates/biome_js_analyze/src/services/dependency_graph.rs b/crates/biome_js_analyze/src/services/dependency_graph.rs
new file mode 100644
index 000000000000..2cef5d03d81f
--- /dev/null
+++ b/crates/biome_js_analyze/src/services/dependency_graph.rs
@@ -0,0 +1,67 @@
+use biome_analyze::{
+    AddVisitor, FromServices, MissingServicesDiagnostic, Phase, Phases, QueryKey, QueryMatch,
+    Queryable, RuleKey, ServiceBag, SyntaxVisitor,
+};
+use biome_dependency_graph::{DependencyGraph, ModuleImports};
+use biome_js_syntax::{AnyJsImportLike, AnyJsRoot, JsLanguage, JsSyntaxNode};
+use biome_rowan::{AstNode, TextRange};
+use camino::Utf8Path;
+use std::sync::Arc;
+
+#[derive(Debug, Clone)]
+pub struct DependencyGraphService(Arc<DependencyGraph>);
+
+impl DependencyGraphService {
+    pub fn imports_for_path(&self, path: &Utf8Path) -> Option<ModuleImports> {
+        self.0.imports_for_path(path)
+    }
+}
+
+impl FromServices for DependencyGraphService {
+    fn from_services(
+        rule_key: &RuleKey,
+        services: &ServiceBag,
+    ) -> Result<Self, MissingServicesDiagnostic> {
+        let dependency_graph: &Arc<DependencyGraph> = services.get_service().ok_or_else(|| {
+            MissingServicesDiagnostic::new(rule_key.rule_name(), &["DependencyGraph"])
+        })?;
+        Ok(Self(dependency_graph.clone()))
+    }
+}
+
+impl Phase for DependencyGraphService {
+    fn phase() -> Phases {
+        Phases::Syntax
+    }
+}
+
+/// Query type usable by lint rules that matches import statements and uses the
+/// [DependencyGraph] to resolve their specifiers.
+#[derive(Clone)]
+pub struct ResolvedImports(AnyJsImportLike);
+
+impl QueryMatch for ResolvedImports {
+    fn text_range(&self) -> TextRange {
+        self.0.range()
+    }
+}
+
+impl Queryable for ResolvedImports {
+    type Input = JsSyntaxNode;
+    type Output = AnyJsImportLike;
+
+    type Language = JsLanguage;
+    type Services = DependencyGraphService;
+
+    fn build_visitor(analyzer: &mut impl AddVisitor<JsLanguage>, _: &AnyJsRoot) {
+        analyzer.add_visitor(Phases::Syntax, SyntaxVisitor::default);
+    }
+
+    fn key() -> QueryKey<Self::Language> {
+        QueryKey::Syntax(AnyJsImportLike::KIND_SET)
+    }
+
+    fn unwrap_match(_: &ServiceBag, node: &Self::Input) -> Self::Output {
+        AnyJsImportLike::unwrap_cast(node.clone())
+    }
+}
diff --git a/crates/biome_js_analyze/src/services/mod.rs b/crates/biome_js_analyze/src/services/mod.rs
index e4e4e47065ef..a98c7c9309c4 100644
--- a/crates/biome_js_analyze/src/services/mod.rs
+++ b/crates/biome_js_analyze/src/services/mod.rs
@@ -1,5 +1,5 @@
 pub mod aria;
 pub mod control_flow;
-pub mod semantic;
-
+pub mod dependency_graph;
 pub mod manifest;
+pub mod semantic;
diff --git a/crates/biome_js_parser/src/lib.rs b/crates/biome_js_parser/src/lib.rs
index fba30d8bc1fc..f4174c802a12 100644
--- a/crates/biome_js_parser/src/lib.rs
+++ b/crates/biome_js_parser/src/lib.rs
@@ -78,7 +78,7 @@ pub use crate::{
     parse::*,
 };
 use biome_js_factory::JsSyntaxFactory;
-use biome_js_syntax::{JsLanguage, JsSyntaxKind, LanguageVariant};
+use biome_js_syntax::{JsSyntaxKind, LanguageVariant};
 use biome_parser::tree_sink::LosslessTreeSink;
 pub(crate) use parser::{JsParser, ParseRecoveryTokenSet};
 pub(crate) use state::{JsParserState, StrictMode};
diff --git a/crates/biome_js_parser/src/parse.rs b/crates/biome_js_parser/src/parse.rs
index 28487f8c405d..89de8dcc1d06 100644
--- a/crates/biome_js_parser/src/parse.rs
+++ b/crates/biome_js_parser/src/parse.rs
@@ -1,7 +1,7 @@
 //! Utilities for high level parsing of js code.
 
 use crate::*;
-use biome_js_syntax::{
+pub use biome_js_syntax::{
     AnyJsRoot, JsFileSource, JsLanguage, JsModule, JsScript, JsSyntaxNode, ModuleKind,
 };
 use biome_parser::token_source::Trivia;
diff --git a/crates/biome_js_syntax/src/file_source.rs b/crates/biome_js_syntax/src/file_source.rs
index 6764e4b8612d..f85c88ca2606 100644
--- a/crates/biome_js_syntax/src/file_source.rs
+++ b/crates/biome_js_syntax/src/file_source.rs
@@ -306,9 +306,22 @@ impl JsFileSource {
     }
 
     /// Try to return the JS file source corresponding to this file name from well-known files
-    pub fn try_from_well_known(_: &Utf8Path) -> Result<Self, FileSourceError> {
-        // TODO: to be implemented
-        Err(FileSourceError::UnknownFileName)
+    pub fn try_from_well_known(path: &Utf8Path) -> Result<Self, FileSourceError> {
+        // Be careful with definition files, because `Path::extension()` only
+        // returns the extension after the _last_ dot:
+        let file_name = path.file_name().ok_or(FileSourceError::MissingFileName)?;
+        if file_name.ends_with(".d.ts") {
+            return Self::try_from_extension("d.ts");
+        } else if file_name.ends_with(".d.mts") {
+            return Self::try_from_extension("d.mts");
+        } else if file_name.ends_with(".d.cts") {
+            return Self::try_from_extension("d.cts");
+        }
+
+        match path.extension() {
+            Some(extension) => Self::try_from_extension(extension),
+            None => Err(FileSourceError::MissingFileExtension),
+        }
     }
 
     /// Try to return the JS file source corresponding to this file extension
diff --git a/crates/biome_service/src/file_handlers/css.rs b/crates/biome_service/src/file_handlers/css.rs
index 308652faa78a..30990bb48ea2 100644
--- a/crates/biome_service/src/file_handlers/css.rs
+++ b/crates/biome_service/src/file_handlers/css.rs
@@ -553,6 +553,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
         range,
         workspace,
         path,
+        dependency_graph: _,
         project_layout,
         language,
         only,
diff --git a/crates/biome_service/src/file_handlers/graphql.rs b/crates/biome_service/src/file_handlers/graphql.rs
index 66616269d189..d21eb9747822 100644
--- a/crates/biome_service/src/file_handlers/graphql.rs
+++ b/crates/biome_service/src/file_handlers/graphql.rs
@@ -477,6 +477,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
         range,
         workspace,
         path,
+        dependency_graph: _,
         project_layout,
         language,
         only,
diff --git a/crates/biome_service/src/file_handlers/javascript.rs b/crates/biome_service/src/file_handlers/javascript.rs
index 1e0a1e82e08e..ef49d842bd94 100644
--- a/crates/biome_service/src/file_handlers/javascript.rs
+++ b/crates/biome_service/src/file_handlers/javascript.rs
@@ -39,7 +39,9 @@ use biome_formatter::{
 };
 use biome_fs::BiomePath;
 use biome_js_analyze::utils::rename::{RenameError, RenameSymbolExtensions};
-use biome_js_analyze::{analyze, analyze_with_inspect_matcher, ControlFlowGraph};
+use biome_js_analyze::{
+    analyze, analyze_with_inspect_matcher, ControlFlowGraph, JsAnalyzerServices,
+};
 use biome_js_formatter::context::trailing_commas::TrailingCommas;
 use biome_js_formatter::context::{ArrowParentheses, JsFormatOptions, QuoteProperties, Semicolons};
 use biome_js_formatter::format_node;
@@ -617,7 +619,6 @@ fn debug_control_flow(parse: AnyParse, cursor: TextSize) -> String {
         },
         &options,
         Vec::new(),
-        JsFileSource::default(),
         Default::default(),
         |_| ControlFlow::<Never>::Continue(()),
     );
@@ -678,13 +679,14 @@ pub(crate) fn lint(params: LintParams) -> LintResults {
     };
 
     let mut process_lint = ProcessLint::new(&params);
+    let services =
+        JsAnalyzerServices::from((params.dependency_graph, params.project_layout, file_source));
     let (_, analyze_diagnostics) = analyze(
         &tree,
         filter,
         &analyzer_options,
         Vec::new(),
-        file_source,
-        params.project_layout,
+        services,
         |signal| process_lint.process_signal(signal),
     );
 
@@ -698,6 +700,7 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
         range,
         workspace,
         path,
+        dependency_graph,
         project_layout,
         language,
         only,
@@ -738,14 +741,15 @@ pub(crate) fn code_actions(params: CodeActionsParams) -> PullActionsResult {
         };
     };
 
+    let services = JsAnalyzerServices::from((dependency_graph, project_layout, source_type));
+
     debug!("Javascript runs the analyzer");
     analyze(
         &tree,
         filter,
         &analyzer_options,
         Vec::new(),
-        source_type,
-        project_layout,
+        services,
         |signal| {
             actions.extend(signal.actions().into_code_action_iter().map(|item| {
                 debug!("Pulled action category {:?}", item.category);
@@ -813,13 +817,18 @@ pub(crate) fn fix_all(params: FixAllParams) -> Result<FixFileResult, WorkspaceEr
     let mut errors: u16 = 0;
 
     loop {
+        let services = JsAnalyzerServices::from((
+            params.dependency_graph.clone(),
+            params.project_layout.clone(),
+            file_source,
+        ));
+
         let (action, _) = analyze(
             &tree,
             filter,
             &analyzer_options,
             Vec::new(),
-            file_source,
-            params.project_layout.clone(),
+            services,
             |signal| {
                 let current_diagnostic = signal.diagnostic();
 
diff --git a/crates/biome_service/src/file_handlers/json.rs b/crates/biome_service/src/file_handlers/json.rs
index 7f09dd5392c9..8ab82728a4f8 100644
--- a/crates/biome_service/src/file_handlers/json.rs
+++ b/crates/biome_service/src/file_handlers/json.rs
@@ -569,6 +569,7 @@ fn code_actions(params: CodeActionsParams) -> PullActionsResult {
         range,
         workspace,
         path,
+        dependency_graph: _,
         project_layout,
         language,
         skip,
diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs
index 1e8ae98d32e7..fd3295a596a8 100644
--- a/crates/biome_service/src/file_handlers/mod.rs
+++ b/crates/biome_service/src/file_handlers/mod.rs
@@ -22,6 +22,7 @@ use biome_console::fmt::Formatter;
 use biome_console::markup;
 use biome_css_analyze::METADATA as css_metadata;
 use biome_css_syntax::{CssFileSource, CssLanguage};
+use biome_dependency_graph::DependencyGraph;
 use biome_diagnostics::{category, Diagnostic, DiagnosticExt, Severity};
 use biome_formatter::Printed;
 use biome_fs::BiomePath;
@@ -388,6 +389,7 @@ pub struct FixAllParams<'a> {
     /// Whether it should format the code action
     pub(crate) should_format: bool,
     pub(crate) biome_path: &'a BiomePath,
+    pub(crate) dependency_graph: Arc<DependencyGraph>,
     pub(crate) project_layout: Arc<ProjectLayout>,
     pub(crate) document_file_source: DocumentFileSource,
     pub(crate) only: Vec<RuleSelector>,
@@ -457,6 +459,7 @@ pub(crate) struct LintParams<'a> {
     pub(crate) only: Vec<RuleSelector>,
     pub(crate) skip: Vec<RuleSelector>,
     pub(crate) categories: RuleCategories,
+    pub(crate) dependency_graph: Arc<DependencyGraph>,
     pub(crate) project_layout: Arc<ProjectLayout>,
     pub(crate) suppression_reason: Option<String>,
     pub(crate) enabled_rules: Vec<RuleSelector>,
@@ -580,6 +583,7 @@ pub(crate) struct CodeActionsParams<'a> {
     pub(crate) range: Option<TextRange>,
     pub(crate) workspace: &'a WorkspaceSettingsHandle,
     pub(crate) path: &'a BiomePath,
+    pub(crate) dependency_graph: Arc<DependencyGraph>,
     pub(crate) project_layout: Arc<ProjectLayout>,
     pub(crate) language: DocumentFileSource,
     pub(crate) only: Vec<RuleSelector>,
diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs
index 67c03b2eaf37..15e9165db188 100644
--- a/crates/biome_service/src/workspace/server.rs
+++ b/crates/biome_service/src/workspace/server.rs
@@ -904,6 +904,7 @@ impl Workspace for WorkspaceServer {
                     skip,
                     language: self.get_file_source(&path),
                     categories,
+                    dependency_graph: self.dependency_graph.clone(),
                     project_layout: self.project_layout.clone(),
                     suppression_reason: None,
                     enabled_rules,
@@ -980,6 +981,7 @@ impl Workspace for WorkspaceServer {
             range,
             workspace: &settings.into(),
             path: &path,
+            dependency_graph: self.dependency_graph.clone(),
             project_layout: self.project_layout.clone(),
             language,
             only,
@@ -1117,6 +1119,7 @@ impl Workspace for WorkspaceServer {
             workspace: settings.into(),
             should_format,
             biome_path: &path,
+            dependency_graph: self.dependency_graph.clone(),
             project_layout: self.project_layout.clone(),
             document_file_source: language,
             only,
diff --git a/crates/biome_unicode_table/src/tables.rs b/crates/biome_unicode_table/src/tables.rs
index 2016f15b83d8..1e7c1e35849e 100644
--- a/crates/biome_unicode_table/src/tables.rs
+++ b/crates/biome_unicode_table/src/tables.rs
@@ -170,8 +170,8 @@ pub mod derived_property {
         ('ಪ', 'ಳ'),
         ('ವ', 'ಹ'),
         ('\u{cbc}', 'ೄ'),
-        ('\u{cc6}', 'ೈ'),
-        ('ೊ', '\u{ccd}'),
+        ('\u{cc6}', '\u{cc8}'),
+        ('\u{cca}', '\u{ccd}'),
         ('\u{cd5}', '\u{cd6}'),
         ('ೝ', 'ೞ'),
         ('ೠ', '\u{ce3}'),
@@ -256,8 +256,8 @@ pub mod derived_property {
         ('ᚁ', 'ᚚ'),
         ('ᚠ', 'ᛪ'),
         ('ᛮ', 'ᛸ'),
-        ('ᜀ', '᜕'),
-        ('ᜟ', '᜴'),
+        ('ᜀ', '\u{1715}'),
+        ('ᜟ', '\u{1734}'),
         ('ᝀ', '\u{1753}'),
         ('ᝠ', 'ᝬ'),
         ('ᝮ', 'ᝰ'),
@@ -290,11 +290,11 @@ pub mod derived_property {
         ('\u{1b00}', 'ᭌ'),
         ('᭐', '᭙'),
         ('\u{1b6b}', '\u{1b73}'),
-        ('\u{1b80}', '᯳'),
+        ('\u{1b80}', '\u{1bf3}'),
         ('ᰀ', '\u{1c37}'),
         ('᱀', '᱉'),
         ('ᱍ', 'ᱽ'),
-        ('ᲀ', '\u{1c8a}'),
+        ('ᲀ', 'ᲊ'),
         ('Ა', 'Ჺ'),
         ('Ჽ', 'Ჿ'),
         ('\u{1cd0}', '\u{1cd2}'),
@@ -378,10 +378,10 @@ pub mod derived_property {
         ('ꙿ', '\u{a6f1}'),
         ('ꜗ', 'ꜟ'),
         ('Ꜣ', 'ꞈ'),
-        ('Ꞌ', '\u{a7cd}'),
+        ('Ꞌ', 'ꟍ'),
         ('Ꟑ', 'ꟑ'),
         ('ꟓ', 'ꟓ'),
-        ('ꟕ', '\u{a7dc}'),
+        ('ꟕ', 'Ƛ'),
         ('ꟲ', 'ꠧ'),
         ('\u{a82c}', '\u{a82c}'),
         ('ꡀ', 'ꡳ'),
@@ -390,9 +390,9 @@ pub mod derived_property {
         ('\u{a8e0}', 'ꣷ'),
         ('ꣻ', 'ꣻ'),
         ('ꣽ', '\u{a92d}'),
-        ('ꤰ', '꥓'),
+        ('ꤰ', '\u{a953}'),
         ('ꥠ', 'ꥼ'),
-        ('\u{a980}', '꧀'),
+        ('\u{a980}', '\u{a9c0}'),
         ('ꧏ', '꧙'),
         ('ꧠ', 'ꧾ'),
         ('ꨀ', '\u{aa36}'),
@@ -479,7 +479,7 @@ pub mod derived_property {
         ('𐖣', '𐖱'),
         ('𐖳', '𐖹'),
         ('𐖻', '𐖼'),
-        ('\u{105c0}', '\u{105f3}'),
+        ('𐗀', '𐗳'),
         ('𐘀', '𐜶'),
         ('𐝀', '𐝕'),
         ('𐝠', '𐝧'),
@@ -520,13 +520,13 @@ pub mod derived_property {
         ('𐳀', '𐳲'),
         ('𐴀', '\u{10d27}'),
         ('𐴰', '𐴹'),
-        ('\u{10d40}', '\u{10d65}'),
+        ('𐵀', '𐵥'),
         ('\u{10d69}', '\u{10d6d}'),
-        ('\u{10d6f}', '\u{10d85}'),
+        ('𐵯', '𐶅'),
         ('𐺀', '𐺩'),
         ('\u{10eab}', '\u{10eac}'),
         ('𐺰', '𐺱'),
-        ('\u{10ec2}', '\u{10ec4}'),
+        ('𐻂', '𐻄'),
         ('\u{10efc}', '𐼜'),
         ('𐼧', '𐼧'),
         ('𐼰', '\u{10f50}'),
@@ -567,21 +567,21 @@ pub mod derived_property {
         ('𑌵', '𑌹'),
         ('\u{1133b}', '𑍄'),
         ('𑍇', '𑍈'),
-        ('𑍋', '𑍍'),
+        ('𑍋', '\u{1134d}'),
         ('𑍐', '𑍐'),
         ('\u{11357}', '\u{11357}'),
         ('𑍝', '𑍣'),
         ('\u{11366}', '\u{1136c}'),
         ('\u{11370}', '\u{11374}'),
-        ('\u{11380}', '\u{11389}'),
-        ('\u{1138b}', '\u{1138b}'),
-        ('\u{1138e}', '\u{1138e}'),
-        ('\u{11390}', '\u{113b5}'),
-        ('\u{113b7}', '\u{113c0}'),
+        ('𑎀', '𑎉'),
+        ('𑎋', '𑎋'),
+        ('𑎎', '𑎎'),
+        ('𑎐', '𑎵'),
+        ('𑎷', '\u{113c0}'),
         ('\u{113c2}', '\u{113c2}'),
         ('\u{113c5}', '\u{113c5}'),
-        ('\u{113c7}', '\u{113ca}'),
-        ('\u{113cc}', '\u{113d3}'),
+        ('\u{113c7}', '𑏊'),
+        ('𑏌', '𑏓'),
         ('\u{113e1}', '\u{113e2}'),
         ('𑐀', '𑑊'),
         ('𑑐', '𑑙'),
@@ -597,7 +597,7 @@ pub mod derived_property {
         ('𑙐', '𑙙'),
         ('𑚀', '𑚸'),
         ('𑛀', '𑛉'),
-        ('\u{116d0}', '\u{116e3}'),
+        ('𑛐', '𑛣'),
         ('𑜀', '𑜚'),
         ('\u{1171d}', '\u{1172b}'),
         ('𑜰', '𑜹'),
@@ -621,8 +621,8 @@ pub mod derived_property {
         ('𑩐', '\u{11a99}'),
         ('𑪝', '𑪝'),
         ('𑪰', '𑫸'),
-        ('\u{11bc0}', '\u{11be0}'),
-        ('\u{11bf0}', '\u{11bf9}'),
+        ('𑯀', '𑯠'),
+        ('𑯰', '𑯹'),
         ('𑰀', '𑰈'),
         ('𑰊', '\u{11c36}'),
         ('\u{11c38}', '𑱀'),
@@ -655,9 +655,9 @@ pub mod derived_property {
         ('𒾐', '𒿰'),
         ('𓀀', '𓐯'),
         ('\u{13440}', '\u{13455}'),
-        ('\u{13460}', '\u{143fa}'),
+        ('𓑠', '𔏺'),
         ('𔐀', '𔙆'),
-        ('\u{16100}', '\u{16139}'),
+        ('𖄀', '𖄹'),
         ('𖠀', '𖨸'),
         ('𖩀', '𖩞'),
         ('𖩠', '𖩩'),
@@ -670,18 +670,18 @@ pub mod derived_property {
         ('𖭐', '𖭙'),
         ('𖭣', '𖭷'),
         ('𖭽', '𖮏'),
-        ('\u{16d40}', '\u{16d6c}'),
-        ('\u{16d70}', '\u{16d79}'),
+        ('𖵀', '𖵬'),
+        ('𖵰', '𖵹'),
         ('𖹀', '𖹿'),
         ('𖼀', '𖽊'),
         ('\u{16f4f}', '𖾇'),
         ('\u{16f8f}', '𖾟'),
         ('𖿠', '𖿡'),
         ('𖿣', '\u{16fe4}'),
-        ('𖿰', '𖿱'),
+        ('\u{16ff0}', '\u{16ff1}'),
         ('𗀀', '𘟷'),
         ('𘠀', '𘳕'),
-        ('\u{18cff}', '𘴈'),
+        ('𘳿', '𘴈'),
         ('𚿰', '𚿳'),
         ('𚿵', '𚿻'),
         ('𚿽', '𚿾'),
@@ -696,11 +696,11 @@ pub mod derived_property {
         ('𛲀', '𛲈'),
         ('𛲐', '𛲙'),
         ('\u{1bc9d}', '\u{1bc9e}'),
-        ('\u{1ccf0}', '\u{1ccf9}'),
+        ('𜳰', '𜳹'),
         ('\u{1cf00}', '\u{1cf2d}'),
         ('\u{1cf30}', '\u{1cf46}'),
         ('\u{1d165}', '\u{1d169}'),
-        ('𝅭', '\u{1d172}'),
+        ('\u{1d16d}', '\u{1d172}'),
         ('\u{1d17b}', '\u{1d182}'),
         ('\u{1d185}', '\u{1d18b}'),
         ('\u{1d1aa}', '\u{1d1ad}'),
@@ -758,7 +758,7 @@ pub mod derived_property {
         ('𞊐', '\u{1e2ae}'),
         ('𞋀', '𞋹'),
         ('𞓐', '𞓹'),
-        ('\u{1e5d0}', '\u{1e5fa}'),
+        ('𞗐', '𞗺'),
         ('𞟠', '𞟦'),
         ('𞟨', '𞟫'),
         ('𞟭', '𞟮'),
@@ -1040,7 +1040,7 @@ pub mod derived_property {
         ('ᰀ', 'ᰣ'),
         ('ᱍ', 'ᱏ'),
         ('ᱚ', 'ᱽ'),
-        ('ᲀ', '\u{1c8a}'),
+        ('ᲀ', 'ᲊ'),
         ('Ა', 'Ჺ'),
         ('Ჽ', 'Ჿ'),
         ('ᳩ', 'ᳬ'),
@@ -1123,10 +1123,10 @@ pub mod derived_property {
         ('ꚠ', 'ꛯ'),
         ('ꜗ', 'ꜟ'),
         ('Ꜣ', 'ꞈ'),
-        ('Ꞌ', '\u{a7cd}'),
+        ('Ꞌ', 'ꟍ'),
         ('Ꟑ', 'ꟑ'),
         ('ꟓ', 'ꟓ'),
-        ('ꟕ', '\u{a7dc}'),
+        ('ꟕ', 'Ƛ'),
         ('ꟲ', 'ꠁ'),
         ('ꠃ', 'ꠅ'),
         ('ꠇ', 'ꠊ'),
@@ -1224,7 +1224,7 @@ pub mod derived_property {
         ('𐖣', '𐖱'),
         ('𐖳', '𐖹'),
         ('𐖻', '𐖼'),
-        ('\u{105c0}', '\u{105f3}'),
+        ('𐗀', '𐗳'),
         ('𐘀', '𐜶'),
         ('𐝀', '𐝕'),
         ('𐝠', '𐝧'),
@@ -1261,11 +1261,11 @@ pub mod derived_property {
         ('𐲀', '𐲲'),
         ('𐳀', '𐳲'),
         ('𐴀', '𐴣'),
-        ('\u{10d4a}', '\u{10d65}'),
-        ('\u{10d6f}', '\u{10d85}'),
+        ('𐵊', '𐵥'),
+        ('𐵯', '𐶅'),
         ('𐺀', '𐺩'),
         ('𐺰', '𐺱'),
-        ('\u{10ec2}', '\u{10ec4}'),
+        ('𐻂', '𐻄'),
         ('𐼀', '𐼜'),
         ('𐼧', '𐼧'),
         ('𐼰', '𐽅'),
@@ -1304,13 +1304,13 @@ pub mod derived_property {
         ('𑌽', '𑌽'),
         ('𑍐', '𑍐'),
         ('𑍝', '𑍡'),
-        ('\u{11380}', '\u{11389}'),
-        ('\u{1138b}', '\u{1138b}'),
-        ('\u{1138e}', '\u{1138e}'),
-        ('\u{11390}', '\u{113b5}'),
-        ('\u{113b7}', '\u{113b7}'),
-        ('\u{113d1}', '\u{113d1}'),
-        ('\u{113d3}', '\u{113d3}'),
+        ('𑎀', '𑎉'),
+        ('𑎋', '𑎋'),
+        ('𑎎', '𑎎'),
+        ('𑎐', '𑎵'),
+        ('𑎷', '𑎷'),
+        ('𑏑', '𑏑'),
+        ('𑏓', '𑏓'),
         ('𑐀', '𑐴'),
         ('𑑇', '𑑊'),
         ('𑑟', '𑑡'),
@@ -1345,7 +1345,7 @@ pub mod derived_property {
         ('𑩜', '𑪉'),
         ('𑪝', '𑪝'),
         ('𑪰', '𑫸'),
-        ('\u{11bc0}', '\u{11be0}'),
+        ('𑯀', '𑯠'),
         ('𑰀', '𑰈'),
         ('𑰊', '𑰮'),
         ('𑱀', '𑱀'),
@@ -1369,9 +1369,9 @@ pub mod derived_property {
         ('𒾐', '𒿰'),
         ('𓀀', '𓐯'),
         ('𓑁', '𓑆'),
-        ('\u{13460}', '\u{143fa}'),
+        ('𓑠', '𔏺'),
         ('𔐀', '𔙆'),
-        ('\u{16100}', '\u{1611d}'),
+        ('𖄀', '𖄝'),
         ('𖠀', '𖨸'),
         ('𖩀', '𖩞'),
         ('𖩰', '𖪾'),
@@ -1380,7 +1380,7 @@ pub mod derived_property {
         ('𖭀', '𖭃'),
         ('𖭣', '𖭷'),
         ('𖭽', '𖮏'),
-        ('\u{16d40}', '\u{16d6c}'),
+        ('𖵀', '𖵬'),
         ('𖹀', '𖹿'),
         ('𖼀', '𖽊'),
         ('𖽐', '𖽐'),
@@ -1389,7 +1389,7 @@ pub mod derived_property {
         ('𖿣', '𖿣'),
         ('𗀀', '𘟷'),
         ('𘠀', '𘳕'),
-        ('\u{18cff}', '𘴈'),
+        ('𘳿', '𘴈'),
         ('𚿰', '𚿳'),
         ('𚿵', '𚿻'),
         ('𚿽', '𚿾'),
@@ -1442,8 +1442,8 @@ pub mod derived_property {
         ('𞊐', '𞊭'),
         ('𞋀', '𞋫'),
         ('𞓐', '𞓫'),
-        ('\u{1e5d0}', '\u{1e5ed}'),
-        ('\u{1e5f0}', '\u{1e5f0}'),
+        ('𞗐', '𞗭'),
+        ('𞗰', '𞗰'),
         ('𞟠', '𞟦'),
         ('𞟨', '𞟫'),
         ('𞟭', '𞟮'),
diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
index 3e9432f10af2..709b096c0224 100644
--- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts
+++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
@@ -1502,6 +1502,10 @@ export interface Nursery {
 	 * Prevent usage of \<img> element in a Next.js project.
 	 */
 	noImgElement?: RuleConfiguration_for_Null;
+	/**
+	 * Prevent import cycles.
+	 */
+	noImportCycles?: RuleConfiguration_for_Null;
 	/**
 	 * Disallows the use of irregular whitespace characters.
 	 */
@@ -3281,6 +3285,7 @@ export type Category =
 	| "lint/nursery/noHeadElement"
 	| "lint/nursery/noHeadImportInDocument"
 	| "lint/nursery/noImgElement"
+	| "lint/nursery/noImportCycles"
 	| "lint/nursery/noImportantInKeyframe"
 	| "lint/nursery/noInvalidDirectionInLinearGradient"
 	| "lint/nursery/noInvalidGridAreas"
diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json
index ad222938f77c..730bb7828237 100644
--- a/packages/@biomejs/biome/configuration_schema.json
+++ b/packages/@biomejs/biome/configuration_schema.json
@@ -2568,6 +2568,13 @@
 						{ "type": "null" }
 					]
 				},
+				"noImportCycles": {
+					"description": "Prevent import cycles.",
+					"anyOf": [
+						{ "$ref": "#/definitions/RuleConfiguration" },
+						{ "type": "null" }
+					]
+				},
 				"noIrregularWhitespace": {
 					"description": "Disallows the use of irregular whitespace characters.",
 					"anyOf": [
diff --git a/xtask/bench/src/language.rs b/xtask/bench/src/language.rs
index d44eb8a9556d..c68050c6d27a 100644
--- a/xtask/bench/src/language.rs
+++ b/xtask/bench/src/language.rs
@@ -199,7 +199,6 @@ impl Analyze {
                     filter,
                     &options,
                     Vec::new(),
-                    JsFileSource::default(),
                     Default::default(),
                     |event| {
                         black_box(event.diagnostic());
diff --git a/xtask/rules_check/src/lib.rs b/xtask/rules_check/src/lib.rs
index 98f0e046b965..9f664ea93a9c 100644
--- a/xtask/rules_check/src/lib.rs
+++ b/xtask/rules_check/src/lib.rs
@@ -14,6 +14,7 @@ use biome_deserialize::json::deserialize_from_json_ast;
 use biome_diagnostics::{DiagnosticExt, PrintDiagnostic, Severity};
 use biome_fs::BiomePath;
 use biome_graphql_syntax::GraphqlLanguage;
+use biome_js_analyze::JsAnalyzerServices;
 use biome_js_parser::JsParserOptions;
 use biome_js_syntax::{EmbeddingKind, JsFileSource, JsLanguage, TextSize};
 use biome_json_factory::make;
@@ -426,34 +427,29 @@ fn assert_lint(
                     test,
                 );
 
-                biome_js_analyze::analyze(
-                    &root,
-                    filter,
-                    &options,
-                    vec![],
-                    file_source,
-                    Default::default(),
-                    |signal| {
-                        if let Some(mut diag) = signal.diagnostic() {
-                            for action in signal.actions() {
-                                if !action.is_suppression() {
-                                    diag = diag.add_code_suggestion(action.into());
-                                }
+                let services =
+                    JsAnalyzerServices::from((Default::default(), Default::default(), file_source));
+
+                biome_js_analyze::analyze(&root, filter, &options, vec![], services, |signal| {
+                    if let Some(mut diag) = signal.diagnostic() {
+                        for action in signal.actions() {
+                            if !action.is_suppression() {
+                                diag = diag.add_code_suggestion(action.into());
                             }
+                        }
 
-                            let error = diag.with_file_path(&file_path).with_file_source_code(code);
-                            let res = diagnostics.write_diagnostic(error);
+                        let error = diag.with_file_path(&file_path).with_file_source_code(code);
+                        let res = diagnostics.write_diagnostic(error);
 
-                            // Abort the analysis on error
-                            if let Err(err) = res {
-                                eprintln!("Error: {err}");
-                                return ControlFlow::Break(err);
-                            }
+                        // Abort the analysis on error
+                        if let Err(err) = res {
+                            eprintln!("Error: {err}");
+                            return ControlFlow::Break(err);
                         }
+                    }
 
-                        ControlFlow::Continue(())
-                    },
-                );
+                    ControlFlow::Continue(())
+                });
             }
         }
         DocumentFileSource::Json(file_source) => {


Test patch:
diff --git a/crates/biome_js_analyze/tests/quick_test.rs b/crates/biome_js_analyze/tests/quick_test.rs
index 062abdfc3c15..30c56951c100 100644
--- a/crates/biome_js_analyze/tests/quick_test.rs
+++ b/crates/biome_js_analyze/tests/quick_test.rs
@@ -1,11 +1,12 @@
 use biome_analyze::{AnalysisFilter, ControlFlow, Never, RuleFilter};
 use biome_diagnostics::advice::CodeSuggestionAdvice;
 use biome_diagnostics::{DiagnosticExt, Severity};
+use biome_js_analyze::JsAnalyzerServices;
 use biome_js_parser::{parse, JsParserOptions};
 use biome_js_syntax::JsFileSource;
 use biome_test_utils::{
-    code_fix_to_string, create_analyzer_options, diagnostic_to_string, parse_test_path,
-    project_layout_with_node_manifest, scripts_from_json,
+    code_fix_to_string, create_analyzer_options, dependency_graph_for_test_file,
+    diagnostic_to_string, parse_test_path, project_layout_with_node_manifest, scripts_from_json,
 };
 use camino::Utf8Path;
 use std::ops::Deref;
@@ -15,7 +16,7 @@ use std::{fs::read_to_string, slice};
 #[ignore]
 #[test]
 fn quick_test() {
-    let input_file = Utf8Path::new("tests/specs/complexity/noUselessFragments/issue_4751.jsx");
+    let input_file = Utf8Path::new("tests/specs/nursery/noImportCycles/invalidFoobar.js");
     let file_name = input_file.file_name().unwrap();
 
     let (group, rule) = parse_test_path(input_file);
@@ -73,14 +74,12 @@ fn analyze(
     let options = create_analyzer_options(input_file, &mut diagnostics);
     let project_layout = project_layout_with_node_manifest(input_file, &mut diagnostics);
 
-    let (_, errors) = biome_js_analyze::analyze(
-        &root,
-        filter,
-        &options,
-        Vec::new(),
-        source_type,
-        project_layout,
-        |event| {
+    let dependency_graph = dependency_graph_for_test_file(input_file, &project_layout);
+
+    let services = JsAnalyzerServices::from((dependency_graph, project_layout, source_type));
+
+    let (_, errors) =
+        biome_js_analyze::analyze(&root, filter, &options, Vec::new(), services, |event| {
             if let Some(mut diag) = event.diagnostic() {
                 for action in event.actions() {
                     diag = diag.add_code_suggestion(CodeSuggestionAdvice::from(action));
@@ -96,8 +95,7 @@ fn analyze(
             }
 
             ControlFlow::<Never>::Continue(())
-        },
-    );
+        });
 
     for error in errors {
         diagnostics.push(diagnostic_to_string(file_name, input_code, error));
diff --git a/crates/biome_js_analyze/tests/spec_tests.rs b/crates/biome_js_analyze/tests/spec_tests.rs
index 528964ef62b0..0dd9d0656075 100644
--- a/crates/biome_js_analyze/tests/spec_tests.rs
+++ b/crates/biome_js_analyze/tests/spec_tests.rs
@@ -4,17 +4,19 @@ use biome_analyze::{
 use biome_diagnostics::advice::CodeSuggestionAdvice;
 use biome_diagnostics::{DiagnosticExt, Severity};
 use biome_fs::OsFileSystem;
+use biome_js_analyze::JsAnalyzerServices;
 use biome_js_parser::{parse, JsParserOptions};
 use biome_js_syntax::{JsFileSource, JsLanguage, ModuleKind};
 use biome_package::PackageType;
 use biome_plugin_loader::AnalyzerGritPlugin;
 use biome_rowan::AstNode;
 use biome_test_utils::{
-    assert_errors_are_absent, code_fix_to_string, create_analyzer_options, diagnostic_to_string,
-    has_bogus_nodes_or_empty_slots, parse_test_path, project_layout_with_node_manifest,
-    register_leak_checker, scripts_from_json, write_analyzer_snapshot, CheckActionType,
+    assert_errors_are_absent, code_fix_to_string, create_analyzer_options,
+    dependency_graph_for_test_file, diagnostic_to_string, has_bogus_nodes_or_empty_slots,
+    parse_test_path, project_layout_with_node_manifest, register_leak_checker, scripts_from_json,
+    write_analyzer_snapshot, CheckActionType,
 };
-use camino::Utf8Path;
+use camino::{Utf8Component, Utf8Path};
 use std::ops::Deref;
 use std::{fs::read_to_string, slice};
 
@@ -129,14 +131,21 @@ pub(crate) fn analyze_and_snap(
 
     let options = create_analyzer_options(input_file, &mut diagnostics);
 
-    let (_, errors) = biome_js_analyze::analyze(
-        &root,
-        filter,
-        &options,
-        plugins,
-        source_type,
-        project_layout,
-        |event| {
+    // FIXME: We probably want to enable it for all rules? Right now it seems to
+    //        trigger a leak panic...
+    let dependency_graph = if input_file
+        .components()
+        .any(|component| component == Utf8Component::Normal("noImportCycles"))
+    {
+        dependency_graph_for_test_file(input_file, &project_layout)
+    } else {
+        Default::default()
+    };
+
+    let services = JsAnalyzerServices::from((dependency_graph, project_layout, source_type));
+
+    let (_, errors) =
+        biome_js_analyze::analyze(&root, filter, &options, plugins, services, |event| {
             if let Some(mut diag) = event.diagnostic() {
                 for action in event.actions() {
                     if check_action_type.is_suppression() {
@@ -192,8 +201,7 @@ pub(crate) fn analyze_and_snap(
             }
 
             ControlFlow::<Never>::Continue(())
-        },
-    );
+        });
 
     for error in errors {
         diagnostics.push(diagnostic_to_string(file_name, input_code, error));
@@ -207,6 +215,18 @@ pub(crate) fn analyze_and_snap(
         source_type.file_extension(),
     );
 
+    // FIXME: I wish we could do this more generically, but we cannot do this
+    //        for all tests, since it would cause many incorrect replacements.
+    //        Maybe there's a regular expression that could work, but it feels
+    //        flimsy too...
+    if input_file
+        .components()
+        .any(|component| component == Utf8Component::Normal("noImportCycles"))
+    {
+        // Normalize Windows paths.
+        *snapshot = snapshot.replace('\\', "/");
+    }
+
     diagnostics.len()
 }
 
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidBaz.js b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidBaz.js
new file mode 100644
index 000000000000..b7ad4239cdc5
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidBaz.js
@@ -0,0 +1,5 @@
+import { bar } from "./invalidFoobar.js";
+
+export function baz() {
+    bar();
+}
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidBaz.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidBaz.js.snap
new file mode 100644
index 000000000000..9618141baa8a
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidBaz.js.snap
@@ -0,0 +1,31 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: invalidBaz.js
+---
+# Input
+```js
+import { bar } from "./invalidFoobar.js";
+
+export function baz() {
+    bar();
+}
+
+```
+
+# Diagnostics
+```
+invalidBaz.js:1:21 lint/nursery/noImportCycles ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This import is part of a cycle.
+  
+  > 1 │ import { bar } from "./invalidFoobar.js";
+      │                     ^^^^^^^^^^^^^^^^^^^^
+    2 │ 
+    3 │ export function baz() {
+  
+  i This import resolves to tests/specs/nursery/noImportCycles/invalidFoobar.js
+        ... which imports tests/specs/nursery/noImportCycles/invalidBaz.js
+        ... which is the file we're importing from.
+  
+
+```
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidFoobar.js b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidFoobar.js
new file mode 100644
index 000000000000..e43e481bd384
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidFoobar.js
@@ -0,0 +1,9 @@
+import { baz } from "./invalidBaz.js";
+
+export function foo() {
+    baz();
+}
+
+export function bar() {
+    console.log("foobar");
+}
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidFoobar.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidFoobar.js.snap
new file mode 100644
index 000000000000..de4d30881915
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/invalidFoobar.js.snap
@@ -0,0 +1,35 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: invalidFoobar.js
+---
+# Input
+```js
+import { baz } from "./invalidBaz.js";
+
+export function foo() {
+    baz();
+}
+
+export function bar() {
+    console.log("foobar");
+}
+
+```
+
+# Diagnostics
+```
+invalidFoobar.js:1:21 lint/nursery/noImportCycles ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This import is part of a cycle.
+  
+  > 1 │ import { baz } from "./invalidBaz.js";
+      │                     ^^^^^^^^^^^^^^^^^
+    2 │ 
+    3 │ export function foo() {
+  
+  i This import resolves to tests/specs/nursery/noImportCycles/invalidBaz.js
+        ... which imports tests/specs/nursery/noImportCycles/invalidFoobar.js
+        ... which is the file we're importing from.
+  
+
+```
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/valid.js
new file mode 100644
index 000000000000..3088aadc2998
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/valid.js
@@ -0,0 +1,5 @@
+/* should not generate diagnostics */
+
+import { foo } from "./invalidFoobar";
+
+foo();
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/valid.js.snap
new file mode 100644
index 000000000000..fef0753daec3
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noImportCycles/valid.js.snap
@@ -0,0 +1,13 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: valid.js
+---
+# Input
+```js
+/* should not generate diagnostics */
+
+import { foo } from "./invalidFoobar";
+
+foo();
+
+```
diff --git a/crates/biome_test_utils/Cargo.toml b/crates/biome_test_utils/Cargo.toml
index 37064da24c26..fd261ba93a22 100644
--- a/crates/biome_test_utils/Cargo.toml
+++ b/crates/biome_test_utils/Cargo.toml
@@ -14,22 +14,25 @@ version              = "0.0.0"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-biome_analyze        = { workspace = true }
-biome_configuration  = { workspace = true }
-biome_console        = { workspace = true }
-biome_deserialize    = { workspace = true }
-biome_diagnostics    = { workspace = true }
-biome_formatter      = { workspace = true }
-biome_json_parser    = { workspace = true }
-biome_package        = { workspace = true }
-biome_project_layout = { workspace = true }
-biome_rowan          = { workspace = true }
-biome_service        = { workspace = true }
-camino               = { workspace = true }
-countme              = { workspace = true, features = ["enable"] }
-json_comments        = "0.2.2"
-serde_json           = { workspace = true }
-similar              = { workspace = true }
+biome_analyze          = { workspace = true }
+biome_configuration    = { workspace = true }
+biome_console          = { workspace = true }
+biome_dependency_graph = { workspace = true }
+biome_deserialize      = { workspace = true }
+biome_diagnostics      = { workspace = true }
+biome_formatter        = { workspace = true }
+biome_fs               = { workspace = true }
+biome_js_parser        = { workspace = true }
+biome_json_parser      = { workspace = true }
+biome_package          = { workspace = true }
+biome_project_layout   = { workspace = true }
+biome_rowan            = { workspace = true }
+biome_service          = { workspace = true }
+camino                 = { workspace = true }
+countme                = { workspace = true, features = ["enable"] }
+json_comments          = "0.2.2"
+serde_json             = { workspace = true }
+similar                = { workspace = true }
 
 [lints]
 workspace = true
diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs
index 1522a39a0b4a..e624ddb4c015 100644
--- a/crates/biome_test_utils/src/lib.rs
+++ b/crates/biome_test_utils/src/lib.rs
@@ -3,8 +3,11 @@ use biome_analyze::{AnalyzerAction, AnalyzerConfiguration, AnalyzerOptions};
 use biome_configuration::Configuration;
 use biome_console::fmt::{Formatter, Termcolor};
 use biome_console::markup;
+use biome_dependency_graph::DependencyGraph;
 use biome_diagnostics::termcolor::Buffer;
 use biome_diagnostics::{DiagnosticExt, Error, PrintDiagnostic};
+use biome_fs::{BiomePath, FileSystem, OsFileSystem};
+use biome_js_parser::{JsFileSource, JsParserOptions};
 use biome_json_parser::{JsonParserOptions, ParseDiagnostic};
 use biome_package::PackageJson;
 use biome_project_layout::ProjectLayout;
@@ -158,6 +161,46 @@ where
     }
 }
 
+/// Creates a dependency graph that is initialized for the given `input_file`.
+///
+/// It uses an [OsFileSystem] initialized for the directory in which the test
+/// file resides and inserts all files from that directory, so that files
+/// importing each other within that directory will be picked up correctly.
+///
+/// The `project_layout` should be initialized in advance if you want any
+/// manifest files to be discovered.
+pub fn dependency_graph_for_test_file(
+    input_file: &Utf8Path,
+    project_layout: &ProjectLayout,
+) -> Arc<DependencyGraph> {
+    let dependency_graph = DependencyGraph::default();
+
+    let dir = input_file.parent().unwrap().to_path_buf();
+    let paths: Vec<_> = std::fs::read_dir(&dir)
+        .unwrap()
+        .filter_map(|path| {
+            let path = Utf8PathBuf::try_from(path.unwrap().path()).unwrap();
+            DocumentFileSource::from_well_known(&path)
+                .is_javascript_like()
+                .then(|| BiomePath::new(path))
+        })
+        .collect();
+    let fs = OsFileSystem::new(dir);
+
+    dependency_graph.update_imports_for_js_paths(&fs, project_layout, &paths, &[], |path| {
+        fs.read_file_from_path(path).ok().and_then(|content| {
+            let file_source = path
+                .extension()
+                .and_then(|extension| JsFileSource::try_from_extension(extension).ok())
+                .unwrap_or_default();
+            let parsed = biome_js_parser::parse(&content, file_source, JsParserOptions::default());
+            parsed.try_tree()
+        })
+    });
+
+    Arc::new(dependency_graph)
+}
+
 pub fn project_layout_with_node_manifest(
     input_file: &Utf8Path,
     diagnostics: &mut Vec<String>,