Repository: biomejs/biome (Rust)

Issue:
🐛  [2.0 Beta] Language Server Crashes in VSCode w/ Plugins
### VS Code version

1.98.2

### Extension version

2025.3.151143 (pre-release)

### Biome version

2.0.0-beta.1

### Operating system

- [ ] Windows
- [x] macOS
- [ ] Linux

### Description

When a plugin is enabled, the language server crashes on a simple lint check from a grit file.

The MRE below is a minimal Qwik application that has one grit query and one route.

### Steps to reproduce

1. Go to https://github.com/thejackshelton/biome-plugin/tree/main
2. Install the pre-release biome plugin in VSCode
3. Run pnpm i
4. Exit and enter the VSCode app OR reload window
5. Navigate to index.tsx
6. You will see language server crashes from the lib.rs file




### Expected behavior

The language server should not crash.

### Does this issue occur when using the CLI directly?

Not sure / Not applicable

### Link to a minimal reproduction

https://github.com/thejackshelton/biome-plugin/tree/main

### Logs

```plain text
Stack trace:


2025-03-26 10:56:54.620 [info] Biome encountered an unexpected error

This is a bug in Biome, not an error in your code, and we would appreciate it if you could report it to https://github.com/biomejs/biome/issues/ along with the following information to help us fixing the issue:

Source Location: /Users/runner/work/biome/biome/crates/biome_analyze/src/lib.rs:196:50
Thread Name: tokio-runtime-worker
Message: missing span


2025-03-26 10:56:54.642 [info] [Error - 10:56:54 AM] Biome language server closed
```


Discussion: cc @arendjr 
Does the plugin also cause crashes with a regular invocation of `biome check` or `biome lint`, or it only crashes in the LSP?
Hey @arendjr. When I ran pnpm biome lint I got:

```
jackshelton@MacBook-Pro biome-plugin % pnpm biome lint
Biome encountered an unexpected error

This is a bug in Biome, not an error in your code, and we would appreciate it if you could report it to https://github.com/biomejs/biome/issues/ along with the following information to help us fixing the issue:

Source Location: /Users/runner/work/biome/biome/crates/biome_analyze/src/lib.rs:196:50
Thread Name: biome::workspace_worker_9
Message: missing span

/Users/jackshelton/dev/playground/biome-plugin/src/routes/index.tsx internalError/panic  INTERNAL  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ processing panicked: missing span
  
  ⚠ This diagnostic was derived from an internal Biome error. Potential bug, please report it if necessary.
```
So both the LSP and lint commands?
Great, thanks!



Fix (patch):
diff --git a/Cargo.lock b/Cargo.lock
index c7751bf5a29c..43a7afa887f7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1300,7 +1300,7 @@ dependencies = [
  "camino",
  "crossbeam",
  "enumflags2",
- "getrandom 0.3.2",
+ "getrandom 0.2.15",
  "ignore",
  "insta",
  "notify",
@@ -2275,8 +2275,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -2286,11 +2288,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
 dependencies = [
  "cfg-if",
- "js-sys",
  "libc",
  "r-efi",
  "wasi 0.14.2+wasi-0.2.4",
- "wasm-bindgen",
 ]
 
 [[package]]
diff --git a/crates/biome_analyze/src/lib.rs b/crates/biome_analyze/src/lib.rs
index 3b92e4b7ea40..e534071685ea 100644
--- a/crates/biome_analyze/src/lib.rs
+++ b/crates/biome_analyze/src/lib.rs
@@ -192,43 +192,50 @@ where
         for plugin in plugins {
             let root: AnyParse = ctx.root.syntax().as_send().expect("not a root node").into();
             for diagnostic in plugin.evaluate(root, ctx.options.file_path.clone()) {
-                let name = diagnostic.subcategory.clone().expect("missing subcategory");
-                let text_range = diagnostic.span.expect("missing span");
+                let name = diagnostic
+                    .subcategory
+                    .clone()
+                    .unwrap_or_else(|| "anonymous".into());
 
-                // 1. check for top level suppression
+                // 1. Check for top level suppression:
                 if suppressions.top_level_suppression.suppressed_plugin(&name)
                     || suppressions.top_level_suppression.suppress_all
                 {
                     break;
                 }
 
-                // 2. check for range suppression is not supprted because:
-                // plugin is handled separately after basic analyze phases
-                // at this point, we have read to the end of file, all `// biome-ignore-end` is processed, thus all range suppressions is cleared
-
-                // 3. check for line suppression
-                let suppression = {
-                    let index = suppressions
-                        .line_suppressions
-                        .binary_search_by(|suppression| {
-                            if suppression.text_range.end() < text_range.start() {
-                                Ordering::Less
-                            } else if text_range.end() < suppression.text_range.start() {
-                                Ordering::Greater
-                            } else {
-                                Ordering::Equal
-                            }
-                        });
-
-                    index
-                        .ok()
-                        .map(|index| &mut suppressions.line_suppressions[index])
-                };
-
-                let suppression = suppression.filter(|suppression| {
-                    suppression.suppress_all
-                        || suppression.suppress_all_plugins
-                        || suppression.suppressed_plugins.contains(&name)
+                let suppression = diagnostic.span.and_then(|text_range| {
+                    // 2. Check for range suppression is not supported because
+                    //    plugins are handled separately after the basic analyze
+                    //    phases. At this point, we have read to the end of the
+                    //    file, all `// biome-ignore-end` comments are
+                    //    processed, thus all range suppressions are cleared.
+
+                    // 3. Check for line suppression:
+                    let suppression = {
+                        let index =
+                            suppressions
+                                .line_suppressions
+                                .binary_search_by(|suppression| {
+                                    if suppression.text_range.end() < text_range.start() {
+                                        Ordering::Less
+                                    } else if text_range.end() < suppression.text_range.start() {
+                                        Ordering::Greater
+                                    } else {
+                                        Ordering::Equal
+                                    }
+                                });
+
+                        index
+                            .ok()
+                            .map(|index| &mut suppressions.line_suppressions[index])
+                    };
+
+                    suppression.filter(|suppression| {
+                        suppression.suppress_all
+                            || suppression.suppress_all_plugins
+                            || suppression.suppressed_plugins.contains(&name)
+                    })
                 });
 
                 if let Some(suppression) = suppression {
diff --git a/crates/biome_analyze/src/rule.rs b/crates/biome_analyze/src/rule.rs
index db4606b866fc..96fe3b75b7ca 100644
--- a/crates/biome_analyze/src/rule.rs
+++ b/crates/biome_analyze/src/rule.rs
@@ -1435,7 +1435,8 @@ impl RuleDiagnostic {
         self.footer(LogCategory::Warn, msg)
     }
 
-    pub(crate) fn span(&self) -> Option<TextRange> {
+    #[inline]
+    pub fn span(&self) -> Option<TextRange> {
         self.span
     }
 
diff --git a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs
index ab0e0ff9a1ae..dbc8b24d9275 100644
--- a/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs
+++ b/crates/biome_plugin_loader/src/analyzer_grit_plugin.rs
@@ -47,20 +47,39 @@ impl AnalyzerPlugin for AnalyzerGritPlugin {
 
         let file = GritTargetFile { parse: root, path };
         match self.grit_query.execute(file) {
-            Ok(result) => result
-                .logs
-                .iter()
-                .map(|log| {
-                    RuleDiagnostic::new(
+            Ok(result) => {
+                let mut diagnostics: Vec<_> = result
+                    .logs
+                    .iter()
+                    .map(|log| {
+                        RuleDiagnostic::new(
                         category!("plugin"),
                         log.range.map(from_grit_range),
                         markup!(<Emphasis>{name}</Emphasis>" logged: "<Info>{log.message}</Info>),
                     )
                     .verbose()
-                })
-                .chain(result.diagnostics)
-                .map(|diagnos| diagnos.subcategory(name.to_string()))
-                .collect(),
+                    })
+                    .chain(result.diagnostics)
+                    .map(|diagnostics| diagnostics.subcategory(name.to_string()))
+                    .collect();
+
+                if diagnostics
+                    .iter()
+                    .any(|diagnostic| diagnostic.span().is_none())
+                {
+                    diagnostics.push(RuleDiagnostic::new(
+                        category!("plugin"),
+                        None::<TextRange>,
+                        markup!(
+                            "Plugin "<Emphasis>{name}</Emphasis>" reported one or more diagnostics, "
+                            "but it didn't specify a valid "<Emphasis>"span"</Emphasis>". "
+                            "Diagnostics have been shown without context."
+                        ),
+                    ));
+                }
+
+                diagnostics
+            }
             Err(error) => vec![RuleDiagnostic::new(
                 category!("plugin"),
                 None::<TextRange>,
diff --git a/crates/biome_service/Cargo.toml b/crates/biome_service/Cargo.toml
index ba969bfb9f8a..5e781bc16193 100644
--- a/crates/biome_service/Cargo.toml
+++ b/crates/biome_service/Cargo.toml
@@ -59,7 +59,7 @@ biome_text_edit         = { workspace = true }
 camino                  = { workspace = true }
 crossbeam               = { workspace = true }
 enumflags2              = { workspace = true, features = ["serde"] }
-getrandom               = { workspace = true, features = ["wasm_js"] }
+getrandom               = { workspace = true, features = ["js"] }
 ignore                  = { workspace = true }
 notify                  = { version = "8.0", features = ["crossbeam-channel"] }
 papaya                  = { workspace = true }