Skip to content

Commit 9c47b6d

Browse files
authored
[red-knot] Detect version-related syntax errors (#16379)
## Summary This PR extends version-related syntax error detection to red-knot. The main changes here are: 1. Passing `ParseOptions` specifying a `PythonVersion` to parser calls 2. Adding a `python_version` method to the `Db` trait to make this possible 3. Converting `UnsupportedSyntaxError`s to `Diagnostic`s 4. Updating existing mdtests to avoid unrelated syntax errors My initial draft of (1) and (2) in #16090 instead tried passing a `PythonVersion` down to every parser call, but @MichaReiser suggested the `Db` approach instead [here](#16090 (comment)), and I think it turned out much nicer. All of the new `python_version` methods look like this: ```rust fn python_version(&self) -> ruff_python_ast::PythonVersion { Program::get(self).python_version(self) } ``` with the exception of the `TestDb` in `ruff_db`, which hard-codes `PythonVersion::latest()`. ## Test Plan Existing mdtests, plus a new mdtest to see at least one of the new diagnostics.
1 parent d2ebfd6 commit 9c47b6d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+353
-14
lines changed

crates/red_knot_ide/src/db.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub(crate) mod tests {
1010

1111
use super::Db;
1212
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
13-
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb};
13+
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb, Program};
1414
use ruff_db::files::{File, Files};
1515
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
1616
use ruff_db::vendored::VendoredFileSystem;
@@ -83,6 +83,10 @@ pub(crate) mod tests {
8383
fn files(&self) -> &Files {
8484
&self.files
8585
}
86+
87+
fn python_version(&self) -> ruff_python_ast::PythonVersion {
88+
Program::get(self).python_version(self)
89+
}
8690
}
8791

8892
impl Upcast<dyn SourceDb> for TestDb {

crates/red_knot_project/src/db.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ impl SourceDb for ProjectDatabase {
149149
fn files(&self) -> &Files {
150150
&self.files
151151
}
152+
153+
fn python_version(&self) -> ruff_python_ast::PythonVersion {
154+
Program::get(self).python_version(self)
155+
}
152156
}
153157

154158
#[salsa::db]
@@ -207,7 +211,7 @@ pub(crate) mod tests {
207211
use salsa::Event;
208212

209213
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
210-
use red_knot_python_semantic::Db as SemanticDb;
214+
use red_knot_python_semantic::{Db as SemanticDb, Program};
211215
use ruff_db::files::Files;
212216
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
213217
use ruff_db::vendored::VendoredFileSystem;
@@ -281,6 +285,10 @@ pub(crate) mod tests {
281285
fn files(&self) -> &Files {
282286
&self.files
283287
}
288+
289+
fn python_version(&self) -> ruff_python_ast::PythonVersion {
290+
Program::get(self).python_version(self)
291+
}
284292
}
285293

286294
impl Upcast<dyn SemanticDb> for TestDb {

crates/red_knot_project/src/lib.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSele
1010
use red_knot_python_semantic::register_lints;
1111
use red_knot_python_semantic::types::check_types;
1212
use ruff_db::diagnostic::{
13-
create_parse_diagnostic, Annotation, Diagnostic, DiagnosticId, Severity, Span,
13+
create_parse_diagnostic, create_unsupported_syntax_diagnostic, Annotation, Diagnostic,
14+
DiagnosticId, Severity, Span,
1415
};
1516
use ruff_db::files::File;
1617
use ruff_db::parsed::parsed_module;
@@ -424,6 +425,13 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
424425
.map(|error| create_parse_diagnostic(file, error)),
425426
);
426427

428+
diagnostics.extend(
429+
parsed
430+
.unsupported_syntax_errors()
431+
.iter()
432+
.map(|error| create_unsupported_syntax_diagnostic(file, error)),
433+
);
434+
427435
diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned());
428436

429437
diagnostics.sort_unstable_by_key(|diagnostic| {
@@ -520,18 +528,30 @@ mod tests {
520528
use crate::db::tests::TestDb;
521529
use crate::{check_file_impl, ProjectMetadata};
522530
use red_knot_python_semantic::types::check_types;
531+
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
523532
use ruff_db::files::system_path_to_file;
524533
use ruff_db::source::source_text;
525534
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
526535
use ruff_db::testing::assert_function_query_was_not_run;
527536
use ruff_python_ast::name::Name;
537+
use ruff_python_ast::PythonVersion;
528538

529539
#[test]
530540
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
531541
let project = ProjectMetadata::new(Name::new_static("test"), SystemPathBuf::from("/"));
532542
let mut db = TestDb::new(project);
533543
let path = SystemPath::new("test.py");
534544

545+
Program::from_settings(
546+
&db,
547+
ProgramSettings {
548+
python_version: PythonVersion::default(),
549+
python_platform: PythonPlatform::default(),
550+
search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]),
551+
},
552+
)
553+
.expect("Failed to configure program settings");
554+
535555
db.write_file(path, "x = 10")?;
536556
let file = system_path_to_file(&db, path).unwrap();
537557

crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ def _(c: Callable[[Concatenate[int, str, ...], int], int]):
237237

238238
## Using `typing.ParamSpec`
239239

240+
```toml
241+
[environment]
242+
python-version = "3.12"
243+
```
244+
240245
Using a `ParamSpec` in a `Callable` annotation:
241246

242247
```py

crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ reveal_type(get_foo()) # revealed: Foo
4848

4949
## Deferred self-reference annotations in a class definition
5050

51+
```toml
52+
[environment]
53+
python-version = "3.12"
54+
```
55+
5156
```py
5257
from __future__ import annotations
5358

@@ -94,6 +99,11 @@ class Foo:
9499

95100
## Non-deferred self-reference annotations in a class definition
96101

102+
```toml
103+
[environment]
104+
python-version = "3.12"
105+
```
106+
97107
```py
98108
class Foo:
99109
# error: [unresolved-reference]

crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Starred expression annotations
22

3+
```toml
4+
[environment]
5+
python-version = "3.11"
6+
```
7+
38
Type annotations for `*args` can be starred expressions themselves:
49

510
```py

crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable
7979

8080
## Subscriptability
8181

82+
```toml
83+
[environment]
84+
python-version = "3.12"
85+
```
86+
8287
Some of these are not subscriptable:
8388

8489
```py

crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
2525

2626
## Tuple annotations are understood
2727

28+
```toml
29+
[environment]
30+
python-version = "3.12"
31+
```
32+
2833
`module.py`:
2934

3035
```py

crates/red_knot_python_semantic/resources/mdtest/call/function.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
2121

2222
## Generic
2323

24+
```toml
25+
[environment]
26+
python-version = "3.12"
27+
```
28+
2429
```py
2530
def get_int[T]() -> int:
2631
return 42

crates/red_knot_python_semantic/resources/mdtest/call/methods.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,11 @@ reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound metho
399399

400400
### Classmethods mixed with other decorators
401401

402+
```toml
403+
[environment]
404+
python-version = "3.12"
405+
```
406+
402407
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
403408
class method:
404409

crates/red_knot_python_semantic/resources/mdtest/class/super.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,11 @@ def f(flag: bool):
265265

266266
## Supers with Generic Classes
267267

268+
```toml
269+
[environment]
270+
python-version = "3.12"
271+
```
272+
268273
```py
269274
from knot_extensions import TypeOf, static_assert, is_subtype_of
270275

@@ -316,6 +321,11 @@ class A:
316321

317322
### Failing Condition Checks
318323

324+
```toml
325+
[environment]
326+
python-version = "3.12"
327+
```
328+
319329
`super()` requires its first argument to be a valid class, and its second argument to be either an
320330
instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at
321331
runtime.

crates/red_knot_python_semantic/resources/mdtest/conditional/match.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Pattern matching
22

3+
```toml
4+
[environment]
5+
python-version = "3.10"
6+
```
7+
38
## With wildcard
49

510
```py

crates/red_knot_python_semantic/resources/mdtest/dataclasses.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,11 @@ reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool
297297

298298
### `order`
299299

300+
```toml
301+
[environment]
302+
python-version = "3.12"
303+
```
304+
300305
`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__`
301306
methods will be generated:
302307

@@ -471,6 +476,11 @@ reveal_type(C.__init__) # revealed: (x: int = Literal[15], y: int = Literal[0],
471476

472477
## Generic dataclasses
473478

479+
```toml
480+
[environment]
481+
python-version = "3.12"
482+
```
483+
474484
```py
475485
from dataclasses import dataclass
476486

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Version-related syntax error diagnostics
2+
3+
## `match` statement
4+
5+
The `match` statement was introduced in Python 3.10.
6+
7+
### Before 3.10
8+
9+
<!-- snapshot-diagnostics -->
10+
11+
We should emit a syntax error before 3.10.
12+
13+
```toml
14+
[environment]
15+
python-version = "3.9"
16+
```
17+
18+
```py
19+
match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
20+
case 1:
21+
print("it's one")
22+
```
23+
24+
### After 3.10
25+
26+
On or after 3.10, no error should be reported.
27+
28+
```toml
29+
[environment]
30+
python-version = "3.10"
31+
```
32+
33+
```py
34+
match 2:
35+
case 1:
36+
print("it's one")
37+
```

crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
2626

2727
## Use case: Type narrowing and exhaustiveness checking
2828

29+
```toml
30+
[environment]
31+
python-version = "3.10"
32+
```
33+
2934
`assert_never` can be used in combination with type narrowing as a way to make sure that all cases
3035
are handled in a series of `isinstance` checks or other narrowing patterns that are supported.
3136

crates/red_knot_python_semantic/resources/mdtest/function/parameters.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ def g(x: Any = "foo"):
7676

7777
## Stub functions
7878

79+
```toml
80+
[environment]
81+
python-version = "3.12"
82+
```
83+
7984
### In Protocol
8085

8186
```py

crates/red_knot_python_semantic/resources/mdtest/function/return_type.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ def f() -> int:
5656

5757
### In Protocol
5858

59+
```toml
60+
[environment]
61+
python-version = "3.12"
62+
```
63+
5964
```py
6065
from typing import Protocol, TypeVar
6166

@@ -85,6 +90,11 @@ class Lorem(t[0]):
8590

8691
### In abstract method
8792

93+
```toml
94+
[environment]
95+
python-version = "3.12"
96+
```
97+
8898
```py
8999
from abc import ABC, abstractmethod
90100

crates/red_knot_python_semantic/resources/mdtest/generics/classes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Generic classes
22

3+
```toml
4+
[environment]
5+
python-version = "3.13"
6+
```
7+
38
## PEP 695 syntax
49

510
TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.

crates/red_knot_python_semantic/resources/mdtest/generics/functions.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Generic functions
22

3+
```toml
4+
[environment]
5+
python-version = "3.12"
6+
```
7+
38
## Typevar must be used at least twice
49

510
If you're only using a typevar for a single parameter, you don't need the typevar — just use

crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# PEP 695 Generics
22

3+
```toml
4+
[environment]
5+
python-version = "3.12"
6+
```
7+
38
[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables.
49

510
## Type variables

crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Scoping rules for type variables
22

3+
```toml
4+
[environment]
5+
python-version = "3.12"
6+
```
7+
38
Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing
49
spec.
510

0 commit comments

Comments
 (0)