diff --git a/crates/core/src/parser/level.rs b/crates/core/src/parser/level.rs index 3c0c610..9634e0b 100644 --- a/crates/core/src/parser/level.rs +++ b/crates/core/src/parser/level.rs @@ -100,13 +100,21 @@ fn detect_level_from_text(line: &str) -> Option { ) } +// ─── is_ident_char ───────────────────────────────────────────────────────── +/// Whether a byte looks like an ASCII identifier continuation character +/// (letter / digit / underscore). Log-level keywords must NOT be adjacent to +/// such characters to count as a valid word boundary. +fn is_ident_char(b: u8) -> bool { + b.is_ascii_alphanumeric() || b == b'_' +} + // ─── is_word_boundary ─────────────────────────────────────────────────────── -/// Check that the match at `start..start+len` is surrounded by non-alphabetic +/// Check that the match at `start..start+len` is surrounded by non-identifier /// characters (or the string edge). fn is_word_boundary(text: &str, start: usize, len: usize) -> bool { - let before_ok = start == 0 || !text.as_bytes()[start - 1].is_ascii_alphabetic(); + let before_ok = start == 0 || !is_ident_char(text.as_bytes()[start - 1]); let after_idx = start + len; - let after_ok = after_idx >= text.len() || !text.as_bytes()[after_idx].is_ascii_alphabetic(); + let after_ok = after_idx >= text.len() || !is_ident_char(text.as_bytes()[after_idx]); before_ok && after_ok } @@ -209,4 +217,36 @@ mod tests { let line = format!("{prefix} ERROR something"); assert_eq!(detect_level(&line), Some(LogLevel::Error)); } + + #[test] + fn test_boundary_rejects_trailing_digits() { + assert_eq!(detect_level("ERROR123"), None); + assert_eq!(detect_level("WARN2: bad"), None); + assert_eq!(detect_level("ERR2"), None); + } + + #[test] + fn test_boundary_rejects_underscore() { + assert_eq!(detect_level("INFO_foo"), None); + assert_eq!(detect_level("DBG_value=5"), None); + } + + #[test] + fn test_boundary_rejects_leading_digits_and_underscore() { + assert_eq!(detect_level("123ERROR: fail"), None); + assert_eq!(detect_level("foo_ERROR: fail"), None); + assert_eq!(detect_level("1WRN"), None); + } + + #[test] + fn test_boundary_accepts_valid_suffixes() { + assert_eq!(detect_level("ERROR: fail"), Some(LogLevel::Error)); + assert_eq!(detect_level("[ERROR] fail"), Some(LogLevel::Error)); + assert_eq!(detect_level("ERROR fail"), Some(LogLevel::Error)); + } + + #[test] + fn test_boundary_camel_case_regression() { + assert_eq!(detect_level("errorLevel"), None); + } }