mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-02 18:02:23 +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 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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
# Pattern matching
|
# Pattern matching
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
## With wildcard
|
## With wildcard
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
## 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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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): ...
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
## `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
|
||||||
|
|
|
@ -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]): ...`.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue