use crate::PerlLexer;
impl PerlLexer<'_> {
#[allow(dead_code)] #[inline]
pub(crate) fn consume_balanced_segment(&mut self, open: char, close: char) -> Option<usize> {
if self.current_char() != Some(open) {
return None;
}
let mut depth = 1usize;
self.advance();
while let Some(ch) = self.current_char() {
match ch {
'\\' => {
self.advance();
if self.current_char().is_some() {
self.advance();
}
}
c if c == open => {
depth += 1;
self.advance();
}
c if c == close => {
self.advance();
depth -= 1;
if depth == 0 {
return Some(self.position);
}
}
_ => self.advance(),
}
}
None
}
#[inline]
pub(crate) fn consume_balanced_segment_in_string(
&mut self,
open: char,
close: char,
terminator: char,
) -> Option<usize> {
if self.current_char() != Some(open) {
return None;
}
let mut depth = 1usize;
self.advance();
while let Some(ch) = self.current_char() {
match ch {
'\\' => {
self.advance();
if self.current_char().is_some() {
self.advance();
}
}
c if c == terminator => {
return None;
}
c if c == open => {
depth += 1;
self.advance();
}
c if c == close => {
self.advance();
depth -= 1;
if depth == 0 {
return Some(self.position);
}
}
_ => self.advance(),
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn consume_balanced_segment_handles_nested_segments_and_escapes() {
let mut lexer = PerlLexer::new("(a(\\)b)c)");
let end = lexer.consume_balanced_segment('(', ')');
assert_eq!(end, Some(9));
assert_eq!(lexer.position, 9);
}
#[test]
fn consume_balanced_segment_returns_none_for_unbalanced_segment() {
let mut lexer = PerlLexer::new("(a(b)");
let end = lexer.consume_balanced_segment('(', ')');
assert_eq!(end, None);
}
#[test]
fn consume_balanced_segment_in_string_stops_at_terminator_for_recovery() {
let mut lexer = PerlLexer::new("(${foo\"tail");
let end = lexer.consume_balanced_segment_in_string('(', ')', '"');
assert_eq!(end, None);
assert_eq!(lexer.current_char(), Some('"'));
}
}
#[cfg(test)]
mod balanced_segment_conformance {
use super::*;
fn normalize(input: &str, open: char, close: char) -> (bool, Option<usize>) {
let mut lexer = PerlLexer::new(input);
let result = lexer.consume_balanced_segment(open, close);
(result.is_some(), result)
}
fn normalize_in_string(
input: &str,
open: char,
close: char,
terminator: char,
) -> (bool, Option<usize>) {
let mut lexer = PerlLexer::new(input);
let result = lexer.consume_balanced_segment_in_string(open, close, terminator);
(result.is_some(), result)
}
#[test]
fn simple_parens_balanced() {
let (balanced, end) = normalize("(a b c)", '(', ')');
assert!(balanced, "lexer: '(a b c)' should be balanced");
assert_eq!(end, Some(7), "lexer: end offset after '(a b c)'");
}
#[test]
fn simple_braces_balanced() {
let (balanced, end) = normalize("{x}", '{', '}');
assert!(balanced, "lexer: '{{x}}' should be balanced");
assert_eq!(end, Some(3), "lexer: end offset after '{{x}}'");
}
#[test]
fn simple_brackets_balanced() {
let (balanced, end) = normalize("[1]", '[', ']');
assert!(balanced, "lexer: '[1]' should be balanced");
assert_eq!(end, Some(3), "lexer: end offset after '[1]'");
}
#[test]
fn nested_parens_balanced() {
let (balanced, end) = normalize("(a (b) c)", '(', ')');
assert!(balanced, "lexer: '(a (b) c)' should be balanced");
assert_eq!(end, Some(9), "lexer: end offset after '(a (b) c)'");
}
#[test]
fn nested_braces_balanced() {
let (balanced, end) = normalize("{ {x} {y} }", '{', '}');
assert!(balanced, "lexer: '{{ {{x}} {{y}} }}' should be balanced");
assert_eq!(end, Some(11), "lexer: end offset after '{{ {{x}} {{y}} }}'");
}
#[test]
fn escaped_close_in_middle_balanced() {
let (balanced, end) = normalize("(a \\) b)", '(', ')');
assert!(balanced, "lexer: escaped close '\\\\)' does not close; ')' at end closes");
assert_eq!(end, Some(8), "lexer: end offset after '(a \\\\) b)'");
}
#[test]
fn escaped_open_in_middle_balanced() {
let (balanced, end) = normalize("(a \\( b)", '(', ')');
assert!(balanced, "lexer: escaped open '\\\\(' does not nest; one close suffices");
assert_eq!(end, Some(8), "lexer: end offset after '(a \\\\( b)'");
}
#[test]
fn backslash_at_eof_unbalanced() {
let (balanced, end) = normalize("(a \\", '(', ')');
assert!(!balanced, "lexer: trailing backslash at EOF → unbalanced");
assert_eq!(end, None, "lexer: no end offset for trailing-backslash input");
}
#[test]
fn escaped_close_only_unbalanced() {
let (balanced, end) = normalize("(\\)", '(', ')');
assert!(!balanced, "lexer: '(\\\\)' has only an escaped close → unbalanced");
assert_eq!(end, None, "lexer: no end offset for '(\\\\)'");
}
#[test]
fn unbalanced_open_only_no_close() {
let (balanced, end) = normalize("(a b c", '(', ')');
assert!(!balanced, "lexer: '(a b c' (no close) → unbalanced");
assert_eq!(end, None, "lexer: no end offset for '(a b c'");
}
#[test]
fn empty_pair_balanced() {
let (balanced, end) = normalize("()", '(', ')');
assert!(balanced, "lexer: '()' should be balanced");
assert_eq!(end, Some(2), "lexer: end offset after '()'");
}
#[test]
fn in_string_stops_at_terminator_unbalanced() {
let (balanced, end) = normalize_in_string("(foo\"bar", '(', ')', '"');
assert!(!balanced, "lexer in_string: '\"' terminator hit → unbalanced");
assert_eq!(end, None, "lexer in_string: no end offset when stopped by terminator");
}
#[test]
fn in_string_balanced_before_terminator() {
let (balanced, end) = normalize_in_string("(foo)\"tail", '(', ')', '"');
assert!(balanced, "lexer in_string: '(foo)' closes before '\"' → balanced");
assert_eq!(end, Some(5), "lexer in_string: end offset after '(foo)'");
}
#[test]
fn in_string_nested_balanced_before_terminator() {
let (balanced, end) = normalize_in_string("(a(b)c)\"rest", '(', ')', '"');
assert!(balanced, "lexer in_string: nested '(a(b)c)' balanced before '\"'");
assert_eq!(end, Some(7), "lexer in_string: end offset after '(a(b)c)'");
}
#[test]
fn in_string_escaped_terminator_keeps_scanning() {
let (balanced, end) = normalize_in_string("(a\\\"b)", '(', ')', '"');
assert!(balanced, "lexer in_string: escaped '\\\"' is not a terminator; ')' closes");
assert_eq!(end, Some(6), "lexer in_string: end offset after '(a\\\\\"b)'");
}
}