[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](https://github.com/astral-sh/ruff/pull/16090#discussion_r1969198407),
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.
This commit is contained in:
Brent Westbrook 2025-04-17 14:00:30 -04:00 committed by GitHub
parent d2ebfd6ed7
commit 9c47b6dbb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 353 additions and 14 deletions

View file

@ -10,7 +10,7 @@ pub(crate) mod tests {
use super::Db; use super::Db;
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb}; use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb, Program};
use ruff_db::files::{File, Files}; use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, TestSystem}; use ruff_db::system::{DbWithTestSystem, System, TestSystem};
use ruff_db::vendored::VendoredFileSystem; use ruff_db::vendored::VendoredFileSystem;
@ -83,6 +83,10 @@ pub(crate) mod tests {
fn files(&self) -> &Files { fn files(&self) -> &Files {
&self.files &self.files
} }
fn python_version(&self) -> ruff_python_ast::PythonVersion {
Program::get(self).python_version(self)
}
} }
impl Upcast<dyn SourceDb> for TestDb { impl Upcast<dyn SourceDb> for TestDb {

View file

@ -149,6 +149,10 @@ impl SourceDb for ProjectDatabase {
fn files(&self) -> &Files { fn files(&self) -> &Files {
&self.files &self.files
} }
fn python_version(&self) -> ruff_python_ast::PythonVersion {
Program::get(self).python_version(self)
}
} }
#[salsa::db] #[salsa::db]
@ -207,7 +211,7 @@ pub(crate) mod tests {
use salsa::Event; use salsa::Event;
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::Db as SemanticDb; use red_knot_python_semantic::{Db as SemanticDb, Program};
use ruff_db::files::Files; use ruff_db::files::Files;
use ruff_db::system::{DbWithTestSystem, System, TestSystem}; use ruff_db::system::{DbWithTestSystem, System, TestSystem};
use ruff_db::vendored::VendoredFileSystem; use ruff_db::vendored::VendoredFileSystem;
@ -281,6 +285,10 @@ pub(crate) mod tests {
fn files(&self) -> &Files { fn files(&self) -> &Files {
&self.files &self.files
} }
fn python_version(&self) -> ruff_python_ast::PythonVersion {
Program::get(self).python_version(self)
}
} }
impl Upcast<dyn SemanticDb> for TestDb { impl Upcast<dyn SemanticDb> for TestDb {

View file

@ -10,7 +10,8 @@ use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSele
use red_knot_python_semantic::register_lints; use red_knot_python_semantic::register_lints;
use red_knot_python_semantic::types::check_types; use red_knot_python_semantic::types::check_types;
use ruff_db::diagnostic::{ use ruff_db::diagnostic::{
create_parse_diagnostic, Annotation, Diagnostic, DiagnosticId, Severity, Span, create_parse_diagnostic, create_unsupported_syntax_diagnostic, Annotation, Diagnostic,
DiagnosticId, Severity, Span,
}; };
use ruff_db::files::File; use ruff_db::files::File;
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
@ -424,6 +425,13 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
.map(|error| create_parse_diagnostic(file, error)), .map(|error| create_parse_diagnostic(file, error)),
); );
diagnostics.extend(
parsed
.unsupported_syntax_errors()
.iter()
.map(|error| create_unsupported_syntax_diagnostic(file, error)),
);
diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned()); diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned());
diagnostics.sort_unstable_by_key(|diagnostic| { diagnostics.sort_unstable_by_key(|diagnostic| {
@ -520,11 +528,13 @@ mod tests {
use crate::db::tests::TestDb; use crate::db::tests::TestDb;
use crate::{check_file_impl, ProjectMetadata}; use crate::{check_file_impl, ProjectMetadata};
use red_knot_python_semantic::types::check_types; use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
use ruff_db::files::system_path_to_file; use ruff_db::files::system_path_to_file;
use ruff_db::source::source_text; use ruff_db::source::source_text;
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf}; use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
use ruff_db::testing::assert_function_query_was_not_run; use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::PythonVersion;
#[test] #[test]
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> { fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
@ -532,6 +542,16 @@ mod tests {
let mut db = TestDb::new(project); let mut db = TestDb::new(project);
let path = SystemPath::new("test.py"); let path = SystemPath::new("test.py");
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]),
},
)
.expect("Failed to configure program settings");
db.write_file(path, "x = 10")?; db.write_file(path, "x = 10")?;
let file = system_path_to_file(&db, path).unwrap(); let file = system_path_to_file(&db, path).unwrap();

View file

@ -237,6 +237,11 @@ def _(c: Callable[[Concatenate[int, str, ...], int], int]):
## Using `typing.ParamSpec` ## Using `typing.ParamSpec`
```toml
[environment]
python-version = "3.12"
```
Using a `ParamSpec` in a `Callable` annotation: Using a `ParamSpec` in a `Callable` annotation:
```py ```py

View file

@ -48,6 +48,11 @@ reveal_type(get_foo()) # revealed: Foo
## Deferred self-reference annotations in a class definition ## Deferred self-reference annotations in a class definition
```toml
[environment]
python-version = "3.12"
```
```py ```py
from __future__ import annotations from __future__ import annotations
@ -94,6 +99,11 @@ class Foo:
## Non-deferred self-reference annotations in a class definition ## Non-deferred self-reference annotations in a class definition
```toml
[environment]
python-version = "3.12"
```
```py ```py
class Foo: class Foo:
# error: [unresolved-reference] # error: [unresolved-reference]

View file

@ -1,5 +1,10 @@
# Starred expression annotations # Starred expression annotations
```toml
[environment]
python-version = "3.11"
```
Type annotations for `*args` can be starred expressions themselves: Type annotations for `*args` can be starred expressions themselves:
```py ```py

View file

@ -79,6 +79,11 @@ reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable
## Subscriptability ## Subscriptability
```toml
[environment]
python-version = "3.12"
```
Some of these are not subscriptable: Some of these are not subscriptable:
```py ```py

View file

@ -25,6 +25,11 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
## Tuple annotations are understood ## Tuple annotations are understood
```toml
[environment]
python-version = "3.12"
```
`module.py`: `module.py`:
```py ```py

View file

@ -21,6 +21,11 @@ reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
## Generic ## Generic
```toml
[environment]
python-version = "3.12"
```
```py ```py
def get_int[T]() -> int: def get_int[T]() -> int:
return 42 return 42

View file

@ -399,6 +399,11 @@ reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound metho
### Classmethods mixed with other decorators ### Classmethods mixed with other decorators
```toml
[environment]
python-version = "3.12"
```
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
class method: class method:

View file

@ -265,6 +265,11 @@ def f(flag: bool):
## Supers with Generic Classes ## Supers with Generic Classes
```toml
[environment]
python-version = "3.12"
```
```py ```py
from knot_extensions import TypeOf, static_assert, is_subtype_of from knot_extensions import TypeOf, static_assert, is_subtype_of
@ -316,6 +321,11 @@ class A:
### Failing Condition Checks ### Failing Condition Checks
```toml
[environment]
python-version = "3.12"
```
`super()` requires its first argument to be a valid class, and its second argument to be either an `super()` requires its first argument to be a valid class, and its second argument to be either an
instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at
runtime. runtime.

View file

@ -1,5 +1,10 @@
# Pattern matching # Pattern matching
```toml
[environment]
python-version = "3.10"
```
## With wildcard ## With wildcard
```py ```py

View file

@ -297,6 +297,11 @@ reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool
### `order` ### `order`
```toml
[environment]
python-version = "3.12"
```
`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__` `order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__`
methods will be generated: methods will be generated:
@ -471,6 +476,11 @@ reveal_type(C.__init__) # revealed: (x: int = Literal[15], y: int = Literal[0],
## Generic dataclasses ## Generic dataclasses
```toml
[environment]
python-version = "3.12"
```
```py ```py
from dataclasses import dataclass from dataclasses import dataclass

View file

@ -0,0 +1,37 @@
# Version-related syntax error diagnostics
## `match` statement
The `match` statement was introduced in Python 3.10.
### Before 3.10
<!-- snapshot-diagnostics -->
We should emit a syntax error before 3.10.
```toml
[environment]
python-version = "3.9"
```
```py
match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
case 1:
print("it's one")
```
### After 3.10
On or after 3.10, no error should be reported.
```toml
[environment]
python-version = "3.10"
```
```py
match 2:
case 1:
print("it's one")
```

View file

@ -26,6 +26,11 @@ def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
## Use case: Type narrowing and exhaustiveness checking ## Use case: Type narrowing and exhaustiveness checking
```toml
[environment]
python-version = "3.10"
```
`assert_never` can be used in combination with type narrowing as a way to make sure that all cases `assert_never` can be used in combination with type narrowing as a way to make sure that all cases
are handled in a series of `isinstance` checks or other narrowing patterns that are supported. are handled in a series of `isinstance` checks or other narrowing patterns that are supported.

View file

@ -76,6 +76,11 @@ def g(x: Any = "foo"):
## Stub functions ## Stub functions
```toml
[environment]
python-version = "3.12"
```
### In Protocol ### In Protocol
```py ```py

View file

@ -56,6 +56,11 @@ def f() -> int:
### In Protocol ### In Protocol
```toml
[environment]
python-version = "3.12"
```
```py ```py
from typing import Protocol, TypeVar from typing import Protocol, TypeVar
@ -85,6 +90,11 @@ class Lorem(t[0]):
### In abstract method ### In abstract method
```toml
[environment]
python-version = "3.12"
```
```py ```py
from abc import ABC, abstractmethod from abc import ABC, abstractmethod

View file

@ -1,5 +1,10 @@
# Generic classes # Generic classes
```toml
[environment]
python-version = "3.13"
```
## PEP 695 syntax ## PEP 695 syntax
TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic. TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.

View file

@ -1,5 +1,10 @@
# Generic functions # Generic functions
```toml
[environment]
python-version = "3.12"
```
## Typevar must be used at least twice ## Typevar must be used at least twice
If you're only using a typevar for a single parameter, you don't need the typevar — just use If you're only using a typevar for a single parameter, you don't need the typevar — just use

View file

@ -1,5 +1,10 @@
# PEP 695 Generics # PEP 695 Generics
```toml
[environment]
python-version = "3.12"
```
[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables. [PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables.
## Type variables ## Type variables

View file

@ -1,5 +1,10 @@
# Scoping rules for type variables # Scoping rules for type variables
```toml
[environment]
python-version = "3.12"
```
Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing
spec. spec.

View file

@ -122,6 +122,11 @@ from c import Y # error: [unresolved-import]
## Esoteric definitions and redefinintions ## Esoteric definitions and redefinintions
```toml
[environment]
python-version = "3.12"
```
We understand all public symbols defined in an external module as being imported by a `*` import, We understand all public symbols defined in an external module as being imported by a `*` import,
not just those that are defined in `StmtAssign` nodes and `StmtAnnAssign` nodes. This section not just those that are defined in `StmtAssign` nodes and `StmtAnnAssign` nodes. This section
provides tests for definitions, and redefinitions, that use more esoteric AST nodes. provides tests for definitions, and redefinitions, that use more esoteric AST nodes.

View file

@ -216,6 +216,11 @@ reveal_type(A.__class__) # revealed: type[Unknown]
## PEP 695 generic ## PEP 695 generic
```toml
[environment]
python-version = "3.12"
```
```py ```py
class M(type): ... class M(type): ...
class A[T: str](metaclass=M): ... class A[T: str](metaclass=M): ...

View file

@ -1,5 +1,10 @@
# Narrowing for `match` statements # Narrowing for `match` statements
```toml
[environment]
python-version = "3.10"
```
## Single `match` pattern ## Single `match` pattern
```py ```py

View file

@ -111,6 +111,11 @@ def _(x: A | B):
## Narrowing for generic classes ## Narrowing for generic classes
```toml
[environment]
python-version = "3.13"
```
Note that `type` returns the runtime class of an object, which does _not_ include specializations in Note that `type` returns the runtime class of an object, which does _not_ include specializations in
the case of a generic class. (The typevars are erased.) That means we cannot narrow the type to the the case of a generic class. (The typevars are erased.) That means we cannot narrow the type to the
specialization that we compare with; we must narrow to an unknown specialization of the generic specialization that we compare with; we must narrow to an unknown specialization of the generic

View file

@ -15,6 +15,11 @@ types, on the other hand: a type which is defined by its properties and behaviou
## Defining a protocol ## Defining a protocol
```toml
[environment]
python-version = "3.12"
```
A protocol is defined by inheriting from the `Protocol` class, which is annotated as an instance of A protocol is defined by inheriting from the `Protocol` class, which is annotated as an instance of
`_SpecialForm` in typeshed's stubs. `_SpecialForm` in typeshed's stubs.

View file

@ -0,0 +1,32 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: version_related_syntax_errors.md - Version-related syntax error diagnostics - `match` statement - Before 3.10
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md
---
# Python source files
## mdtest_snippet.py
```
1 | match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
2 | case 1:
3 | print("it's one")
```
# Diagnostics
```
error: invalid-syntax
--> /src/mdtest_snippet.py:1:1
|
1 | match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
| ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
2 | case 1:
3 | print("it's one")
|
```

View file

@ -996,6 +996,11 @@ reveal_type(x) # revealed: Literal[1]
## `match` statements ## `match` statements
```toml
[environment]
python-version = "3.10"
```
### Single-valued types, always true ### Single-valued types, always true
```py ```py
@ -1118,6 +1123,7 @@ def _(s: str):
```toml ```toml
[environment] [environment]
python-platform = "darwin" python-platform = "darwin"
python-version = "3.10"
``` ```
```py ```py

View file

@ -2,6 +2,11 @@
## Cyclical class definition ## Cyclical class definition
```toml
[environment]
python-version = "3.12"
```
In type stubs, classes can reference themselves in their base class definitions. For example, in In type stubs, classes can reference themselves in their base class definitions. For example, in
`typeshed`, we have `class str(Sequence[str]): ...`. `typeshed`, we have `class str(Sequence[str]): ...`.

View file

@ -42,6 +42,11 @@ def static_truthiness(not_one: Not[Literal[1]]) -> None:
### Intersection ### Intersection
```toml
[environment]
python-version = "3.12"
```
```py ```py
from knot_extensions import Intersection, Not, is_subtype_of, static_assert from knot_extensions import Intersection, Not, is_subtype_of, static_assert
from typing_extensions import Literal, Never from typing_extensions import Literal, Never

View file

@ -246,6 +246,11 @@ static_assert(is_disjoint_from(Intersection[LiteralString, Not[AlwaysFalsy]], No
### Class, module and function literals ### Class, module and function literals
```toml
[environment]
python-version = "3.12"
```
```py ```py
from types import ModuleType, FunctionType from types import ModuleType, FunctionType
from knot_extensions import TypeOf, is_disjoint_from, static_assert from knot_extensions import TypeOf, is_disjoint_from, static_assert

View file

@ -1,5 +1,10 @@
# Subtype relation # Subtype relation
```toml
[environment]
python-version = "3.12"
```
The `is_subtype_of(S, T)` relation below checks if type `S` is a subtype of type `T`. The `is_subtype_of(S, T)` relation below checks if type `S` is a subtype of type `T`.
A fully static type `S` is a subtype of another fully static type `T` iff the set of values A fully static type `S` is a subtype of another fully static type `T` iff the set of values

View file

@ -98,6 +98,10 @@ pub(crate) mod tests {
fn files(&self) -> &Files { fn files(&self) -> &Files {
&self.files &self.files
} }
fn python_version(&self) -> PythonVersion {
Program::get(self).python_version(self)
}
} }
impl Upcast<dyn SourceDb> for TestDb { impl Upcast<dyn SourceDb> for TestDb {

View file

@ -497,11 +497,10 @@ impl FusedIterator for ChildrenIter<'_> {}
mod tests { mod tests {
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_db::system::DbWithWritableSystem as _; use ruff_python_ast::{self as ast};
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::db::tests::TestDb; use crate::db::tests::{TestDb, TestDbBuilder};
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId}; use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::symbol::{ use crate::semantic_index::symbol::{
@ -528,11 +527,15 @@ mod tests {
file: File, file: File,
} }
fn test_case(content: impl AsRef<str>) -> TestCase { fn test_case(content: &str) -> TestCase {
let mut db = TestDb::new(); const FILENAME: &str = "test.py";
db.write_file("test.py", content).unwrap();
let file = system_path_to_file(&db, "test.py").unwrap(); let db = TestDbBuilder::new()
.with_file(FILENAME, content)
.build()
.unwrap();
let file = system_path_to_file(&db, FILENAME).unwrap();
TestCase { db, file } TestCase { db, file }
} }

View file

@ -489,13 +489,27 @@ pub(crate) enum ErrorAssertionParseError<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
use ruff_db::files::system_path_to_file; use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithWritableSystem as _; use ruff_db::system::DbWithWritableSystem as _;
use ruff_python_ast::PythonVersion;
use ruff_python_trivia::textwrap::dedent; use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::OneIndexed; use ruff_source_file::OneIndexed;
fn get_assertions(source: &str) -> InlineFileAssertions { fn get_assertions(source: &str) -> InlineFileAssertions {
let mut db = Db::setup(); let mut db = Db::setup();
let settings = ProgramSettings {
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(Vec::new()),
};
match Program::try_get(&db) {
Some(program) => program.update_from_settings(&mut db, settings),
None => Program::from_settings(&db, settings).map(|_| ()),
}
.expect("Failed to update Program settings in TestDb");
db.write_file("/src/test.py", source).unwrap(); db.write_file("/src/test.py", source).unwrap();
let file = system_path_to_file(&db, "/src/test.py").unwrap(); let file = system_path_to_file(&db, "/src/test.py").unwrap();
InlineFileAssertions::from_file(&db, file) InlineFileAssertions::from_file(&db, file)

View file

@ -1,6 +1,6 @@
use camino::{Utf8Component, Utf8PathBuf}; use camino::{Utf8Component, Utf8PathBuf};
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb}; use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb, Program};
use ruff_db::files::{File, Files}; use ruff_db::files::{File, Files};
use ruff_db::system::{ use ruff_db::system::{
CaseSensitivity, DbWithWritableSystem, InMemorySystem, OsSystem, System, SystemPath, CaseSensitivity, DbWithWritableSystem, InMemorySystem, OsSystem, System, SystemPath,
@ -64,6 +64,10 @@ impl SourceDb for Db {
fn files(&self) -> &Files { fn files(&self) -> &Files {
&self.files &self.files
} }
fn python_version(&self) -> ruff_python_ast::PythonVersion {
Program::get(self).python_version(self)
}
} }
impl Upcast<dyn SourceDb> for Db { impl Upcast<dyn SourceDb> for Db {

View file

@ -8,7 +8,10 @@ use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::{ use red_knot_python_semantic::{
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, SysPrefixPathOrigin, Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, SysPrefixPathOrigin,
}; };
use ruff_db::diagnostic::{create_parse_diagnostic, Diagnostic, DisplayDiagnosticConfig}; use ruff_db::diagnostic::{
create_parse_diagnostic, create_unsupported_syntax_diagnostic, Diagnostic,
DisplayDiagnosticConfig,
};
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::panic::catch_unwind; use ruff_db::panic::catch_unwind;
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
@ -305,6 +308,13 @@ fn run_test(
.map(|error| create_parse_diagnostic(test_file.file, error)) .map(|error| create_parse_diagnostic(test_file.file, error))
.collect(); .collect();
diagnostics.extend(
parsed
.unsupported_syntax_errors()
.iter()
.map(|error| create_unsupported_syntax_diagnostic(test_file.file, error)),
);
let type_diagnostics = match catch_unwind(|| check_types(db, test_file.file)) { let type_diagnostics = match catch_unwind(|| check_types(db, test_file.file)) {
Ok(type_diagnostics) => type_diagnostics, Ok(type_diagnostics) => type_diagnostics,
Err(info) => { Err(info) => {

View file

@ -343,9 +343,11 @@ impl Matcher {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::FailuresByLine; use super::FailuresByLine;
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span}; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::DbWithWritableSystem as _; use ruff_db::system::DbWithWritableSystem as _;
use ruff_python_ast::PythonVersion;
use ruff_python_trivia::textwrap::dedent; use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::OneIndexed; use ruff_source_file::OneIndexed;
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
@ -385,6 +387,18 @@ mod tests {
colored::control::set_override(false); colored::control::set_override(false);
let mut db = crate::db::Db::setup(); let mut db = crate::db::Db::setup();
let settings = ProgramSettings {
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(Vec::new()),
};
match Program::try_get(&db) {
Some(program) => program.update_from_settings(&mut db, settings),
None => Program::from_settings(&db, settings).map(|_| ()),
}
.expect("Failed to update Program settings in TestDb");
db.write_file("/src/test.py", source).unwrap(); db.write_file("/src/test.py", source).unwrap();
let file = system_path_to_file(&db, "/src/test.py").unwrap(); let file = system_path_to_file(&db, "/src/test.py").unwrap();

View file

@ -832,3 +832,16 @@ pub fn create_parse_diagnostic(file: File, err: &ruff_python_parser::ParseError)
diag.annotate(Annotation::primary(span).message(&err.error)); diag.annotate(Annotation::primary(span).message(&err.error));
diag diag
} }
/// Creates a `Diagnostic` from an unsupported syntax error.
///
/// See [`create_parse_diagnostic`] for more details.
pub fn create_unsupported_syntax_diagnostic(
file: File,
err: &ruff_python_parser::UnsupportedSyntaxError,
) -> Diagnostic {
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, "");
let span = Span::from(file).with_range(err.range);
diag.annotate(Annotation::primary(span).message(err.to_string()));
diag
}

View file

@ -1,5 +1,6 @@
use std::hash::BuildHasherDefault; use std::hash::BuildHasherDefault;
use ruff_python_ast::PythonVersion;
use rustc_hash::FxHasher; use rustc_hash::FxHasher;
use crate::files::Files; use crate::files::Files;
@ -27,6 +28,7 @@ pub trait Db: salsa::Database {
fn vendored(&self) -> &VendoredFileSystem; fn vendored(&self) -> &VendoredFileSystem;
fn system(&self) -> &dyn System; fn system(&self) -> &dyn System;
fn files(&self) -> &Files; fn files(&self) -> &Files;
fn python_version(&self) -> PythonVersion;
} }
/// Trait for upcasting a reference to a base trait object. /// Trait for upcasting a reference to a base trait object.
@ -107,6 +109,10 @@ mod tests {
fn files(&self) -> &Files { fn files(&self) -> &Files {
&self.files &self.files
} }
fn python_version(&self) -> ruff_python_ast::PythonVersion {
ruff_python_ast::PythonVersion::latest()
}
} }
impl DbWithTestSystem for TestDb { impl DbWithTestSystem for TestDb {

View file

@ -3,7 +3,7 @@ use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use ruff_python_ast::ModModule; use ruff_python_ast::ModModule;
use ruff_python_parser::{parse_unchecked_source, Parsed}; use ruff_python_parser::{parse_unchecked, ParseOptions, Parsed};
use crate::files::File; use crate::files::File;
use crate::source::source_text; use crate::source::source_text;
@ -27,7 +27,13 @@ pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
let source = source_text(db, file); let source = source_text(db, file);
let ty = file.source_type(db); let ty = file.source_type(db);
ParsedModule::new(parse_unchecked_source(&source, ty)) let target_version = db.python_version();
let options = ParseOptions::from(ty).with_target_version(target_version);
let parsed = parse_unchecked(&source, options)
.try_into_module()
.expect("PySourceType always parses into a module");
ParsedModule::new(parsed)
} }
/// Cheap cloneable wrapper around the parsed module. /// Cheap cloneable wrapper around the parsed module.

View file

@ -71,6 +71,10 @@ impl SourceDb for ModuleDb {
fn files(&self) -> &Files { fn files(&self) -> &Files {
&self.files &self.files
} }
fn python_version(&self) -> PythonVersion {
Program::get(self).python_version(self)
}
} }
#[salsa::db] #[salsa::db]

View file

@ -62,6 +62,10 @@ impl SourceDb for TestDb {
fn files(&self) -> &Files { fn files(&self) -> &Files {
&self.files &self.files
} }
fn python_version(&self) -> PythonVersion {
Program::get(self).python_version(self)
}
} }
impl DbWithTestSystem for TestDb { impl DbWithTestSystem for TestDb {