Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
776ab5d
Match CPython's SyntaxError range for unparenthesized `except` types
zzarbttoo Sep 6, 2026
e491d0d
Clarify that the `except` error range stops before the closing colon
zzarbttoo Sep 20, 2026
abacad2
Fix the `except ... as` error column for non-ASCII names and line joins
zzarbttoo Sep 20, 2026
5931324
Update the Ruff fork to 0.16.10
youknowone Oct 4, 2026
264650b
Drop the source scan for the `except ... as` error range
youknowone Oct 4, 2026
c1ed646
Take parse error messages from the parser as they are
youknowone Oct 4, 2026
fe81dff
Take starred and bytes literal error ranges from the parser
youknowone Oct 4, 2026
718e61a
Take indentation error kinds and positions from the parser
youknowone Oct 4, 2026
645631b
Take unterminated string messages from the parser
youknowone Oct 4, 2026
8f74913
Take bracket errors and tokenizer error priority from the parser
youknowone Oct 4, 2026
61b4032
Take number literal, string prefix and non-printable errors from the …
youknowone Oct 4, 2026
efe3856
Take the missing indented block error from the parser
youknowone Oct 4, 2026
b4660b4
Take binding target errors from the parser
youknowone Oct 4, 2026
2523208
Take parameter, argument, star, comma and dictionary errors from the …
youknowone Oct 4, 2026
13f1a00
Take expression errors from the parser
youknowone Oct 4, 2026
a47ad11
Take statement errors from the parser
youknowone Oct 5, 2026
450242a
Take string and f-string errors from the parser
youknowone Oct 5, 2026
f5af0dd
Restore the bracket depth check and fix incomplete unclosed strings
youknowone Oct 6, 2026
8cfafa1
Report multiple statements before later errors in single mode
youknowone Oct 6, 2026
a099d0b
Update the Ruff fork for parser speed and starred annotations
youknowone Oct 6, 2026
820917a
Use the published Ruff fork 0.16.10 crates
youknowone Oct 7, 2026
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
Prev Previous commit
Next Next commit
Drop the source scan for the except ... as error range
The parser now reports the unparenthesized exception types error with a
range ending at the `:` that closes the clause, so
`invalid_except_stmt_end` and the message match that widened the range
are removed.

Assisted-by: Claude Code:claude-opus-5-5
  • Loading branch information
youknowone committed Oct 4, 2026
commit 264650b981108b79fcbff86245c68913c85c274f
92 changes: 0 additions & 92 deletions crates/vm/src/vm/vm_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -982,26 +982,6 @@ impl VirtualMachine {
}
_ => false,
};
// CPython reports the unparenthesized exception types error with a range stopping
// just before the `:` that closes the `except` clause, which covers the `as NAME`
// part, while the parser reports the exception types alone. The exclusive end that
// yields is the column of the `:`. See `invalid_except_stmt_end`.
let except_as_end = cfg_select! {
feature = "parser" => {
if msg == "multiple exception types must be parenthesized when using 'as'"
&& let crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
raw_location,
..
}) = error
&& let Some(source) = source
{
invalid_except_stmt_end(source, raw_location.end().to_usize())
} else {
None
}
}
_ => None,
};

let syntax_error: PyBaseExceptionRef = self
.new_payload_exception::<PySyntaxError>(
Expand Down Expand Up @@ -1040,8 +1020,6 @@ impl VirtualMachine {
} else if narrow_caret {
let (l, o) = error.python_location();
(l, (o + 1) as isize)
} else if let Some((l, o)) = except_as_end {
(l, o as isize)
} else {
(end_lineno, end_offset as isize)
};
Expand Down Expand Up @@ -1345,73 +1323,3 @@ fn scan_quoted_string_for_incomplete(bytes: &[u8], quote_index: usize) -> Quoted
unescaped_newline: false,
}
}

/// Returns the exclusive end of the range CPython reports for its `invalid_except_stmt`
/// rule, as a 1-based `(line, column)` pair whose column counts characters, not bytes.
/// Being exclusive, that column is the one the `:` closing the `except` clause sits on:
/// the `:` itself is not part of the range.
///
/// CPython raises that error only once the whole clause has matched, and reports a range
/// starting at the first exception type and stopping just before the `:`, so the range
/// covers the `as NAME` part as well:
///
/// ```text
/// except A, B as e:
/// ^^^^^^^^^ offset = 8, end_offset = 17
/// ```
///
/// ```text
/// invalid_except_stmt:
/// | 'except' a=expression ',' expressions 'as' NAME ':' {
/// RAISE_SYNTAX_ERROR_STARTING_FROM(a, "multiple exception types must be parenthesized when using 'as'") }
/// ```
///
/// The parser reports the exception types alone, so the `:` is looked up here. Only
/// `as NAME` can follow the exception types, which is why the first `:` after them is the
/// one closing the clause. `types_end` is a byte offset into `source`.
///
/// Returns `None` when the clause has no `:`, in which case CPython reports a different
/// error and the range is left alone.
#[cfg(feature = "parser")]
fn invalid_except_stmt_end(source: &str, types_end: usize) -> Option<(usize, usize)> {
let bytes = source.as_bytes();
let mut index = types_end;

let colon = loop {
match *bytes.get(index)? {
b':' => break index,
// An explicit line join continues the clause on the next line.
b'\\' => {
index += 1;
if bytes.get(index) == Some(&b'\r') {
index += 1;
}
if bytes.get(index) != Some(&b'\n') {
return None;
}
index += 1;
}
b'\n' | b'\r' | b'#' => return None,
_ => index += 1,
}
};

// Only `as NAME` may appear between the exception types and the `:`. CPython reports a
// different error when the name is missing, so leave the range alone in that case.
// An explicit line join may sit anywhere in between, so drop the backslashes and let
// the newline they escape count as the ordinary whitespace around `as`.
let between = source.get(types_end..colon)?.replace('\\', " ");
let name = between.trim().strip_prefix("as")?;
if !name.starts_with(char::is_whitespace) || name.trim().is_empty() {
return None;
}

let before = source.get(..colon)?;
let line = before.bytes().filter(|&byte| byte == b'\n').count() + 1;
let line_start = before.rfind('\n').map_or(0, |index| index + 1);
// `line_start` and `colon` are byte offsets, but the column counts characters, so a
// non-ASCII exception type or `as NAME` would otherwise push the column too far right.
let column = source.get(line_start..colon)?.chars().count() + 1;

Some((line, column))
}