diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index 49bf5bc6aea..8ede6e22fab 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -2525,7 +2525,7 @@ def printsolution(self, x): ... SyntaxError: 'yield from' outside function ->>> def f(): x = yield = y # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE +>>> def f(): x = yield = y Traceback (most recent call last): ... SyntaxError: assignment to yield expression not possible diff --git a/Lib/test/test_genexps.py b/Lib/test/test_genexps.py index ae16318ff45..fde12f13cdc 100644 --- a/Lib/test/test_genexps.py +++ b/Lib/test/test_genexps.py @@ -154,7 +154,7 @@ Verify that syntax error's are raised for genexps used as lvalues - >>> (y for y in (1,2)) = 10 # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + >>> (y for y in (1,2)) = 10 Traceback (most recent call last): ... SyntaxError: cannot assign to generator expression diff --git a/Lib/test/test_named_expressions.py b/Lib/test/test_named_expressions.py index 617107e108e..4f92176b301 100644 --- a/Lib/test/test_named_expressions.py +++ b/Lib/test/test_named_expressions.py @@ -90,7 +90,6 @@ def test_named_expression_invalid_14(self): with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_15(self): code = """(lambda: x := 1)""" diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 1c6e536c783..0934f22d470 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -31,7 +31,7 @@ Traceback (most recent call last): SyntaxError: invalid syntax ->>> None = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> None = 1 Traceback (most recent call last): SyntaxError: cannot assign to None @@ -39,11 +39,11 @@ Traceback (most recent call last): SyntaxError: invalid syntax ->>> True = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> True = 1 Traceback (most recent call last): SyntaxError: cannot assign to True ->>> (True := 1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (True := 1) Traceback (most recent call last): SyntaxError: cannot use assignment expressions with True @@ -79,7 +79,7 @@ Traceback (most recent call last): SyntaxError: cannot assign to function call here. Maybe you meant '==' instead of '='? ->>> yield = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> yield = 1 Traceback (most recent call last): SyntaxError: assignment to yield expression not possible @@ -91,23 +91,23 @@ Traceback (most recent call last): SyntaxError: cannot assign to expression here. Maybe you meant '==' instead of '='? ->>> (x for x in x) = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (x for x in x) = 1 Traceback (most recent call last): SyntaxError: cannot assign to generator expression ->>> 1 = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> 1 = 1 Traceback (most recent call last): SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='? ->>> "abc" = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> "abc" = 1 Traceback (most recent call last): SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='? ->>> b"" = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> b"" = 1 Traceback (most recent call last): SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='? ->>> ... = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> ... = 1 Traceback (most recent call last): SyntaxError: cannot assign to ellipsis here. Maybe you meant '==' instead of '='? @@ -124,7 +124,7 @@ Traceback (most recent call last): SyntaxError: cannot assign to literal ->>> (a, True, c) = (1, 2, 3) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (a, True, c) = (1, 2, 3) Traceback (most recent call last): SyntaxError: cannot assign to True @@ -132,7 +132,7 @@ Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> (a, *True, c) = (1, 2, 3) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (a, *True, c) = (1, 2, 3) Traceback (most recent call last): SyntaxError: cannot assign to True @@ -140,19 +140,19 @@ Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> [a, b, c + 1] = [1, 2, 3] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [a, b, c + 1] = [1, 2, 3] Traceback (most recent call last): SyntaxError: cannot assign to expression ->>> [a, b[1], c + 1] = [1, 2, 3] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [a, b[1], c + 1] = [1, 2, 3] Traceback (most recent call last): SyntaxError: cannot assign to expression ->>> [a, b.c.d, c + 1] = [1, 2, 3] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [a, b.c.d, c + 1] = [1, 2, 3] Traceback (most recent call last): SyntaxError: cannot assign to expression ->>> a if 1 else b = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> a if 1 else b = 1 Traceback (most recent call last): SyntaxError: cannot assign to conditional expression @@ -188,15 +188,15 @@ Traceback (most recent call last): SyntaxError: invalid syntax ->>> True = True = 3 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> True = True = 3 Traceback (most recent call last): SyntaxError: cannot assign to True ->>> x = y = True = z = 3 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> x = y = True = z = 3 Traceback (most recent call last): SyntaxError: cannot assign to True ->>> x = y = yield = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> x = y = yield = 1 Traceback (most recent call last): SyntaxError: assignment to yield expression not possible @@ -215,31 +215,31 @@ Invalid targets in `for` loops and `with` statements should also produce a specialized error message ->>> for a() in b: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> for a() in b: pass Traceback (most recent call last): SyntaxError: cannot assign to function call ->>> for (a, b()) in b: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> for (a, b()) in b: pass Traceback (most recent call last): SyntaxError: cannot assign to function call ->>> for [a, b()] in b: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> for [a, b()] in b: pass Traceback (most recent call last): SyntaxError: cannot assign to function call ->>> for (*a, b, c+1) in b: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> for (*a, b, c+1) in b: pass Traceback (most recent call last): SyntaxError: cannot assign to expression ->>> for (x, *(y, z.d())) in b: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> for (x, *(y, z.d())) in b: pass Traceback (most recent call last): SyntaxError: cannot assign to function call ->>> for a, b() in c: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> for a, b() in c: pass Traceback (most recent call last): SyntaxError: cannot assign to function call ->>> for a, b, (c + 1, d()): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> for a, b, (c + 1, d()): pass Traceback (most recent call last): SyntaxError: cannot assign to expression @@ -251,27 +251,27 @@ Traceback (most recent call last): SyntaxError: invalid syntax ->>> with a as b(): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> with a as b(): pass Traceback (most recent call last): SyntaxError: cannot assign to function call ->>> with a as (b, c()): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> with a as (b, c()): pass Traceback (most recent call last): SyntaxError: cannot assign to function call ->>> with a as [b, c()]: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> with a as [b, c()]: pass Traceback (most recent call last): SyntaxError: cannot assign to function call ->>> with a as (*b, c, d+1): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> with a as (*b, c, d+1): pass Traceback (most recent call last): SyntaxError: cannot assign to expression ->>> with a as (x, *(y, z.d())): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> with a as (x, *(y, z.d())): pass Traceback (most recent call last): SyntaxError: cannot assign to function call ->>> with a as b, c as d(): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> with a as b, c as d(): pass Traceback (most recent call last): SyntaxError: cannot assign to function call @@ -293,11 +293,11 @@ Traceback (most recent call last): SyntaxError: 'in' expected after for-loop variables ->>> [x for x() in a] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x for x() in a] Traceback (most recent call last): SyntaxError: cannot assign to function call ->>> [x for a, b, (c + 1, d()) in y] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x for a, b, (c + 1, d()) in y] Traceback (most recent call last): SyntaxError: cannot assign to expression @@ -305,11 +305,11 @@ Traceback (most recent call last): SyntaxError: 'in' expected after for-loop variables ->>> [x for x+1 in y] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x for x+1 in y] Traceback (most recent call last): SyntaxError: cannot assign to expression ->>> [x for x+1, x() in y] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x for x+1, x() in y] Traceback (most recent call last): SyntaxError: cannot assign to expression @@ -334,19 +334,19 @@ # produce special error messages regarding missing # parentheses, but about missing commas instead ->>> [1, 2 3] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [1, 2 3] Traceback (most recent call last): SyntaxError: invalid syntax. Perhaps you forgot a comma? ->>> {1, 2 3} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {1, 2 3} Traceback (most recent call last): SyntaxError: invalid syntax. Perhaps you forgot a comma? ->>> {1:2, 2:5 3:12} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {1:2, 2:5 3:12} Traceback (most recent call last): SyntaxError: invalid syntax. Perhaps you forgot a comma? ->>> (1, 2 3) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (1, 2 3) Traceback (most recent call last): SyntaxError: invalid syntax. Perhaps you forgot a comma? @@ -2114,19 +2114,19 @@ # Check that we don't raise a "cannot use name as import target" error # if there is an error in an unrelated statement after ';' ->>> import a as b; None = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a as b; None = 1 Traceback (most recent call last): SyntaxError: cannot assign to None ->>> import a, b as c; d = 1; None = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a, b as c; d = 1; None = 1 Traceback (most recent call last): SyntaxError: cannot assign to None ->>> from a import b as c; None = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import b as c; None = 1 Traceback (most recent call last): SyntaxError: cannot assign to None ->>> from a import b, c as d; e = 1; None = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import b, c as d; e = 1; None = 1 Traceback (most recent call last): SyntaxError: cannot assign to None diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index bc0a2363e33..9c0884c7520 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -1,4 +1,9 @@ +pub use ruff_python_ast::token::TokenKind; +use ruff_python_parser::{LexicalErrorType, ParseErrorType}; use ruff_source_file::{PositionEncoding, SourceFile, SourceFileBuilder, SourceLocation}; +use ruff_text_size::TextSlice; +use thiserror::Error; + use rustpython_codegen::{compile, symboltable}; pub use rustpython_codegen::compile::CompileOpts; @@ -9,20 +14,19 @@ pub use ruff_python_ast as ast; pub use ruff_python_parser as parser; pub use rustpython_codegen as codegen; pub use rustpython_compiler_core as core; -use thiserror::Error; #[derive(Error, Debug)] pub enum CompileErrorType { #[error(transparent)] Codegen(#[from] codegen::error::CodegenErrorType), #[error(transparent)] - Parse(#[from] parser::ParseErrorType), + Parse(#[from] ParseErrorType), } #[derive(Error, Debug)] pub struct ParseError { #[source] - pub error: parser::ParseErrorType, + pub error: ParseErrorType, pub raw_location: ruff_text_size::TextRange, pub location: SourceLocation, pub end_location: SourceLocation, @@ -54,57 +58,140 @@ impl CompileError { // For EOF errors (unclosed brackets), find the unclosed bracket position // and adjust both the error location and message let mut is_unclosed_bracket = false; - let (error_type, location, end_location) = if matches!( - &error.error, - parser::ParseErrorType::Lexical(parser::LexicalErrorType::Eof) - ) { - if let Some((bracket_char, bracket_offset)) = find_unclosed_bracket(source_text) { - let bracket_text_size = ruff_text_size::TextSize::new(bracket_offset as u32); - let loc = source_code.source_location(bracket_text_size, PositionEncoding::Utf8); + let (error_type, location, end_location) = match &error.error { + ParseErrorType::Lexical(LexicalErrorType::Eof) => { + if let Some((bracket_char, bracket_offset)) = find_unclosed_bracket(source_text) { + let bracket_text_size = ruff_text_size::TextSize::new(bracket_offset as u32); + let loc = + source_code.source_location(bracket_text_size, PositionEncoding::Utf8); + let end_loc = SourceLocation { + line: loc.line, + character_offset: loc.character_offset.saturating_add(1), + }; + let msg = format!("'{bracket_char}' was never closed"); + is_unclosed_bracket = true; + (ParseErrorType::OtherError(msg), loc, end_loc) + } else { + let loc = + source_code.source_location(error.location.start(), PositionEncoding::Utf8); + let end_loc = + source_code.source_location(error.location.end(), PositionEncoding::Utf8); + (error.error, loc, end_loc) + } + } + + ParseErrorType::Lexical(LexicalErrorType::IndentationError) => { + // For IndentationError, point the offset to the end of the line content + // instead of the beginning + let loc = + source_code.source_location(error.location.start(), PositionEncoding::Utf8); + let line_idx = loc.line.to_zero_indexed(); + let line = source_text.split('\n').nth(line_idx).unwrap_or(""); + let line_end_col = line.chars().count() + 1; // 1-indexed, past last char let end_loc = SourceLocation { line: loc.line, - character_offset: loc.character_offset.saturating_add(1), + character_offset: ruff_source_file::OneIndexed::new(line_end_col) + .unwrap_or(loc.character_offset), }; - let msg = format!("'{bracket_char}' was never closed"); - is_unclosed_bracket = true; - (parser::ParseErrorType::OtherError(msg), loc, end_loc) - } else { + (error.error, end_loc, end_loc) + } + ParseErrorType::ExpectedToken { expected, found } + if matches!((expected, found), (TokenKind::Comma, TokenKind::Int)) => + { let loc = source_code.source_location(error.location.start(), PositionEncoding::Utf8); - let end_loc = + let mut end_loc = source_code.source_location(error.location.end(), PositionEncoding::Utf8); - (error.error, loc, end_loc) + + // 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_loc.character_offset.get() == 1 && end_loc.line > loc.line { + let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1); + end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8); + end_loc.character_offset = end_loc.character_offset.saturating_add(1); + } + let msg = "invalid syntax. Perhaps you forgot a comma?".into(); + (ParseErrorType::OtherError(msg), loc, end_loc) } - } else if matches!( - &error.error, - parser::ParseErrorType::Lexical(parser::LexicalErrorType::IndentationError) - ) { - // For IndentationError, point the offset to the end of the line content - // instead of the beginning - let loc = source_code.source_location(error.location.start(), PositionEncoding::Utf8); - let line_idx = loc.line.to_zero_indexed(); - let line = source_text.split('\n').nth(line_idx).unwrap_or(""); - let line_end_col = line.chars().count() + 1; // 1-indexed, past last char - let end_loc = SourceLocation { - line: loc.line, - character_offset: ruff_source_file::OneIndexed::new(line_end_col) - .unwrap_or(loc.character_offset), - }; - (error.error, end_loc, end_loc) - } else { - let loc = source_code.source_location(error.location.start(), PositionEncoding::Utf8); - let mut end_loc = - 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_loc.character_offset.get() == 1 && end_loc.line > loc.line { - let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1); - end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8); - end_loc.character_offset = end_loc.character_offset.saturating_add(1); + + ParseErrorType::InvalidAssignmentTarget => { + let loc = + source_code.source_location(error.location.start(), PositionEncoding::Utf8); + let mut end_loc = + 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_loc.character_offset.get() == 1 && end_loc.line > loc.line { + let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1); + end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8); + end_loc.character_offset = end_loc.character_offset.saturating_add(1); + } + + let expr_str = source_file.source_text().slice(error.location); + + let msg = parser::parse_expression(expr_str).map_or_else( + |_| match expr_str { + "yield" => "assignment to yield expression not possible".into(), + _ => format!("cannot assign to {expr_str}"), + }, + |parsed| match *parsed.syntax().body { + ast::Expr::Call(_) => "cannot assign to function call".into(), + ast::Expr::BinOp(_) => "cannot assign to expression".into(), + ast::Expr::If(_) => "cannot assign to conditional expression".into(), + ast::Expr::Generator(_) => "cannot assign to generator expression".into(), + ast::Expr::StringLiteral(_) + | ast::Expr::BytesLiteral(_) + | ast::Expr::NumberLiteral(_) => { + "cannot assign to literal here. Maybe you meant '==' instead of '='?" + .into() + } + ast::Expr::EllipsisLiteral(_) => { + "cannot assign to ellipsis here. Maybe you meant '==' instead of '='?" + .into() + } + _ => format!("cannot assign to {expr_str}"), + }, + ); + + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + ParseErrorType::InvalidNamedAssignmentTarget => { + let loc = + source_code.source_location(error.location.start(), PositionEncoding::Utf8); + let mut end_loc = + 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_loc.character_offset.get() == 1 && end_loc.line > loc.line { + let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1); + end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8); + end_loc.character_offset = end_loc.character_offset.saturating_add(1); + } + + let target = source_file.source_text().slice(error.location); + let msg = format!("cannot use assignment expressions with {target}"); + (ParseErrorType::OtherError(msg), loc, end_loc) } - (error.error, loc, end_loc) + _ => { + let loc = + source_code.source_location(error.location.start(), PositionEncoding::Utf8); + let mut end_loc = + 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_loc.character_offset.get() == 1 && end_loc.line > loc.line { + let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1); + end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8); + end_loc.character_offset = end_loc.character_offset.saturating_add(1); + } + + (error.error, loc, end_loc) + } }; Self::Parse(ParseError {