diff --git a/crates/red_knot_ide/src/db.rs b/crates/red_knot_ide/src/db.rs index ba49ee864f..9d98db1dcb 100644 --- a/crates/red_knot_ide/src/db.rs +++ b/crates/red_knot_ide/src/db.rs @@ -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 for TestDb { diff --git a/crates/red_knot_project/src/db.rs b/crates/red_knot_project/src/db.rs index e1d19983e0..d724ebc3f3 100644 --- a/crates/red_knot_project/src/db.rs +++ b/crates/red_knot_project/src/db.rs @@ -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 for TestDb { diff --git a/crates/red_knot_project/src/lib.rs b/crates/red_knot_project/src/lib.rs index a0a7c531d7..fd6545972a 100644 --- a/crates/red_knot_project/src/lib.rs +++ b/crates/red_knot_project/src/lib.rs @@ -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 { .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(); diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md index 06388bb72f..e906b5de39 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md index 7f44fd7f76..7ee6e84060 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md @@ -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] diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md index a71a89bcc1..61887e7f29 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md @@ -1,5 +1,10 @@ # Starred expression annotations +```toml +[environment] +python-version = "3.11" +``` + Type annotations for `*args` can be starred expressions themselves: ```py diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 175ada1236..80161644ab 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md index 42a70bf04d..171ade213e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/function.md b/crates/red_knot_python_semantic/resources/mdtest/call/function.md index 10db8aaa80..54c35384c8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/function.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/function.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md index 0d3e94cd92..c7a63beff2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -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: diff --git a/crates/red_knot_python_semantic/resources/mdtest/class/super.md b/crates/red_knot_python_semantic/resources/mdtest/class/super.md index 2fa7d0d767..5c5ca3a063 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/class/super.md +++ b/crates/red_knot_python_semantic/resources/mdtest/class/super.md @@ -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. diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md index 5037484212..8b8a3dca34 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md @@ -1,5 +1,10 @@ # Pattern matching +```toml +[environment] +python-version = "3.10" +``` + ## With wildcard ```py diff --git a/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md b/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md index 000c600317..28baa67ba4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md new file mode 100644 index 0000000000..0a35fe4153 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md @@ -0,0 +1,37 @@ +# Version-related syntax error diagnostics + +## `match` statement + +The `match` statement was introduced in Python 3.10. + +### Before 3.10 + + + +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") +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md b/crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md index b4d51b3927..712fbad35c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md +++ b/crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md @@ -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. diff --git a/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md b/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md index 8c7cdfd42c..a4efccbd3a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md +++ b/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md @@ -76,6 +76,11 @@ def g(x: Any = "foo"): ## Stub functions +```toml +[environment] +python-version = "3.12" +``` + ### In Protocol ```py diff --git a/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md b/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md index 9afeda9f75..f3ce3f4b20 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md index 85574c615c..9421d16c36 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -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. diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md index 7fe3cf9ef0..10732bf3a2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 907e18c307..7870e8b62f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index b4f9992a9a..da09b7c7bc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -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. diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/star.md b/crates/red_knot_python_semantic/resources/mdtest/import/star.md index 023d46ab10..5cad39a51e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/star.md +++ b/crates/red_knot_python_semantic/resources/mdtest/import/star.md @@ -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. diff --git a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md index 33cd463848..fc66cc1220 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md +++ b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md @@ -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): ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md index d6f0246f29..27b01efe7b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md @@ -1,5 +1,10 @@ # Narrowing for `match` statements +```toml +[environment] +python-version = "3.10" +``` + ## Single `match` pattern ```py diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md index e77a6152e7..927670b508 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 0b2c0eacc3..cf7f49ac5e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -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. diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/version_related_syntax_errors.md_-_Version-related_syntax_error_diagnostics_-_`match`_statement_-_Before_3.10.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/version_related_syntax_errors.md_-_Version-related_syntax_error_diagnostics_-_`match`_statement_-_Before_3.10.snap new file mode 100644 index 0000000000..5cdf0e8838 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/version_related_syntax_errors.md_-_Version-related_syntax_error_diagnostics_-_`match`_statement_-_Before_3.10.snap @@ -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") + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/statically_known_branches.md b/crates/red_knot_python_semantic/resources/mdtest/statically_known_branches.md index 20f8efe7bb..c1d979d955 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/statically_known_branches.md +++ b/crates/red_knot_python_semantic/resources/mdtest/statically_known_branches.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md index 233fa3f6f2..715571e021 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md +++ b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md @@ -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]): ...`. diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_api.md b/crates/red_knot_python_semantic/resources/mdtest/type_api.md index d68656d725..c76f0f1374 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_api.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_api.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index fb5fe478cf..173e643f3a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 7763b3b1a6..aa5f025e13 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -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 diff --git a/crates/red_knot_python_semantic/src/db.rs b/crates/red_knot_python_semantic/src/db.rs index 976235f926..00956ccf07 100644 --- a/crates/red_knot_python_semantic/src/db.rs +++ b/crates/red_knot_python_semantic/src/db.rs @@ -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 for TestDb { diff --git a/crates/red_knot_python_semantic/src/semantic_index.rs b/crates/red_knot_python_semantic/src/semantic_index.rs index 5771220788..7bcfa969fc 100644 --- a/crates/red_knot_python_semantic/src/semantic_index.rs +++ b/crates/red_knot_python_semantic/src/semantic_index.rs @@ -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) -> 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 } } diff --git a/crates/red_knot_test/src/assertion.rs b/crates/red_knot_test/src/assertion.rs index d88c7d2b27..8f31b5de66 100644 --- a/crates/red_knot_test/src/assertion.rs +++ b/crates/red_knot_test/src/assertion.rs @@ -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) diff --git a/crates/red_knot_test/src/db.rs b/crates/red_knot_test/src/db.rs index 770be68128..b4fcccb927 100644 --- a/crates/red_knot_test/src/db.rs +++ b/crates/red_knot_test/src/db.rs @@ -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 for Db { diff --git a/crates/red_knot_test/src/lib.rs b/crates/red_knot_test/src/lib.rs index c31a68040a..0efa683f2e 100644 --- a/crates/red_knot_test/src/lib.rs +++ b/crates/red_knot_test/src/lib.rs @@ -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) => { diff --git a/crates/red_knot_test/src/matcher.rs b/crates/red_knot_test/src/matcher.rs index 8a68b5483a..63ae8c44af 100644 --- a/crates/red_knot_test/src/matcher.rs +++ b/crates/red_knot_test/src/matcher.rs @@ -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(); diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 3bb2b24070..61a3ca6805 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -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 +} diff --git a/crates/ruff_db/src/lib.rs b/crates/ruff_db/src/lib.rs index 55a47d96d1..85beefc1d8 100644 --- a/crates/ruff_db/src/lib.rs +++ b/crates/ruff_db/src/lib.rs @@ -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 { diff --git a/crates/ruff_db/src/parsed.rs b/crates/ruff_db/src/parsed.rs index a72ef55f71..3d9988cffd 100644 --- a/crates/ruff_db/src/parsed.rs +++ b/crates/ruff_db/src/parsed.rs @@ -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. diff --git a/crates/ruff_graph/src/db.rs b/crates/ruff_graph/src/db.rs index e768c6282a..368d571505 100644 --- a/crates/ruff_graph/src/db.rs +++ b/crates/ruff_graph/src/db.rs @@ -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] diff --git a/fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs b/fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs index 1da7da48dd..225bd10089 100644 --- a/fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs +++ b/fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs @@ -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 {