diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index 9fc240de818..d2b3a7d3e40 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -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: @@ -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: diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 7fa695c0c71..11e3bf0d07a 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -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, } @@ -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(), }) } @@ -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, diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index b6995d6f595..8660d1f2e27 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -1997,6 +1997,8 @@ 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) { @@ -2004,6 +2006,19 @@ fn calculate_meta_class( 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", diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index 5d0c1c6f3ed..a2b08655822 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -223,37 +223,49 @@ impl VirtualMachine { if let Some(offset) = maybe_offset { let maybe_end_offset: Option = getattr("end_offset").and_then(|obj| obj.try_to_value::(vm).ok()); - - let mut end_offset = match maybe_end_offset { - Some(0) | None => offset, - Some(end_offset) => end_offset, + let maybe_end_lineno: Option = + getattr("end_lineno").and_then(|obj| obj.try_to_value::(vm).ok()); + let maybe_lineno_int: Option = + getattr("lineno").and_then(|obj| obj.try_to_value::(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::(); - - 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::(); + + let mut error_width = end_colno - colno; + if error_width < 1 { + error_width = 1; + } + + writeln!( + output, + " {}{}", + caret_space, + "^".repeat(error_width as usize) + )?; + } } } } diff --git a/crates/vm/src/stdlib/ast.rs b/crates/vm/src/stdlib/ast.rs index b8df86fb091..31aad306f96 100644 --- a/crates/vm/src/stdlib/ast.rs +++ b/crates/vm/src/stdlib/ast.rs @@ -271,13 +271,15 @@ pub(crate) fn parse( ) -> Result { 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: "".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: "".to_string(), + } })? .into_syntax(); let top = match top { diff --git a/crates/vm/src/vm/vm_new.rs b/crates/vm/src/vm/vm_new.rs index c74fb922a82..119444be75c 100644 --- a/crates/vm/src/vm/vm_new.rs +++ b/crates/vm/src/vm/vm_new.rs @@ -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)