diff --git a/Lib/test/test_dictcomps.py b/Lib/test/test_dictcomps.py index fc7ebb0f5a6..26b56dac503 100644 --- a/Lib/test/test_dictcomps.py +++ b/Lib/test/test_dictcomps.py @@ -75,7 +75,6 @@ def test_local_visibility(self): self.assertEqual(actual, expected) self.assertEqual(v, "Local variable") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_illegal_assignment(self): with self.assertRaisesRegex(SyntaxError, "cannot assign"): compile("{x: y for y, x in ((1, 2), (3, 4))} = 5", "", diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index f4fca1caec7..1c6cbc7d1c4 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1292,7 +1292,6 @@ def test_nested_fstrings(self): self.assertEqual(f'{f"{0}"*3}', '000') self.assertEqual(f'{f"{y}"*3}', '555') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_invalid_string_prefixes(self): single_quote_cases = ["fu''", "uf''", diff --git a/Lib/test/test_genexps.py b/Lib/test/test_genexps.py index fde12f13cdc..17d2d137074 100644 --- a/Lib/test/test_genexps.py +++ b/Lib/test/test_genexps.py @@ -159,7 +159,7 @@ ... SyntaxError: cannot assign to generator expression - >>> (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: 'generator expression' is an illegal expression for augmented assignment diff --git a/Lib/test/test_named_expressions.py b/Lib/test/test_named_expressions.py index 4f92176b301..910f896b240 100644 --- a/Lib/test/test_named_expressions.py +++ b/Lib/test/test_named_expressions.py @@ -4,35 +4,30 @@ class NamedExpressionInvalidTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_01(self): code = """x := 0""" with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_02(self): code = """x = y := 0""" with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_03(self): code = """y := f(x)""" with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_04(self): code = """y0 = y1 := f(x)""" with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_06(self): code = """((a, b) := (1, 2))""" @@ -103,7 +98,6 @@ def test_named_expression_invalid_16(self): with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_named_expression_invalid_17(self): code = "[i := 0, j := 1 for i, j in [(1, 2), (3, 4)]]" diff --git a/Lib/test/test_patma.py b/Lib/test/test_patma.py index 8d359a646d9..bc8afa2d555 100644 --- a/Lib/test/test_patma.py +++ b/Lib/test/test_patma.py @@ -2955,7 +2955,6 @@ def test_invalid_syntax_2(self): pass """) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_invalid_syntax_3(self): self.assert_syntax_error(""" match ...: diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 0934f22d470..6d33a2f97e1 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -59,15 +59,15 @@ Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> def __debug__(): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def __debug__(): pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> async def __debug__(): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> async def __debug__(): pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> class __debug__: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> class __debug__: pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ @@ -75,7 +75,7 @@ Traceback (most recent call last): SyntaxError: cannot delete __debug__ ->>> f() = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f() = 1 Traceback (most recent call last): SyntaxError: cannot assign to function call here. Maybe you meant '==' instead of '='? @@ -83,11 +83,11 @@ Traceback (most recent call last): SyntaxError: assignment to yield expression not possible ->>> del f() # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> del f() Traceback (most recent call last): SyntaxError: cannot delete function call ->>> a + 1 = 2 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> a + 1 = 2 Traceback (most recent call last): SyntaxError: cannot assign to expression here. Maybe you meant '==' instead of '='? @@ -120,7 +120,7 @@ This test just checks a couple of cases rather than enumerating all of them. ->>> (a, "b", c) = (1, 2, 3) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (a, "b", c) = (1, 2, 3) Traceback (most recent call last): SyntaxError: cannot assign to literal @@ -168,15 +168,15 @@ Traceback (most recent call last): SyntaxError: expected 'else' after 'if' expression ->>> x = 1 if 1 else pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> x = 1 if 1 else pass Traceback (most recent call last): SyntaxError: expected expression after 'else', but statement is given ->>> x = pass if 1 else 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> x = pass if 1 else 1 Traceback (most recent call last): SyntaxError: expected expression before 'if', but statement is given ->>> x = pass if 1 else pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> x = pass if 1 else pass Traceback (most recent call last): SyntaxError: expected expression before 'if', but statement is given @@ -200,15 +200,15 @@ Traceback (most recent call last): SyntaxError: assignment to yield expression not possible ->>> a, b += 1, 2 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> a, b += 1, 2 Traceback (most recent call last): SyntaxError: 'tuple' is an illegal expression for augmented assignment ->>> (a, b) += 1, 2 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (a, b) += 1, 2 Traceback (most recent call last): SyntaxError: 'tuple' is an illegal expression for augmented assignment ->>> [a, b] += 1, 2 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [a, b] += 1, 2 Traceback (most recent call last): SyntaxError: 'list' is an illegal expression for augmented assignment @@ -243,7 +243,7 @@ Traceback (most recent call last): SyntaxError: cannot assign to expression ->>> for i < (): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> for i < (): pass Traceback (most recent call last): SyntaxError: invalid syntax @@ -285,11 +285,11 @@ Comprehensions without 'in' keyword: ->>> [x for x if range(1)] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x for x if range(1)] Traceback (most recent call last): SyntaxError: 'in' expected after for-loop variables ->>> tuple(x for x if range(1)) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> tuple(x for x if range(1)) Traceback (most recent call last): SyntaxError: 'in' expected after for-loop variables @@ -301,7 +301,7 @@ Traceback (most recent call last): SyntaxError: cannot assign to expression ->>> [x for a, b, (c + 1, d()) if y] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x for a, b, (c + 1, d()) if y] Traceback (most recent call last): SyntaxError: 'in' expected after for-loop variables @@ -316,17 +316,17 @@ Comprehensions creating tuples without parentheses should produce a specialized error message: ->>> [x,y for x,y in range(100)] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x,y for x,y in range(100)] Traceback (most recent call last): SyntaxError: did you forget parentheses around the comprehension target? ->>> {x,y for x,y in range(100)} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {x,y for x,y in range(100)} Traceback (most recent call last): SyntaxError: did you forget parentheses around the comprehension target? # Incorrectly closed strings ->>> "The interesting object "The important object" is very important" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> "The interesting object "The important object" is very important" Traceback (most recent call last): SyntaxError: invalid syntax. Is this intended to be part of the string? @@ -353,7 +353,7 @@ # Make sure soft keywords constructs don't raise specialized # errors regarding missing commas or other spezialiced errors ->>> match x: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> match x: ... y = 3 Traceback (most recent call last): SyntaxError: invalid syntax @@ -370,7 +370,7 @@ Traceback (most recent call last): SyntaxError: invalid syntax ->>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> match ...: ... case {**rest, "key": value}: ... ... Traceback (most recent call last): @@ -385,7 +385,7 @@ # But prefixes of soft keywords should # still raise specialized errors ->>> (mat x) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (mat x) Traceback (most recent call last): SyntaxError: invalid syntax. Perhaps you forgot a comma? @@ -413,7 +413,7 @@ Traceback (most recent call last): SyntaxError: invalid syntax ->>> def f(*None): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def f(*None): ... pass Traceback (most recent call last): SyntaxError: invalid syntax @@ -468,12 +468,12 @@ Traceback (most recent call last): SyntaxError: var-positional argument cannot have default value ->>> def foo(a,**b=3): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a,**b=3): ... pass Traceback (most recent call last): SyntaxError: var-keyword argument cannot have default value ->>> def foo(a,**b: int=3): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a,**b: int=3): ... pass Traceback (most recent call last): SyntaxError: var-keyword argument cannot have default value @@ -523,22 +523,22 @@ Traceback (most recent call last): SyntaxError: * argument may appear only once ->>> def foo(a=1,/*,b,c): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a=1,/*,b,c): ... pass Traceback (most recent call last): SyntaxError: expected comma between / and * ->>> def foo(a=1,d=,c): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a=1,d=,c): ... pass Traceback (most recent call last): SyntaxError: expected default value expression ->>> def foo(a,d=,c): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a,d=,c): ... pass Traceback (most recent call last): SyntaxError: expected default value expression ->>> def foo(a,d: int=,c): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a,d: int=,c): ... pass Traceback (most recent call last): SyntaxError: expected default value expression @@ -571,7 +571,7 @@ Traceback (most recent call last): SyntaxError: / must be ahead of * ->>> lambda a=1,/*,b,c: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> lambda a=1,/*,b,c: None Traceback (most recent call last): SyntaxError: expected comma between / and * @@ -579,7 +579,7 @@ Traceback (most recent call last): SyntaxError: var-positional argument cannot have default value ->>> lambda a,**b=3: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> lambda a,**b=3: None Traceback (most recent call last): SyntaxError: var-keyword argument cannot have default value @@ -619,11 +619,11 @@ Traceback (most recent call last): SyntaxError: * argument may appear only once ->>> lambda a=1,d=,c: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> lambda a=1,d=,c: None Traceback (most recent call last): SyntaxError: expected default value expression ->>> lambda a,d=,c: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> lambda a,d=,c: None Traceback (most recent call last): SyntaxError: expected default value expression @@ -784,7 +784,7 @@ ... 290, 291, 292, 293, 294, 295, 296, 297, 298, 299) # doctest: +ELLIPSIS (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ..., 297, 298, 299) ->>> f(lambda x: x[0] = 3) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(lambda x: x[0] = 3) Traceback (most recent call last): SyntaxError: expression cannot contain assignment, perhaps you meant "=="? @@ -796,25 +796,25 @@ The grammar accepts any test (basically, any expression) in the keyword slot of a call site. Test a few different options. ->>> f(x()=2) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(x()=2) Traceback (most recent call last): SyntaxError: expression cannot contain assignment, perhaps you meant "=="? ->>> f(a or b=1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a or b=1) Traceback (most recent call last): SyntaxError: expression cannot contain assignment, perhaps you meant "=="? ->>> f(x.y=1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(x.y=1) Traceback (most recent call last): SyntaxError: expression cannot contain assignment, perhaps you meant "=="? >>> f((x)=2) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE Traceback (most recent call last): SyntaxError: expression cannot contain assignment, perhaps you meant "=="? ->>> f(True=1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(True=1) Traceback (most recent call last): SyntaxError: cannot assign to True ->>> f(False=1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(False=1) Traceback (most recent call last): SyntaxError: cannot assign to False ->>> f(None=1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(None=1) Traceback (most recent call last): SyntaxError: cannot assign to None >>> f(__debug__=1) @@ -826,42 +826,42 @@ >>> x.__debug__: int Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> f(a=) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a=) Traceback (most recent call last): SyntaxError: expected argument value expression ->>> f(a, b, c=) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a, b, c=) Traceback (most recent call last): SyntaxError: expected argument value expression ->>> f(a, b, c=, d) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a, b, c=, d) Traceback (most recent call last): SyntaxError: expected argument value expression ->>> f(*args=[0]) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(*args=[0]) Traceback (most recent call last): SyntaxError: cannot assign to iterable argument unpacking ->>> f(a, b, *args=[0]) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a, b, *args=[0]) Traceback (most recent call last): SyntaxError: cannot assign to iterable argument unpacking ->>> f(**kwargs={'a': 1}) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(**kwargs={'a': 1}) Traceback (most recent call last): SyntaxError: cannot assign to keyword argument unpacking ->>> f(a, b, *args, **kwargs={'a': 1}) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a, b, *args, **kwargs={'a': 1}) Traceback (most recent call last): SyntaxError: cannot assign to keyword argument unpacking More set_context(): ->>> (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: 'generator expression' is an illegal expression for augmented assignment ->>> None += 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> None += 1 Traceback (most recent call last): SyntaxError: 'None' is an illegal expression for augmented assignment >>> __debug__ += 1 Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> f() += 1 # TODO: RUSTPYTHON; Raises an exception # doctest: +SKIP -Traceback (most recent call last): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f() += 1 +Traceback (most recent call last): SyntaxError: 'function call' is an illegal expression for augmented assignment @@ -957,7 +957,7 @@ elif can't come after an else. - >>> if a % 2 == 0: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if a % 2 == 0: ... pass ... else: ... pass @@ -1013,7 +1013,7 @@ ... SyntaxError: name 'x' is parameter and nonlocal - >>> def f(): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def f(): ... global x ... nonlocal x Traceback (most recent call last): @@ -1045,7 +1045,7 @@ a complex 'if' (one with 'elif') would fail to notice an invalid suite, leading to spurious errors. - >>> if 1: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if 1: ... x() = 1 ... elif 1: ... pass @@ -1053,7 +1053,7 @@ ... SyntaxError: cannot assign to function call here. Maybe you meant '==' instead of '='? - >>> if 1: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if 1: ... pass ... elif 1: ... x() = 1 @@ -1061,7 +1061,7 @@ ... SyntaxError: cannot assign to function call here. Maybe you meant '==' instead of '='? - >>> if 1: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if 1: ... x() = 1 ... elif 1: ... pass @@ -1071,7 +1071,7 @@ ... SyntaxError: cannot assign to function call here. Maybe you meant '==' instead of '='? - >>> if 1: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if 1: ... pass ... elif 1: ... x() = 1 @@ -1081,7 +1081,7 @@ ... SyntaxError: cannot assign to function call here. Maybe you meant '==' instead of '='? - >>> if 1: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if 1: ... pass ... elif 1: ... pass @@ -1264,22 +1264,22 @@ Traceback (most recent call last): SyntaxError: cannot assign to __debug__ - >>> if x = 3: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if x = 3: ... pass Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? - >>> while x = 3: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> while x = 3: ... pass Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? - >>> if x.a = 3: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if x.a = 3: ... pass Traceback (most recent call last): SyntaxError: cannot assign to attribute here. Maybe you meant '==' instead of '='? - >>> while x.a = 3: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> while x.a = 3: ... pass Traceback (most recent call last): SyntaxError: cannot assign to attribute here. Maybe you meant '==' instead of '='? @@ -1313,39 +1313,39 @@ Parenthesized arguments in function definitions - >>> def f(x, (y, z), w): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def f(x, (y, z), w): ... pass Traceback (most recent call last): SyntaxError: Function parameters cannot be parenthesized - >>> def f((x, y, z, w)): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def f((x, y, z, w)): ... pass Traceback (most recent call last): SyntaxError: Function parameters cannot be parenthesized - >>> def f(x, (y, z, w)): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def f(x, (y, z, w)): ... pass Traceback (most recent call last): SyntaxError: Function parameters cannot be parenthesized - >>> def f((x, y, z), w): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def f((x, y, z), w): ... pass Traceback (most recent call last): SyntaxError: Function parameters cannot be parenthesized - >>> lambda x, (y, z), w: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> lambda x, (y, z), w: None Traceback (most recent call last): SyntaxError: Lambda expression parameters cannot be parenthesized - >>> lambda (x, y, z, w): None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> lambda (x, y, z, w): None Traceback (most recent call last): SyntaxError: Lambda expression parameters cannot be parenthesized - >>> lambda x, (y, z, w): None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> lambda x, (y, z, w): None Traceback (most recent call last): SyntaxError: Lambda expression parameters cannot be parenthesized - >>> lambda (x, y, z), w: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> lambda (x, y, z), w: None Traceback (most recent call last): SyntaxError: Lambda expression parameters cannot be parenthesized @@ -1361,7 +1361,7 @@ >>> try: ... pass - ... except TypeError as __debug__: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + ... except TypeError as __debug__: ... pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ @@ -1410,28 +1410,28 @@ Better error message for using `except as` with not a name: - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... pass ... except TypeError as obj.attr: ... pass Traceback (most recent call last): SyntaxError: cannot use except statement with attribute - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... pass ... except TypeError as obj[1]: ... pass Traceback (most recent call last): SyntaxError: cannot use except statement with subscript - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... pass ... except* TypeError as (obj, name): ... pass Traceback (most recent call last): SyntaxError: cannot use except* statement with tuple - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... pass ... except* TypeError as 1: ... pass @@ -1441,17 +1441,17 @@ Regression tests for gh-133999: >>> try: pass - ... except TypeError as name: raise from None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + ... except TypeError as name: raise from None Traceback (most recent call last): SyntaxError: invalid syntax >>> try: pass - ... except* TypeError as name: raise from None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + ... except* TypeError as name: raise from None Traceback (most recent call last): SyntaxError: invalid syntax >>> match 1: - ... case 1 | 2 as abc: raise from None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + ... case 1 | 2 as abc: raise from None Traceback (most recent call last): SyntaxError: invalid syntax @@ -1474,27 +1474,27 @@ Incomplete dictionary literals - >>> {1:2, 3:4, 5} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1:2, 3:4, 5} Traceback (most recent call last): SyntaxError: ':' expected after dictionary key - >>> {1:2, 3:4, 5:} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1:2, 3:4, 5:} Traceback (most recent call last): SyntaxError: expression expected after dictionary key and ':' - >>> {1: *12+1, 23: 1} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1: *12+1, 23: 1} Traceback (most recent call last): SyntaxError: cannot use a starred expression in a dictionary value - >>> {1: *12+1} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1: *12+1} Traceback (most recent call last): SyntaxError: cannot use a starred expression in a dictionary value - >>> {1: 23, 1: *12+1} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1: 23, 1: *12+1} Traceback (most recent call last): SyntaxError: cannot use a starred expression in a dictionary value - >>> {1:} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1:} Traceback (most recent call last): SyntaxError: expression expected after dictionary key and ':' @@ -1506,7 +1506,7 @@ # Ensure that the error is not raised for invalid expressions - >>> {1: 2, 3: foo(,), 4: 5} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1: 2, 3: foo(,), 4: 5} Traceback (most recent call last): SyntaxError: invalid syntax @@ -1516,48 +1516,48 @@ Specialized indentation errors: - >>> while condition: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> while condition: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'while' statement on line 1 - >>> for x in range(10): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> for x in range(10): ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'for' statement on line 1 - >>> for x in range(10): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> for x in range(10): ... pass ... else: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'else' statement on line 3 - >>> async for x in range(10): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> async for x in range(10): ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'for' statement on line 1 - >>> async for x in range(10): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> async for x in range(10): ... pass ... else: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'else' statement on line 3 - >>> if something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if something: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'if' statement on line 1 - >>> if something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if something: ... pass ... elif something_else: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'elif' statement on line 3 - >>> if something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if something: ... pass ... elif something_else: ... pass @@ -1566,33 +1566,33 @@ Traceback (most recent call last): IndentationError: expected an indented block after 'else' statement on line 5 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'try' statement on line 1 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... something() ... except: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'except' statement on line 3 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... something() ... except A: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'except' statement on line 3 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... something() ... except* A: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'except*' statement on line 3 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... something() ... except A: ... pass @@ -1601,7 +1601,7 @@ Traceback (most recent call last): IndentationError: expected an indented block after 'finally' statement on line 5 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... something() ... except* A: ... pass @@ -1610,57 +1610,57 @@ Traceback (most recent call last): IndentationError: expected an indented block after 'finally' statement on line 5 - >>> with A: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> with A: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> with A as a, B as b: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> with A as a, B as b: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> with (A as a, B as b): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> with (A as a, B as b): ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> async with A: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> async with A: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> async with A as a, B as b: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> async with A as a, B as b: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> async with (A as a, B as b): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> async with (A as a, B as b): ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> def foo(x, /, y, *, z=2): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def foo(x, /, y, *, z=2): ... pass Traceback (most recent call last): IndentationError: expected an indented block after function definition on line 1 - >>> def foo[T](x, /, y, *, z=2): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def foo[T](x, /, y, *, z=2): ... pass Traceback (most recent call last): IndentationError: expected an indented block after function definition on line 1 - >>> class Blech(A): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class Blech(A): ... pass Traceback (most recent call last): IndentationError: expected an indented block after class definition on line 1 - >>> class Blech[T](A): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class Blech[T](A): ... pass Traceback (most recent call last): IndentationError: expected an indented block after class definition on line 1 - >>> class C(__debug__=42): ... # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class C(__debug__=42): ... Traceback (most recent call last): SyntaxError: cannot assign to __debug__ @@ -1668,23 +1668,23 @@ ... def __new__(*args, **kwargs): ... pass - >>> class C(metaclass=Meta, __debug__=42): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class C(metaclass=Meta, __debug__=42): ... pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ - >>> match something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match something: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'match' statement on line 1 - >>> match something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match something: ... case []: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'case' statement on line 2 - >>> match something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match something: ... case []: ... ... ... case {}: @@ -1693,11 +1693,11 @@ IndentationError: expected an indented block after 'case' statement on line 4 Make sure that the old "raise X, Y[, Z]" form is gone: - >>> raise X, Y # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> raise X, Y Traceback (most recent call last): ... SyntaxError: invalid syntax - >>> raise X, Y, Z # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> raise X, Y, Z Traceback (most recent call last): ... SyntaxError: invalid syntax @@ -1885,119 +1885,119 @@ ... SyntaxError: keyword argument repeated: a ->>> {1, 2, 3} = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {1, 2, 3} = 42 Traceback (most recent call last): SyntaxError: cannot assign to set display here. Maybe you meant '==' instead of '='? ->>> {1: 2, 3: 4} = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {1: 2, 3: 4} = 42 Traceback (most recent call last): SyntaxError: cannot assign to dict literal here. Maybe you meant '==' instead of '='? ->>> f'{x}' = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f'{x}' = 42 Traceback (most recent call last): SyntaxError: cannot assign to f-string expression here. Maybe you meant '==' instead of '='? ->>> f'{x}-{y}' = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f'{x}-{y}' = 42 Traceback (most recent call last): SyntaxError: cannot assign to f-string expression here. Maybe you meant '==' instead of '='? ->>> ub'' # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> ub'' Traceback (most recent call last): SyntaxError: 'u' and 'b' prefixes are incompatible ->>> bu"привет" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> bu"привет" Traceback (most recent call last): SyntaxError: 'u' and 'b' prefixes are incompatible ->>> ur'' # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> ur'' Traceback (most recent call last): SyntaxError: 'u' and 'r' prefixes are incompatible ->>> ru"\t" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> ru"\t" Traceback (most recent call last): SyntaxError: 'u' and 'r' prefixes are incompatible ->>> uf'{1 + 1}' # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> uf'{1 + 1}' Traceback (most recent call last): SyntaxError: 'u' and 'f' prefixes are incompatible ->>> fu"" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> fu"" Traceback (most recent call last): SyntaxError: 'u' and 'f' prefixes are incompatible ->>> ut'{1}' # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> ut'{1}' Traceback (most recent call last): SyntaxError: 'u' and 't' prefixes are incompatible ->>> tu"234" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> tu"234" Traceback (most recent call last): SyntaxError: 'u' and 't' prefixes are incompatible ->>> bf'{x!r}' # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> bf'{x!r}' Traceback (most recent call last): SyntaxError: 'b' and 'f' prefixes are incompatible ->>> fb"text" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> fb"text" Traceback (most recent call last): SyntaxError: 'b' and 'f' prefixes are incompatible ->>> bt"text" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> bt"text" Traceback (most recent call last): SyntaxError: 'b' and 't' prefixes are incompatible ->>> tb'' # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> tb'' Traceback (most recent call last): SyntaxError: 'b' and 't' prefixes are incompatible ->>> tf"{0.3:.02f}" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> tf"{0.3:.02f}" Traceback (most recent call last): SyntaxError: 'f' and 't' prefixes are incompatible ->>> ft'{x=}' # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> ft'{x=}' Traceback (most recent call last): SyntaxError: 'f' and 't' prefixes are incompatible ->>> tfu"{x=}" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> tfu"{x=}" Traceback (most recent call last): SyntaxError: 'u' and 'f' prefixes are incompatible ->>> turf"{x=}" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> turf"{x=}" Traceback (most recent call last): SyntaxError: 'u' and 'r' prefixes are incompatible ->>> burft"{x=}" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> burft"{x=}" Traceback (most recent call last): SyntaxError: 'u' and 'b' prefixes are incompatible ->>> brft"{x=}" # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> brft"{x=}" Traceback (most recent call last): SyntaxError: 'b' and 'f' prefixes are incompatible ->>> t'{x}' = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> t'{x}' = 42 Traceback (most recent call last): SyntaxError: cannot assign to t-string expression here. Maybe you meant '==' instead of '='? ->>> t'{x}-{y}' = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> t'{x}-{y}' = 42 Traceback (most recent call last): SyntaxError: cannot assign to t-string expression here. Maybe you meant '==' instead of '='? ->>> (x, y, z=3, d, e) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (x, y, z=3, d, e) Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? ->>> [x, y, z=3, d, e] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x, y, z=3, d, e] Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? ->>> [z=3] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [z=3] Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? ->>> {x, y, z=3, d, e} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {x, y, z=3, d, e} Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? ->>> {z=3} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {z=3} Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? @@ -2009,35 +2009,35 @@ Traceback (most recent call last): SyntaxError: trailing comma not allowed without surrounding parentheses ->>> import a from b # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a from b Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a.y.z from b.y.z # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.y.z from b.y.z Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a from b as bar # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a from b as bar Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a.y.z from b.y.z as bar # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.y.z from b.y.z as bar Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a, b,c from b # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a, b,c from b Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a.y.z, b.y.z, c.y.z from b.y.z # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.y.z, b.y.z, c.y.z from b.y.z Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a,b,c from b as bar # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a,b,c from b as bar Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a.y.z, b.y.z, c.y.z from b.y.z as bar # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.y.z, b.y.z, c.y.z from b.y.z as bar Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? @@ -2061,53 +2061,53 @@ Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> import a as b.c # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a as b.c Traceback (most recent call last): SyntaxError: cannot use attribute as import target ->>> import a.b as (a, b) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.b as (a, b) Traceback (most recent call last): SyntaxError: cannot use tuple as import target ->>> import a, a.b as 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a, a.b as 1 Traceback (most recent call last): SyntaxError: cannot use literal as import target ->>> import a.b as 'a', a # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.b as 'a', a Traceback (most recent call last): SyntaxError: cannot use literal as import target ->>> from a import (b as c.d) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import (b as c.d) Traceback (most recent call last): SyntaxError: cannot use attribute as import target ->>> from a import b as 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import b as 1 Traceback (most recent call last): SyntaxError: cannot use literal as import target ->>> from a import ( # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import ( ... b as f()) Traceback (most recent call last): SyntaxError: cannot use function call as import target ->>> from a import ( # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import ( ... b as [], ... ) Traceback (most recent call last): SyntaxError: cannot use list as import target ->>> from a import ( # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import ( ... b, ... c as () ... ) Traceback (most recent call last): SyntaxError: cannot use tuple as import target ->>> from a import b, с as d[e] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import b, с as d[e] Traceback (most recent call last): SyntaxError: cannot use subscript as import target ->>> from a import с as d[e], b # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import с as d[e], b Traceback (most recent call last): SyntaxError: cannot use subscript as import target @@ -2233,43 +2233,43 @@ Traceback (most recent call last): SyntaxError: cannot assign to __debug__ - >>> import ä £ # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> import ä £ Traceback (most recent call last): SyntaxError: invalid character '£' (U+00A3) Invalid pattern matching constructs: - >>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match ...: ... case 42 as _: ... ... Traceback (most recent call last): SyntaxError: cannot use '_' as a target - >>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match ...: ... case 42 as 1+2+4: ... ... Traceback (most recent call last): SyntaxError: cannot use expression as pattern target - >>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match ...: ... case 42 as a.b: ... ... Traceback (most recent call last): SyntaxError: cannot use attribute as pattern target - >>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match ...: ... case 42 as (a, b): ... ... Traceback (most recent call last): SyntaxError: cannot use tuple as pattern target - >>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match ...: ... case 42 as (a + 1): ... ... Traceback (most recent call last): SyntaxError: cannot use expression as pattern target - >>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match ...: ... case (32 as x) | (42 as a()): ... ... Traceback (most recent call last): @@ -2307,7 +2307,7 @@ Traceback (most recent call last): ... SyntaxError: invalid syntax - >>> A[:(*b)] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> A[:(*b)] Traceback (most recent call last): ... SyntaxError: cannot use starred expression here @@ -2326,7 +2326,7 @@ Traceback (most recent call last): ... SyntaxError: invalid syntax - >>> A[(*b):] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> A[(*b):] Traceback (most recent call last): ... SyntaxError: cannot use starred expression here @@ -2360,22 +2360,22 @@ A[*(1:2)] - >>> A[*(1:2)] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> A[*(1:2)] Traceback (most recent call last): ... SyntaxError: Invalid star expression - >>> A[*(1:2)] = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> A[*(1:2)] = 1 Traceback (most recent call last): ... SyntaxError: Invalid star expression - >>> del A[*(1:2)] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> del A[*(1:2)] Traceback (most recent call last): ... SyntaxError: Invalid star expression A[*:] and A[:*] - >>> A[*:] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> A[*:] Traceback (most recent call last): ... SyntaxError: Invalid star expression @@ -2386,7 +2386,7 @@ A[*] - >>> A[*] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> A[*] Traceback (most recent call last): ... SyntaxError: Invalid star expression @@ -2636,26 +2636,26 @@ def f(x: *b) Traceback (most recent call last): SyntaxError: cannot assign to __debug__ - >>> class A[__debug__]: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class A[__debug__]: pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ - >>> class A[T]((x := 3)): ... # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class A[T]((x := 3)): ... Traceback (most recent call last): ... SyntaxError: named expression cannot be used within the definition of a generic - >>> class A[T]((yield 3)): ... # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class A[T]((yield 3)): ... Traceback (most recent call last): ... SyntaxError: yield expression cannot be used within the definition of a generic - >>> class A[T]((await 3)): ... # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class A[T]((await 3)): ... Traceback (most recent call last): ... SyntaxError: await expression cannot be used within the definition of a generic - >>> class A[T]((yield from [])): ... # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class A[T]((yield from [])): ... Traceback (most recent call last): ... SyntaxError: yield expression cannot be used within the definition of a generic @@ -2664,23 +2664,23 @@ def f(x: *b) Traceback (most recent call last): SyntaxError: iterable argument unpacking follows keyword argument unpacking - >>> f(**x, *) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> f(**x, *) Traceback (most recent call last): SyntaxError: Invalid star expression - >>> f(x, *:) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> f(x, *:) Traceback (most recent call last): SyntaxError: Invalid star expression - >>> f(x, *) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> f(x, *) Traceback (most recent call last): SyntaxError: Invalid star expression - >>> f(x = 5, *) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> f(x = 5, *) Traceback (most recent call last): SyntaxError: Invalid star expression - >>> f(x = 5, *:) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> f(x = 5, *:) Traceback (most recent call last): SyntaxError: Invalid star expression """ @@ -2807,7 +2807,6 @@ def _check_error(self, code, errtext, else: self.fail("compile() did not raise SyntaxError") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_expression_with_assignment(self): self._check_error( "print(end1 + end2 = ' ')", @@ -3287,7 +3286,6 @@ def test_match_stmt_invalid_as_expr(self): end_offset=15 + len("obj.attr"), ) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_ifexp_else_stmt(self): msg = "expected expression after 'else', but statement is given" @@ -3308,7 +3306,6 @@ def test_ifexp_else_stmt(self): ]: self._check_error(f"x = 1 if 1 else {stmt}", msg) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_ifexp_body_stmt_else_expression(self): msg = "expected expression before 'if', but statement is given" @@ -3319,7 +3316,6 @@ def test_ifexp_body_stmt_else_expression(self): ]: self._check_error(f"x = {stmt} if 1 else 1", msg) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_ifexp_body_stmt_else_stmt(self): msg = "expected expression before 'if', but statement is given" for lhs_stmt, rhs_stmt in [ diff --git a/Lib/test/test_unicode_identifiers.py b/Lib/test/test_unicode_identifiers.py index 27749a0805c..3680072d643 100644 --- a/Lib/test/test_unicode_identifiers.py +++ b/Lib/test/test_unicode_identifiers.py @@ -17,7 +17,6 @@ def test_non_bmp_normalized(self): 𝔘𝔫𝔦𝔠𝔬𝔡𝔢 = 1 self.assertIn("Unicode", dir()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_invalid(self): try: from test.tokenizedata import badsyntax_3131 # noqa: F401 diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index c3b86ce46bc..df9a9e10f15 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -4698,6 +4698,27 @@ impl Compiler { Ok(true) } + /// Reject `__debug__` used as a type-parameter name, e.g. + /// `def f[__debug__](): ...` or `class C[__debug__]: ...`. + fn validate_type_params_no_debug( + &mut self, + type_params: Option<&ast::TypeParams>, + ) -> CompileResult<()> { + if let Some(params) = type_params { + for tp in ¶ms.type_params { + let tp_name = match tp { + ast::TypeParam::TypeVar(t) => &t.name, + ast::TypeParam::TypeVarTuple(t) => &t.name, + ast::TypeParam::ParamSpec(t) => &t.name, + }; + if tp_name.as_str() == "__debug__" { + return Err(self.error(CodegenErrorType::Assign("__debug__"))); + } + } + } + Ok(()) + } + // = compiler_function #[expect(clippy::too_many_arguments, reason = "ignore warning for now")] fn compile_function_def( @@ -4711,6 +4732,11 @@ impl Compiler { type_params: Option<&ast::TypeParams>, preserve_value_before_store: bool, ) -> CompileResult<()> { + if name == "__debug__" { + return Err(self.error(CodegenErrorType::Assign("__debug__"))); + } + // Reject `def f[__debug__](): ...` type parameter (mirrors class defs). + self.validate_type_params_no_debug(type_params)?; // CPython's FunctionDef/AsyncFunctionDef LOC(s) starts at the // definition line even when decorators are present. let stmt_source_range = self.current_source_range; @@ -5275,6 +5301,20 @@ impl Compiler { arguments: Option<&ast::Arguments>, preserve_value_before_store: bool, ) -> CompileResult<()> { + if name == "__debug__" { + return Err(self.error(CodegenErrorType::Assign("__debug__"))); + } + // Reject `class C(__debug__=...)` keyword argument. + if let Some(args) = arguments + && args + .keywords + .iter() + .any(|kw| kw.arg.as_ref().is_some_and(|n| n.as_str() == "__debug__")) + { + return Err(self.error(CodegenErrorType::Assign("__debug__"))); + } + // Reject `class C[__debug__]: ...` type parameter. + self.validate_type_params_no_debug(type_params)?; // CPython's ClassDef LOC(s) starts at the class line even when // decorators are present. let stmt_source_range = self.current_source_range; @@ -6096,6 +6136,13 @@ impl Compiler { Ok(()) } Some(name) => { + // `case … as _` — `_` is the wildcard and cannot be a capture + // target (CPython: "cannot use '_' as a target"). + if name.as_str() == "_" { + return Err(self.error(CodegenErrorType::SyntaxError( + "cannot use '_' as a target".to_owned(), + ))); + } // Check if the name is forbidden for storing. if self.forbidden_name(name.as_str(), NameUsage::Store)? { return Err(self.compile_error_forbidden_name(name.as_str())); diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index 10144597f46..30d263f52f6 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -1804,6 +1804,9 @@ impl SymbolTableBuilder { self.scan_expression(expression, ExpressionContext::Load)?; } if let Some(name) = name { + // `except ... as NAME` binds NAME, so reject `__debug__` + // ("cannot assign to __debug__") like any other store. + self.check_name(name.as_str(), ExpressionContext::Store, name.range)?; self.register_ident(name, SymbolUsage::Assigned)?; } self.scan_statements(body)?; @@ -1955,7 +1958,9 @@ impl SymbolTableBuilder { } else if let Some(table) = self.tables.last() && table.typ == CompilerScope::TypeParams { - Some("a type parameter") + // CPython phrases the bare type-param scope (a generic class/def's + // bases & keywords) as "the definition of a generic". + Some("the definition of a generic") } else if self.in_annotation { Some("an annotation") } else if self.in_type_alias { @@ -2910,6 +2915,12 @@ impl SymbolTableBuilder { // Role already set.. match role { SymbolUsage::Global if !symbol.is_global() => { + if flags.contains(SymbolFlags::NONLOCAL) { + return Err(SymbolTableError { + error: format!("name '{name}' is nonlocal and global"), + location, + }); + } if flags.contains(SymbolFlags::PARAMETER) { return Err(SymbolTableError { error: format!("name '{name}' is parameter and global"), @@ -2938,6 +2949,12 @@ impl SymbolTableBuilder { } } SymbolUsage::Nonlocal => { + if flags.contains(SymbolFlags::GLOBAL) { + return Err(SymbolTableError { + error: format!("name '{name}' is nonlocal and global"), + location, + }); + } if flags.contains(SymbolFlags::PARAMETER) { return Err(SymbolTableError { error: format!("name '{name}' is parameter and nonlocal"), diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 9c0884c7520..df25cab82da 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -115,81 +115,581 @@ impl CompileError { } 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 (loc, end_loc) = adjusted_locations(&source_code, error.location); let expr_str = source_file.source_text().slice(error.location); + let followed_by_eq = next_nonspace_after(source_text, error.location) == Some('='); 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() + |parsed| { + // A comparison / bool-op `for` target (`for i < (): ...`) + // is plain "invalid syntax" in CPython, not a + // "cannot assign to comparison" message. + if is_for_loop_target(source_text, error.location) + && matches!(*parsed.syntax().body, ast::Expr::Compare(_)) + { + "invalid syntax".to_owned() + } else { + assign_target_message(&parsed.syntax().body, expr_str, followed_by_eq) } - _ => format!("cannot assign to {expr_str}"), }, ); (ParseErrorType::OtherError(msg), loc, end_loc) } + ParseErrorType::InvalidAugmentedAssignmentTarget => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let expr_str = source_file.source_text().slice(error.location); + + let kind = parser::parse_expression(expr_str) + .ok() + .map_or("expression", |parsed| expr_kind_name(&parsed.syntax().body)); + let msg = format!("'{kind}' is an illegal expression for augmented assignment"); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + ParseErrorType::InvalidDeleteTarget => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let expr_str = source_file.source_text().slice(error.location); + + // A bare starred target (`del *x,`) doesn't parse as a + // standalone expression, so fall back to detecting the `*`. + let kind = parser::parse_expression(expr_str).ok().map_or_else( + || { + if expr_str.trim_start().starts_with('*') { + "starred" + } else { + "expression" + } + }, + |parsed| expr_kind_name(&parsed.syntax().body), + ); + let msg = format!("cannot delete {kind}"); + (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); + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let target = source_file.source_text().slice(error.location); + // CPython names the target kind (`tuple`, `attribute`, `True`, …) + // rather than echoing the raw source; fall back to the raw text + // if it doesn't parse as an expression. + let kind = parser::parse_expression(target).ok().map_or_else( + || target.to_owned(), + |p| expr_kind_name(&p.syntax().body).to_owned(), + ); + let msg = format!("cannot use assignment expressions with {kind}"); + (ParseErrorType::OtherError(msg), 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); + // Convert "Expected an indented block after `X` " (ruff) + // into "expected an indented block after 'X' on line N" + // (CPython). Replaces backticks with single quotes and appends + // the originating statement's line number. + ParseErrorType::OtherError(s) if s.starts_with("Expected an indented block after") => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let mut msg = normalize_indented_block_message(s); + // Find the keyword in backticks to compute the line number. + let lineno = find_indented_block_keyword_line(source_text, error.location, s) + .unwrap_or_else(|| loc.line.get()); + // For `except`, CPython distinguishes `except*` from `except`. + if msg.contains("'except' statement") + && source_uses_except_star(source_text, lineno) + { + msg = msg.replace("'except' statement", "'except*' statement"); } + // Keep the leading "Expected" capital so vm_new.rs still + // detects the message and picks `IndentationError`. + msg.push_str(&format!(" on line {lineno}")); + (ParseErrorType::OtherError(msg), loc, end_loc) + } - let target = source_file.source_text().slice(error.location); - let msg = format!("cannot use assignment expressions with {target}"); + // `def f(.../*,...)` / `lambda .../*,...:`: missing comma between + // `/` and `*` markers. + ParseErrorType::ExpectedToken { + expected: TokenKind::Comma, + found: TokenKind::Star, + } if is_slash_star_in_params(source_text, error.location) => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "expected comma between / and *".to_owned(); (ParseErrorType::OtherError(msg), 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); + // `f(**kwargs=...)`: assignment to a `**` unpacking. + ParseErrorType::ExpectedToken { + expected: TokenKind::Comma, + found: TokenKind::Equal, + } if is_kwarg_unpacking_assignment(source_text, error.location) => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "cannot assign to keyword argument unpacking".to_owned(); + (ParseErrorType::OtherError(msg), 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); + // Comprehension with comma'd unparenthesised target: + // [x,y for x,y in range(100)] / {x,y for x,y in range(100)}. + ParseErrorType::ExpectedToken { + expected: TokenKind::Rsqb | TokenKind::Rbrace, + found: TokenKind::For, + } if is_unparen_comprehension_target(source_text, error.location) => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "did you forget parentheses around the comprehension target?".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // `raise X, Y` (old Python-2 syntax): ruff parses `X, Y` as an + // unparenthesised tuple and complains; CPython just says + // "invalid syntax". + ParseErrorType::UnparenthesizedTupleExpression + if source_starts_with_raise(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "invalid syntax".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Bare `*` in a function call argument list: + // f(x, *), f(**x, *), f(x = 5, *), f(x, *:) — CPython says + // "Invalid star expression". + ParseErrorType::ExpectedExpression | ParseErrorType::ExpectedToken { .. } + if is_bare_star_in_call(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "Invalid star expression".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // `except[*] T as `: ruff rejects with either + // "Expected name after `as`" or "invalid syntax". CPython + // distinguishes the kind of the bad target. + ParseErrorType::OtherError(s) + if s == "Expected name after `as`" + && let Some(msg) = + except_as_bad_target_message(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + ParseErrorType::ExpectedToken { .. } | ParseErrorType::OtherError(_) + if let Some(msg) = except_as_bad_target_message(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Comprehension with `if` after the for-target instead of `in`: + // [x for x if range(1)] / (x for x if y) / {... if ...}. + // Ruff treats `x if y` as a ternary expression and complains about + // a missing `else`; CPython points out the missing `in`. + ParseErrorType::ExpectedToken { + expected: TokenKind::Else, + .. + } if is_in_comprehension_if(source_text, error.location) => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "'in' expected after for-loop variables".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Subscript with bare or partial starred expression: + // A[*], A[*:], A[*(1:2)], A[*(1:2)] = 1, del A[*(1:2)]. + // CPython reports "Invalid star expression". + ParseErrorType::ExpectedExpression | ParseErrorType::ExpectedToken { .. } + if is_invalid_star_in_subscript(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "Invalid star expression".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Subscript with parenthesised starred expression as slice arg: + // A[(*b):], A[:(*b)] — "cannot use starred expression here". + // Only fires when the starred expression is parenthesised (the + // unparenthesised `A[:*b]` case stays "invalid syntax" per CPython). + ParseErrorType::InvalidStarredExpressionUsage + if is_paren_starred_in_subscript(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "cannot use starred expression here".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Bare `*` as the leading element of a set/dict display `{*}` / + // `{*, 1}` or a non-call parenthesised group `(*)` / `(*,)`. CPython + // reports "Invalid star expression". A non-leading star (`{1, *}`), + // a double star (`{**}`), or a dict value (`{1: *}`) is excluded. + ParseErrorType::ExpectedExpression + if is_bare_star_first_in_group(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "Invalid star expression".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Dict literal: `{1:}` / `{1: 2, 3: 4, 5: }` — *missing* value. CPython's + // "expression expected after dictionary key and ':'" is specific to a + // genuinely empty value; a present-but-unparseable value (`{1: *}`, + // `{1: 2*}`, `{1: +}`) is plain "invalid syntax", so require emptiness. + ParseErrorType::ExpectedExpression + if is_dict_value_position(source_text, error.location) + && dict_value_is_empty(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "expression expected after dictionary key and ':'".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Dict literal: `{1: 2, 3: 4, 5}` — missing `:` after last key. + // (Not inside a match `case {…}` mapping pattern → "invalid syntax".) + ParseErrorType::ExpectedToken { + expected: TokenKind::Colon, + found: TokenKind::Rbrace, + } if !is_in_case_pattern(source_text, error.location) => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "':' expected after dictionary key".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Dict literal: `{1: *12+1}` — starred expression as value. + ParseErrorType::InvalidStarredExpressionUsage + if is_dict_value_position(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "cannot use a starred expression in a dictionary value".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Detect missing default-value expression (`def f(a=, b): ...`) + // and missing argument-value expression (`f(a=)`). + _ if let Some(msg) = missing_default_or_argument_value(source_text, error.location) => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Detect incompatible string prefixes (`ub''`, `bf""`, etc.). + // CPython reports "'X' and 'Y' prefixes are incompatible". + _ if let Some(msg) = + incompatible_string_prefix_message(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Detect parenthesized parameters in `def f(x, (y, z), w): ...` + // or `lambda x, (y, z), w: None` — CPython rejects with + // "Function parameters cannot be parenthesized" / "Lambda expression + // parameters cannot be parenthesized". + ParseErrorType::ExpectedToken { .. } + | ParseErrorType::OtherError(_) + | ParseErrorType::ExpectedExpression + if let Some(msg) = parenthesized_param_message(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Detect bad keyword-argument LHS in a function call: + // f(x()=2), f(a or b=1), f(x.y=1), f((x)=2), + // f(True=1), f(False=1), f(None=1), + // f(*args=[0]), f(**kwargs={...}). + // Ruff emits "Expected a parameter name". CPython varies the + // message by the LHS shape. + ParseErrorType::OtherError(s) + if s == "Expected a parameter name" + && is_call_keyword_assignment(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let lhs = call_keyword_lhs(source_text, error.location); + let trimmed = lhs.trim(); + let msg = if trimmed.starts_with("**") { + "cannot assign to keyword argument unpacking".to_owned() + } else if trimmed.starts_with('*') { + "cannot assign to iterable argument unpacking".to_owned() + } else { + match trimmed { + "True" => "cannot assign to True".to_owned(), + "False" => "cannot assign to False".to_owned(), + "None" => "cannot assign to None".to_owned(), + "__debug__" => "cannot assign to __debug__".to_owned(), + _ => r#"expression cannot contain assignment, perhaps you meant "=="?"# + .to_owned(), + } + }; + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Detect `X=Y` in a tuple/list/set literal (not a call): + // (x, y, z=3), [a=1], {a=1}. + _ if let Some(msg) = collection_kwarg_message(source_text, error.location) => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Detect `pass` (or other statement keywords) used in expression + // position within a ternary ` if else ` — + // CPython suggests "expected expression after/before '...'". + ParseErrorType::ExpectedExpression + | ParseErrorType::ExpectedToken { .. } + | ParseErrorType::InvalidYieldExpressionUsage + | ParseErrorType::InvalidStarredExpressionUsage + | ParseErrorType::OtherError(_) + if let Some(msg) = + ternary_statement_keyword_message(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Detect `if X = Y:` / `while X = Y:` where `=` should be `==` or `:=`. + // Ruff emits `ExpectedToken { Colon, Equal }`. CPython varies the + // message by the LHS expression kind. + ParseErrorType::ExpectedToken { + expected: TokenKind::Colon, + found: TokenKind::Equal, + } if precedes_if_or_while(source_text, error.location) => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let start: usize = error.location.start().into(); + let line_start = source_text[..start].rfind('\n').map_or(0, |i| i + 1); + // Pull out the LHS expression between the keyword and `=`. + let lhs_slice = lhs_after_keyword(&source_text[line_start..start]); + let msg = match parser::parse_expression(lhs_slice) { + Ok(parsed) => match *parsed.syntax().body { + ast::Expr::Attribute(_) => { + "cannot assign to attribute here. Maybe you meant '==' instead of '='?" + .to_owned() + } + ast::Expr::Subscript(_) => { + "cannot assign to subscript here. Maybe you meant '==' instead of '='?" + .to_owned() + } + _ => "invalid syntax. Maybe you meant '==' or ':=' instead of '='?" + .to_owned(), + }, + Err(_) => { + "invalid syntax. Maybe you meant '==' or ':=' instead of '='?".to_owned() + } + }; + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Detect `import X from Y` — CPython suggests the correct syntax. + ParseErrorType::SimpleStatementsOnSameLine + | ParseErrorType::ExpectedToken { .. } + | ParseErrorType::OtherError(_) + if is_old_import_from(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = "Did you mean to use 'from ... import ...' instead?".to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Detect `import X as Y.Z` / `import X as Y[Z]` / `import X as Y()` + // where the parser successfully parsed the asname as a Name but + // the following token is `.`, `[`, or `(`. CPython distinguishes + // these as "cannot use attribute/subscript/function call as import target". + ParseErrorType::ExpectedToken { .. } | ParseErrorType::OtherError(_) + if is_import_target_continuation(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let start: usize = error.location.start().into(); + let first = source_text[start..].chars().next(); + let kind = match first { + Some('.') => "attribute", + Some('[') => "subscript", + Some('(') => "function call", + _ => "expression", + }; + let msg = format!("cannot use {kind} as import target"); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + ParseErrorType::OtherError(s) if s == "Expected symbol after `as`" => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let start: usize = error.location.start().into(); + let after = source_text[start..].trim_start(); + let kind = if let Some(first) = after.chars().next() { + match first { + '(' => Some("tuple"), + '[' => Some("list"), + '0'..='9' | '\'' | '"' | '.' => Some("literal"), + _ => None, + } + } else { + None + }; + let msg = match kind { + Some(k) => format!("cannot use {k} as import target"), + None => "Expected symbol after `as`".to_owned(), + }; + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + ParseErrorType::VarParameterWithDefault => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + // The error location is just the current token (e.g. `=` or the + // default value), not the whole `*X=Y` / `**X=Y`. Find the most + // recent `**` / `*` before the error to know which one applies. + let start: usize = error.location.start().into(); + let prefix = &source_text[..start]; + let last_star = prefix.rfind('*'); + let is_kwarg = + last_star.is_some_and(|idx| idx > 0 && prefix.as_bytes()[idx - 1] == b'*'); + let msg = if is_kwarg { + "var-keyword argument cannot have default value" + } else { + "var-positional argument cannot have default value" } + .to_owned(); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // A non-ASCII unrecognized character → CPython's + // "invalid character 'X' (U+XXXX)". ASCII junk (`$`, `?`, `@`) stays + // "invalid syntax" via the default path. + ParseErrorType::Lexical(LexicalErrorType::UnrecognizedToken { tok }) + if !tok.is_ascii() => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + let msg = format!("invalid character '{tok}' (U+{:04X})", *tok as u32); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + // Ruff `OtherError` strings that CPython collapses to "invalid syntax": + // `match x:\n y=3` (no case), `case {**rest, ...}` (pattern after + // double-star), `… raise from None` (missing exception), `foo(,)`, + // `{1:2,, 3}` / `[1,, 2]` (double comma in dict/set/list display). + ParseErrorType::OtherError(s) + if matches!( + s.as_str(), + "Expected `case` block" + | "Pattern cannot follow a double star pattern" + | "Exception missing in `raise` statement with cause" + | "Expected an expression or a ')'" + | "Expected an expression or a '}'" + | "Expected an expression or a ']'" + ) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + ( + ParseErrorType::OtherError("invalid syntax".to_owned()), + loc, + end_loc, + ) + } + + // `if …: else: … elif …:` — an `elif` after the `else` block. + ParseErrorType::OtherError(s) + if s == "Expected a statement" + && source_text.get( + usize::from(error.location.start())..usize::from(error.location.end()), + ) == Some("elif") => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + ( + ParseErrorType::OtherError("'elif' block follows an 'else' block".to_owned()), + loc, + end_loc, + ) + } + + // Any other "Expected a statement" (e.g. a bare `x := 0`, `@`, + // `else: pass`) is plain "invalid syntax" in CPython. + ParseErrorType::OtherError(s) if s == "Expected a statement" => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + ( + ParseErrorType::OtherError("invalid syntax".to_owned()), + loc, + end_loc, + ) + } + + // `(mat x)` / `(a b)` — a name where `)` (i.e. a comma) was expected. + // (Not inside a match `case (a b)` sequence pattern, where CPython + // reports plain "invalid syntax".) + ParseErrorType::ExpectedToken { + expected: TokenKind::Rpar, + found: TokenKind::Name, + } if !is_in_case_pattern(source_text, error.location) => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + ( + ParseErrorType::OtherError( + "invalid syntax. Perhaps you forgot a comma?".to_owned(), + ), + loc, + end_loc, + ) + } + + // `def f(*None): …` — bare `*` followed by a keyword (not a name). + ParseErrorType::ExpectedKeywordParam + if next_word_is_keyword(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + ( + ParseErrorType::OtherError("invalid syntax".to_owned()), + loc, + end_loc, + ) + } + + // `a, b += 1, 2` — unparenthesized tuple as an augmented-assignment + // target (ruff wants a comma but found an augmented-assign operator). + ParseErrorType::ExpectedToken { + expected: TokenKind::Comma, + .. + } if source_text + .get(usize::from(error.location.start())..usize::from(error.location.end())) + .is_some_and(is_augassign_op) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + ( + ParseErrorType::OtherError( + "'tuple' is an illegal expression for augmented assignment".to_owned(), + ), + loc, + end_loc, + ) + } + + // `from a import b, c as d[e]` — a subscript as an import target. + ParseErrorType::SimpleStatementsOnSameLine + if is_import_subscript_target(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + ( + ParseErrorType::OtherError("cannot use subscript as import target".to_owned()), + loc, + end_loc, + ) + } + // `"a "b" c"` — a string literal immediately followed by a bare word, + // i.e. an unintended split string. + ParseErrorType::SimpleStatementsOnSameLine + if looks_like_split_string(source_text, error.location) => + { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + ( + ParseErrorType::OtherError( + "invalid syntax. Is this intended to be part of the string?".to_owned(), + ), + loc, + end_loc, + ) + } + + // `case 42 as a.b` / `as (a, b)` / `as a()` etc. — an invalid + // capture target in a match `case`. CPython reports + // "cannot use {kind} as pattern target". + _ if let Some(msg) = match_pattern_target_message(source_text, error.location) => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); + (ParseErrorType::OtherError(msg), loc, end_loc) + } + + _ => { + let (loc, end_loc) = adjusted_locations(&source_code, error.location); (error.error, loc, end_loc) } }; @@ -241,6 +741,1258 @@ impl CompileError { } } +/// Compute the `(start, end)` `SourceLocation` for `range`, adjusting an +/// end that lands at column 1 of the following line back to the end of the +/// previous line. Used to keep the caret on the offending token rather than +/// wrapping to the next line. +fn adjusted_locations( + source_code: &ruff_source_file::SourceCode<'_, '_>, + range: ruff_text_size::TextRange, +) -> (SourceLocation, SourceLocation) { + let loc = source_code.source_location(range.start(), PositionEncoding::Utf8); + let mut end_loc = source_code.source_location(range.end(), PositionEncoding::Utf8); + if end_loc.character_offset.get() == 1 && end_loc.line > loc.line { + let prev_line_end = range.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); + } + (loc, end_loc) +} + +/// Convert ruff's wording (`'else' clause`, `'class' definition`, …) into +/// CPython's wording for indented-block errors. +fn normalize_indented_block_message(s: &str) -> String { + let mut msg = s.replace('`', "'"); + // Clause keywords that CPython calls "statement" instead of "clause"/"block". + for kw in &["else", "elif", "except", "finally"] { + msg = msg.replace(&format!("'{kw}' clause"), &format!("'{kw}' statement")); + } + // ruff says "`case` block"; CPython says "'case' statement". + msg = msg.replace("'case' block", "'case' statement"); + // CPython prints "class definition" (without quotes), not "'class' definition". + msg = msg.replace("'class' definition", "class definition"); + msg +} + +/// Detect whether the statement containing `lineno` uses `except*` rather +/// than `except`. +fn source_uses_except_star(source: &str, lineno: usize) -> bool { + let line = source + .split('\n') + .nth(lineno.saturating_sub(1)) + .unwrap_or(""); + line.trim_start().starts_with("except*") +} + +/// Find the line number of the statement keyword referenced by an +/// "Expected an indented block after `X` " message. +fn find_indented_block_keyword_line( + source: &str, + range: ruff_text_size::TextRange, + msg: &str, +) -> Option { + // Extract the keyword (the token between the backticks); when the + // message has no backticks (e.g. "after function definition"), look up + // the matching source keyword. + let keyword_owned; + let keyword: &str = if let Some(kw_start) = msg.find('`') { + let kw_start = kw_start + 1; + let kw_end = kw_start + msg[kw_start..].find('`')?; + &msg[kw_start..kw_end] + } else if msg.contains("function definition") { + keyword_owned = "def".to_string(); + &keyword_owned + } else if msg.contains("class definition") { + keyword_owned = "class".to_string(); + &keyword_owned + } else { + return None; + }; + let start: usize = range.start().into(); + // Search backward in the source for `` (whole word, possibly + // followed by whitespace, `:` or `(`). + let prefix = &source[..start]; + let needle = keyword.to_string(); + let mut search_from = prefix.len(); + while let Some(idx) = prefix[..search_from].rfind(&needle) { + // Ensure it is a whole-word match. + let before_ok = idx == 0 + || prefix + .as_bytes() + .get(idx.wrapping_sub(1)) + .is_none_or(|b| !(b.is_ascii_alphanumeric() || *b == b'_')); + let after_ok = prefix + .as_bytes() + .get(idx + needle.len()) + .is_none_or(|b| !(b.is_ascii_alphanumeric() || *b == b'_')); + if before_ok && after_ok { + // Count newlines before this index (1-indexed line number). + let lineno = source[..idx].chars().filter(|c| *c == '\n').count() + 1; + return Some(lineno); + } + if idx == 0 { + break; + } + search_from = idx; + } + None +} + +/// Detect a missing default value (`def f(a=, ...)`) or argument value +/// (`f(a=)`). Returns the matching CPython message if applicable. +fn missing_default_or_argument_value( + source: &str, + range: ruff_text_size::TextRange, +) -> Option { + let start: usize = range.start().into(); + let first = source.get(start..)?.chars().next()?; + if !matches!(first, ',' | ')' | ':') { + return None; + } + // Look backward, skipping whitespace, for `=` (and ensure it's not `==`). + let prefix_bytes = &source.as_bytes()[..start]; + let mut idx = prefix_bytes.len(); + while idx > 0 && prefix_bytes[idx - 1].is_ascii_whitespace() { + idx -= 1; + } + if idx == 0 || prefix_bytes[idx - 1] != b'=' { + return None; + } + // The character immediately before `=` must not be another `=` (would be + // a `==` token). + if idx >= 2 && prefix_bytes[idx - 2] == b'=' { + return None; + } + // Distinguish: in a function call `f(...)`, the closest unclosed `(` is + // preceded by an identifier (the callee). In a def/lambda parameter list, + // the `(` is preceded by `def IDENT` or the `lambda` keyword (no leading + // `(` for lambda). + let prefix = &source[..start]; + // Find the relevant opening token by tracking depth. + let mut depth = 0i32; + let mut open_idx = None; + for (i, c) in prefix.char_indices().rev() { + match c { + ')' | ']' | '}' => depth += 1, + '(' | '[' | '{' => { + if depth == 0 { + open_idx = Some((i, c)); + break; + } + depth -= 1; + } + _ => {} + } + } + let in_def_or_lambda = match open_idx { + Some((i, '(')) => { + let before = prefix[..i].trim_end(); + // `lambda` (no parens) or `def IDENT(...`. + if before.ends_with("lambda") { + true + } else { + let last_word = before + .rsplit_once(|c: char| c.is_whitespace()) + .map_or(before, |(_, w)| w); + let before_word = before.trim_end_matches(last_word).trim_end(); + before_word.ends_with("def") || before_word.ends_with("async def") + } + } + // `lambda x=, y: ...` has no surrounding `(`. + None => prefix.contains("lambda"), + _ => false, + }; + // `lambda x=: x` — an empty default immediately before the lambda body `:` + // is plain "invalid syntax" in CPython (only a `,`-terminated empty default, + // `lambda x=, y: …`, gets the "expected default value expression" hint). + if first == ':' { + return None; + } + Some(if in_def_or_lambda { + "expected default value expression".to_owned() + } else { + "expected argument value expression".to_owned() + }) +} + +/// Detect string literals with incompatible prefixes (e.g. `ub''`, `bf""`, +/// `tfu"…"`). CPython rejects these with "'X' and 'Y' prefixes are +/// incompatible". Returns `None` if there is no string literal at `range`. +fn incompatible_string_prefix_message( + source: &str, + range: ruff_text_size::TextRange, +) -> Option { + let start: usize = range.start().into(); + let first = source.get(start..)?.chars().next()?; + if first != '\'' && first != '"' { + return None; + } + // Walk back collecting `[a-zA-Z]+` characters as the prefix. + let prefix_bytes = source.as_bytes()[..start] + .iter() + .rev() + .take_while(|b| b.is_ascii_alphabetic()) + .copied() + .collect::>(); + if prefix_bytes.is_empty() { + return None; + } + // Reverse back to get source order. + let prefix: Vec = prefix_bytes + .into_iter() + .rev() + .map(|b| (b as char).to_ascii_lowercase()) + .collect(); + // Single prefix is fine. + if prefix.len() < 2 { + return None; + } + // A real string prefix uses only {b,r,f,u,t}; otherwise we are looking at + // a bare identifier (e.g. `data`, `about`) juxtaposed with a string. (No + // length cap: CPython still diagnoses long invalid runs like `turf"…"`.) + if !prefix + .iter() + .all(|c| matches!(c, 'b' | 'r' | 'f' | 'u' | 't')) + { + return None; + } + // The character preceding the prefix must not be a name continuation — + // otherwise we are looking at an identifier ending in prefix letters glued + // to a string. Inspect the full char (not a single byte) so multibyte + // identifier chars are handled too; `is_alphanumeric` mirrors Python rules. + let pre_idx = start - prefix.len(); + if source[..pre_idx] + .chars() + .next_back() + .is_some_and(|pc| pc.is_alphanumeric() || pc == '_') + { + return None; + } + + // Match CPython's algorithm in `Parser/lexer/lexer.c` — + // `maybe_raise_syntax_error_for_string_prefixes`. The checks fire in this + // fixed order; the first matching pair is the message. + let saw_u = prefix.contains(&'u'); + let saw_b = prefix.contains(&'b'); + let saw_r = prefix.contains(&'r'); + let saw_f = prefix.contains(&'f'); + let saw_t = prefix.contains(&'t'); + let pair = if saw_u && saw_b { + Some(("u", "b")) + } else if saw_u && saw_r { + Some(("u", "r")) + } else if saw_u && saw_f { + Some(("u", "f")) + } else if saw_u && saw_t { + Some(("u", "t")) + } else if saw_b && saw_f { + Some(("b", "f")) + } else if saw_b && saw_t { + Some(("b", "t")) + } else if saw_f && saw_t { + Some(("f", "t")) + } else { + // CPython treats `rr"…"` / `bb"…"` as a name-then-string error + // ("invalid syntax"), so fall through rather than fabricate a message. + None + }; + pair.map(|(a, b)| format!("'{a}' and '{b}' prefixes are incompatible")) +} + +/// Detect parenthesized parameters in a `def`/`lambda` parameter list +/// (e.g. `def f(x, (y, z), w)` or `lambda (x, y): None`). +fn parenthesized_param_message(source: &str, range: ruff_text_size::TextRange) -> Option { + let start: usize = range.start().into(); + // Only fires when the error points at `(`. + if source.get(start..)?.chars().next()? != '(' { + return None; + } + let prefix = &source[..start]; + + // Lambda parameter context: the nearest preceding `lambda` (possibly on an + // earlier line) with no `:` body-separator before the error → in its params. + if let Some(idx) = prefix.rfind("lambda") { + let after_lambda = &prefix[idx + "lambda".len()..]; + if !after_lambda.contains(':') { + return Some("Lambda expression parameters cannot be parenthesized".to_owned()); + } + } + // Function parameter context: the nearest preceding `def `; if the error + // `(` is still inside its parameter list (paren depth never returns to 0, + // possibly across multiple lines) → a parenthesized parameter. + if let Some(def_idx) = prefix.rfind("def ") + && let Some(open) = prefix[def_idx..].find('(') + { + let inner = &prefix[def_idx + open + 1..]; + let mut depth = 1i32; + for c in inner.chars() { + match c { + '(' => depth += 1, + ')' => depth -= 1, + _ => {} + } + if depth == 0 { + return None; + } + } + if depth >= 1 { + return Some("Function parameters cannot be parenthesized".to_owned()); + } + } + None +} + +/// Detect whether we are inside a subscript `[...]` whose current "argument" +/// starts with `*` (i.e. CPython's "Invalid star expression"). +fn is_invalid_star_in_subscript(source: &str, range: ruff_text_size::TextRange) -> bool { + if !is_inside_subscript(source, range) { + return false; + } + let start: usize = range.start().into(); + let prefix = &source[..start]; + // Walk back to the start of the current subscript "slot" (i.e. the most + // recent enclosing `[` or `,` at depth 0). + let mut depth = 0i32; + for (i, c) in prefix.char_indices().rev() { + match c { + ')' | ']' | '}' => depth += 1, + '(' | '{' => { + if depth > 0 { + depth -= 1; + } + } + '[' if depth == 0 => { + let slot = prefix[i + 1..].trim_start(); + return slot.starts_with('*'); + } + '[' => depth -= 1, + ',' if depth == 0 => { + let slot = prefix[i + 1..].trim_start(); + return slot.starts_with('*'); + } + _ => {} + } + } + false +} + +/// Detect `def f(.../*,...): ...` or `lambda .../*,...: None`: a `*` token +/// that follows `/` with no separating comma. +fn is_slash_star_in_params(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + // The token immediately before the `*` should be `/`. + if !prefix.trim_end().ends_with('/') { + return false; + } + // We must be inside a def/lambda parameter list. + let line_start = prefix.rfind('\n').map_or(0, |i| i + 1); + let line = &prefix[line_start..]; + line.contains("def ") || line.contains("lambda") +} + +/// Detect `f(**kwargs=...)`: an `=` token where ruff expected a comma, with +/// `**IDENT` immediately preceding. +fn is_kwarg_unpacking_assignment(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + // Strip a trailing identifier and look for `**`. + let trimmed = prefix.trim_end_matches(|c: char| c.is_ascii_alphanumeric() || c == '_'); + trimmed.trim_end().ends_with("**") +} + +/// Detect `[a, b for a, b in ...]` / `{a, b for a, b in ...}` — the comma +/// before `for` makes ruff stop at `Rsqb`/`Rbrace`. CPython's hint is +/// "did you forget parentheses around the comprehension target?". +fn is_unparen_comprehension_target(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + // Look back to the enclosing `[` / `{` and check there is a `,` at + // depth 0 between it and the error position. + let mut depth = 0i32; + let mut open_idx = None; + for (i, c) in prefix.char_indices().rev() { + match c { + ')' | ']' | '}' => depth += 1, + '(' | '[' | '{' => { + if depth == 0 { + open_idx = Some(i); + break; + } + depth -= 1; + } + _ => {} + } + } + let Some(open_idx) = open_idx else { + return false; + }; + if !matches!(prefix.as_bytes()[open_idx], b'[' | b'{') { + return false; + } + let inside = &prefix[open_idx + 1..]; + let mut d = 0i32; + for c in inside.chars() { + match c { + '(' | '[' | '{' => d += 1, + ')' | ']' | '}' => d -= 1, + ',' if d == 0 => return true, + _ => {} + } + } + false +} + +/// Detect that the current logical line begins with `raise `. +fn source_starts_with_raise(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + let line_start = prefix.rfind('\n').map_or(0, |i| i + 1); + prefix[line_start..].trim_start().starts_with("raise ") +} + +/// Detect a bare `*` (followed by `)`, `:`, or another argument) in a +/// function-call argument list. +fn is_bare_star_in_call(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + // Closest unclosed `(` should be preceded by an identifier (callee). + let mut depth = 0i32; + let mut open_idx = None; + for (i, c) in prefix.char_indices().rev() { + match c { + ')' | ']' | '}' => depth += 1, + '(' => { + if depth == 0 { + open_idx = Some(i); + break; + } + depth -= 1; + } + '[' | '{' => { + if depth == 0 { + return false; + } + depth -= 1; + } + _ => {} + } + } + let Some(open_idx) = open_idx else { + return false; + }; + let before_paren = prefix[..open_idx] + .chars() + .rev() + .find(|c| !c.is_whitespace()); + if !matches!(before_paren, Some(c) if c.is_ascii_alphanumeric() || c == '_' || c == ')' || c == ']') + { + return false; + } + // The current argument slot (after the enclosing `(` or the last depth-0 + // comma) must begin with a bare `*`; a `*` after an operand (`f(a *)`, a + // binary multiply) is plain "invalid syntax". + let args = &prefix[open_idx + 1..]; + let mut d = 0i32; + let mut slot_start = 0; + for (j, c) in args.char_indices() { + match c { + '(' | '[' | '{' => d += 1, + ')' | ']' | '}' => d -= 1, + ',' if d == 0 => slot_start = j + 1, + _ => {} + } + } + slot_starts_with_bare_star(&args[slot_start..]) +} + +/// Detect a bare single `*` as the leading element of a set/dict display +/// `{ ... }` or a non-call parenthesised group/tuple `( ... )` — CPython's +/// "Invalid star expression" (`{*}`, `{*, 1}`, `(*)`, `(*,)`, `(*, 1)`). The +/// star must START the (leading) slot: a star following an operand (`{1 *}`, a +/// binary multiply), a comma (`{1, *}`), or a dict colon (`{1: *}`), as well as +/// a double star (`{**}`), a subscript/list (`[*]`), or a call (`f(*)`) are +/// excluded (the last two are handled by `is_invalid_star_in_subscript` / +/// `is_bare_star_in_call`). +fn is_bare_star_first_in_group(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + // Walk back to the nearest depth-0 opener; the leading slot (opener..error) + // must begin with a bare `*`. Bail at a depth-0 `,`/`:` (non-leading slot / + // dict value) or `[` (subscript/list, handled by `is_invalid_star_in_subscript`). + let mut depth = 0i32; + for (i, c) in prefix.char_indices().rev() { + match c { + ')' | ']' | '}' => depth += 1, + ',' | ':' if depth == 0 => return false, + '[' if depth == 0 => return false, + '{' if depth == 0 => return slot_starts_with_bare_star(&prefix[i + 1..]), + '(' if depth == 0 => { + // Only a non-call group: the token before `(` must not be a + // callee (identifier / `)` / `]`). + let before = prefix[..i].chars().rev().find(|c| !c.is_whitespace()); + if matches!( + before, + Some(c) if c.is_ascii_alphanumeric() || c == '_' || c == ')' || c == ']' + ) { + return false; + } + return slot_starts_with_bare_star(&prefix[i + 1..]); + } + '(' | '{' | '[' => depth -= 1, + _ => {} + } + } + false +} + +/// Whether `slot` (the text from an opener to the error) begins with a single +/// bare `*` (not `**`) — i.e. a leading starred element with no operand. +fn slot_starts_with_bare_star(slot: &str) -> bool { + let s = slot.trim_start(); + s.starts_with('*') && !s.starts_with("**") +} + +/// Detect bad target in `except[*] T as :`. Returns the matching CPython +/// message (e.g. "cannot use except statement with attribute") or `None`. +fn except_as_bad_target_message(source: &str, range: ruff_text_size::TextRange) -> Option { + let start: usize = range.start().into(); + let prefix = &source[..start]; + // Find the most recent `except` / `except*` keyword on the logical line. + let line_start = prefix.rfind('\n').map_or(0, |i| i + 1); + let logical = &prefix[line_start..]; + let kw = if logical.contains("except*") { + "except*" + } else if logical.contains("except ") || logical.trim_start() == "except" { + "except" + } else { + return None; + }; + // Require an `as ` before the error position. + let as_idx = prefix.rfind(" as ")?; + // Pull out everything between `as` and the error. + let after_as = prefix[as_idx + 4..].trim(); + // The first character at error.location signals the kind of bad target. + let at_error = source.get(start..)?.chars().next()?; + let kind = if !after_as.is_empty() { + // ruff parsed `as IDENT`, then saw `.` / `[` (attribute / subscript). + match at_error { + '.' => "attribute", + '[' => "subscript", + _ => return None, + } + } else { + // ruff didn't even reach a name — bad target was `(`, `[`, literal, … + match at_error { + '(' => "tuple", + '[' => "list", + '0'..='9' | '\'' | '"' => "literal", + _ => return None, + } + }; + Some(format!("cannot use {kw} statement with {kind}")) +} + +/// Detect `[x for x if ...]` / `(x for x if ...)` / `{... if ...}` — i.e. +/// a comprehension where `if` was used in place of `in` after the for-target. +fn is_in_comprehension_if(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + // Walk back to find the *outermost* unclosed bracket that contains the + // error position. Any of `(`, `[`, `{` qualifies for a comprehension. + let mut depth = 0i32; + let mut open_idx = None; + for (i, c) in prefix.char_indices().rev() { + match c { + ')' | ']' | '}' => depth += 1, + '(' | '[' | '{' => { + if depth == 0 { + open_idx = Some(i); + break; + } + depth -= 1; + } + _ => {} + } + } + let Some(open_idx) = open_idx else { + return false; + }; + let inside = &prefix[open_idx + 1..]; + inside.contains(" for ") && inside.matches(" for ").count() == inside.matches(" if ").count() +} + +/// Detect a "tight" parenthesised starred expression (`(*x)`) used inside +/// subscript brackets, e.g. `A[:(*b)]` or `A[(*b):]`. +/// More complex constructs like `A[(*b:*b)]` are intentionally NOT matched — +/// they keep CPython's "invalid syntax" message. +fn is_paren_starred_in_subscript(source: &str, range: ruff_text_size::TextRange) -> bool { + if !is_inside_subscript(source, range) { + return false; + } + let start: usize = range.start().into(); + // The character immediately before `*` should be `(`. + if !source[..start].trim_end().ends_with('(') { + return false; + } + // The character immediately after the starred name should be `)`, + // ensuring the parens tightly wrap `*X`. + let end: usize = range.end().into(); + let rest = source.get(end..).unwrap_or(""); + rest.trim_start().starts_with(')') +} + +/// Detect whether `range` sits inside subscript brackets `[ ... ]` at any +/// level of enclosing nesting. +fn is_inside_subscript(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + let mut depth = 0i32; + for c in prefix.chars().rev() { + match c { + ')' | ']' | '}' => depth += 1, + '(' | '{' => { + if depth > 0 { + depth -= 1; + } + // depth == 0 ⇒ unmatched opening of a *different* kind; keep + // walking to see if `[` further out encloses us. + } + '[' => { + if depth == 0 { + return true; + } + depth -= 1; + } + _ => {} + } + } + false +} + +/// Detect whether `range` points at a position that follows a `:` inside a +/// `{ ... }` dict literal (i.e. a missing or starred dict value). +fn is_dict_value_position(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + // Find the enclosing `{`. + let mut depth = 0i32; + let mut found_brace = false; + for c in prefix.chars().rev() { + match c { + '}' | ')' | ']' => depth += 1, + '{' => { + if depth == 0 { + found_brace = true; + break; + } + depth -= 1; + } + '(' | '[' => { + if depth == 0 { + return false; + } + depth -= 1; + } + _ => {} + } + } + if !found_brace { + return false; + } + // The most recent `:` (at depth 0 inside the `{ ... }`) must appear + // before any `,` at the same depth, meaning we are in a `key:_value`. + let mut d = 0i32; + let mut last_relevant = None; + for c in prefix.chars().rev() { + match c { + ')' | ']' | '}' => d += 1, + '(' | '[' => d -= 1, + '{' if d == 0 => break, + '{' => d -= 1, + ',' | ':' if d == 0 => { + last_relevant = Some(c); + break; + } + _ => {} + } + } + matches!(last_relevant, Some(':')) +} + +/// Whether the dict value position (after the nearest depth-0 `:` inside the +/// enclosing `{`) is empty — the next non-whitespace character is the `,`/`}` +/// delimiter (or end of input): `{1:}`, `{1: }`, `{1: ,}`. Only then does +/// CPython emit "expression expected after dictionary key and ':'"; a value +/// that is present but fails to parse (`{1: *}`, `{1: 2*}`, `{1: +}`) is plain +/// "invalid syntax". Used together with [`is_dict_value_position`], which +/// guarantees the nearest depth-0 separator is the `:`. +fn dict_value_is_empty(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + let mut depth = 0i32; + for (i, c) in prefix.char_indices().rev() { + match c { + ')' | ']' | '}' => depth += 1, + ',' if depth == 0 => return false, + ':' if depth == 0 => { + return matches!( + source[i + 1..].trim_start().chars().next(), + None | Some(',' | '}') + ); + } + '(' | '[' | '{' if depth == 0 => return false, + '(' | '[' | '{' => depth -= 1, + _ => {} + } + } + false +} + +/// Detect `X=Y` in tuple/list/set literals (i.e. not a function call). +/// CPython suggests "invalid syntax. Maybe you meant '==' or ':=' instead of +/// '='?". +fn collection_kwarg_message(source: &str, range: ruff_text_size::TextRange) -> Option { + let start: usize = range.start().into(); + let end: usize = range.end().into(); + // Find the surrounding `(`, `[`, or `{`. + let prefix = &source[..start]; + let mut depth = 0i32; + let mut opening = None; + for (i, c) in prefix.char_indices().rev() { + match c { + ')' | ']' | '}' => depth += 1, + '(' | '[' | '{' => { + if depth == 0 { + opening = Some((i, c)); + break; + } + depth -= 1; + } + _ => {} + } + } + let (open_idx, open_char) = opening?; + // Excluding function-call parens: the char before `(` is a name/`)`/`]`. + if open_char == '(' { + let prev = prefix[..open_idx] + .chars() + .rev() + .find(|c| !c.is_whitespace()); + if let Some(prev) = prev + && (prev.is_ascii_alphanumeric() || prev == '_' || prev == ')' || prev == ']') + { + return None; + } + } + // Look for a `IDENT = VALUE` chunk inside the display. The `=` must be a + // bare assignment, NOT part of `:=` / `==` / `!=` / `<=` / `>=` — otherwise + // a parenthesised walrus like `(b := 2)` would be misread as a kwarg. + let seg = &source[open_idx + 1..end.min(source.len())]; + seg.split(',') + .find_map(|chunk| bare_assignment_message(chunk.trim())) +} + +/// If `chunk` is a bare `LHS = …` (a single `=`, not `:=`/`==`/`!=`/`<=`/`>=`) +/// inside a tuple/list/set display, return CPython's message for that misplaced +/// `=`, classified by the LHS kind; otherwise `None`. +fn bare_assignment_message(chunk: &str) -> Option { + let bytes = chunk.as_bytes(); + for (i, &b) in bytes.iter().enumerate() { + if b != b'=' { + continue; + } + if matches!( + i.checked_sub(1).map(|p| bytes[p]), + Some(b':' | b'=' | b'!' | b'<' | b'>') + ) { + continue; + } + if bytes.get(i + 1) == Some(&b'=') { + continue; + } + let lhs = chunk[..i].trim(); + let mut lhs_chars = lhs.chars(); + let msg = match lhs_chars.next() { + // A real identifier (Unicode, e.g. `α`): the `==`/`:=` hint — unless + // it's a keyword literal, which CPython reports as plain "invalid + // syntax". + Some(first) + if (first.is_alphabetic() || first == '_') + && lhs_chars.all(|c| c.is_alphanumeric() || c == '_') => + { + if matches!(lhs, "True" | "False" | "None") { + Some("invalid syntax".to_owned()) + } else { + Some("invalid syntax. Maybe you meant '==' or ':=' instead of '='?".to_owned()) + } + } + // A number/string literal target → "cannot assign to literal …". + Some(first) if first.is_ascii_digit() || first == '"' || first == '\'' => Some( + "cannot assign to literal here. Maybe you meant '==' instead of '='?".to_owned(), + ), + _ => None, + }; + if msg.is_some() { + return msg; + } + } + None +} + +/// Detect whether `range` covers the LHS expression of a bad +/// `f(=)` keyword argument. +fn is_call_keyword_assignment(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let end: usize = range.end().into(); + // The error must sit inside a call: there must be an unclosed `(` before it. + let prefix = &source[..start]; + let depth = prefix.chars().fold(0i32, |d, c| match c { + '(' => d + 1, + ')' => d - 1, + _ => d, + }); + if depth <= 0 { + return false; + } + // The character immediately after the bad LHS (skipping whitespace) must + // be `=` (but not `==` — that's a comparison, not an assignment). + let mut chars = source[end..].chars(); + while let Some(c) = chars.clone().next() { + if c.is_whitespace() { + chars.next(); + } else { + break; + } + } + let next = chars.next(); + let next2 = chars.next(); + matches!(next, Some('=')) && !matches!(next2, Some('=')) +} + +/// Pull out the LHS expression of a bad keyword-argument assignment using +/// the error range as the LHS span. +fn call_keyword_lhs(source: &str, range: ruff_text_size::TextRange) -> &str { + let start: usize = range.start().into(); + let end: usize = range.end().into(); + source[start..end].trim() +} + +/// If `range` covers a statement keyword (`pass`, `break`, `continue`, etc.) +/// that appears in expression position before or after `if`/`else` in a +/// ternary expression, return the matching CPython error message. +fn ternary_statement_keyword_message( + source: &str, + range: ruff_text_size::TextRange, +) -> Option { + const STMT_KEYWORDS: &[&str] = &[ + "pass", "break", "continue", "return", "raise", "import", "from", "global", "nonlocal", + "del", "assert", "with", "try", "while", "for", "if", "elif", "else", "class", "def", + "yield", + ]; + let start: usize = range.start().into(); + let end: usize = range.end().into(); + let slice = source[start..end].trim(); + // Accept either an exact match (`pass`) or a keyword + arguments + // (`return 2`, `yield 2`, `raise Exception('a')`, `import ast`). + let first_word = slice + .split(|c: char| c.is_whitespace() || c == '(') + .next() + .unwrap_or(""); + if !STMT_KEYWORDS.contains(&first_word) { + return None; + } + // Look backward to find `if ` or `else ` on the same logical statement. + let before = &source[..start]; + let trailing = before.trim_end(); + if trailing.ends_with(" else") || trailing.ends_with("else") { + // After `else` in a ternary: ` if else pass`. + // CPython: "expected expression after 'else', but statement is given". + return Some("expected expression after 'else', but statement is given".to_owned()); + } + // Look forward to see whether `if` follows the keyword on the same line. + let after = source[end..].split('\n').next().unwrap_or("").trim_start(); + if after.starts_with("if ") || after == "if" { + // Before `if` in a ternary: ` = pass if else `. + // CPython only emits this hint for the bare simple statements + // pass/break/continue; other keywords (`return if …`, `import if …`) + // fail earlier and are plain "invalid syntax". + if matches!(first_word, "pass" | "break" | "continue") { + return Some("expected expression before 'if', but statement is given".to_owned()); + } + } + None +} + +/// Detect whether the `=` at `range` is the head of an `if X = Y:` / +/// `while X = Y:` / `elif X = Y:` clause that CPython points out with the +/// "Maybe you meant '==' or ':=' instead of '='?" message. +fn precedes_if_or_while(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let line_start = source[..start].rfind('\n').map_or(0, |i| i + 1); + let line_before = &source[line_start..start]; + let trimmed = line_before.trim_start(); + trimmed.starts_with("if ") || trimmed.starts_with("elif ") || trimmed.starts_with("while ") +} + +/// Strip the leading `if `/`elif `/`while ` keyword from `prefix` and return +/// the candidate LHS expression up to (but not including) `=`. +fn lhs_after_keyword(prefix: &str) -> &str { + let trimmed = prefix.trim_start(); + for kw in ["elif ", "while ", "if "] { + if let Some(rest) = trimmed.strip_prefix(kw) { + return rest.trim(); + } + } + trimmed.trim() +} + +/// Detect the legacy `import X from Y` form where the user likely meant +/// `from Y import X`. +fn is_old_import_from(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let line_start = source[..start].rfind('\n').map_or(0, |i| i + 1); + let line_before = &source[line_start..start]; + let trimmed = line_before.trim_start(); + if !trimmed.starts_with("import ") { + return false; + } + // The error must be on or before the `from` keyword in the rest of the line. + let after = source[start..] + .split('\n') + .next() + .unwrap_or("") + .trim_start(); + after.starts_with("from ") || source[start..].starts_with("from") +} + +/// Detect whether `range` points to a `.`, `[`, or `(` that continues an +/// `import X as Y` / `from X import Y as Z` clause. CPython rejects these +/// with "cannot use {attribute,subscript,function call} as import target". +fn is_import_target_continuation(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let Some(first) = source[start..].chars().next() else { + return false; + }; + if !matches!(first, '.' | '[' | '(') { + return false; + } + // Walk back to the start of the current physical line. + let line_start = source[..start].rfind('\n').map_or(0, |i| i + 1); + let prefix = &source[line_start..start]; + + // For a multi-line `from a import (\n b as f()\n)` form the `from` / + // `import` keyword lives on a previous physical line. Allow that case too + // by also searching the whole text before the error. + let in_import_line = prefix.contains("import ") + || prefix.starts_with("import") + || prefix.trim_start().starts_with("import"); + let in_from_block = source[..start] + .rfind("from ") + .is_some_and(|idx| source[idx..start].contains("import")); + if !(in_import_line || in_from_block) { + return false; + } + + // Require an `as ` sequence immediately before the error. + let trimmed = prefix.trim_end(); + // strip the trailing identifier + let after_ident = trimmed.trim_end_matches(|c: char| c.is_alphanumeric() || c == '_'); + after_ident.trim_end().ends_with(" as") || after_ident.trim_end().ends_with("\tas") +} + +/// Return the next non-whitespace character after `range` in `source`, if any. +fn next_nonspace_after(source: &str, range: ruff_text_size::TextRange) -> Option { + let end: usize = range.end().into(); + source[end..] + .chars() + .find(|c| !c.is_whitespace() && *c != '\\') +} + +/// Whether the logical line containing `range` is a `for` / `async for` header +/// (so an invalid target there should read "invalid syntax", not "cannot +/// assign to …"). +fn is_for_loop_target(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let line_start = source[..start].rfind('\n').map_or(0, |i| i + 1); + let line = source[line_start..start].trim_start(); + line.starts_with("for ") || line.starts_with("async for ") +} + +/// Whether the word immediately following `range` (skipping `*`/whitespace) is +/// a Python keyword — used to tell `def f(*None)` (→ "invalid syntax") from a +/// genuine bare `*` (→ "named arguments must follow bare *"). +fn next_word_is_keyword(source: &str, range: ruff_text_size::TextRange) -> bool { + let end: usize = range.end().into(); + let rest = source[end..].trim_start_matches(|c: char| c.is_whitespace() || c == '*'); + let word: String = rest + .chars() + .take_while(|c| c.is_alphanumeric() || *c == '_') + .collect(); + matches!( + word.as_str(), + "None" + | "True" + | "False" + | "and" + | "or" + | "not" + | "if" + | "else" + | "lambda" + | "yield" + | "await" + | "import" + | "from" + | "class" + | "def" + | "return" + | "pass" + | "in" + | "is" + ) +} + +/// Whether `s` is an augmented-assignment operator (`+=`, `**=`, `<<=`, …) as +/// opposed to a comparison/assignment (`==`, `!=`, `<=`, `>=`, `=`). +fn is_augassign_op(s: &str) -> bool { + s.len() >= 2 && s.ends_with('=') && !matches!(s, "==" | "!=" | "<=" | ">=" | ":=") +} + +/// Detect `from a import b, c as d[e]` — a subscript (`[`) right after an +/// `as NAME` inside an `import` statement. +fn is_import_subscript_target(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + if !source[start..].trim_start().starts_with('[') { + return false; + } + let line_start = source[..start].rfind('\n').map_or(0, |i| i + 1); + let line = &source[line_start..start]; + if !(line.trim_start().starts_with("import ") || line.contains(" import ")) { + return false; + } + // The token before `[` must be `as `. + let before = line.trim_end(); + let after_ident = before.trim_end_matches(|c: char| c.is_alphanumeric() || c == '_'); + after_ident.trim_end().ends_with(" as") +} + +/// Detect the `STRING WORD STRING` shape of an unintended split string, e.g. +/// `"a "b" c"` — a closing quote immediately before `range` AND another quote +/// later on the line. (A plain `STRING WORD` adjacency stays "invalid syntax".) +fn looks_like_split_string(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let preceded = matches!( + source[..start].trim_end().chars().next_back(), + Some('"' | '\'') + ); + if !preceded { + return false; + } + let end: usize = range.end().into(); + let rest_of_line = source[end..].split('\n').next().unwrap_or(""); + rest_of_line.contains('"') || rest_of_line.contains('\'') +} + +/// Whether `range` sits inside a match `case ` header. Used to keep +/// parse errors there as plain "invalid syntax" instead of borrowing +/// expression-context messages ("forgot a comma", "':' expected after +/// dictionary key"). Requires a `case ` line (soft keyword, not `case = …`) +/// with an enclosing lower-indent `match ` block, so module-level `case (a b)` +/// (a call) is left untouched. +fn is_in_case_pattern(source: &str, range: ruff_text_size::TextRange) -> bool { + let start: usize = range.start().into(); + let prefix = &source[..start]; + // Find the nearest `case ` keyword starting a current/preceding line (the + // pattern may span multiple lines inside an unclosed bracket). + let mut search = prefix.len(); + loop { + let line_start = prefix[..search].rfind('\n').map_or(0, |i| i + 1); + let line = &prefix[line_start..]; + let trimmed = line.trim_start(); + if let Some(rest) = trimmed.strip_prefix("case ") { + // `case = …` is an assignment to a var named `case`, not a pattern. + if rest.trim_start().starts_with('=') { + return false; + } + // The error must still be in the pattern: no top-level `:` (which + // ends the header) between this `case` and the error. Otherwise the + // error is in the case BODY (a normal expression context). + let case_kw = line_start + (line.len() - trimmed.len()); + let mut depth = 0i32; + for c in source[case_kw..start].chars() { + match c { + '(' | '[' | '{' => depth += 1, + ')' | ']' | '}' => depth -= 1, + ':' if depth == 0 => return false, + _ => {} + } + } + // Require an enclosing lower-indent `match ` block (so module-level + // `case (a b)` — a call — is left untouched). + let case_indent = line.chars().take_while(|c| c.is_whitespace()).count(); + return source[..line_start].split('\n').any(|prev| { + let indent = prev.chars().take_while(|c| c.is_whitespace()).count(); + indent < case_indent && prev.trim_start().starts_with("match ") + }); + } + if line_start == 0 { + return false; + } + search = line_start - 1; + } +} + +/// Detect an invalid capture target in a match `case … as ` (e.g. +/// `case 42 as a.b`, `as (a, b)`, `as a()`), returning CPython's +/// "cannot use {kind} as pattern target". Only fires when the text after the +/// last ` as ` parses as a non-`Name` expression, so a valid `case P as name:` +/// (a plain identifier) is left untouched. +fn match_pattern_target_message(source: &str, range: ruff_text_size::TextRange) -> Option { + let start: usize = range.start().into(); + let prefix = &source[..start]; + // Require a `case … as` header: the last ` as ` must be preceded by a + // `case ` with no intervening `:` (which would end the case header). + let as_pos = prefix.rfind(" as ")?; + let case_pos = prefix[..as_pos].rfind("case ")?; + if prefix[case_pos..as_pos].contains(':') { + return None; + } + // Extract the target text after ` as `, up to the case `:` / `|` / `,` at + // bracket-depth 0, a bracket that closes an enclosing group, or newline. + let after = &source[as_pos + 4..]; + let mut depth = 0i32; + let mut end = after.len(); + for (i, c) in after.char_indices() { + match c { + '(' | '[' | '{' => depth += 1, + ')' | ']' | '}' => { + if depth == 0 { + end = i; + break; + } + depth -= 1; + } + ':' | '|' | ',' if depth == 0 => { + end = i; + break; + } + '\n' => { + end = i; + break; + } + _ => {} + } + } + let target = after[..end].trim(); + if target.is_empty() { + return None; + } + // A plain name is a valid capture target; only non-`Name` exprs are errors. + match *parser::parse_expression(target).ok()?.syntax().body { + ast::Expr::Name(_) => None, + ref e => Some(format!( + "cannot use {} as pattern target", + expr_kind_name(e) + )), + } +} + +/// Map an [`ast::Expr`] to the human-readable name CPython uses in syntax +/// error messages (mirrors CPython's expression-kind names in +/// `Parser/action_helpers.c`). +fn expr_kind_name(expr: &ast::Expr) -> &'static str { + match expr { + ast::Expr::Subscript(_) => "subscript", + ast::Expr::Starred(_) => "starred", + ast::Expr::Name(_) => "name", + ast::Expr::List(_) => "list", + ast::Expr::Tuple(_) => "tuple", + ast::Expr::Lambda(_) => "lambda", + ast::Expr::Call(_) => "function call", + ast::Expr::BoolOp(_) | ast::Expr::BinOp(_) | ast::Expr::UnaryOp(_) => "expression", + ast::Expr::Generator(_) => "generator expression", + ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) => "yield expression", + ast::Expr::Await(_) => "await expression", + ast::Expr::ListComp(_) => "list comprehension", + ast::Expr::SetComp(_) => "set comprehension", + ast::Expr::DictComp(_) => "dict comprehension", + ast::Expr::Dict(_) => "dict literal", + ast::Expr::Set(_) => "set display", + ast::Expr::FString(_) => "f-string expression", + ast::Expr::TString(_) => "t-string expression", + ast::Expr::Compare(_) => "comparison", + ast::Expr::If(_) => "conditional expression", + ast::Expr::Attribute(_) => "attribute", + ast::Expr::Named(_) => "named expression", + ast::Expr::NoneLiteral(_) => "None", + ast::Expr::EllipsisLiteral(_) => "ellipsis", + ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => { + if *value { + "True" + } else { + "False" + } + } + ast::Expr::NumberLiteral(_) | ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) => { + "literal" + } + ast::Expr::Slice(_) => "slice", + ast::Expr::IpyEscapeCommand(_) => "expression", + } +} + +/// CPython's message for ` = ` where `` is an invalid +/// assignment target. Whether the helpful `here. Maybe you meant '==' instead +/// of '='?` suffix applies depends on both the kind of target and whether the +/// target is the whole LHS (followed by `=`) or a sub-element of a tuple/list +/// target (followed by `,` / `)` / `]` / etc.). +fn assign_target_message(expr: &ast::Expr, expr_str: &str, followed_by_eq: bool) -> String { + let here_suffix = " here. Maybe you meant '==' instead of '='?"; + let with_suffix = |base: &str| -> String { + if followed_by_eq { + format!("{base}{here_suffix}") + } else { + base.to_owned() + } + }; + match expr { + ast::Expr::Call(_) => with_suffix("cannot assign to function call"), + ast::Expr::BinOp(_) | ast::Expr::UnaryOp(_) => with_suffix("cannot assign to expression"), + // CPython always names a bool-op target "expression" (no `=` gating). + ast::Expr::BoolOp(_) => "cannot assign to expression".to_owned(), + ast::Expr::Compare(_) if followed_by_eq => "cannot assign to comparison".to_owned(), + ast::Expr::If(_) => "cannot assign to conditional expression".to_owned(), + ast::Expr::Generator(_) => "cannot assign to generator expression".to_owned(), + ast::Expr::Lambda(_) => "cannot assign to lambda".to_owned(), + ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) => { + "assignment to yield expression not possible".to_owned() + } + ast::Expr::Await(_) => "cannot assign to await expression".to_owned(), + ast::Expr::ListComp(_) => "cannot assign to list comprehension".to_owned(), + ast::Expr::SetComp(_) => "cannot assign to set comprehension".to_owned(), + ast::Expr::DictComp(_) => "cannot assign to dict comprehension".to_owned(), + ast::Expr::Set(_) => with_suffix("cannot assign to set display"), + ast::Expr::Dict(_) => with_suffix("cannot assign to dict literal"), + ast::Expr::FString(_) => with_suffix("cannot assign to f-string expression"), + ast::Expr::TString(_) => with_suffix("cannot assign to t-string expression"), + ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) | ast::Expr::NumberLiteral(_) => { + with_suffix("cannot assign to literal") + } + ast::Expr::EllipsisLiteral(_) => with_suffix("cannot assign to ellipsis"), + ast::Expr::NoneLiteral(_) => "cannot assign to None".to_owned(), + ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => { + if *value { + "cannot assign to True".to_owned() + } else { + "cannot assign to False".to_owned() + } + } + ast::Expr::Attribute(_) => with_suffix("cannot assign to attribute"), + _ => format!("cannot assign to {expr_str}"), + } +} + /// Find the last unclosed opening bracket in source code. /// Returns the bracket character and its byte offset, or None if all brackets are balanced. fn find_unclosed_bracket(source: &str) -> Option<(char, usize)> { diff --git a/crates/vm/src/stdlib/_ast.rs b/crates/vm/src/stdlib/_ast.rs index 57da9b3c3a8..ab57c3fe4f4 100644 --- a/crates/vm/src/stdlib/_ast.rs +++ b/crates/vm/src/stdlib/_ast.rs @@ -369,21 +369,15 @@ pub(crate) fn parse( target_version: Option, type_comments: bool, ) -> Result { - let source_file = SourceFileBuilder::new("".to_owned(), source.to_owned()).finish(); + let source_file = SourceFileBuilder::new("".to_owned(), source.to_owned()).finish(); let mut options = parser::ParseOptions::from(mode); let target_version = target_version.unwrap_or(ast::PythonVersion::PY314); options = options.with_target_version(target_version); - let parsed = parser::parse(source, options).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(), - is_unclosed_bracket: false, - } - })?; + // Route parse errors through `from_ruff_parse_error` so the `ast.parse()` / + // `compile(..., PyCF_ONLY_AST)` path gets the same CPython-aligned messages + // as the normal exec/compile path (which goes through `_compile`). + let parsed = parser::parse(source, options) + .map_err(|parse_error| CompileError::from_ruff_parse_error(parse_error, &source_file))?; if let Some(error) = parsed.unsupported_syntax_errors().first() { let range = text_range_to_source_range(&source_file, error.range()); @@ -476,17 +470,10 @@ pub(crate) fn parse_func_type( let right = source[split_at + 2..].trim(); let parse_expr = |expr_src: &str| -> Result { - let source_file = SourceFileBuilder::new("".to_owned(), expr_src.to_owned()).finish(); + let source_file = + SourceFileBuilder::new("".to_owned(), expr_src.to_owned()).finish(); let parsed = parser::parse_expression(expr_src).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(), - is_unclosed_bracket: false, - } + CompileError::from_ruff_parse_error(parse_error, &source_file) })?; Ok(*parsed.into_syntax().body) }; diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index cfb9562b793..266ea4df0f0 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -1007,8 +1007,8 @@ mod builtins { modulus, } = args; let modulus = modulus - .as_ref() - .map_or_else(|| vm.ctx.none.as_object(), |m| m); + .as_deref() + .unwrap_or_else(|| vm.ctx.none.as_object()); vm._pow(&x, &y, modulus) } diff --git a/crates/vm/src/vm/vm_new.rs b/crates/vm/src/vm/vm_new.rs index 3b50c25695f..51dadccdce9 100644 --- a/crates/vm/src/vm/vm_new.rs +++ b/crates/vm/src/vm/vm_new.rs @@ -25,6 +25,24 @@ use crate::{ vm::VirtualMachine, }; +/// Recognise [`ParseErrorType::OtherError`] messages whose CPython equivalent +/// preserves the initial uppercase letter, so we can opt out of the generic +/// lowercase-first-letter step applied to default ruff messages. +#[cfg(feature = "parser")] +fn starts_with_uppercase_message(s: &str) -> bool { + [ + "Did you mean to use 'from ... import ...' instead?", + "Function parameters cannot be parenthesized", + "Lambda expression parameters cannot be parenthesized", + "Generator expression must be parenthesized", + "Invalid star expression", + "Type parameter list cannot be empty", + "Star import must be the only import", + "Yield expression cannot be used here", + ] + .contains(&s) +} + macro_rules! define_exception_fn { ( fn $fn_name:ident, $attr:ident, $python_repr:ident @@ -117,8 +135,6 @@ impl SyntaxErrorInfo { "invalid syntax".into() } - ParseErrorType::InvalidDeleteTarget => "invalid syntax".into(), - ParseErrorType::Lexical(LexicalErrorType::LineContinuationError) => { "unexpected character after line continuation character".into() } @@ -153,10 +169,6 @@ impl SyntaxErrorInfo { "parameter without a default follows parameter with a default".into() } - ParseErrorType::VarParameterWithDefault => { - "var-positional argument cannot have default value".into() - } - ParseErrorType::PositionalAfterKeywordArgument => { "positional argument follows keyword argument".into() } @@ -258,6 +270,11 @@ impl SyntaxErrorInfo { r#"cannot have both 'except' and 'except*' on the same 'try'"#.into() } + // Messages that intentionally start with an uppercase letter + // (CPython preserves case here). Override the unconditional + // lowercase done above. + ParseErrorType::OtherError(s) if starts_with_uppercase_message(s) => s.clone(), + _ => return, }; diff --git a/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs b/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs index 4219b698753..c73d21308ed 100644 --- a/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs +++ b/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs @@ -1,5 +1,9 @@ use rustpython_vm::Interpreter; +// These are resolved at runtime from the host environment (see the wasmer +// `imports! { "env" => { … } }` in ../../wasm-runtime/src/main.rs). The +// `wasm_import_module` link attribute marks them as wasm imports so the linker +// emits them in the import section instead of failing with "undefined symbol". #[link(wasm_import_module = "env")] unsafe extern "C" { fn kv_get(kp: i32, kl: i32, vp: i32, vl: i32) -> i32;