From 02872db44a4003352103eca05ae3cea2f21a9ab5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 10:15:04 +0000 Subject: [PATCH 1/9] Initial plan From ee30c8c8aa9a611512ed3a49fce99ac0ec8de8e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 10:25:06 +0000 Subject: [PATCH 2/9] Add set_depth_limit feature to prevent stack overflow Co-authored-by: tomtau <2410580+tomtau@users.noreply.github.com> --- pest/src/lib.rs | 4 +- pest/src/parser_state.rs | 113 +++++++++++++++++++++-- pest/tests/depth_limit.rs | 186 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 pest/tests/depth_limit.rs diff --git a/pest/src/lib.rs b/pest/src/lib.rs index c9e6c8865..ad7ec8f13 100644 --- a/pest/src/lib.rs +++ b/pest/src/lib.rs @@ -344,8 +344,8 @@ extern crate std; pub use crate::parser::Parser; pub use crate::parser_state::{ - set_call_limit, set_error_detail, state, Atomicity, Lookahead, MatchDir, ParseResult, - ParserState, + set_call_limit, set_depth_limit, set_error_detail, state, Atomicity, Lookahead, MatchDir, + ParseResult, ParserState, }; pub use crate::position::Position; pub use crate::span::{merge_spans, Lines, LinesSpan, Span}; diff --git a/pest/src/parser_state.rs b/pest/src/parser_state.rs index 823c5a74f..d842eadd0 100644 --- a/pest/src/parser_state.rs +++ b/pest/src/parser_state.rs @@ -89,6 +89,7 @@ pub enum MatchDir { } static CALL_LIMIT: AtomicUsize = AtomicUsize::new(0); +static DEPTH_LIMIT: AtomicUsize = AtomicUsize::new(0); /// Sets the maximum call limit for the parser state /// to prevent stack overflows or excessive execution times @@ -105,6 +106,31 @@ pub fn set_call_limit(limit: Option) { CALL_LIMIT.store(limit.map(|f| f.get()).unwrap_or(0), Ordering::Relaxed); } +/// Sets the maximum recursion depth limit for the parser state +/// to prevent stack overflows caused by deeply nested grammars. +/// If set, the depth is tracked across recursive rule invocations. +/// +/// # Arguments +/// +/// * `limit` - The maximum recursion depth. If None, +/// the recursion depth is unlimited. +/// +/// # Examples +/// +/// ``` +/// use pest; +/// use core::num::NonZeroUsize; +/// +/// // Set a depth limit of 100 +/// pest::set_depth_limit(Some(NonZeroUsize::new(100).unwrap())); +/// +/// // Remove the depth limit +/// pest::set_depth_limit(None); +/// ``` +pub fn set_depth_limit(limit: Option) { + DEPTH_LIMIT.store(limit.map(|f| f.get()).unwrap_or(0), Ordering::Relaxed); +} + static ERROR_DETAIL: AtomicBool = AtomicBool::new(false); /// Sets whether information for more error details @@ -124,27 +150,78 @@ pub fn set_error_detail(enabled: bool) { #[derive(Debug)] struct CallLimitTracker { current_call_limit: Option<(usize, usize)>, + current_depth_limit: Option<(usize, usize)>, + limit_was_reached: bool, } impl Default for CallLimitTracker { fn default() -> Self { - let limit = CALL_LIMIT.load(Ordering::Relaxed); - let current_call_limit = if limit > 0 { Some((0, limit)) } else { None }; - Self { current_call_limit } + let call_limit = CALL_LIMIT.load(Ordering::Relaxed); + let current_call_limit = if call_limit > 0 { + Some((0, call_limit)) + } else { + None + }; + + let depth_limit = DEPTH_LIMIT.load(Ordering::Relaxed); + let current_depth_limit = if depth_limit > 0 { + Some((0, depth_limit)) + } else { + None + }; + + Self { + current_call_limit, + current_depth_limit, + limit_was_reached: false, + } } } impl CallLimitTracker { fn limit_reached(&self) -> bool { + self.limit_was_reached + || self.current_call_limit + .is_some_and(|(current, limit)| current >= limit) + || self.current_depth_limit + .is_some_and(|(current, limit)| current >= limit) + } + + fn call_limit_reached(&self) -> bool { self.current_call_limit .is_some_and(|(current, limit)| current >= limit) } - fn increment_depth(&mut self) { + fn depth_limit_reached(&self) -> bool { + self.current_depth_limit + .is_some_and(|(current, limit)| current >= limit) + } + + fn increment_call(&mut self) { if let Some((current, _)) = &mut self.current_call_limit { *current += 1; } } + + fn increment_depth(&mut self) { + if let Some((current, _)) = &mut self.current_depth_limit { + *current += 1; + } + } + + fn decrement_depth(&mut self) { + // Don't decrement if limit was already reached - we want to preserve the error state + if self.limit_was_reached { + return; + } + if let Some((current, _)) = &mut self.current_depth_limit { + *current = current.saturating_sub(1); + } + } + + fn mark_limit_reached(&mut self) { + self.limit_was_reached = true; + } } /// Number of call stacks that may result from a sequence of rules parsing. @@ -516,7 +593,11 @@ where Ok(new(Rc::new(state.queue), input, None, 0, len)) } Err(mut state) => { - let variant = if state.reached_call_limit() { + let variant = if state.call_tracker.depth_limit_reached() { + ErrorVariant::CustomError { + message: "depth limit reached".to_owned(), + } + } else if state.call_tracker.call_limit_reached() { ErrorVariant::CustomError { message: "call limit reached".to_owned(), } @@ -624,12 +705,29 @@ impl<'i, R: RuleType> ParserState<'i, R> { #[inline] fn inc_call_check_limit(mut self: Box) -> ParseResult> { if self.call_tracker.limit_reached() { + self.call_tracker.mark_limit_reached(); return Err(self); } + self.call_tracker.increment_call(); + Ok(self) + } + + #[inline] + fn inc_depth_check_limit(mut self: Box) -> ParseResult> { self.call_tracker.increment_depth(); + if self.call_tracker.limit_reached() { + self.call_tracker.mark_limit_reached(); + return Err(self); + } Ok(self) } + #[inline] + fn dec_depth(mut self: Box) -> Box { + self.call_tracker.decrement_depth(); + self + } + #[inline] fn reached_call_limit(&self) -> bool { self.call_tracker.limit_reached() @@ -661,6 +759,7 @@ impl<'i, R: RuleType> ParserState<'i, R> { F: FnOnce(Box) -> ParseResult>, { self = self.inc_call_check_limit()?; + self = self.inc_depth_check_limit()?; // Position from which this `rule` starts parsing. let actual_pos = self.position.pos(); // Remember index of the `self.queue` element that will be associated with this `rule`. @@ -755,7 +854,7 @@ impl<'i, R: RuleType> ParserState<'i, R> { if new_state.parse_attempts.enabled { try_add_rule_to_stack(&mut new_state); } - Ok(new_state) + Ok(new_state.dec_depth()) } Err(mut new_state) => { if new_state.lookahead != Lookahead::Negative { @@ -777,7 +876,7 @@ impl<'i, R: RuleType> ParserState<'i, R> { new_state.queue.truncate(index); } - Err(new_state) + Err(new_state.dec_depth()) } } } diff --git a/pest/tests/depth_limit.rs b/pest/tests/depth_limit.rs new file mode 100644 index 000000000..bc54522a8 --- /dev/null +++ b/pest/tests/depth_limit.rs @@ -0,0 +1,186 @@ +// pest. The Elegant Parser +// Copyright (c) 2018 DragoČ™ Tiselice +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use pest::error::Error; +use pest::iterators::Pairs; +use pest::{state, ParseResult, Parser, ParserState}; +use core::num::NonZeroUsize; + +#[allow(dead_code, non_camel_case_types)] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +enum Rule { + expression, + add_expr, + mul_expr, + primary, + number, +} + +struct TestParser; + +impl Parser for TestParser { + fn parse(rule: Rule, input: &str) -> Result, Error> { + fn expression( + state: Box>, + ) -> ParseResult>> { + state.rule(Rule::expression, |s| { + s.sequence(|s| { + s.start_of_input() + .and_then(add_expr) + .and_then(|s| s.end_of_input()) + }) + }) + } + + fn add_expr( + state: Box>, + ) -> ParseResult>> { + state.rule(Rule::add_expr, |s| { + s.sequence(|s| { + mul_expr(s).and_then(|s| { + s.repeat(|s| { + s.sequence(|s| { + s.match_string("+").and_then(mul_expr) + }) + }) + }) + }) + }) + } + + fn mul_expr( + state: Box>, + ) -> ParseResult>> { + state.rule(Rule::mul_expr, |s| { + s.sequence(|s| { + primary(s).and_then(|s| { + s.repeat(|s| { + s.sequence(|s| { + s.match_string("*").and_then(primary) + }) + }) + }) + }) + }) + } + + fn primary( + state: Box>, + ) -> ParseResult>> { + state.rule(Rule::primary, |s| { + number(s).or_else(|s| { + s.sequence(|s| { + s.match_string("(") + .and_then(add_expr) + .and_then(|s| s.match_string(")")) + }) + }) + }) + } + + fn number( + state: Box>, + ) -> ParseResult>> { + state.rule(Rule::number, |s| { + s.match_char_by(|c| c.is_ascii_digit()) + .and_then(|s| { + s.repeat(|s| s.match_char_by(|c| c.is_ascii_digit())) + }) + }) + } + + state(input, |s| match rule { + Rule::expression => expression(s), + Rule::add_expr => add_expr(s), + Rule::mul_expr => mul_expr(s), + Rule::primary => primary(s), + Rule::number => number(s), + }) + } +} + +#[test] +fn test_depth_limit_simple() { + // Set a depth limit + pest::set_depth_limit(Some(NonZeroUsize::new(10).unwrap())); + + // This should parse successfully - depth is less than 10 + let result = TestParser::parse(Rule::expression, "1"); + assert!(result.is_ok()); + + // Reset depth limit + pest::set_depth_limit(None); +} + +#[test] +fn test_depth_limit_nested_parens() { + // Set a very low depth limit + pest::set_depth_limit(Some(NonZeroUsize::new(5).unwrap())); + + // Simple expression should fail with this very low limit + // because even "1" requires multiple rule invocations: + // expression -> add_expr -> mul_expr -> primary -> number + let result = TestParser::parse(Rule::expression, "1"); + + match result { + Ok(_) => { + panic!("Expected depth limit error with very low limit"); + } + Err(e) => { + let error_msg = format!("{}", e); + eprintln!("Error message: {}", error_msg); + eprintln!("Error variant: {:?}", e.variant); + // Check if it's a custom error with depth limit message + if let pest::error::ErrorVariant::CustomError { message } = &e.variant { + assert!(message.contains("depth limit reached") || message.contains("call limit reached"), + "Expected depth/call limit error, got: {}", message); + } else { + panic!("Expected CustomError variant, got: {:?}", e.variant); + } + } + } + + // Reset depth limit + pest::set_depth_limit(None); +} + +#[test] +fn test_depth_limit_allows_simple_parse() { + // Set a reasonable depth limit + pest::set_depth_limit(Some(NonZeroUsize::new(50).unwrap())); + + // Simple expression should work with reasonable limit + let result = TestParser::parse(Rule::expression, "1+2"); + assert!(result.is_ok()); + + // Reset depth limit + pest::set_depth_limit(None); +} + +#[test] +fn test_no_depth_limit() { + // Make sure no limit allows parsing + pest::set_depth_limit(None); + + // Even deeply nested expressions should work without a limit + let nested = "((((((1))))))"; + let result = TestParser::parse(Rule::expression, nested); + assert!(result.is_ok()); +} + +#[test] +fn test_depth_limit_reset() { + // Set a limit, then remove it + pest::set_depth_limit(Some(NonZeroUsize::new(5).unwrap())); + pest::set_depth_limit(None); + + // Should work after reset + let result = TestParser::parse(Rule::expression, "((((((1))))))"); + assert!(result.is_ok()); +} From 3c62932845f93fcb8de83aea4be6b59b9da6a0ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 10:27:54 +0000 Subject: [PATCH 3/9] Improve documentation and add comprehensive depth limit tests Co-authored-by: tomtau <2410580+tomtau@users.noreply.github.com> --- pest/src/parser_state.rs | 37 +++++++++++++++++++++++++------ pest/tests/depth_limit.rs | 43 +++++++++++++++++++++++++++++++++++++ pest/tests/nested_expr.pest | 9 ++++++++ 3 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 pest/tests/nested_expr.pest diff --git a/pest/src/parser_state.rs b/pest/src/parser_state.rs index d842eadd0..bcca680c4 100644 --- a/pest/src/parser_state.rs +++ b/pest/src/parser_state.rs @@ -110,6 +110,16 @@ pub fn set_call_limit(limit: Option) { /// to prevent stack overflows caused by deeply nested grammars. /// If set, the depth is tracked across recursive rule invocations. /// +/// This is particularly useful for grammars that can have deeply nested +/// structures (like nested parentheses, nested function calls, etc.) +/// which could cause stack overflow on the system stack. +/// +/// The depth limit is different from the call limit: +/// - Call limit (`set_call_limit`) tracks the total number of parser function +/// calls and never decreases during parsing. +/// - Depth limit (`set_depth_limit`) tracks the current recursion depth and +/// decreases as the parser returns from recursive calls. +/// /// # Arguments /// /// * `limit` - The maximum recursion depth. If None, @@ -121,12 +131,30 @@ pub fn set_call_limit(limit: Option) { /// use pest; /// use core::num::NonZeroUsize; /// -/// // Set a depth limit of 100 +/// // Set a depth limit of 100 to prevent stack overflow +/// // from deeply nested grammar rules /// pest::set_depth_limit(Some(NonZeroUsize::new(100).unwrap())); /// -/// // Remove the depth limit +/// // Parse your input... +/// // If the depth exceeds 100, parsing will fail with +/// // an error message "depth limit reached" +/// +/// // Remove the depth limit when done /// pest::set_depth_limit(None); /// ``` +/// +/// # Use Cases +/// +/// This is useful for: +/// - Protecting against malicious input with excessive nesting +/// - Preventing stack overflow in resource-constrained environments +/// - Ensuring consistent behavior across platforms with different stack sizes +/// +/// # Note +/// +/// Setting the limit too low may prevent parsing of legitimate deeply nested +/// expressions. The appropriate limit depends on your grammar complexity +/// and the expected depth of valid inputs. pub fn set_depth_limit(limit: Option) { DEPTH_LIMIT.store(limit.map(|f| f.get()).unwrap_or(0), Ordering::Relaxed); } @@ -728,11 +756,6 @@ impl<'i, R: RuleType> ParserState<'i, R> { self } - #[inline] - fn reached_call_limit(&self) -> bool { - self.call_tracker.limit_reached() - } - /// Wrapper needed to generate tokens. This will associate the `R` type rule to the closure /// meant to match the rule. /// diff --git a/pest/tests/depth_limit.rs b/pest/tests/depth_limit.rs index bc54522a8..343d6374b 100644 --- a/pest/tests/depth_limit.rs +++ b/pest/tests/depth_limit.rs @@ -184,3 +184,46 @@ fn test_depth_limit_reset() { let result = TestParser::parse(Rule::expression, "((((((1))))))"); assert!(result.is_ok()); } + +/// This test demonstrates the issue from the GitHub issue: +/// https://github.com/pest-parser/pest/issues/... +/// +/// The grammar from the issue can cause stack overflow with deeply +/// nested parentheses. With set_depth_limit, we can prevent this. +#[test] +fn test_prevents_stack_overflow_from_issue() { + // Set a reasonable depth limit to prevent stack overflow + // On Windows with 1MB stack, 300 nested parentheses would fail + // With depth limit, we can catch this before stack overflow + pest::set_depth_limit(Some(NonZeroUsize::new(100).unwrap())); + + // Create a deeply nested expression with 150 levels of nesting + // (which would be 300 parentheses total) + // This would cause stack overflow without the depth limit + let mut deeply_nested = String::new(); + let nesting_depth = 150; + + // Add opening parens + for _ in 0..nesting_depth { + deeply_nested.push('('); + } + deeply_nested.push('1'); + // Add closing parens + for _ in 0..nesting_depth { + deeply_nested.push(')'); + } + + let result = TestParser::parse(Rule::expression, &deeply_nested); + + // Should fail with depth limit reached, not stack overflow + assert!(result.is_err()); + if let Err(e) = result { + let error_msg = format!("{}", e); + assert!(error_msg.contains("depth limit reached"), + "Expected depth limit error, got: {}", error_msg); + } + + // Reset depth limit + pest::set_depth_limit(None); +} + diff --git a/pest/tests/nested_expr.pest b/pest/tests/nested_expr.pest new file mode 100644 index 000000000..19a5bc22f --- /dev/null +++ b/pest/tests/nested_expr.pest @@ -0,0 +1,9 @@ +// Example grammar from the issue demonstrating deep nesting +// This grammar can cause stack overflow with deeply nested parentheses + +WHITESPACE = _{ " " | "\t" | "\n" | "\r" } +expression = { SOI ~ add_expr ~ EOI } +add_expr = { mul_expr ~ ("+" ~ mul_expr)* } +mul_expr = { primary ~ ("*" ~ primary)* } +primary = { number | "(" ~ add_expr ~ ")" } +number = @{ ASCII_DIGIT+ } From a69895d6f211a0ff56382a7fe16e398227a798b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 10:31:00 +0000 Subject: [PATCH 4/9] Address code review feedback on depth tracking logic Co-authored-by: tomtau <2410580+tomtau@users.noreply.github.com> --- pest/src/parser_state.rs | 32 ++++++++++++++++++++++++-------- pest/tests/depth_limit.rs | 4 ++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/pest/src/parser_state.rs b/pest/src/parser_state.rs index bcca680c4..41fce50df 100644 --- a/pest/src/parser_state.rs +++ b/pest/src/parser_state.rs @@ -179,7 +179,8 @@ pub fn set_error_detail(enabled: bool) { struct CallLimitTracker { current_call_limit: Option<(usize, usize)>, current_depth_limit: Option<(usize, usize)>, - limit_was_reached: bool, + /// When set, indicates depth tracking should stop at this value (limit was reached) + depth_at_limit: Option, } impl Default for CallLimitTracker { @@ -201,14 +202,14 @@ impl Default for CallLimitTracker { Self { current_call_limit, current_depth_limit, - limit_was_reached: false, + depth_at_limit: None, } } } impl CallLimitTracker { fn limit_reached(&self) -> bool { - self.limit_was_reached + self.depth_at_limit.is_some() || self.current_call_limit .is_some_and(|(current, limit)| current >= limit) || self.current_depth_limit @@ -221,8 +222,15 @@ impl CallLimitTracker { } fn depth_limit_reached(&self) -> bool { + self.depth_at_limit.is_some() + || self.current_depth_limit + .is_some_and(|(current, limit)| current >= limit) + } + + fn depth_limit_would_be_exceeded(&self) -> bool { + // Check if incrementing depth would exceed the limit self.current_depth_limit - .is_some_and(|(current, limit)| current >= limit) + .is_some_and(|(current, limit)| current + 1 > limit) } fn increment_call(&mut self) { @@ -232,6 +240,10 @@ impl CallLimitTracker { } fn increment_depth(&mut self) { + // Don't increment if we've already hit the limit + if self.depth_at_limit.is_some() { + return; + } if let Some((current, _)) = &mut self.current_depth_limit { *current += 1; } @@ -239,7 +251,7 @@ impl CallLimitTracker { fn decrement_depth(&mut self) { // Don't decrement if limit was already reached - we want to preserve the error state - if self.limit_was_reached { + if self.depth_at_limit.is_some() { return; } if let Some((current, _)) = &mut self.current_depth_limit { @@ -248,7 +260,10 @@ impl CallLimitTracker { } fn mark_limit_reached(&mut self) { - self.limit_was_reached = true; + // Record the current depth when limit is reached + if let Some((current, _)) = self.current_depth_limit { + self.depth_at_limit = Some(current); + } } } @@ -742,11 +757,12 @@ impl<'i, R: RuleType> ParserState<'i, R> { #[inline] fn inc_depth_check_limit(mut self: Box) -> ParseResult> { - self.call_tracker.increment_depth(); - if self.call_tracker.limit_reached() { + // Check limit before incrementing + if self.call_tracker.depth_limit_would_be_exceeded() { self.call_tracker.mark_limit_reached(); return Err(self); } + self.call_tracker.increment_depth(); Ok(self) } diff --git a/pest/tests/depth_limit.rs b/pest/tests/depth_limit.rs index 343d6374b..a29294f95 100644 --- a/pest/tests/depth_limit.rs +++ b/pest/tests/depth_limit.rs @@ -185,8 +185,8 @@ fn test_depth_limit_reset() { assert!(result.is_ok()); } -/// This test demonstrates the issue from the GitHub issue: -/// https://github.com/pest-parser/pest/issues/... +/// This test demonstrates the issue from GitHub: +/// Pest grammars can cause overflow within Pest itself with deeply nested structures. /// /// The grammar from the issue can cause stack overflow with deeply /// nested parentheses. With set_depth_limit, we can prevent this. From d9b905bfc987a3e5a4a36c3836da0e733604ee51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 10:35:16 +0000 Subject: [PATCH 5/9] Fix depth limit checking and address remaining code review feedback Co-authored-by: tomtau <2410580+tomtau@users.noreply.github.com> --- pest/src/parser_state.rs | 7 +++++-- pest/tests/depth_limit.rs | 37 +++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/pest/src/parser_state.rs b/pest/src/parser_state.rs index 41fce50df..ccc9aef48 100644 --- a/pest/src/parser_state.rs +++ b/pest/src/parser_state.rs @@ -636,6 +636,8 @@ where Ok(new(Rc::new(state.queue), input, None, 0, len)) } Err(mut state) => { + // Note: When both limits might be reached, depth limit is checked first + // to provide the most specific error message for recursion issues. let variant = if state.call_tracker.depth_limit_reached() { ErrorVariant::CustomError { message: "depth limit reached".to_owned(), @@ -747,8 +749,9 @@ impl<'i, R: RuleType> ParserState<'i, R> { #[inline] fn inc_call_check_limit(mut self: Box) -> ParseResult> { - if self.call_tracker.limit_reached() { - self.call_tracker.mark_limit_reached(); + // Only check call limit here, not depth limit + // Depth limit is checked in inc_depth_check_limit + if self.call_tracker.call_limit_reached() || self.call_tracker.depth_at_limit.is_some() { return Err(self); } self.call_tracker.increment_call(); diff --git a/pest/tests/depth_limit.rs b/pest/tests/depth_limit.rs index a29294f95..32f51c2ff 100644 --- a/pest/tests/depth_limit.rs +++ b/pest/tests/depth_limit.rs @@ -120,12 +120,14 @@ fn test_depth_limit_simple() { #[test] fn test_depth_limit_nested_parens() { - // Set a very low depth limit - pest::set_depth_limit(Some(NonZeroUsize::new(5).unwrap())); + // Make sure call limit is disabled + pest::set_call_limit(None); + // Set a very low depth limit (lower than what "1" needs) + // Parsing "1" requires: expression -> add_expr -> mul_expr -> primary -> number + // That's 5 rule invocations, so limit of 4 should fail + pest::set_depth_limit(Some(NonZeroUsize::new(4).unwrap())); // Simple expression should fail with this very low limit - // because even "1" requires multiple rule invocations: - // expression -> add_expr -> mul_expr -> primary -> number let result = TestParser::parse(Rule::expression, "1"); match result { @@ -134,14 +136,12 @@ fn test_depth_limit_nested_parens() { } Err(e) => { let error_msg = format!("{}", e); - eprintln!("Error message: {}", error_msg); - eprintln!("Error variant: {:?}", e.variant); - // Check if it's a custom error with depth limit message + // Check specifically for depth limit error if let pest::error::ErrorVariant::CustomError { message } = &e.variant { - assert!(message.contains("depth limit reached") || message.contains("call limit reached"), - "Expected depth/call limit error, got: {}", message); + assert_eq!(message, "depth limit reached", + "Expected depth limit error, got: {}", message); } else { - panic!("Expected CustomError variant, got: {:?}", e.variant); + panic!("Expected CustomError variant with depth limit, got: {:?}", e.variant); } } } @@ -192,16 +192,17 @@ fn test_depth_limit_reset() { /// nested parentheses. With set_depth_limit, we can prevent this. #[test] fn test_prevents_stack_overflow_from_issue() { - // Set a reasonable depth limit to prevent stack overflow - // On Windows with 1MB stack, 300 nested parentheses would fail - // With depth limit, we can catch this before stack overflow - pest::set_depth_limit(Some(NonZeroUsize::new(100).unwrap())); + // Make sure call limit is disabled + pest::set_call_limit(None); + // Set a depth limit that's lower than what 150 nested parens would need + // Each level of parentheses requires several rule invocations + // (expression, add_expr, mul_expr, primary for opening, then same for closing) + pest::set_depth_limit(Some(NonZeroUsize::new(50).unwrap())); - // Create a deeply nested expression with 150 levels of nesting - // (which would be 300 parentheses total) - // This would cause stack overflow without the depth limit + // Create a deeply nested expression with 30 levels of nesting + // This would need more than 50 depth let mut deeply_nested = String::new(); - let nesting_depth = 150; + let nesting_depth = 30; // Add opening parens for _ in 0..nesting_depth { From 45046b70d9c8c797b28dde22449defe7d255cbfa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 10:37:50 +0000 Subject: [PATCH 6/9] Simplify depth checking logic per code review Co-authored-by: tomtau <2410580+tomtau@users.noreply.github.com> --- pest/src/parser_state.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pest/src/parser_state.rs b/pest/src/parser_state.rs index ccc9aef48..944fc7bd5 100644 --- a/pest/src/parser_state.rs +++ b/pest/src/parser_state.rs @@ -228,9 +228,10 @@ impl CallLimitTracker { } fn depth_limit_would_be_exceeded(&self) -> bool { - // Check if incrementing depth would exceed the limit + // Check if we're already at the limit + // (incrementing would exceed it) self.current_depth_limit - .is_some_and(|(current, limit)| current + 1 > limit) + .is_some_and(|(current, limit)| current >= limit) } fn increment_call(&mut self) { @@ -749,8 +750,10 @@ impl<'i, R: RuleType> ParserState<'i, R> { #[inline] fn inc_call_check_limit(mut self: Box) -> ParseResult> { - // Only check call limit here, not depth limit - // Depth limit is checked in inc_depth_check_limit + // Check if call limit is reached, or if depth limit was previously reached + // Note: We check depth_at_limit (not depth_limit_reached) because we only want + // to fail if the depth limit was ALREADY hit in a previous call. + // The actual depth limit check happens in inc_depth_check_limit. if self.call_tracker.call_limit_reached() || self.call_tracker.depth_at_limit.is_some() { return Err(self); } From 2b956db501c210c3a7cae4bac9999f3dbb5716fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 12:17:46 +0000 Subject: [PATCH 7/9] Merge depth limit into set_call_limit - track recursion depth properly As requested by @tomtau, removed separate set_depth_limit function and fixed set_call_limit to track actual recursion depth (increment on entry, decrement on exit) instead of just total calls. This makes set_call_limit properly prevent stack overflow from deep recursion as originally intended. Changes: - Removed DEPTH_LIMIT static and set_depth_limit function - Modified CallLimitTracker to only track recursion depth (not total calls) - Simplified implementation by removing separate call/depth tracking - Updated all tests to use set_call_limit instead of set_depth_limit - Updated documentation to reflect that set_call_limit now tracks depth Co-authored-by: tomtau <2410580+tomtau@users.noreply.github.com> --- pest/src/lib.rs | 4 +- pest/src/parser_state.rs | 94 +++++---------------------------------- pest/tests/depth_limit.rs | 63 +++++++++++++------------- 3 files changed, 46 insertions(+), 115 deletions(-) diff --git a/pest/src/lib.rs b/pest/src/lib.rs index ad7ec8f13..c9e6c8865 100644 --- a/pest/src/lib.rs +++ b/pest/src/lib.rs @@ -344,8 +344,8 @@ extern crate std; pub use crate::parser::Parser; pub use crate::parser_state::{ - set_call_limit, set_depth_limit, set_error_detail, state, Atomicity, Lookahead, MatchDir, - ParseResult, ParserState, + set_call_limit, set_error_detail, state, Atomicity, Lookahead, MatchDir, ParseResult, + ParserState, }; pub use crate::position::Position; pub use crate::span::{merge_spans, Lines, LinesSpan, Span}; diff --git a/pest/src/parser_state.rs b/pest/src/parser_state.rs index 944fc7bd5..035fd82a3 100644 --- a/pest/src/parser_state.rs +++ b/pest/src/parser_state.rs @@ -89,36 +89,14 @@ pub enum MatchDir { } static CALL_LIMIT: AtomicUsize = AtomicUsize::new(0); -static DEPTH_LIMIT: AtomicUsize = AtomicUsize::new(0); - -/// Sets the maximum call limit for the parser state -/// to prevent stack overflows or excessive execution times -/// in some grammars. -/// If set, the calls are tracked as a running total -/// over all non-terminal rules that can nest closures -/// (which are passed to transform the parser state). -/// -/// # Arguments -/// -/// * `limit` - The maximum number of calls. If None, -/// the number of calls is unlimited. -pub fn set_call_limit(limit: Option) { - CALL_LIMIT.store(limit.map(|f| f.get()).unwrap_or(0), Ordering::Relaxed); -} /// Sets the maximum recursion depth limit for the parser state /// to prevent stack overflows caused by deeply nested grammars. -/// If set, the depth is tracked across recursive rule invocations. -/// -/// This is particularly useful for grammars that can have deeply nested -/// structures (like nested parentheses, nested function calls, etc.) -/// which could cause stack overflow on the system stack. /// -/// The depth limit is different from the call limit: -/// - Call limit (`set_call_limit`) tracks the total number of parser function -/// calls and never decreases during parsing. -/// - Depth limit (`set_depth_limit`) tracks the current recursion depth and -/// decreases as the parser returns from recursive calls. +/// The depth is tracked across recursive rule invocations, incrementing +/// when entering a rule and decrementing when exiting. This provides +/// protection against stack overflow from deeply nested structures +/// (like nested parentheses, nested function calls, etc.). /// /// # Arguments /// @@ -133,14 +111,14 @@ pub fn set_call_limit(limit: Option) { /// /// // Set a depth limit of 100 to prevent stack overflow /// // from deeply nested grammar rules -/// pest::set_depth_limit(Some(NonZeroUsize::new(100).unwrap())); +/// pest::set_call_limit(Some(NonZeroUsize::new(100).unwrap())); /// /// // Parse your input... /// // If the depth exceeds 100, parsing will fail with -/// // an error message "depth limit reached" +/// // an error message "call limit reached" /// -/// // Remove the depth limit when done -/// pest::set_depth_limit(None); +/// // Remove the limit when done +/// pest::set_call_limit(None); /// ``` /// /// # Use Cases @@ -155,8 +133,8 @@ pub fn set_call_limit(limit: Option) { /// Setting the limit too low may prevent parsing of legitimate deeply nested /// expressions. The appropriate limit depends on your grammar complexity /// and the expected depth of valid inputs. -pub fn set_depth_limit(limit: Option) { - DEPTH_LIMIT.store(limit.map(|f| f.get()).unwrap_or(0), Ordering::Relaxed); +pub fn set_call_limit(limit: Option) { + CALL_LIMIT.store(limit.map(|f| f.get()).unwrap_or(0), Ordering::Relaxed); } static ERROR_DETAIL: AtomicBool = AtomicBool::new(false); @@ -177,7 +155,6 @@ pub fn set_error_detail(enabled: bool) { #[derive(Debug)] struct CallLimitTracker { - current_call_limit: Option<(usize, usize)>, current_depth_limit: Option<(usize, usize)>, /// When set, indicates depth tracking should stop at this value (limit was reached) depth_at_limit: Option, @@ -185,14 +162,7 @@ struct CallLimitTracker { impl Default for CallLimitTracker { fn default() -> Self { - let call_limit = CALL_LIMIT.load(Ordering::Relaxed); - let current_call_limit = if call_limit > 0 { - Some((0, call_limit)) - } else { - None - }; - - let depth_limit = DEPTH_LIMIT.load(Ordering::Relaxed); + let depth_limit = CALL_LIMIT.load(Ordering::Relaxed); let current_depth_limit = if depth_limit > 0 { Some((0, depth_limit)) } else { @@ -200,7 +170,6 @@ impl Default for CallLimitTracker { }; Self { - current_call_limit, current_depth_limit, depth_at_limit: None, } @@ -209,19 +178,6 @@ impl Default for CallLimitTracker { impl CallLimitTracker { fn limit_reached(&self) -> bool { - self.depth_at_limit.is_some() - || self.current_call_limit - .is_some_and(|(current, limit)| current >= limit) - || self.current_depth_limit - .is_some_and(|(current, limit)| current >= limit) - } - - fn call_limit_reached(&self) -> bool { - self.current_call_limit - .is_some_and(|(current, limit)| current >= limit) - } - - fn depth_limit_reached(&self) -> bool { self.depth_at_limit.is_some() || self.current_depth_limit .is_some_and(|(current, limit)| current >= limit) @@ -234,12 +190,6 @@ impl CallLimitTracker { .is_some_and(|(current, limit)| current >= limit) } - fn increment_call(&mut self) { - if let Some((current, _)) = &mut self.current_call_limit { - *current += 1; - } - } - fn increment_depth(&mut self) { // Don't increment if we've already hit the limit if self.depth_at_limit.is_some() { @@ -637,13 +587,7 @@ where Ok(new(Rc::new(state.queue), input, None, 0, len)) } Err(mut state) => { - // Note: When both limits might be reached, depth limit is checked first - // to provide the most specific error message for recursion issues. - let variant = if state.call_tracker.depth_limit_reached() { - ErrorVariant::CustomError { - message: "depth limit reached".to_owned(), - } - } else if state.call_tracker.call_limit_reached() { + let variant = if state.call_tracker.limit_reached() { ErrorVariant::CustomError { message: "call limit reached".to_owned(), } @@ -750,19 +694,6 @@ impl<'i, R: RuleType> ParserState<'i, R> { #[inline] fn inc_call_check_limit(mut self: Box) -> ParseResult> { - // Check if call limit is reached, or if depth limit was previously reached - // Note: We check depth_at_limit (not depth_limit_reached) because we only want - // to fail if the depth limit was ALREADY hit in a previous call. - // The actual depth limit check happens in inc_depth_check_limit. - if self.call_tracker.call_limit_reached() || self.call_tracker.depth_at_limit.is_some() { - return Err(self); - } - self.call_tracker.increment_call(); - Ok(self) - } - - #[inline] - fn inc_depth_check_limit(mut self: Box) -> ParseResult> { // Check limit before incrementing if self.call_tracker.depth_limit_would_be_exceeded() { self.call_tracker.mark_limit_reached(); @@ -804,7 +735,6 @@ impl<'i, R: RuleType> ParserState<'i, R> { F: FnOnce(Box) -> ParseResult>, { self = self.inc_call_check_limit()?; - self = self.inc_depth_check_limit()?; // Position from which this `rule` starts parsing. let actual_pos = self.position.pos(); // Remember index of the `self.queue` element that will be associated with this `rule`. diff --git a/pest/tests/depth_limit.rs b/pest/tests/depth_limit.rs index 32f51c2ff..3e75fbd30 100644 --- a/pest/tests/depth_limit.rs +++ b/pest/tests/depth_limit.rs @@ -107,66 +107,66 @@ impl Parser for TestParser { #[test] fn test_depth_limit_simple() { - // Set a depth limit - pest::set_depth_limit(Some(NonZeroUsize::new(10).unwrap())); + // Set a recursion depth limit + pest::set_call_limit(Some(NonZeroUsize::new(10).unwrap())); // This should parse successfully - depth is less than 10 let result = TestParser::parse(Rule::expression, "1"); assert!(result.is_ok()); - // Reset depth limit - pest::set_depth_limit(None); + // Reset limit + pest::set_call_limit(None); } #[test] fn test_depth_limit_nested_parens() { - // Make sure call limit is disabled + // Make sure call limit is reset first pest::set_call_limit(None); - // Set a very low depth limit (lower than what "1" needs) + // Set a very low recursion depth limit (lower than what "1" needs) // Parsing "1" requires: expression -> add_expr -> mul_expr -> primary -> number // That's 5 rule invocations, so limit of 4 should fail - pest::set_depth_limit(Some(NonZeroUsize::new(4).unwrap())); + pest::set_call_limit(Some(NonZeroUsize::new(4).unwrap())); // Simple expression should fail with this very low limit let result = TestParser::parse(Rule::expression, "1"); match result { Ok(_) => { - panic!("Expected depth limit error with very low limit"); + panic!("Expected call limit error with very low limit"); } Err(e) => { let error_msg = format!("{}", e); - // Check specifically for depth limit error + // Check specifically for call limit error if let pest::error::ErrorVariant::CustomError { message } = &e.variant { - assert_eq!(message, "depth limit reached", - "Expected depth limit error, got: {}", message); + assert_eq!(message, "call limit reached", + "Expected call limit error, got: {}", message); } else { - panic!("Expected CustomError variant with depth limit, got: {:?}", e.variant); + panic!("Expected CustomError variant with call limit, got: {:?}", e.variant); } } } - // Reset depth limit - pest::set_depth_limit(None); + // Reset limit + pest::set_call_limit(None); } #[test] fn test_depth_limit_allows_simple_parse() { - // Set a reasonable depth limit - pest::set_depth_limit(Some(NonZeroUsize::new(50).unwrap())); + // Set a reasonable recursion depth limit + pest::set_call_limit(Some(NonZeroUsize::new(50).unwrap())); // Simple expression should work with reasonable limit let result = TestParser::parse(Rule::expression, "1+2"); assert!(result.is_ok()); - // Reset depth limit - pest::set_depth_limit(None); + // Reset limit + pest::set_call_limit(None); } #[test] fn test_no_depth_limit() { // Make sure no limit allows parsing - pest::set_depth_limit(None); + pest::set_call_limit(None); // Even deeply nested expressions should work without a limit let nested = "((((((1))))))"; @@ -177,8 +177,8 @@ fn test_no_depth_limit() { #[test] fn test_depth_limit_reset() { // Set a limit, then remove it - pest::set_depth_limit(Some(NonZeroUsize::new(5).unwrap())); - pest::set_depth_limit(None); + pest::set_call_limit(Some(NonZeroUsize::new(5).unwrap())); + pest::set_call_limit(None); // Should work after reset let result = TestParser::parse(Rule::expression, "((((((1))))))"); @@ -189,15 +189,16 @@ fn test_depth_limit_reset() { /// Pest grammars can cause overflow within Pest itself with deeply nested structures. /// /// The grammar from the issue can cause stack overflow with deeply -/// nested parentheses. With set_depth_limit, we can prevent this. +/// nested parentheses. With set_call_limit (which now tracks recursion depth), +/// we can prevent this. #[test] fn test_prevents_stack_overflow_from_issue() { - // Make sure call limit is disabled + // Make sure call limit is disabled initially pest::set_call_limit(None); - // Set a depth limit that's lower than what 150 nested parens would need + // Set a recursion depth limit that's lower than what 30 nested parens would need // Each level of parentheses requires several rule invocations - // (expression, add_expr, mul_expr, primary for opening, then same for closing) - pest::set_depth_limit(Some(NonZeroUsize::new(50).unwrap())); + // (expression, add_expr, mul_expr, primary for opening, then recursion for content) + pest::set_call_limit(Some(NonZeroUsize::new(50).unwrap())); // Create a deeply nested expression with 30 levels of nesting // This would need more than 50 depth @@ -216,15 +217,15 @@ fn test_prevents_stack_overflow_from_issue() { let result = TestParser::parse(Rule::expression, &deeply_nested); - // Should fail with depth limit reached, not stack overflow + // Should fail with call limit reached (recursion depth limit), not stack overflow assert!(result.is_err()); if let Err(e) = result { let error_msg = format!("{}", e); - assert!(error_msg.contains("depth limit reached"), - "Expected depth limit error, got: {}", error_msg); + assert!(error_msg.contains("call limit reached"), + "Expected call limit error, got: {}", error_msg); } - // Reset depth limit - pest::set_depth_limit(None); + // Reset limit + pest::set_call_limit(None); } From 292d31c35d25efd98c083a290479f833dedd61b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:23:18 +0000 Subject: [PATCH 8/9] Fix cargo fmt formatting issues Co-authored-by: tomtau <2410580+tomtau@users.noreply.github.com> --- pest/src/parser_state.rs | 5 ++- pest/tests/depth_limit.rs | 86 +++++++++++++++++---------------------- 2 files changed, 41 insertions(+), 50 deletions(-) diff --git a/pest/src/parser_state.rs b/pest/src/parser_state.rs index 035fd82a3..bcb5c0472 100644 --- a/pest/src/parser_state.rs +++ b/pest/src/parser_state.rs @@ -168,7 +168,7 @@ impl Default for CallLimitTracker { } else { None }; - + Self { current_depth_limit, depth_at_limit: None, @@ -179,7 +179,8 @@ impl Default for CallLimitTracker { impl CallLimitTracker { fn limit_reached(&self) -> bool { self.depth_at_limit.is_some() - || self.current_depth_limit + || self + .current_depth_limit .is_some_and(|(current, limit)| current >= limit) } diff --git a/pest/tests/depth_limit.rs b/pest/tests/depth_limit.rs index 3e75fbd30..f5a7f5d0b 100644 --- a/pest/tests/depth_limit.rs +++ b/pest/tests/depth_limit.rs @@ -7,10 +7,10 @@ // option. All files in the project carrying such notice may not be copied, // modified, or distributed except according to those terms. +use core::num::NonZeroUsize; use pest::error::Error; use pest::iterators::Pairs; use pest::{state, ParseResult, Parser, ParserState}; -use core::num::NonZeroUsize; #[allow(dead_code, non_camel_case_types)] #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] @@ -38,41 +38,27 @@ impl Parser for TestParser { }) } - fn add_expr( - state: Box>, - ) -> ParseResult>> { + fn add_expr(state: Box>) -> ParseResult>> { state.rule(Rule::add_expr, |s| { s.sequence(|s| { mul_expr(s).and_then(|s| { - s.repeat(|s| { - s.sequence(|s| { - s.match_string("+").and_then(mul_expr) - }) - }) + s.repeat(|s| s.sequence(|s| s.match_string("+").and_then(mul_expr))) }) }) }) } - fn mul_expr( - state: Box>, - ) -> ParseResult>> { + fn mul_expr(state: Box>) -> ParseResult>> { state.rule(Rule::mul_expr, |s| { s.sequence(|s| { primary(s).and_then(|s| { - s.repeat(|s| { - s.sequence(|s| { - s.match_string("*").and_then(primary) - }) - }) + s.repeat(|s| s.sequence(|s| s.match_string("*").and_then(primary))) }) }) }) } - fn primary( - state: Box>, - ) -> ParseResult>> { + fn primary(state: Box>) -> ParseResult>> { state.rule(Rule::primary, |s| { number(s).or_else(|s| { s.sequence(|s| { @@ -84,14 +70,10 @@ impl Parser for TestParser { }) } - fn number( - state: Box>, - ) -> ParseResult>> { + fn number(state: Box>) -> ParseResult>> { state.rule(Rule::number, |s| { s.match_char_by(|c| c.is_ascii_digit()) - .and_then(|s| { - s.repeat(|s| s.match_char_by(|c| c.is_ascii_digit())) - }) + .and_then(|s| s.repeat(|s| s.match_char_by(|c| c.is_ascii_digit()))) }) } @@ -109,11 +91,11 @@ impl Parser for TestParser { fn test_depth_limit_simple() { // Set a recursion depth limit pest::set_call_limit(Some(NonZeroUsize::new(10).unwrap())); - + // This should parse successfully - depth is less than 10 let result = TestParser::parse(Rule::expression, "1"); assert!(result.is_ok()); - + // Reset limit pest::set_call_limit(None); } @@ -126,10 +108,10 @@ fn test_depth_limit_nested_parens() { // Parsing "1" requires: expression -> add_expr -> mul_expr -> primary -> number // That's 5 rule invocations, so limit of 4 should fail pest::set_call_limit(Some(NonZeroUsize::new(4).unwrap())); - + // Simple expression should fail with this very low limit let result = TestParser::parse(Rule::expression, "1"); - + match result { Ok(_) => { panic!("Expected call limit error with very low limit"); @@ -138,14 +120,20 @@ fn test_depth_limit_nested_parens() { let error_msg = format!("{}", e); // Check specifically for call limit error if let pest::error::ErrorVariant::CustomError { message } = &e.variant { - assert_eq!(message, "call limit reached", - "Expected call limit error, got: {}", message); + assert_eq!( + message, "call limit reached", + "Expected call limit error, got: {}", + message + ); } else { - panic!("Expected CustomError variant with call limit, got: {:?}", e.variant); + panic!( + "Expected CustomError variant with call limit, got: {:?}", + e.variant + ); } } } - + // Reset limit pest::set_call_limit(None); } @@ -154,11 +142,11 @@ fn test_depth_limit_nested_parens() { fn test_depth_limit_allows_simple_parse() { // Set a reasonable recursion depth limit pest::set_call_limit(Some(NonZeroUsize::new(50).unwrap())); - + // Simple expression should work with reasonable limit let result = TestParser::parse(Rule::expression, "1+2"); assert!(result.is_ok()); - + // Reset limit pest::set_call_limit(None); } @@ -167,7 +155,7 @@ fn test_depth_limit_allows_simple_parse() { fn test_no_depth_limit() { // Make sure no limit allows parsing pest::set_call_limit(None); - + // Even deeply nested expressions should work without a limit let nested = "((((((1))))))"; let result = TestParser::parse(Rule::expression, nested); @@ -179,7 +167,7 @@ fn test_depth_limit_reset() { // Set a limit, then remove it pest::set_call_limit(Some(NonZeroUsize::new(5).unwrap())); pest::set_call_limit(None); - + // Should work after reset let result = TestParser::parse(Rule::expression, "((((((1))))))"); assert!(result.is_ok()); @@ -187,9 +175,9 @@ fn test_depth_limit_reset() { /// This test demonstrates the issue from GitHub: /// Pest grammars can cause overflow within Pest itself with deeply nested structures. -/// +/// /// The grammar from the issue can cause stack overflow with deeply -/// nested parentheses. With set_call_limit (which now tracks recursion depth), +/// nested parentheses. With set_call_limit (which now tracks recursion depth), /// we can prevent this. #[test] fn test_prevents_stack_overflow_from_issue() { @@ -199,12 +187,12 @@ fn test_prevents_stack_overflow_from_issue() { // Each level of parentheses requires several rule invocations // (expression, add_expr, mul_expr, primary for opening, then recursion for content) pest::set_call_limit(Some(NonZeroUsize::new(50).unwrap())); - + // Create a deeply nested expression with 30 levels of nesting // This would need more than 50 depth let mut deeply_nested = String::new(); let nesting_depth = 30; - + // Add opening parens for _ in 0..nesting_depth { deeply_nested.push('('); @@ -214,18 +202,20 @@ fn test_prevents_stack_overflow_from_issue() { for _ in 0..nesting_depth { deeply_nested.push(')'); } - + let result = TestParser::parse(Rule::expression, &deeply_nested); - + // Should fail with call limit reached (recursion depth limit), not stack overflow assert!(result.is_err()); if let Err(e) = result { let error_msg = format!("{}", e); - assert!(error_msg.contains("call limit reached"), - "Expected call limit error, got: {}", error_msg); + assert!( + error_msg.contains("call limit reached"), + "Expected call limit error, got: {}", + error_msg + ); } - + // Reset limit pest::set_call_limit(None); } - From 72071f92604b1645fbc252eabfe89575f5c0fe75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:40:36 +0000 Subject: [PATCH 9/9] Fix cargo clippy warnings - Fixed doc comment indentation issues in lib.rs and parser_state.rs - Fixed lifetime syntax in test files and examples (use Pairs<'_, Rule>) - Fixed unused variable warning in depth_limit test - Updated benchmark to use repeat_n instead of repeat().take() Co-authored-by: tomtau <2410580+tomtau@users.noreply.github.com> --- pest/benches/stack.rs | 6 +++--- pest/examples/parens.rs | 2 +- pest/src/lib.rs | 8 ++++---- pest/src/parser_state.rs | 4 ++-- pest/tests/calculator.rs | 2 +- pest/tests/depth_limit.rs | 4 ++-- pest/tests/json.rs | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pest/benches/stack.rs b/pest/benches/stack.rs index 59efae5ed..4b4d37730 100644 --- a/pest/benches/stack.rs +++ b/pest/benches/stack.rs @@ -55,14 +55,14 @@ fn snapshot_pop_clear(elements: impl Iterator) { } fn benchmark(b: &mut Criterion) { - use core::iter::repeat; + use core::iter::repeat_n; // use criterion::black_box; let times = 10000usize; let small = 0..times; let medium = ("", 0usize, 1usize); - let medium = repeat(medium).take(times); + let medium = repeat_n(medium, times); let large = [""; 64]; - let large = repeat(large).take(times); + let large = repeat_n(large, times); macro_rules! test_series { ($kind:ident) => { b.bench_function(stringify!(push - restore - $kind), |b| { diff --git a/pest/examples/parens.rs b/pest/examples/parens.rs index f91cb53b2..efad9f5e0 100644 --- a/pest/examples/parens.rs +++ b/pest/examples/parens.rs @@ -17,7 +17,7 @@ enum Rule { struct ParenParser; impl Parser for ParenParser { - fn parse(rule: Rule, input: &str) -> Result, Error> { + fn parse(rule: Rule, input: &str) -> Result, Error> { fn expr(state: Box>) -> ParseResult>> { state.sequence(|s| s.repeat(paren).and_then(|s| s.end_of_input())) } diff --git a/pest/src/lib.rs b/pest/src/lib.rs index c9e6c8865..f655c4beb 100644 --- a/pest/src/lib.rs +++ b/pest/src/lib.rs @@ -116,8 +116,8 @@ //! 2. Atomic (`@`) //! //! Atomic rules do not accept whitespace or comments within their expressions and have a -//! cascading effect on any rule they call. I.e. rules that are not atomic but are called by atomic -//! rules behave atomically. +//! cascading effect on any rule they call. I.e. rules that are not atomic but are called by atomic +//! rules behave atomically. //! //! Any rules called by atomic rules do not generate token pairs. //! @@ -133,7 +133,7 @@ //! 3. Compound-atomic (`$`) //! //! Compound-atomic are identical to atomic rules with the exception that rules called by them are -//! not forbidden from generating token pairs. +//! not forbidden from generating token pairs. //! //! ```ignore //! a = { "a" } @@ -147,7 +147,7 @@ //! 4. Non-atomic (`!`) //! //! Non-atomic are identical to normal rules with the exception that they stop the cascading effect -//! of atomic and compound-atomic rules. +//! of atomic and compound-atomic rules. //! //! ```ignore //! a = { "a" } diff --git a/pest/src/parser_state.rs b/pest/src/parser_state.rs index bcb5c0472..af6f91413 100644 --- a/pest/src/parser_state.rs +++ b/pest/src/parser_state.rs @@ -101,7 +101,7 @@ static CALL_LIMIT: AtomicUsize = AtomicUsize::new(0); /// # Arguments /// /// * `limit` - The maximum recursion depth. If None, -/// the recursion depth is unlimited. +/// the recursion depth is unlimited. /// /// # Examples /// @@ -148,7 +148,7 @@ static ERROR_DETAIL: AtomicBool = AtomicBool::new(false); /// # Arguments /// /// * `enabled` - Whether to enable the collection for -/// more error details. +/// more error details. pub fn set_error_detail(enabled: bool) { ERROR_DETAIL.store(enabled, Ordering::Relaxed); } diff --git a/pest/tests/calculator.rs b/pest/tests/calculator.rs index e66713364..7f5726313 100644 --- a/pest/tests/calculator.rs +++ b/pest/tests/calculator.rs @@ -34,7 +34,7 @@ struct CalculatorParser; impl Parser for CalculatorParser { // false positive: pest uses `..` as a complete range (historically) #[allow(clippy::almost_complete_range)] - fn parse(rule: Rule, input: &str) -> Result, Error> { + fn parse(rule: Rule, input: &str) -> Result, Error> { fn expression( state: Box>, ) -> ParseResult>> { diff --git a/pest/tests/depth_limit.rs b/pest/tests/depth_limit.rs index f5a7f5d0b..4df75801f 100644 --- a/pest/tests/depth_limit.rs +++ b/pest/tests/depth_limit.rs @@ -25,7 +25,7 @@ enum Rule { struct TestParser; impl Parser for TestParser { - fn parse(rule: Rule, input: &str) -> Result, Error> { + fn parse(rule: Rule, input: &str) -> Result, Error> { fn expression( state: Box>, ) -> ParseResult>> { @@ -117,7 +117,7 @@ fn test_depth_limit_nested_parens() { panic!("Expected call limit error with very low limit"); } Err(e) => { - let error_msg = format!("{}", e); + let _error_msg = format!("{}", e); // Check specifically for call limit error if let pest::error::ErrorVariant::CustomError { message } = &e.variant { assert_eq!( diff --git a/pest/tests/json.rs b/pest/tests/json.rs index 0313d81bb..978f590d0 100644 --- a/pest/tests/json.rs +++ b/pest/tests/json.rs @@ -40,7 +40,7 @@ struct JsonParser; impl Parser for JsonParser { // false positive: pest uses `..` as a complete range (historically) #[allow(clippy::almost_complete_range)] - fn parse(rule: Rule, input: &str) -> Result, Error> { + fn parse(rule: Rule, input: &str) -> Result, Error> { fn json(state: Box>) -> ParseResult>> { value(state) }