Repository: biomejs/biome (language: rust)

Issue:
🐛 Panic `Bad offset: range 0..8 offset 9` when linting file
### Environment information

```block
CLI:
  Version:                      0.0.0 (self compiled 4848994e4507d80e56ea07c40ff41a7b303918cd)
  Color support:                true

Platform:
  CPU Architecture:             x86_64
  OS:                           linux

Environment:
  BIOME_LOG_PATH:               unset
  BIOME_LOG_PREFIX_NAME:        unset
  BIOME_CONFIG_PATH:            unset
  NO_COLOR:                     unset
  TERM:                         "xterm-256color"
  JS_RUNTIME_VERSION:           unset
  JS_RUNTIME_NAME:              unset
  NODE_PACKAGE_MANAGER:         unset

Biome Configuration:
  Status:                       unset

Workspace:
  Open Documents:               0
```


### What happened?

File content(at the bottom should be attached raw, not formatted file - github removes some non-printable characters, so copying from here may not work):
```
<b
 //
}
```

command
```
timeout -v 150 biome lint TEST___FILE.js
```


cause this
```
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: crates/biome_rowan/src/cursor/node.rs:326:9
Thread Name: biome::worker_2
Message: Bad offset: range 0..8 offset 9


##### Automatic Fuzzer note, output status "None", output signal "Some(6)"
```

[compressed.zip](https://github.com/user-attachments/files/17892199/compressed.zip)


### Expected result

No crash

### Code of Conduct

- [X] I agree to follow Biome's Code of Conduct


Discussion / hints:

Fix patch:
diff --git a/crates/biome_js_analyze/src/lint/suspicious/no_comment_text.rs b/crates/biome_js_analyze/src/lint/suspicious/no_comment_text.rs
index 0e6d5c490784..23a78bb5f994 100644
--- a/crates/biome_js_analyze/src/lint/suspicious/no_comment_text.rs
+++ b/crates/biome_js_analyze/src/lint/suspicious/no_comment_text.rs
@@ -6,7 +6,7 @@ use biome_console::markup;
 use biome_diagnostics::Severity;
 use biome_js_factory::make;
 use biome_js_syntax::{AnyJsxChild, JsSyntaxKind, JsSyntaxToken, JsxText};
-use biome_rowan::{AstNode, BatchMutationExt, TextRange, TextSize};
+use biome_rowan::{BatchMutationExt, TextRange, TextSize};
 use std::ops::Range;
 
 declare_lint_rule! {
@@ -128,7 +128,7 @@ impl Rule for NoCommentText {
     }
 
     fn diagnostic(ctx: &RuleContext<Self>, range: &Self::State) -> Option<RuleDiagnostic> {
-        let node_range_start = ctx.query().range().start();
+        let node_range_start = ctx.query().value_token().ok()?.text_range().start();
         Some(RuleDiagnostic::new(
             rule_category!(),
             TextRange::new(
diff --git a/crates/biome_rowan/src/cursor/element.rs b/crates/biome_rowan/src/cursor/element.rs
index 6b8bc9fa6455..52add38fe2b8 100644
--- a/crates/biome_rowan/src/cursor/element.rs
+++ b/crates/biome_rowan/src/cursor/element.rs
@@ -1,6 +1,6 @@
 use crate::cursor::{SyntaxNode, SyntaxToken};
 use crate::green::{GreenElement, GreenElementRef};
-use crate::{NodeOrToken, RawSyntaxKind, TokenAtOffset};
+use crate::{NodeOrToken, RawSyntaxKind};
 use biome_text_size::{TextRange, TextSize};
 use std::iter;
 
@@ -86,14 +86,6 @@ impl SyntaxElement {
         }
     }
 
-    pub(super) fn token_at_offset(&self, offset: TextSize) -> TokenAtOffset<SyntaxToken> {
-        assert!(self.text_range().start() <= offset && offset <= self.text_range().end());
-        match self {
-            NodeOrToken::Token(token) => TokenAtOffset::Single(token.clone()),
-            NodeOrToken::Node(node) => node.token_at_offset(offset),
-        }
-    }
-
     #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"]
     pub fn detach(self) -> Self {
         match self {
diff --git a/crates/biome_rowan/src/cursor/node.rs b/crates/biome_rowan/src/cursor/node.rs
index 9959bc1d296d..04d52f010a44 100644
--- a/crates/biome_rowan/src/cursor/node.rs
+++ b/crates/biome_rowan/src/cursor/node.rs
@@ -5,6 +5,7 @@ use crate::{
     WalkEvent,
 };
 use biome_text_size::{TextRange, TextSize};
+use std::borrow::Cow;
 use std::hash::{Hash, Hasher};
 use std::iter::FusedIterator;
 use std::ops;
@@ -319,36 +320,43 @@ impl SyntaxNode {
     }
 
     pub fn token_at_offset(&self, offset: TextSize) -> TokenAtOffset<SyntaxToken> {
-        // TODO: this could be faster if we first drill-down to node, and only
-        // then switch to token search. We should also replace explicit
-        // recursion with a loop.
-        let range = self.text_range();
-        assert!(
-            range.start() <= offset && offset <= range.end(),
-            "Bad offset: range {range:?} offset {offset:?}"
-        );
-        if range.is_empty() {
-            return TokenAtOffset::None;
-        }
+        let mut node = Cow::Borrowed(self);
+        loop {
+            let range = node.text_range();
+            if range.is_empty() || offset < range.start() || offset > range.end() {
+                return TokenAtOffset::None;
+            }
 
-        let mut children = self.children_with_tokens().filter(|child| {
-            let child_range = child.text_range();
-            !child_range.is_empty() && child_range.contains_inclusive(offset)
-        });
+            let mut children = node.children_with_tokens().filter(|child| {
+                let child_range = child.text_range();
+                !child_range.is_empty() && child_range.contains_inclusive(offset)
+            });
+
+            let left = children.next().unwrap();
+            let right = children.next();
+            assert!(children.next().is_none());
+
+            if let Some(right) = right {
+                let token_at_offset =
+                    |node: NodeOrToken<SyntaxNode, SyntaxToken>| -> TokenAtOffset<SyntaxToken> {
+                        match node {
+                            NodeOrToken::Token(token) => TokenAtOffset::Single(token),
+                            NodeOrToken::Node(node) => node.token_at_offset(offset),
+                        }
+                    };
 
-        let left = children.next().unwrap();
-        let right = children.next();
-        assert!(children.next().is_none());
+                return match (token_at_offset(left), token_at_offset(right)) {
+                    (TokenAtOffset::Single(left), TokenAtOffset::Single(right)) => {
+                        TokenAtOffset::Between(left, right)
+                    }
+                    _ => TokenAtOffset::None,
+                };
+            }
 
-        if let Some(right) = right {
-            match (left.token_at_offset(offset), right.token_at_offset(offset)) {
-                (TokenAtOffset::Single(left), TokenAtOffset::Single(right)) => {
-                    TokenAtOffset::Between(left, right)
-                }
-                _ => unreachable!(),
+            match left {
+                NodeOrToken::Node(left) => node = Cow::Owned(left),
+                NodeOrToken::Token(left) => return TokenAtOffset::Single(left),
             }
-        } else {
-            left.token_at_offset(offset)
         }
     }
 


Test patch:
diff --git a/crates/biome_cli/tests/commands/lint.rs b/crates/biome_cli/tests/commands/lint.rs
index aa5d349a0e1b..e43da9250be8 100644
--- a/crates/biome_cli/tests/commands/lint.rs
+++ b/crates/biome_cli/tests/commands/lint.rs
@@ -4005,3 +4005,45 @@ fn linter_finds_nested_package_json_for_no_undeclared_dependencies_inversed() {
         result,
     ));
 }
+
+#[test]
+fn linter_doesnt_crash_on_malformed_code_from_issue_4623() {
+    let mut console = BufferConsole::default();
+    let mut fs = MemoryFileSystem::default();
+
+    fs.insert(
+        Utf8Path::new("biome.json").into(),
+        r#"{
+    "linter": {
+        "rules": {
+            "recommended": false,
+            "suspicious": {
+                "noCommentText": "on"
+            }
+        }
+    }
+}"#
+        .as_bytes(),
+    );
+
+    fs.insert(
+        Utf8PathBuf::from("issue4623.js"),
+        r#"<b
+ //
+}"#
+        .as_bytes(),
+    );
+
+    let (fs, result) = run_cli_with_server_workspace(
+        fs,
+        &mut console,
+        Args::from(["lint", "issue4623.js"].as_slice()),
+    );
+    assert_cli_snapshot(SnapshotPayload::new(
+        module_path!(),
+        "linter_doesnt_crash_on_malformed_code_from_issue_4623",
+        fs,
+        console,
+        result,
+    ));
+}
diff --git a/crates/biome_cli/tests/snapshots/main_commands_lint/linter_doesnt_crash_on_malformed_code_from_issue_4623.snap b/crates/biome_cli/tests/snapshots/main_commands_lint/linter_doesnt_crash_on_malformed_code_from_issue_4623.snap
new file mode 100644
index 000000000000..552a8f58a928
--- /dev/null
+++ b/crates/biome_cli/tests/snapshots/main_commands_lint/linter_doesnt_crash_on_malformed_code_from_issue_4623.snap
@@ -0,0 +1,98 @@
+---
+source: crates/biome_cli/tests/snap_test.rs
+expression: redactor(content)
+---
+## `biome.json`
+
+```json
+{
+  "linter": {
+    "rules": {
+      "recommended": false,
+      "suspicious": {
+        "noCommentText": "on"
+      }
+    }
+  }
+}
+```
+
+## `issue4623.js`
+
+```js
+<b
+ //
+}
+```
+
+# Termination Message
+
+```block
+lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  × Some errors were emitted while running checks.
+  
+
+
+```
+
+# Emitted Messages
+
+```block
+issue4623.js:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  × Unexpected token. Did you mean `{'}'}` or `&rbrace;`?
+  
+    1 │ <b
+    2 │  //
+  > 3 │ }
+      │ ^
+  
+
+```
+
+```block
+issue4623.js:3:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  × expected `<` but instead the file ends
+  
+    1 │ <b
+    2 │  //
+  > 3 │ }
+      │  
+  
+  i the file ends here
+  
+    1 │ <b
+    2 │  //
+  > 3 │ }
+      │  
+  
+
+```
+
+```block
+issue4623.js:2:2 lint/suspicious/noCommentText  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  × Wrap comments inside children within braces.
+  
+    1 │ <b
+  > 2 │  //
+      │  ^^
+    3 │ }
+  
+  i Unsafe fix: Wrap the comments with braces
+  
+    1 1 │   <b
+    2 2 │    //
+      3 │ + 
+      4 │ + ·{/*··*/}
+    3 5 │   }
+  
+
+```
+
+```block
+Checked 1 file in <TIME>. No fixes applied.
+Found 3 errors.
+```