mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 02:12:22 +00:00
[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:
parent
d2ebfd6ed7
commit
9c47b6dbb0
43 changed files with 353 additions and 14 deletions
|
@ -10,7 +10,7 @@ pub(crate) mod tests {
|
|||
|
||||
use super::Db;
|
||||
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::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
|
@ -83,6 +83,10 @@ pub(crate) mod tests {
|
|||
fn 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 {
|
||||
|
|
|
@ -149,6 +149,10 @@ impl SourceDb for ProjectDatabase {
|
|||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||
Program::get(self).python_version(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
@ -207,7 +211,7 @@ pub(crate) mod tests {
|
|||
use salsa::Event;
|
||||
|
||||
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::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
|
@ -281,6 +285,10 @@ pub(crate) mod tests {
|
|||
fn 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 {
|
||||
|
|
|
@ -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::types::check_types;
|
||||
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::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)),
|
||||
);
|
||||
|
||||
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.sort_unstable_by_key(|diagnostic| {
|
||||
|
@ -520,11 +528,13 @@ mod tests {
|
|||
use crate::db::tests::TestDb;
|
||||
use crate::{check_file_impl, ProjectMetadata};
|
||||
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::source::source_text;
|
||||
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
#[test]
|
||||
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 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")?;
|
||||
let file = system_path_to_file(&db, path).unwrap();
|
||||
|
||||
|
|
|
@ -237,6 +237,11 @@ def _(c: Callable[[Concatenate[int, str, ...], int], int]):
|
|||
|
||||
## Using `typing.ParamSpec`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Using a `ParamSpec` in a `Callable` annotation:
|
||||
|
||||
```py
|
||||
|
|
|
@ -48,6 +48,11 @@ reveal_type(get_foo()) # revealed: Foo
|
|||
|
||||
## Deferred self-reference annotations in a class definition
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
|
@ -94,6 +99,11 @@ class Foo:
|
|||
|
||||
## Non-deferred self-reference annotations in a class definition
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
# error: [unresolved-reference]
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# Starred expression annotations
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
Type annotations for `*args` can be starred expressions themselves:
|
||||
|
||||
```py
|
||||
|
|
|
@ -79,6 +79,11 @@ reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable
|
|||
|
||||
## Subscriptability
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Some of these are not subscriptable:
|
||||
|
||||
```py
|
||||
|
|
|
@ -25,6 +25,11 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
|
|||
|
||||
## Tuple annotations are understood
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
|
|
|
@ -21,6 +21,11 @@ reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
|
|||
|
||||
## Generic
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
def get_int[T]() -> int:
|
||||
return 42
|
||||
|
|
|
@ -399,6 +399,11 @@ reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound metho
|
|||
|
||||
### 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
|
||||
class method:
|
||||
|
||||
|
|
|
@ -265,6 +265,11 @@ def f(flag: bool):
|
|||
|
||||
## Supers with Generic Classes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from knot_extensions import TypeOf, static_assert, is_subtype_of
|
||||
|
||||
|
@ -316,6 +321,11 @@ class A:
|
|||
|
||||
### 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
|
||||
instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at
|
||||
runtime.
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# Pattern matching
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
## With wildcard
|
||||
|
||||
```py
|
||||
|
|
|
@ -297,6 +297,11 @@ reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool
|
|||
|
||||
### `order`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__`
|
||||
methods will be generated:
|
||||
|
||||
|
@ -471,6 +476,11 @@ reveal_type(C.__init__) # revealed: (x: int = Literal[15], y: int = Literal[0],
|
|||
|
||||
## Generic dataclasses
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
|
|
@ -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")
|
||||
```
|
|
@ -26,6 +26,11 @@ def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
|
|||
|
||||
## 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
|
||||
are handled in a series of `isinstance` checks or other narrowing patterns that are supported.
|
||||
|
||||
|
|
|
@ -76,6 +76,11 @@ def g(x: Any = "foo"):
|
|||
|
||||
## Stub functions
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
### In Protocol
|
||||
|
||||
```py
|
||||
|
|
|
@ -56,6 +56,11 @@ def f() -> int:
|
|||
|
||||
### In Protocol
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
|
@ -85,6 +90,11 @@ class Lorem(t[0]):
|
|||
|
||||
### In abstract method
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# Generic classes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
## PEP 695 syntax
|
||||
|
||||
TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# Generic functions
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
## 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
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# PEP 695 Generics
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables.
|
||||
|
||||
## Type variables
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# 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
|
||||
spec.
|
||||
|
||||
|
|
|
@ -122,6 +122,11 @@ from c import Y # error: [unresolved-import]
|
|||
|
||||
## 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,
|
||||
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.
|
||||
|
|
|
@ -216,6 +216,11 @@ reveal_type(A.__class__) # revealed: type[Unknown]
|
|||
|
||||
## PEP 695 generic
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A[T: str](metaclass=M): ...
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# Narrowing for `match` statements
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
## Single `match` pattern
|
||||
|
||||
```py
|
||||
|
|
|
@ -111,6 +111,11 @@ def _(x: A | B):
|
|||
|
||||
## 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
|
||||
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
|
||||
|
|
|
@ -15,6 +15,11 @@ types, on the other hand: a type which is defined by its properties and behaviou
|
|||
|
||||
## 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
|
||||
`_SpecialForm` in typeshed's stubs.
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
||||
|
||||
```
|
|
@ -996,6 +996,11 @@ reveal_type(x) # revealed: Literal[1]
|
|||
|
||||
## `match` statements
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
### Single-valued types, always true
|
||||
|
||||
```py
|
||||
|
@ -1118,6 +1123,7 @@ def _(s: str):
|
|||
```toml
|
||||
[environment]
|
||||
python-platform = "darwin"
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
## Cyclical class definition
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
In type stubs, classes can reference themselves in their base class definitions. For example, in
|
||||
`typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
|
||||
|
|
|
@ -42,6 +42,11 @@ def static_truthiness(not_one: Not[Literal[1]]) -> None:
|
|||
|
||||
### Intersection
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
|
||||
from typing_extensions import Literal, Never
|
||||
|
|
|
@ -246,6 +246,11 @@ static_assert(is_disjoint_from(Intersection[LiteralString, Not[AlwaysFalsy]], No
|
|||
|
||||
### Class, module and function literals
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from types import ModuleType, FunctionType
|
||||
from knot_extensions import TypeOf, is_disjoint_from, static_assert
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# 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`.
|
||||
|
||||
A fully static type `S` is a subtype of another fully static type `T` iff the set of values
|
||||
|
|
|
@ -98,6 +98,10 @@ pub(crate) mod tests {
|
|||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
fn python_version(&self) -> PythonVersion {
|
||||
Program::get(self).python_version(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SourceDb> for TestDb {
|
||||
|
|
|
@ -497,11 +497,10 @@ impl FusedIterator for ChildrenIter<'_> {}
|
|||
mod tests {
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::DbWithWritableSystem as _;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{self as ast};
|
||||
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::definition::{Definition, DefinitionKind};
|
||||
use crate::semantic_index::symbol::{
|
||||
|
@ -528,11 +527,15 @@ mod tests {
|
|||
file: File,
|
||||
}
|
||||
|
||||
fn test_case(content: impl AsRef<str>) -> TestCase {
|
||||
let mut db = TestDb::new();
|
||||
db.write_file("test.py", content).unwrap();
|
||||
fn test_case(content: &str) -> TestCase {
|
||||
const FILENAME: &str = "test.py";
|
||||
|
||||
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 }
|
||||
}
|
||||
|
|
|
@ -489,13 +489,27 @@ pub(crate) enum ErrorAssertionParseError<'a> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::DbWithWritableSystem as _;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
fn get_assertions(source: &str) -> InlineFileAssertions {
|
||||
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();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
InlineFileAssertions::from_file(&db, file)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use camino::{Utf8Component, Utf8PathBuf};
|
||||
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::system::{
|
||||
CaseSensitivity, DbWithWritableSystem, InMemorySystem, OsSystem, System, SystemPath,
|
||||
|
@ -64,6 +64,10 @@ impl SourceDb for Db {
|
|||
fn 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 {
|
||||
|
|
|
@ -8,7 +8,10 @@ use red_knot_python_semantic::types::check_types;
|
|||
use red_knot_python_semantic::{
|
||||
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::panic::catch_unwind;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
|
@ -305,6 +308,13 @@ fn run_test(
|
|||
.map(|error| create_parse_diagnostic(test_file.file, error))
|
||||
.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)) {
|
||||
Ok(type_diagnostics) => type_diagnostics,
|
||||
Err(info) => {
|
||||
|
|
|
@ -343,9 +343,11 @@ impl Matcher {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FailuresByLine;
|
||||
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::DbWithWritableSystem as _;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::TextRange;
|
||||
|
@ -385,6 +387,18 @@ mod tests {
|
|||
colored::control::set_override(false);
|
||||
|
||||
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();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::hash::BuildHasherDefault;
|
||||
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHasher;
|
||||
|
||||
use crate::files::Files;
|
||||
|
@ -27,6 +28,7 @@ pub trait Db: salsa::Database {
|
|||
fn vendored(&self) -> &VendoredFileSystem;
|
||||
fn system(&self) -> &dyn System;
|
||||
fn files(&self) -> &Files;
|
||||
fn python_version(&self) -> PythonVersion;
|
||||
}
|
||||
|
||||
/// Trait for upcasting a reference to a base trait object.
|
||||
|
@ -107,6 +109,10 @@ mod tests {
|
|||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||
ruff_python_ast::PythonVersion::latest()
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::ops::Deref;
|
|||
use std::sync::Arc;
|
||||
|
||||
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::source::source_text;
|
||||
|
@ -27,7 +27,13 @@ pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
|
|||
let source = source_text(db, file);
|
||||
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.
|
||||
|
|
|
@ -71,6 +71,10 @@ impl SourceDb for ModuleDb {
|
|||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
fn python_version(&self) -> PythonVersion {
|
||||
Program::get(self).python_version(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
|
|
@ -62,6 +62,10 @@ impl SourceDb for TestDb {
|
|||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
fn python_version(&self) -> PythonVersion {
|
||||
Program::get(self).python_version(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue