diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 2ea046d29c262..9d1849a53e969 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -419,6 +419,8 @@ pub enum LexicalErrorType { LineContinuationError, /// An unexpected end of file was encountered. Eof, + /// Parentheses, brackets, or braces nested past the lexer's nesting limit. + TooManyNestedParentheses, /// An unexpected error occurred. OtherError(Box), } @@ -457,6 +459,7 @@ impl std::fmt::Display for LexicalErrorType { write!(f, "Expected a newline after line continuation character") } Self::Eof => write!(f, "unexpected EOF while parsing"), + Self::TooManyNestedParentheses => write!(f, "too many nested parentheses"), Self::OtherError(msg) => write!(f, "{msg}"), Self::UnclosedStringError => { write!(f, "missing closing quote in string literal") diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index b78f8bf55baf1..8c25252bee19a 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -35,6 +35,10 @@ mod interpolated_string; const BOM: char = '\u{feff}'; +/// Maximum depth of nested parentheses, brackets, and braces. +/// Mirrors CPython's `MAXLEVEL` (`Parser/lexer/state.h`). +const MAX_LEVEL: u32 = 200; + /// A lexer for Python source code. #[derive(Debug)] pub struct Lexer<'src> { @@ -145,6 +149,20 @@ impl<'src> Lexer<'src> { std::mem::take(&mut self.current_value) } + /// Returns an error if opening one more bracket would exceed [`MAX_LEVEL`]. + /// + /// CPython rejects over-nested source in its tokenizer (`Parser/lexer/lexer.c`) + /// rather than in the parser, so the recursive descent never gets deep enough + /// to exhaust the native stack. + fn nesting_limit_error(&self) -> Option { + (self.nesting >= MAX_LEVEL).then(|| { + LexicalError::new( + LexicalErrorType::TooManyNestedParentheses, + self.token_range(), + ) + }) + } + /// Helper function to push the given error, updating the current range with the error location /// and return the [`TokenKind::Unknown`] token. fn push_error(&mut self, error: LexicalError) -> TokenKind { @@ -522,6 +540,9 @@ impl<'src> Lexer<'src> { } '~' => TokenKind::Tilde, '(' => { + if let Some(error) = self.nesting_limit_error() { + return self.push_error(error); + } self.nesting += 1; TokenKind::Lpar } @@ -530,6 +551,9 @@ impl<'src> Lexer<'src> { TokenKind::Rpar } '[' => { + if let Some(error) = self.nesting_limit_error() { + return self.push_error(error); + } self.nesting += 1; TokenKind::Lsqb } @@ -538,6 +562,9 @@ impl<'src> Lexer<'src> { TokenKind::Rsqb } '{' => { + if let Some(error) = self.nesting_limit_error() { + return self.push_error(error); + } self.nesting += 1; TokenKind::Lbrace }