Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/ruff_python_parser/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<str>),
}
Expand Down Expand Up @@ -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")
Expand Down
27 changes: 27 additions & 0 deletions crates/ruff_python_parser/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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<LexicalError> {
(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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down