Skip to content

Commit f584b66

Browse files
authored
Expand Semantic Syntax Coverage (#17725)
Re: #17526 ## Summary Adds tests to red knot and `linter.rs` for the semantic syntax. Specifically add tests for `ReboundComprehensionVariable`, `DuplicateTypeParameter`, and `MultipleCaseAssignment`. Refactor the `test_async_comprehension_in_sync_comprehension` → `test_semantic_error` to be more general for all semantic syntax test cases. ## Test Plan This is a test. ## Question I'm happy to contribute more tests the coming days. Should that happen here or should we merge this PR such that the refactor `test_async_comprehension_in_sync_comprehension` → `test_semantic_error` is available on main and others can chime in, too?
1 parent ad1a8da commit f584b66

10 files changed

+128
-10
lines changed

crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,62 @@ async def g():
130130
(x async for x in g())
131131
```
132132

133+
## Rebound comprehension variable
134+
135+
Walrus operators cannot rebind variables already in use as iterators:
136+
137+
```py
138+
# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable"
139+
[x := 2 for x in range(10)]
140+
141+
# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable"
142+
{y := 5 for y in range(10)}
143+
```
144+
145+
## Multiple case assignments
146+
147+
Variable names in pattern matching must be unique within a single pattern:
148+
149+
```toml
150+
[environment]
151+
python-version = "3.10"
152+
```
153+
154+
```py
155+
x = [1, 2]
156+
match x:
157+
# error: [invalid-syntax] "multiple assignments to name `a` in pattern"
158+
case [a, a]:
159+
pass
160+
case _:
161+
pass
162+
163+
d = {"key": "value"}
164+
match d:
165+
# error: [invalid-syntax] "multiple assignments to name `b` in pattern"
166+
case {"key": b, "other": b}:
167+
pass
168+
```
169+
170+
## Duplicate type parameter
171+
172+
Type parameter names must be unique in a generic class or function definition:
173+
174+
```toml
175+
[environment]
176+
python-version = "3.12"
177+
```
178+
179+
```py
180+
# error: [invalid-syntax] "duplicate type parameter"
181+
class C[T, T]:
182+
pass
183+
184+
# error: [invalid-syntax] "duplicate type parameter"
185+
def f[X, Y, X]():
186+
pass
187+
```
188+
133189
## `await` outside async function
134190

135191
This error includes `await`, `async for`, `async with`, and `async` comprehensions.

crates/ruff_linter/src/linter.rs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,19 +1006,22 @@ mod tests {
10061006
}
10071007

10081008
#[test_case(
1009-
"error_on_310",
1009+
"async_in_sync_error_on_310",
10101010
"async def f(): return [[x async for x in foo(n)] for n in range(3)]",
1011-
PythonVersion::PY310
1011+
PythonVersion::PY310,
1012+
"AsyncComprehensionOutsideAsyncFunction"
10121013
)]
10131014
#[test_case(
1014-
"okay_on_311",
1015+
"async_in_sync_okay_on_311",
10151016
"async def f(): return [[x async for x in foo(n)] for n in range(3)]",
1016-
PythonVersion::PY311
1017+
PythonVersion::PY311,
1018+
"AsyncComprehensionOutsideAsyncFunction"
10171019
)]
10181020
#[test_case(
1019-
"okay_on_310",
1021+
"async_in_sync_okay_on_310",
10201022
"async def test(): return [[x async for x in elements(n)] async for n in range(3)]",
1021-
PythonVersion::PY310
1023+
PythonVersion::PY310,
1024+
"AsyncComprehensionOutsideAsyncFunction"
10221025
)]
10231026
#[test_case(
10241027
"deferred_function_body",
@@ -1028,15 +1031,46 @@ mod tests {
10281031
def g(): ...
10291032
[x async for x in foo()]
10301033
",
1031-
PythonVersion::PY310
1034+
PythonVersion::PY310,
1035+
"AsyncComprehensionOutsideAsyncFunction"
10321036
)]
1033-
#[test_case("false_positive", "[x async for x in y]", PythonVersion::PY310)]
1034-
fn test_async_comprehension_in_sync_comprehension(
1037+
#[test_case(
1038+
"async_in_sync_false_positive",
1039+
"[x async for x in y]",
1040+
PythonVersion::PY310,
1041+
"AsyncComprehensionOutsideAsyncFunction"
1042+
)]
1043+
#[test_case(
1044+
"rebound_comprehension",
1045+
"[x:= 2 for x in range(2)]",
1046+
PythonVersion::PY310,
1047+
"ReboundComprehensionVariable"
1048+
)]
1049+
#[test_case(
1050+
"duplicate_type_param",
1051+
"class C[T, T]: pass",
1052+
PythonVersion::PY312,
1053+
"DuplicateTypeParameter"
1054+
)]
1055+
#[test_case(
1056+
"multiple_case_assignment",
1057+
"
1058+
match x:
1059+
case [a, a]:
1060+
pass
1061+
case _:
1062+
pass
1063+
",
1064+
PythonVersion::PY310,
1065+
"MultipleCaseAssignment"
1066+
)]
1067+
fn test_semantic_errors(
10351068
name: &str,
10361069
contents: &str,
10371070
python_version: PythonVersion,
1071+
error_type: &str,
10381072
) {
1039-
let snapshot = format!("async_comprehension_in_sync_comprehension_{name}_{python_version}");
1073+
let snapshot = format!("semantic_syntax_error_{error_type}_{name}_{python_version}");
10401074
let messages = test_snippet_syntax_errors(
10411075
contents,
10421076
&LinterSettings {

crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap renamed to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap

File renamed without changes.

crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_deferred_function_body_3.10.snap renamed to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_false_positive_3.10.snap

File renamed without changes.

crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_310_3.10.snap renamed to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap

File renamed without changes.

crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_311_3.11.snap renamed to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap

File renamed without changes.

crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap renamed to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
---
22
source: crates/ruff_linter/src/linter.rs
33
---
4+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
source: crates/ruff_linter/src/linter.rs
3+
---
4+
<filename>:1:12: SyntaxError: duplicate type parameter
5+
|
6+
1 | class C[T, T]: pass
7+
| ^
8+
|
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: crates/ruff_linter/src/linter.rs
3+
---
4+
<filename>:3:14: SyntaxError: multiple assignments to name `a` in pattern
5+
|
6+
2 | match x:
7+
3 | case [a, a]:
8+
| ^
9+
4 | pass
10+
5 | case _:
11+
|
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
source: crates/ruff_linter/src/linter.rs
3+
---
4+
<filename>:1:2: SyntaxError: assignment expression cannot rebind comprehension variable
5+
|
6+
1 | [x:= 2 for x in range(2)]
7+
| ^
8+
|

0 commit comments

Comments
 (0)