Skip to content
Merged
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
5 changes: 1 addition & 4 deletions Lib/test/test_cmd_line_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,8 +611,6 @@ def test_issue20500_exit_with_exception_value(self):
text = stderr.decode('ascii')
self.assertEqual(text.rstrip(), "some text")

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_syntaxerror_unindented_caret_position(self):
script = "1 + 1 = 2\n"
with os_helper.temp_dir() as script_dir:
Expand All @@ -622,8 +620,7 @@ def test_syntaxerror_unindented_caret_position(self):
# Confirm that the caret is located under the '=' sign
self.assertIn("\n ^^^^^\n", text)

# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
def test_syntaxerror_indented_caret_position(self):
script = textwrap.dedent("""\
if True:
Expand Down
30 changes: 27 additions & 3 deletions crates/compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct ParseError {
pub error: parser::ParseErrorType,
pub raw_location: ruff_text_size::TextRange,
pub location: SourceLocation,
pub end_location: SourceLocation,
pub source_path: String,
}

Expand All @@ -44,13 +45,26 @@ pub enum CompileError {

impl CompileError {
pub fn from_ruff_parse_error(error: parser::ParseError, source_file: &SourceFile) -> Self {
let location = source_file
.to_source_code()
.source_location(error.location.start(), PositionEncoding::Utf8);
let source_code = source_file.to_source_code();
let location = source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let mut end_location =
source_code.source_location(error.location.end(), PositionEncoding::Utf8);

// If the error range ends at the start of a new line (column 1),
// adjust it to the end of the previous line
if end_location.character_offset.get() == 1 && end_location.line > location.line {
// Get the end of the previous line
let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1);
end_location = source_code.source_location(prev_line_end, PositionEncoding::Utf8);
// Adjust column to be after the last character
end_location.character_offset = end_location.character_offset.saturating_add(1);
}

Self::Parse(ParseError {
error: error.error,
raw_location: error.location,
location,
end_location,
source_path: source_file.name().to_owned(),
})
}
Expand All @@ -70,6 +84,16 @@ impl CompileError {
}
}

pub fn python_end_location(&self) -> Option<(usize, usize)> {
match self {
CompileError::Codegen(_) => None,
CompileError::Parse(parse_error) => Some((
parse_error.end_location.line.get(),
parse_error.end_location.character_offset.get(),
)),
}
}

pub fn source_path(&self) -> &str {
match self {
Self::Codegen(codegen_error) => &codegen_error.source_path,
Expand Down
15 changes: 15 additions & 0 deletions crates/vm/src/builtins/type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1997,13 +1997,28 @@ fn calculate_meta_class(
let mut winner = metatype;
for base in bases {
let base_type = base.class();

// First try fast_issubclass for PyType instances
if winner.fast_issubclass(base_type) {
continue;
} else if base_type.fast_issubclass(&winner) {
winner = base_type.to_owned();
continue;
}

// If fast_issubclass didn't work, fall back to general is_subclass
// This handles cases where metaclasses are not PyType subclasses
let winner_is_subclass = winner.as_object().is_subclass(base_type.as_object(), vm)?;
if winner_is_subclass {
continue;
}

let base_type_is_subclass = base_type.as_object().is_subclass(winner.as_object(), vm)?;
if base_type_is_subclass {
winner = base_type.to_owned();
continue;
}

return Err(vm.new_type_error(
"metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass \
of the metaclasses of all its bases",
Expand Down
64 changes: 38 additions & 26 deletions crates/vm/src/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,37 +223,49 @@ impl VirtualMachine {
if let Some(offset) = maybe_offset {
let maybe_end_offset: Option<isize> =
getattr("end_offset").and_then(|obj| obj.try_to_value::<isize>(vm).ok());

let mut end_offset = match maybe_end_offset {
Some(0) | None => offset,
Some(end_offset) => end_offset,
let maybe_end_lineno: Option<isize> =
getattr("end_lineno").and_then(|obj| obj.try_to_value::<isize>(vm).ok());
let maybe_lineno_int: Option<isize> =
getattr("lineno").and_then(|obj| obj.try_to_value::<isize>(vm).ok());

// Only show caret if end_lineno is same as lineno (or not set)
let same_line = match (maybe_lineno_int, maybe_end_lineno) {
(Some(lineno), Some(end_lineno)) => lineno == end_lineno,
_ => true,
};

if offset == end_offset || end_offset == -1 {
end_offset = offset + 1;
}
if same_line {
let mut end_offset = match maybe_end_offset {
Some(0) | None => offset,
Some(end_offset) => end_offset,
};

// Convert 1-based column offset to 0-based index into stripped text
let colno = offset - 1 - spaces;
let end_colno = end_offset - 1 - spaces;
if colno >= 0 {
let caret_space = l_text
.chars()
.take(colno as usize)
.map(|c| if c.is_whitespace() { c } else { ' ' })
.collect::<String>();

let mut error_width = end_colno - colno;
if error_width < 1 {
error_width = 1;
if offset == end_offset || end_offset == -1 {
end_offset = offset + 1;
}

writeln!(
output,
" {}{}",
caret_space,
"^".repeat(error_width as usize)
)?;
// Convert 1-based column offset to 0-based index into stripped text
let colno = offset - 1 - spaces;
let end_colno = end_offset - 1 - spaces;
if colno >= 0 {
let caret_space = l_text
.chars()
.take(colno as usize)
.map(|c| if c.is_whitespace() { c } else { ' ' })
.collect::<String>();

let mut error_width = end_colno - colno;
if error_width < 1 {
error_width = 1;
}

writeln!(
output,
" {}{}",
caret_space,
"^".repeat(error_width as usize)
)?;
}
}
}
}
Expand Down
16 changes: 9 additions & 7 deletions crates/vm/src/stdlib/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,13 +271,15 @@ pub(crate) fn parse(
) -> Result<PyObjectRef, CompileError> {
let source_file = SourceFileBuilder::new("".to_owned(), source.to_owned()).finish();
let top = parser::parse(source, mode.into())
.map_err(|parse_error| ParseError {
error: parse_error.error,
raw_location: parse_error.location,
location: text_range_to_source_range(&source_file, parse_error.location)
.start
.to_source_location(),
source_path: "<unknown>".to_string(),
.map_err(|parse_error| {
let range = text_range_to_source_range(&source_file, parse_error.location);
ParseError {
error: parse_error.error,
raw_location: parse_error.location,
location: range.start.to_source_location(),
end_location: range.end.to_source_location(),
source_path: "<unknown>".to_string(),
}
})?
.into_syntax();
let top = match top {
Expand Down
14 changes: 14 additions & 0 deletions crates/vm/src/vm/vm_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,20 @@ impl VirtualMachine {
.set_attr("offset", offset, self)
.unwrap();

// Set end_lineno and end_offset if available
if let Some((end_lineno, end_offset)) = error.python_end_location() {
let end_lineno = self.ctx.new_int(end_lineno);
let end_offset = self.ctx.new_int(end_offset);
syntax_error
.as_object()
.set_attr("end_lineno", end_lineno, self)
.unwrap();
syntax_error
.as_object()
.set_attr("end_offset", end_offset, self)
.unwrap();
}

syntax_error
.as_object()
.set_attr("text", statement.to_pyobject(self), self)
Expand Down
Loading