From c36f3f530490543a46b7203dbb795910dc8af974 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 17 Apr 2025 14:49:52 +0100 Subject: [PATCH 0001/1161] [red-knot] Add `KnownFunction` variants for `is_protocol`, `get_protocol_members` and `runtime_checkable` (#17450) --- crates/red_knot_python_semantic/src/types.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ed0174d27392ad..68b849b6713cf9 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -5896,6 +5896,12 @@ pub enum KnownFunction { Cast, /// `typing(_extensions).overload` Overload, + /// `typing(_extensions).is_protocol` + IsProtocol, + /// `typing(_extensions).get_protocol_members` + GetProtocolMembers, + /// `typing(_extensions).runtime_checkable` + RuntimeCheckable, /// `abc.abstractmethod` #[strum(serialize = "abstractmethod")] @@ -5957,6 +5963,9 @@ impl KnownFunction { | Self::Overload | Self::RevealType | Self::Final + | Self::IsProtocol + | Self::GetProtocolMembers + | Self::RuntimeCheckable | Self::NoTypeCheck => { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } @@ -7195,6 +7204,9 @@ pub(crate) mod tests { | KnownFunction::RevealType | KnownFunction::AssertType | KnownFunction::AssertNever + | KnownFunction::IsProtocol + | KnownFunction::GetProtocolMembers + | KnownFunction::RuntimeCheckable | KnownFunction::NoTypeCheck => KnownModule::TypingExtensions, KnownFunction::IsSingleton From d2ebfd6ed7855140806a7cad3e23d58fe5056c31 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 17 Apr 2025 21:58:26 +0800 Subject: [PATCH 0002/1161] [`pyflakes`] Add fix safety section (`F841`) (#17410) add fix safety section to docs for #15584, I'm new to ruff and not sure if the content of this PR is correct, but I hope it can be helpful. --------- Co-authored-by: Brent Westbrook --- .../ruff_linter__message__sarif__tests__results.snap | 2 +- .../ruff_linter/src/rules/pyflakes/rules/unused_variable.rs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap index 373c7aa2ae589a..287d91e6f18643 100644 --- a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap @@ -119,7 +119,7 @@ expression: value }, { "fullDescription": { - "text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Options\n- `lint.dummy-variable-rgx`\n" + "text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Fix safety\n\nThis rule's fix is marked as unsafe because removing an unused variable assignment may\ndelete comments that are attached to the assignment.\n\n## Options\n- `lint.dummy-variable-rgx`\n" }, "help": { "text": "Local variable `{name}` is assigned to but never used" diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs index dc7d7e208cef8a..4c8dbc39081944 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_variable.rs @@ -38,6 +38,11 @@ use crate::fix::edits::delete_stmt; /// return x /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe because removing an unused variable assignment may +/// delete comments that are attached to the assignment. +/// /// ## Options /// - `lint.dummy-variable-rgx` #[derive(ViolationMetadata)] From 9c47b6dbb0f29369fdf38d5048276994b2e09e49 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:00:30 -0400 Subject: [PATCH 0003/1161] [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. --- crates/red_knot_ide/src/db.rs | 6 ++- crates/red_knot_project/src/db.rs | 10 ++++- crates/red_knot_project/src/lib.rs | 22 ++++++++++- .../resources/mdtest/annotations/callable.md | 5 +++ .../resources/mdtest/annotations/deferred.md | 10 +++++ .../resources/mdtest/annotations/starred.md | 5 +++ .../annotations/unsupported_special_forms.md | 5 +++ .../mdtest/assignment/annotations.md | 5 +++ .../resources/mdtest/call/function.md | 5 +++ .../resources/mdtest/call/methods.md | 5 +++ .../resources/mdtest/class/super.md | 10 +++++ .../resources/mdtest/conditional/match.md | 5 +++ .../resources/mdtest/dataclasses.md | 10 +++++ .../version_related_syntax_errors.md | 37 +++++++++++++++++++ .../mdtest/directives/assert_never.md | 5 +++ .../resources/mdtest/function/parameters.md | 5 +++ .../resources/mdtest/function/return_type.md | 10 +++++ .../resources/mdtest/generics/classes.md | 5 +++ .../resources/mdtest/generics/functions.md | 5 +++ .../resources/mdtest/generics/pep695.md | 5 +++ .../resources/mdtest/generics/scoping.md | 5 +++ .../resources/mdtest/import/star.md | 5 +++ .../resources/mdtest/metaclass.md | 5 +++ .../resources/mdtest/narrow/match.md | 5 +++ .../resources/mdtest/narrow/type.md | 5 +++ .../resources/mdtest/protocols.md | 5 +++ ...ics_-_`match`_statement_-_Before_3.10.snap | 32 ++++++++++++++++ .../mdtest/statically_known_branches.md | 6 +++ .../resources/mdtest/stubs/class.md | 5 +++ .../resources/mdtest/type_api.md | 5 +++ .../type_properties/is_disjoint_from.md | 5 +++ .../mdtest/type_properties/is_subtype_of.md | 5 +++ crates/red_knot_python_semantic/src/db.rs | 4 ++ .../src/semantic_index.rs | 17 +++++---- crates/red_knot_test/src/assertion.rs | 14 +++++++ crates/red_knot_test/src/db.rs | 6 ++- crates/red_knot_test/src/lib.rs | 12 +++++- crates/red_knot_test/src/matcher.rs | 14 +++++++ crates/ruff_db/src/diagnostic/mod.rs | 13 +++++++ crates/ruff_db/src/lib.rs | 6 +++ crates/ruff_db/src/parsed.rs | 10 ++++- crates/ruff_graph/src/db.rs | 4 ++ .../red_knot_check_invalid_syntax.rs | 4 ++ 43 files changed, 353 insertions(+), 14 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/version_related_syntax_errors.md_-_Version-related_syntax_error_diagnostics_-_`match`_statement_-_Before_3.10.snap diff --git a/crates/red_knot_ide/src/db.rs b/crates/red_knot_ide/src/db.rs index ba49ee864f73e5..9d98db1dcb663b 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 e1d19983e00ce7..d724ebc3f3f64d 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 a0a7c531d70d87..fd6545972a4664 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 06388bb72f2da7..e906b5de39b2ae 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 7f44fd7f76c5f6..7ee6e84060cab2 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 a71a89bcc13db3..61887e7f29edec 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 175ada12360fab..80161644ab963e 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 42a70bf04d3090..171ade213e99b3 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 10db8aaa80a32e..54c35384c80b12 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 0d3e94cd929bc0..c7a63beff2e38d 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 2fa7d0d767ecf5..5c5ca3a0639972 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 5037484212eb63..8b8a3dca342351 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 000c60031758f1..28baa67ba48fac 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 00000000000000..0a35fe4153c000 --- /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 b4d51b3927841f..712fbad35cda85 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 8c7cdfd42ca5be..a4efccbd3a393c 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 9afeda9f752221..f3ce3f4b200f7c 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 85574c615c9e69..9421d16c3622b3 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 7fe3cf9ef0644f..10732bf3a243ea 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 907e18c3073ac4..7870e8b62f0ff8 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 b4f9992a9a4820..da09b7c7bc8f62 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 023d46ab100bad..5cad39a51e0a6a 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 33cd463848442d..fc66cc12207fe1 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 d6f0246f293f8a..27b01efe7b7338 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 e77a6152e7717d..927670b5081af9 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 0b2c0eacc3d2dc..cf7f49ac5e7eda 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 00000000000000..5cdf0e883866d6 --- /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 20f8efe7bbca2a..c1d979d955553c 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 233fa3f6f28c1c..715571e02114ea 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 d68656d7252a9d..c76f0f13740a3f 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 fb5fe478cfbb14..173e643f3a1ac8 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 7763b3b1a68a93..aa5f025e13d8c4 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 976235f926111c..00956ccf07b295 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 577122078864df..7bcfa969fc26cd 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 d88c7d2b27c784..8f31b5de666a77 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 770be68128aa78..b4fcccb9276aaf 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 c31a68040a4fa8..0efa683f2e8381 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 8a68b5483aee36..63ae8c44af7ab9 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 3bb2b240702879..61a3ca68053b1a 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 55a47d96d1f450..85beefc1d89353 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 a72ef55f71fb45..3d9988cffdf5da 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 e768c6282a0ba3..368d571505878a 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 1da7da48dd3c07..225bd10089a183 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 { From 58807b2980f3f79e6d16d4f1b05231bd66807ee5 Mon Sep 17 00:00:00 2001 From: Nuri Jung Date: Fri, 18 Apr 2025 05:02:12 +0900 Subject: [PATCH 0004/1161] Server: Use `min` instead of `max` to limit the number of threads (#17421) ## Summary Prevent overcommit by using max 4 threads as intended. Unintuitively, `.max()` returns the maximum value of `self` and the argument (not limiting to the argument). To limit the value to 4, one needs to use `.min()`. https://doc.rust-lang.org/std/cmp/trait.Ord.html#method.max --- crates/red_knot_server/src/lib.rs | 2 +- crates/ruff/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_server/src/lib.rs b/crates/red_knot_server/src/lib.rs index f5ba01d6894909..ccd35d702571aa 100644 --- a/crates/red_knot_server/src/lib.rs +++ b/crates/red_knot_server/src/lib.rs @@ -30,7 +30,7 @@ pub fn run_server() -> anyhow::Result<()> { // by default, we set the number of worker threads to `num_cpus`, with a maximum of 4. let worker_threads = std::thread::available_parallelism() .unwrap_or(four) - .max(four); + .min(four); Server::new(worker_threads) .context("Failed to start server")? diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 309bfc6fd05503..6446c51b980fa8 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -228,7 +228,7 @@ fn server(args: ServerCommand) -> Result { // by default, we set the number of worker threads to `num_cpus`, with a maximum of 4. let worker_threads = std::thread::available_parallelism() .unwrap_or(four) - .max(four); + .min(four); commands::server::run_server(worker_threads, args.resolve_preview()) } From 9965cee998d798a9aeacb9ed4d3aea454363beaf Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 17 Apr 2025 21:54:22 +0100 Subject: [PATCH 0005/1161] [red-knot] Understand `typing.Protocol` and `typing_extensions.Protocol` as equivalent (#17446) --- .../resources/mdtest/protocols.md | 26 ++++++++++++------- crates/red_knot_python_semantic/src/types.rs | 12 +++++++-- .../src/types/class.rs | 4 +-- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index cf7f49ac5e7eda..f8620997512994 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -227,13 +227,15 @@ reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool ```py import typing import typing_extensions -from knot_extensions import static_assert, is_equivalent_to +from knot_extensions import static_assert, is_equivalent_to, TypeOf + +static_assert(is_equivalent_to(TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol])) +static_assert(is_equivalent_to(int | str | TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol] | str | int)) class Foo(typing.Protocol): x: int -# TODO: should not error -class Bar(typing_extensions.Protocol): # error: [invalid-base] +class Bar(typing_extensions.Protocol): x: int # TODO: these should pass @@ -249,9 +251,8 @@ The same goes for `typing.runtime_checkable` and `typing_extensions.runtime_chec class RuntimeCheckableFoo(typing.Protocol): x: int -# TODO: should not error @typing.runtime_checkable -class RuntimeCheckableBar(typing_extensions.Protocol): # error: [invalid-base] +class RuntimeCheckableBar(typing_extensions.Protocol): x: int # TODO: these should pass @@ -264,6 +265,15 @@ isinstance(object(), RuntimeCheckableFoo) isinstance(object(), RuntimeCheckableBar) ``` +However, we understand that they are not necessarily the same symbol at the same memory address at +runtime -- these reveal `bool` rather than `Literal[True]` or `Literal[False]`, which would be +incorrect: + +```py +reveal_type(typing.Protocol is typing_extensions.Protocol) # revealed: bool +reveal_type(typing.Protocol is not typing_extensions.Protocol) # revealed: bool +``` + ## Calls to protocol classes Neither `Protocol`, nor any protocol class, can be directly instantiated: @@ -309,8 +319,7 @@ via `typing_extensions`. ```py from typing_extensions import Protocol, get_protocol_members -# TODO: should not error -class Foo(Protocol): # error: [invalid-base] +class Foo(Protocol): x: int @property @@ -351,8 +360,7 @@ Certain special attributes and methods are not considered protocol members at ru not be considered protocol members by type checkers either: ```py -# TODO: should not error -class Lumberjack(Protocol): # error: [invalid-base] +class Lumberjack(Protocol): __slots__ = () __match_args__ = () x: int diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 68b849b6713cf9..4a2b446d2ee45d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1949,8 +1949,16 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(..) | Type::ClassLiteral(..) | Type::GenericAlias(..) - | Type::ModuleLiteral(..) - | Type::KnownInstance(..) => true, + | Type::ModuleLiteral(..) => true, + Type::KnownInstance(known_instance) => { + // Nearly all `KnownInstance` types are singletons, but if a symbol could validly + // originate from either `typing` or `typing_extensions` then this is not guaranteed. + // E.g. `typing.Protocol` is equivalent to `typing_extensions.Protocol`, so both are treated + // as inhabiting the type `KnownInstanceType::Protocol` in our model, but they are actually + // distinct symbols at different memory addresses at runtime. + !(known_instance.check_module(KnownModule::Typing) + && known_instance.check_module(KnownModule::TypingExtensions)) + } Type::Callable(_) => { // A callable type is never a singleton because for any given signature, // there could be any number of distinct objects that are all callable with that diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index b690f6ea8c3065..d59fa8e0068c55 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -2532,7 +2532,7 @@ impl<'db> KnownInstanceType<'db> { /// /// Most variants can only exist in one module, which is the same as `self.class().canonical_module()`. /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. - fn check_module(self, module: KnownModule) -> bool { + pub(super) fn check_module(self, module: KnownModule) -> bool { match self { Self::Any | Self::ClassVar @@ -2545,7 +2545,6 @@ impl<'db> KnownInstanceType<'db> { | Self::Counter | Self::ChainMap | Self::OrderedDict - | Self::Protocol | Self::Optional | Self::Union | Self::NoReturn @@ -2553,6 +2552,7 @@ impl<'db> KnownInstanceType<'db> { | Self::Type | Self::Callable => module.is_typing(), Self::Annotated + | Self::Protocol | Self::Literal | Self::LiteralString | Self::Never From edfa03a692b86dbfd660a2f9f6cdfd238010b7df Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Fri, 18 Apr 2025 01:28:06 +0100 Subject: [PATCH 0006/1161] [red-knot] Add some narrowing for assignment expressions (#17448) ## Summary Fixes #14866 Fixes #17437 ## Test Plan Update mdtests in `narrow/` --- .../resources/mdtest/boolean/short_circuit.md | 2 +- .../mdtest/narrow/conditionals/boolean.md | 12 ++ .../mdtest/narrow/conditionals/elif_else.md | 13 ++ .../mdtest/narrow/conditionals/in.md | 14 ++ .../mdtest/narrow/conditionals/is.md | 13 ++ .../mdtest/narrow/conditionals/is_not.md | 11 ++ .../mdtest/narrow/conditionals/not_eq.md | 15 +++ .../src/types/narrow.rs | 127 ++++++++++++------ 8 files changed, 163 insertions(+), 44 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md b/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md index 6ad75f185bb35c..f77eea2d3140f5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md +++ b/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md @@ -43,7 +43,7 @@ if True and (x := 1): ```py def _(flag: bool): - flag or (x := 1) or reveal_type(x) # revealed: Literal[1] + flag or (x := 1) or reveal_type(x) # revealed: Never # error: [unresolved-reference] flag or reveal_type(y) or (y := 1) # revealed: Unknown diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md index c0e1af2f3dd9a9..566ec10a78de34 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md @@ -223,3 +223,15 @@ def _(x: str | None, y: str | None): if y is not x: reveal_type(y) # revealed: str | None ``` + +## Assignment expressions + +```py +def f() -> bool: + return True + +if x := f(): + reveal_type(x) # revealed: Literal[True] +else: + reveal_type(x) # revealed: Literal[False] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md index 76eae880ef39eb..376c24f1e97f92 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md @@ -47,3 +47,16 @@ def _(flag1: bool, flag2: bool): # TODO should be Never reveal_type(x) # revealed: Literal[1, 2] ``` + +## Assignment expressions + +```py +def f() -> int | str | None: ... + +if isinstance(x := f(), int): + reveal_type(x) # revealed: int +elif isinstance(x, str): + reveal_type(x) # revealed: str & ~int +else: + reveal_type(x) # revealed: None +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/in.md index dad037470270e5..865eb48788c01f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -78,3 +78,17 @@ def _(x: Literal[1, "a", "b", "c", "d"]): else: reveal_type(x) # revealed: Literal[1, "d"] ``` + +## Assignment expressions + +```py +from typing import Literal + +def f() -> Literal[1, 2, 3]: + return 1 + +if (x := f()) in (1,): + reveal_type(x) # revealed: Literal[1] +else: + reveal_type(x) # revealed: Literal[2, 3] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md index 8a95bfc278f813..c7d99c48b2e4fb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -100,3 +100,16 @@ def _(flag: bool): else: reveal_type(x) # revealed: Literal[42] ``` + +## Assignment expressions + +```py +from typing import Literal + +def f() -> Literal[1, 2] | None: ... + +if (x := f()) is None: + reveal_type(x) # revealed: None +else: + reveal_type(x) # revealed: Literal[1, 2] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md index 980a66a68d2eed..fba62e921323be 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md @@ -82,3 +82,14 @@ def _(x_flag: bool, y_flag: bool): reveal_type(x) # revealed: bool reveal_type(y) # revealed: bool ``` + +## Assignment expressions + +```py +def f() -> int | str | None: ... + +if (x := f()) is not None: + reveal_type(x) # revealed: int | str +else: + reveal_type(x) # revealed: None +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md index abe0c4d5aaea10..20f25d9ed4190f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md @@ -89,3 +89,18 @@ def _(flag1: bool, flag2: bool, a: int): else: reveal_type(x) # revealed: Literal[1, 2] ``` + +## Assignment expressions + +```py +from typing import Literal + +def f() -> Literal[1, 2, 3]: + return 1 + +if (x := f()) != 1: + reveal_type(x) # revealed: Literal[2, 3] +else: + # TODO should be Literal[1] + reveal_type(x) # revealed: Literal[1, 2, 3] +``` diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index cf5431b47ec804..04ca2ead84af0a 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -275,7 +275,8 @@ impl<'db> NarrowingConstraintsBuilder<'db> { self.evaluate_expression_node_predicate(&unary_op.operand, expression, !is_positive) } ast::Expr::BoolOp(bool_op) => self.evaluate_bool_op(bool_op, expression, is_positive), - _ => None, // TODO other test expression kinds + ast::Expr::Named(expr_named) => self.evaluate_expr_named(expr_named, is_positive), + _ => None, } } @@ -343,6 +344,18 @@ impl<'db> NarrowingConstraintsBuilder<'db> { NarrowingConstraints::from_iter([(symbol, ty)]) } + fn evaluate_expr_named( + &mut self, + expr_named: &ast::ExprNamed, + is_positive: bool, + ) -> Option> { + if let ast::Expr::Name(expr_name) = expr_named.target.as_ref() { + Some(self.evaluate_expr_name(expr_name, is_positive)) + } else { + None + } + } + fn evaluate_expr_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) { match rhs_ty { @@ -365,6 +378,44 @@ impl<'db> NarrowingConstraintsBuilder<'db> { } } + fn evaluate_expr_compare_op( + &mut self, + lhs_ty: Type<'db>, + rhs_ty: Type<'db>, + op: ast::CmpOp, + ) -> Option> { + match op { + ast::CmpOp::IsNot => { + if rhs_ty.is_singleton(self.db) { + let ty = IntersectionBuilder::new(self.db) + .add_negative(rhs_ty) + .build(); + Some(ty) + } else { + // Non-singletons cannot be safely narrowed using `is not` + None + } + } + ast::CmpOp::Is => Some(rhs_ty), + ast::CmpOp::NotEq => { + if rhs_ty.is_single_valued(self.db) { + let ty = IntersectionBuilder::new(self.db) + .add_negative(rhs_ty) + .build(); + Some(ty) + } else { + None + } + } + ast::CmpOp::Eq if lhs_ty.is_literal_string() => Some(rhs_ty), + ast::CmpOp::In => self.evaluate_expr_in(lhs_ty, rhs_ty), + ast::CmpOp::NotIn => self + .evaluate_expr_in(lhs_ty, rhs_ty) + .map(|ty| ty.negate(self.db)), + _ => None, + } + } + fn evaluate_expr_compare( &mut self, expr_compare: &ast::ExprCompare, @@ -372,7 +423,10 @@ impl<'db> NarrowingConstraintsBuilder<'db> { is_positive: bool, ) -> Option> { fn is_narrowing_target_candidate(expr: &ast::Expr) -> bool { - matches!(expr, ast::Expr::Name(_) | ast::Expr::Call(_)) + matches!( + expr, + ast::Expr::Name(_) | ast::Expr::Call(_) | ast::Expr::Named(_) + ) } let ast::ExprCompare { @@ -423,43 +477,24 @@ impl<'db> NarrowingConstraintsBuilder<'db> { }) => { let symbol = self.expect_expr_name_symbol(id); - match if is_positive { *op } else { op.negate() } { - ast::CmpOp::IsNot => { - if rhs_ty.is_singleton(self.db) { - let ty = IntersectionBuilder::new(self.db) - .add_negative(rhs_ty) - .build(); - constraints.insert(symbol, ty); - } else { - // Non-singletons cannot be safely narrowed using `is not` - } - } - ast::CmpOp::Is => { - constraints.insert(symbol, rhs_ty); - } - ast::CmpOp::NotEq => { - if rhs_ty.is_single_valued(self.db) { - let ty = IntersectionBuilder::new(self.db) - .add_negative(rhs_ty) - .build(); - constraints.insert(symbol, ty); - } - } - ast::CmpOp::Eq if lhs_ty.is_literal_string() => { - constraints.insert(symbol, rhs_ty); - } - ast::CmpOp::In => { - if let Some(ty) = self.evaluate_expr_in(lhs_ty, rhs_ty) { - constraints.insert(symbol, ty); - } - } - ast::CmpOp::NotIn => { - if let Some(ty) = self.evaluate_expr_in(lhs_ty, rhs_ty) { - constraints.insert(symbol, ty.negate(self.db)); - } - } - _ => { - // TODO other comparison types + let op = if is_positive { *op } else { op.negate() }; + + if let Some(ty) = self.evaluate_expr_compare_op(lhs_ty, rhs_ty, op) { + constraints.insert(symbol, ty); + } + } + ast::Expr::Named(ast::ExprNamed { + range: _, + target, + value: _, + }) => { + if let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { + let symbol = self.expect_expr_name_symbol(id); + + let op = if is_positive { *op } else { op.negate() }; + + if let Some(ty) = self.evaluate_expr_compare_op(lhs_ty, rhs_ty, op) { + constraints.insert(symbol, ty); } } } @@ -535,10 +570,16 @@ impl<'db> NarrowingConstraintsBuilder<'db> { Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => { let function = function_type.known(self.db)?.into_constraint_function()?; - let [ast::Expr::Name(ast::ExprName { id, .. }), class_info] = - &*expr_call.arguments.args - else { - return None; + let (id, class_info) = match &*expr_call.arguments.args { + [first, class_info] => match first { + ast::Expr::Named(ast::ExprNamed { target, .. }) => match target.as_ref() { + ast::Expr::Name(ast::ExprName { id, .. }) => (id, class_info), + _ => return None, + }, + ast::Expr::Name(ast::ExprName { id, .. }) => (id, class_info), + _ => return None, + }, + _ => return None, }; let symbol = self.expect_expr_name_symbol(id); From de8f4e62e274dfaf674fc981045a9abbe15eab44 Mon Sep 17 00:00:00 2001 From: Eric Mark Martin Date: Thu, 17 Apr 2025 21:18:34 -0400 Subject: [PATCH 0007/1161] [red-knot] more type-narrowing in match statements (#17302) ## Summary Add more narrowing analysis for match statements: * add narrowing constraints from guard expressions * add negated constraints from previous predicates and guards to subsequent cases This PR doesn't address that guards can mutate your subject, and so theoretically invalidate some of these narrowing constraints that you've previously accumulated. Some prior art on this issue [here][mutable guards]. [mutable guards]: https://www.irif.fr/~scherer/research/mutable-patterns/mutable-patterns-mlworkshop2024-abstract.pdf ## Test Plan Add some new tests, and update some existing ones --------- Co-authored-by: Carl Meyer --- .../resources/mdtest/narrow/match.md | 55 +++++++++++-- .../src/semantic_index/builder.rs | 78 ++++++++++++------- .../src/types/narrow.rs | 33 +++++++- 3 files changed, 130 insertions(+), 36 deletions(-) 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 27b01efe7b7338..8fd2f7cfdde2a4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md @@ -39,8 +39,7 @@ match x: case A(): reveal_type(x) # revealed: A case B(): - # TODO could be `B & ~A` - reveal_type(x) # revealed: B + reveal_type(x) # revealed: B & ~A reveal_type(x) # revealed: object ``` @@ -88,7 +87,7 @@ match x: case 6.0: reveal_type(x) # revealed: float case 1j: - reveal_type(x) # revealed: complex + reveal_type(x) # revealed: complex & ~float case b"foo": reveal_type(x) # revealed: Literal[b"foo"] @@ -134,11 +133,11 @@ match x: case "foo" | 42 | None: reveal_type(x) # revealed: Literal["foo", 42] | None case "foo" | tuple(): - reveal_type(x) # revealed: Literal["foo"] | tuple + reveal_type(x) # revealed: tuple case True | False: reveal_type(x) # revealed: bool case 3.14 | 2.718 | 1.414: - reveal_type(x) # revealed: float + reveal_type(x) # revealed: float & ~tuple reveal_type(x) # revealed: object ``` @@ -165,3 +164,49 @@ match x: reveal_type(x) # revealed: object ``` + +## Narrowing due to guard + +```py +def get_object() -> object: + return object() + +x = get_object() + +reveal_type(x) # revealed: object + +match x: + case str() | float() if type(x) is str: + reveal_type(x) # revealed: str + case "foo" | 42 | None if isinstance(x, int): + reveal_type(x) # revealed: Literal[42] + case False if x: + reveal_type(x) # revealed: Never + case "foo" if x := "bar": + reveal_type(x) # revealed: Literal["bar"] + +reveal_type(x) # revealed: object +``` + +## Guard and reveal_type in guard + +```py +def get_object() -> object: + return object() + +x = get_object() + +reveal_type(x) # revealed: object + +match x: + case str() | float() if type(x) is str and reveal_type(x): # revealed: str + pass + case "foo" | 42 | None if isinstance(x, int) and reveal_type(x): # revealed: Literal[42] + pass + case False if x and reveal_type(x): # revealed: Never + pass + case "foo" if (x := "bar") and reveal_type(x): # revealed: Literal["bar"] + pass + +reveal_type(x) # revealed: object +``` diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index f2d2a30224c515..e4c25f48400b5e 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -1572,54 +1572,76 @@ where return; } - let after_subject = self.flow_snapshot(); - let mut vis_constraints = vec![]; + let mut no_case_matched = self.flow_snapshot(); + + let has_catchall = cases + .last() + .is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard()); + let mut post_case_snapshots = vec![]; - for (i, case) in cases.iter().enumerate() { - if i != 0 { - post_case_snapshots.push(self.flow_snapshot()); - self.flow_restore(after_subject.clone()); - } + let mut match_predicate; + for (i, case) in cases.iter().enumerate() { self.current_match_case = Some(CurrentMatchCase::new(&case.pattern)); self.visit_pattern(&case.pattern); self.current_match_case = None; - let predicate = self.add_pattern_narrowing_constraint( + // unlike in [Stmt::If], we don't reset [no_case_matched] + // here because the effects of visiting a pattern is binding + // symbols, and this doesn't occur unless the pattern + // actually matches + match_predicate = self.add_pattern_narrowing_constraint( subject_expr, &case.pattern, case.guard.as_deref(), ); - self.record_reachability_constraint(predicate); - if let Some(expr) = &case.guard { - self.visit_expr(expr); - } + let vis_constraint_id = self.record_reachability_constraint(match_predicate); + + let match_success_guard_failure = case.guard.as_ref().map(|guard| { + let guard_expr = self.add_standalone_expression(guard); + self.visit_expr(guard); + let post_guard_eval = self.flow_snapshot(); + let predicate = Predicate { + node: PredicateNode::Expression(guard_expr), + is_positive: true, + }; + self.record_negated_narrowing_constraint(predicate); + let match_success_guard_failure = self.flow_snapshot(); + self.flow_restore(post_guard_eval); + self.record_narrowing_constraint(predicate); + match_success_guard_failure + }); + + self.record_visibility_constraint_id(vis_constraint_id); + self.visit_body(&case.body); - for id in &vis_constraints { - self.record_negated_visibility_constraint(*id); - } - let vis_constraint_id = self.record_visibility_constraint(predicate); - vis_constraints.push(vis_constraint_id); - } - // If there is no final wildcard match case, pretend there is one. This is similar to how - // we add an implicit `else` block in if-elif chains, in case it's not present. - if !cases - .last() - .is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard()) - { post_case_snapshots.push(self.flow_snapshot()); - self.flow_restore(after_subject.clone()); - for id in &vis_constraints { - self.record_negated_visibility_constraint(*id); + if i != cases.len() - 1 || !has_catchall { + // We need to restore the state after each case, but not after the last + // one. The last one will just become the state that we merge the other + // snapshots into. + self.flow_restore(no_case_matched.clone()); + self.record_negated_narrowing_constraint(match_predicate); + if let Some(match_success_guard_failure) = match_success_guard_failure { + self.flow_merge(match_success_guard_failure); + } else { + assert!(case.guard.is_none()); + } + } else { + debug_assert!(match_success_guard_failure.is_none()); + debug_assert!(case.guard.is_none()); } + + self.record_negated_visibility_constraint(vis_constraint_id); + no_case_matched = self.flow_snapshot(); } for post_clause_state in post_case_snapshots { self.flow_merge(post_clause_state); } - self.simplify_visibility_constraints(after_subject); + self.simplify_visibility_constraints(no_case_matched); } ast::Stmt::Try(ast::StmtTry { body, diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index 04ca2ead84af0a..50cfb9b931b842 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -50,7 +50,13 @@ pub(crate) fn infer_narrowing_constraint<'db>( all_negative_narrowing_constraints_for_expression(db, expression) } } - PredicateNode::Pattern(pattern) => all_narrowing_constraints_for_pattern(db, pattern), + PredicateNode::Pattern(pattern) => { + if predicate.is_positive { + all_narrowing_constraints_for_pattern(db, pattern) + } else { + all_negative_narrowing_constraints_for_pattern(db, pattern) + } + } PredicateNode::StarImportPlaceholder(_) => return None, }; if let Some(constraints) = constraints { @@ -95,6 +101,15 @@ fn all_negative_narrowing_constraints_for_expression<'db>( NarrowingConstraintsBuilder::new(db, PredicateNode::Expression(expression), false).finish() } +#[allow(clippy::ref_option)] +#[salsa::tracked(return_ref)] +fn all_negative_narrowing_constraints_for_pattern<'db>( + db: &'db dyn Db, + pattern: PatternPredicate<'db>, +) -> Option> { + NarrowingConstraintsBuilder::new(db, PredicateNode::Pattern(pattern), false).finish() +} + #[allow(clippy::ref_option)] fn constraints_for_expression_cycle_recover<'db>( _db: &'db dyn Db, @@ -217,6 +232,12 @@ fn merge_constraints_or<'db>( } } +fn negate_if<'db>(constraints: &mut NarrowingConstraints<'db>, db: &'db dyn Db, yes: bool) { + for (_symbol, ty) in constraints.iter_mut() { + *ty = ty.negate_if(db, yes); + } +} + struct NarrowingConstraintsBuilder<'db> { db: &'db dyn Db, predicate: PredicateNode<'db>, @@ -237,7 +258,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> { PredicateNode::Expression(expression) => { self.evaluate_expression_predicate(expression, self.is_positive) } - PredicateNode::Pattern(pattern) => self.evaluate_pattern_predicate(pattern), + PredicateNode::Pattern(pattern) => { + self.evaluate_pattern_predicate(pattern, self.is_positive) + } PredicateNode::StarImportPlaceholder(_) => return None, }; if let Some(mut constraints) = constraints { @@ -301,10 +324,14 @@ impl<'db> NarrowingConstraintsBuilder<'db> { fn evaluate_pattern_predicate( &mut self, pattern: PatternPredicate<'db>, + is_positive: bool, ) -> Option> { let subject = pattern.subject(self.db); - self.evaluate_pattern_predicate_kind(pattern.kind(self.db), subject) + .map(|mut constraints| { + negate_if(&mut constraints, self.db, !is_positive); + constraints + }) } fn symbols(&self) -> Arc { From c7372d218de365c8298afb37530ee26999ba91b0 Mon Sep 17 00:00:00 2001 From: Hans Date: Fri, 18 Apr 2025 10:45:53 +0800 Subject: [PATCH 0008/1161] [`pyupgrade`] Add fix safety section to docs (`UP036`) (#17444) ## Summary add fix safety section to outdated_version_block, for #15584 --- .../src/rules/pyupgrade/rules/outdated_version_block.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs index 969220eb4481e9..bcae8b3de3ddda 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -44,6 +44,10 @@ use ruff_python_ast::PythonVersion; /// ## Options /// - `target-version` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe because it will remove all code, +/// comments, and annotations within unreachable version blocks. +/// /// ## References /// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[derive(ViolationMetadata)] From 44ad2012622d73373eb7922978f391eb6c1e54fa Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 18 Apr 2025 09:57:40 +0530 Subject: [PATCH 0009/1161] [red-knot] Add support for overloaded functions (#17366) ## Summary Part of #15383, this PR adds support for overloaded callables. Typing spec: https://typing.python.org/en/latest/spec/overload.html Specifically, it does the following: 1. Update the `FunctionType::signature` method to return signatures from a possibly overloaded callable using a new `FunctionSignature` enum 2. Update `CallableType` to accommodate overloaded callable by updating the inner type to `Box<[Signature]>` 3. Update the relation methods on `CallableType` with logic specific to overloads 4. Update the display of callable type to display a list of signatures enclosed by parenthesis 5. Update `CallableTypeOf` special form to recognize overloaded callable 6. Update subtyping, assignability and fully static check to account for callables (equivalence is planned to be done as a follow-up) For (2), it is required to be done in this PR because otherwise I'd need to add some workaround for `into_callable_type` and I though it would be best to include it in here. For (2), another possible design would be convert `CallableType` in an enum with two variants `CallableType::Single` and `CallableType::Overload` but I decided to go with `Box<[Signature]>` for now to (a) mirror it to be equivalent to `overload` field on `CallableSignature` and (b) to avoid any refactor in this PR. This could be done in a follow-up to better split the two kind of callables. ### Design There were two main candidates on how to represent the overloaded definition: 1. To include it in the existing infrastructure which is what this PR is doing by recognizing all the signatures within the `FunctionType::signature` method 2. To create a new `Overload` type variant
For context, this is what I had in mind with the new type variant:

```rs pub enum Type { FunctionLiteral(FunctionType), Overload(OverloadType), BoundMethod(BoundMethodType), ... } pub struct OverloadType { // FunctionLiteral or BoundMethod overloads: Box<[Type]>, // FunctionLiteral or BoundMethod implementation: Option } pub struct BoundMethodType { kind: BoundMethodKind, self_instance: Type, } pub enum BoundMethodKind { Function(FunctionType), Overload(OverloadType), } ```

The main reasons to choose (1) are the simplicity in the implementation, reusing the existing infrastructure, avoiding any complications that the new type variant has specifically around the different variants between function and methods which would require the overload type to use `Type` instead. ### Implementation The core logic is how to collect all the overloaded functions. The way this is done in this PR is by recording a **use** on the `Identifier` node that represents the function name in the use-def map. This is then used to fetch the previous symbol using the same name. This way the signatures are going to be propagated from top to bottom (from first overload to the final overload or the implementation) with each function / method. For example: ```py from typing import overload @overload def foo(x: int) -> int: ... @overload def foo(x: str) -> str: ... def foo(x: int | str) -> int | str: return x ``` Here, each definition of `foo` knows about all the signatures that comes before itself. So, the first overload would only see itself, the second would see the first and itself and so on until the implementation or the final overload. This approach required some updates specifically recognizing `Identifier` node to record the function use because it doesn't use `ExprName`. ## Test Plan Update existing test cases which were limited by the overload support and add test cases for the following cases: * Valid overloads as functions, methods, generics, version specific * Invalid overloads as stated in https://typing.python.org/en/latest/spec/overload.html#invalid-overload-definitions (implementation will be done in a follow-up) * Various relation: fully static, subtyping, and assignability (others in a follow-up) ## Ecosystem changes _WIP_ After going through the ecosystem changes (there are a lot!), here's what I've found: We need assignability check between a callable type and a class literal because a lot of builtins are defined as classes in typeshed whose constructor method is overloaded e.g., `map`, `sorted`, `list.sort`, `max`, `min` with the `key` parameter, `collections.abc.defaultdict`, etc. (https://github.com/astral-sh/ruff/issues/17343). This makes up most of the ecosystem diff **roughly 70 diagnostics**. For example: ```py from collections import defaultdict # red-knot: No overload of bound method `__init__` matches arguments [lint:no-matching-overload] defaultdict(int) # red-knot: No overload of bound method `__init__` matches arguments [lint:no-matching-overload] defaultdict(list) class Foo: def __init__(self, x: int): self.x = x # red-knot: No overload of function `__new__` matches arguments [lint:no-matching-overload] map(Foo, ["a", "b", "c"]) ``` Duplicate diagnostics in unpacking (https://github.com/astral-sh/ruff/issues/16514) has **~16 diagnostics**. Support for the `callable` builtin which requires `TypeIs` support. This is **5 diagnostics**. For example: ```py from typing import Any def _(x: Any | None) -> None: if callable(x): # red-knot: `Any | None` # Pyright: `(...) -> object` # mypy: `Any` # pyrefly: `(...) -> object` reveal_type(x) ``` Narrowing on `assert` which has **11 diagnostics**. This is being worked on in https://github.com/astral-sh/ruff/pull/17345. For example: ```py import re match = re.search("", "") assert match match.group() # error: [possibly-unbound-attribute] ``` Others: * `Self`: 2 * Type aliases: 6 * Generics: 3 * Protocols: 13 * Unpacking in comprehension: 1 (https://github.com/astral-sh/ruff/pull/17396) ## Performance Refer to https://github.com/astral-sh/ruff/pull/17366#issuecomment-2814053046. --- crates/red_knot/tests/cli.rs | 24 +- .../mdtest/annotations/literal_string.md | 6 +- .../resources/mdtest/attributes.md | 4 +- .../resources/mdtest/binary/instances.md | 10 +- .../resources/mdtest/binary/integers.md | 8 +- .../resources/mdtest/dataclasses.md | 13 +- .../resources/mdtest/descriptor_protocol.md | 6 +- .../resources/mdtest/narrow/truthiness.md | 2 +- .../resources/mdtest/overloads.md | 638 ++++++++++++++++++ .../resources/mdtest/subscript/bytes.md | 9 +- .../resources/mdtest/subscript/lists.md | 14 +- .../resources/mdtest/subscript/string.md | 9 +- .../resources/mdtest/subscript/tuple.md | 4 +- .../type_properties/is_assignable_to.md | 31 + .../type_properties/is_equivalent_to.md | 4 + .../mdtest/type_properties/is_fully_static.md | 31 + .../is_gradual_equivalent_to.md | 2 + .../mdtest/type_properties/is_subtype_of.md | 182 +++++ .../src/semantic_index/ast_ids.rs | 15 +- .../src/semantic_index/builder.rs | 11 + crates/red_knot_python_semantic/src/types.rs | 364 ++++++++-- .../src/types/call/bind.rs | 8 +- .../src/types/class.rs | 4 +- .../src/types/display.rs | 100 ++- .../src/types/infer.rs | 33 +- .../types/property_tests/type_generation.rs | 2 +- .../src/types/signatures.rs | 97 ++- 27 files changed, 1434 insertions(+), 197 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/overloads.md diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index 3f2dd4a98ac93c..a37468e82420d5 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -252,7 +252,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> { r#" y = 4 / 0 - for a in range(0, y): + for a in range(0, int(y)): x = a print(x) # possibly-unresolved-reference @@ -271,7 +271,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> { 2 | y = 4 / 0 | ^^^^^ Cannot divide object of type `Literal[4]` by zero 3 | - 4 | for a in range(0, y): + 4 | for a in range(0, int(y)): | warning: lint:possibly-unresolved-reference @@ -307,7 +307,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> { 2 | y = 4 / 0 | ^^^^^ Cannot divide object of type `Literal[4]` by zero 3 | - 4 | for a in range(0, y): + 4 | for a in range(0, int(y)): | Found 1 diagnostic @@ -328,7 +328,7 @@ fn cli_rule_severity() -> anyhow::Result<()> { y = 4 / 0 - for a in range(0, y): + for a in range(0, int(y)): x = a print(x) # possibly-unresolved-reference @@ -358,7 +358,7 @@ fn cli_rule_severity() -> anyhow::Result<()> { 4 | y = 4 / 0 | ^^^^^ Cannot divide object of type `Literal[4]` by zero 5 | - 6 | for a in range(0, y): + 6 | for a in range(0, int(y)): | warning: lint:possibly-unresolved-reference @@ -405,7 +405,7 @@ fn cli_rule_severity() -> anyhow::Result<()> { 4 | y = 4 / 0 | ^^^^^ Cannot divide object of type `Literal[4]` by zero 5 | - 6 | for a in range(0, y): + 6 | for a in range(0, int(y)): | Found 2 diagnostics @@ -426,7 +426,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { r#" y = 4 / 0 - for a in range(0, y): + for a in range(0, int(y)): x = a print(x) # possibly-unresolved-reference @@ -445,7 +445,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { 2 | y = 4 / 0 | ^^^^^ Cannot divide object of type `Literal[4]` by zero 3 | - 4 | for a in range(0, y): + 4 | for a in range(0, int(y)): | warning: lint:possibly-unresolved-reference @@ -482,7 +482,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { 2 | y = 4 / 0 | ^^^^^ Cannot divide object of type `Literal[4]` by zero 3 | - 4 | for a in range(0, y): + 4 | for a in range(0, int(y)): | Found 1 diagnostic @@ -814,7 +814,7 @@ fn user_configuration() -> anyhow::Result<()> { r#" y = 4 / 0 - for a in range(0, y): + for a in range(0, int(y)): x = a print(x) @@ -841,7 +841,7 @@ fn user_configuration() -> anyhow::Result<()> { 2 | y = 4 / 0 | ^^^^^ Cannot divide object of type `Literal[4]` by zero 3 | - 4 | for a in range(0, y): + 4 | for a in range(0, int(y)): | warning: lint:possibly-unresolved-reference @@ -883,7 +883,7 @@ fn user_configuration() -> anyhow::Result<()> { 2 | y = 4 / 0 | ^^^^^ Cannot divide object of type `Literal[4]` by zero 3 | - 4 | for a in range(0, y): + 4 | for a in range(0, int(y)): | error: lint:possibly-unresolved-reference diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md index 45dff62b805104..445c7a05da3aac 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md @@ -72,13 +72,11 @@ reveal_type(baz) # revealed: Literal["bazfoo"] qux = (foo, bar) reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]] -# TODO: Infer "LiteralString" -reveal_type(foo.join(qux)) # revealed: @Todo(return type of overloaded function) +reveal_type(foo.join(qux)) # revealed: LiteralString template: LiteralString = "{}, {}" reveal_type(template) # revealed: Literal["{}, {}"] -# TODO: Infer `LiteralString` -reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of overloaded function) +reveal_type(template.format(foo, bar)) # revealed: LiteralString ``` ### Assignability diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 15882300c114c0..3f6c5321c0349b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1698,9 +1698,9 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`, bools are instances of that class: ```py -# revealed: bound method Literal[True].__and__(**kwargs: @Todo(todo signature **kwargs)) -> @Todo(return type of overloaded function) +# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int] reveal_type(True.__and__) -# revealed: bound method Literal[False].__or__(**kwargs: @Todo(todo signature **kwargs)) -> @Todo(return type of overloaded function) +# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int] reveal_type(False.__or__) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index b435a34d76940d..650e7a636cf42a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -310,9 +310,7 @@ reveal_type(A() + 1) # revealed: A reveal_type(1 + A()) # revealed: A reveal_type(A() + "foo") # revealed: A -# TODO should be `A` since `str.__add__` doesn't support `A` instances -# TODO overloads -reveal_type("foo" + A()) # revealed: @Todo(return type of overloaded function) +reveal_type("foo" + A()) # revealed: A reveal_type(A() + b"foo") # revealed: A # TODO should be `A` since `bytes.__add__` doesn't support `A` instances @@ -320,16 +318,14 @@ reveal_type(b"foo" + A()) # revealed: bytes reveal_type(A() + ()) # revealed: A # TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances -reveal_type(() + A()) # revealed: @Todo(return type of overloaded function) +reveal_type(() + A()) # revealed: @Todo(full tuple[...] support) literal_string_instance = "foo" * 1_000_000_000 # the test is not testing what it's meant to be testing if this isn't a `LiteralString`: reveal_type(literal_string_instance) # revealed: LiteralString reveal_type(A() + literal_string_instance) # revealed: A -# TODO should be `A` since `str.__add__` doesn't support `A` instances -# TODO overloads -reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of overloaded function) +reveal_type(literal_string_instance + A()) # revealed: A ``` ## Operations involving instances of classes inheriting from `Any` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md index a4172c821dce94..dcbc18031754e2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md @@ -50,9 +50,11 @@ reveal_type(1 ** (largest_u32 + 1)) # revealed: int reveal_type(2**largest_u32) # revealed: int def variable(x: int): - reveal_type(x**2) # revealed: @Todo(return type of overloaded function) - reveal_type(2**x) # revealed: @Todo(return type of overloaded function) - reveal_type(x**x) # revealed: @Todo(return type of overloaded function) + reveal_type(x**2) # revealed: int + # TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching + reveal_type(2**x) # revealed: int + # TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching + reveal_type(x**x) # revealed: int ``` If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but diff --git a/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md b/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md index 28baa67ba48fac..94958c883b730e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md @@ -547,14 +547,14 @@ the descriptor's `__get__` method as if it had been called on the class itself, for the `instance` argument. ```py -from typing import overload +from typing import Literal, overload from dataclasses import dataclass class ConvertToLength: _len: int = 0 @overload - def __get__(self, instance: None, owner: type) -> str: ... + def __get__(self, instance: None, owner: type) -> Literal[""]: ... @overload def __get__(self, instance: object, owner: type | None) -> int: ... def __get__(self, instance: object | None, owner: type | None) -> str | int: @@ -570,12 +570,10 @@ class ConvertToLength: class C: converter: ConvertToLength = ConvertToLength() -# TODO: Should be `(converter: str = Literal[""]) -> None` once we understand overloads -reveal_type(C.__init__) # revealed: (converter: str = str | int) -> None +reveal_type(C.__init__) # revealed: (converter: str = Literal[""]) -> None c = C("abc") -# TODO: Should be `int` once we understand overloads -reveal_type(c.converter) # revealed: str | int +reveal_type(c.converter) # revealed: int # This is also okay: C() @@ -611,8 +609,7 @@ class AcceptsStrAndInt: class C: field: AcceptsStrAndInt = AcceptsStrAndInt() -# TODO: Should be `field: str | int = int` once we understand overloads -reveal_type(C.__init__) # revealed: (field: Unknown = int) -> None +reveal_type(C.__init__) # revealed: (field: str | int = int) -> None ``` ## `dataclasses.field` diff --git a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md index aeabb336ecbacd..8f8d17ae76be68 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md @@ -459,11 +459,9 @@ class Descriptor: class C: d: Descriptor = Descriptor() -# TODO: should be `Literal["called on class object"] -reveal_type(C.d) # revealed: LiteralString +reveal_type(C.d) # revealed: Literal["called on class object"] -# TODO: should be `Literal["called on instance"] -reveal_type(C().d) # revealed: LiteralString +reveal_type(C().d) # revealed: Literal["called on instance"] ``` ## Descriptor protocol for dunder methods diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md index 84a14f93f5af85..f8f5ab8c5e686e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md @@ -246,7 +246,7 @@ class MetaTruthy(type): class MetaDeferred(type): def __bool__(self) -> MetaAmbiguous: - return MetaAmbiguous() + raise NotImplementedError class AmbiguousClass(metaclass=MetaAmbiguous): ... class FalsyClass(metaclass=MetaFalsy): ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/overloads.md b/crates/red_knot_python_semantic/resources/mdtest/overloads.md new file mode 100644 index 00000000000000..764b6a44123edb --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/overloads.md @@ -0,0 +1,638 @@ +# Overloads + +Reference: + +## `typing.overload` + +The definition of `typing.overload` in typeshed is an identity function. + +```py +from typing import overload + +def foo(x: int) -> int: + return x + +reveal_type(foo) # revealed: def foo(x: int) -> int +bar = overload(foo) +reveal_type(bar) # revealed: def foo(x: int) -> int +``` + +## Functions + +```py +from typing import overload + +@overload +def add() -> None: ... +@overload +def add(x: int) -> int: ... +@overload +def add(x: int, y: int) -> int: ... +def add(x: int | None = None, y: int | None = None) -> int | None: + return (x or 0) + (y or 0) + +reveal_type(add) # revealed: Overload[() -> None, (x: int) -> int, (x: int, y: int) -> int] +reveal_type(add()) # revealed: None +reveal_type(add(1)) # revealed: int +reveal_type(add(1, 2)) # revealed: int +``` + +## Overriding + +These scenarios are to verify that the overloaded and non-overloaded definitions are correctly +overridden by each other. + +An overloaded function is overriding another overloaded function: + +```py +from typing import overload + +@overload +def foo() -> None: ... +@overload +def foo(x: int) -> int: ... +def foo(x: int | None = None) -> int | None: + return x + +reveal_type(foo) # revealed: Overload[() -> None, (x: int) -> int] +reveal_type(foo()) # revealed: None +reveal_type(foo(1)) # revealed: int + +@overload +def foo() -> None: ... +@overload +def foo(x: str) -> str: ... +def foo(x: str | None = None) -> str | None: + return x + +reveal_type(foo) # revealed: Overload[() -> None, (x: str) -> str] +reveal_type(foo()) # revealed: None +reveal_type(foo("")) # revealed: str +``` + +A non-overloaded function is overriding an overloaded function: + +```py +def foo(x: int) -> int: + return x + +reveal_type(foo) # revealed: def foo(x: int) -> int +``` + +An overloaded function is overriding a non-overloaded function: + +```py +reveal_type(foo) # revealed: def foo(x: int) -> int + +@overload +def foo() -> None: ... +@overload +def foo(x: bytes) -> bytes: ... +def foo(x: bytes | None = None) -> bytes | None: + return x + +reveal_type(foo) # revealed: Overload[() -> None, (x: bytes) -> bytes] +reveal_type(foo()) # revealed: None +reveal_type(foo(b"")) # revealed: bytes +``` + +## Methods + +```py +from typing import overload + +class Foo1: + @overload + def method(self) -> None: ... + @overload + def method(self, x: int) -> int: ... + def method(self, x: int | None = None) -> int | None: + return x + +foo1 = Foo1() +reveal_type(foo1.method) # revealed: Overload[() -> None, (x: int) -> int] +reveal_type(foo1.method()) # revealed: None +reveal_type(foo1.method(1)) # revealed: int + +class Foo2: + @overload + def method(self) -> None: ... + @overload + def method(self, x: str) -> str: ... + def method(self, x: str | None = None) -> str | None: + return x + +foo2 = Foo2() +reveal_type(foo2.method) # revealed: Overload[() -> None, (x: str) -> str] +reveal_type(foo2.method()) # revealed: None +reveal_type(foo2.method("")) # revealed: str +``` + +## Constructor + +```py +from typing import overload + +class Foo: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, x: int) -> None: ... + def __init__(self, x: int | None = None) -> None: + self.x = x + +foo = Foo() +reveal_type(foo) # revealed: Foo +reveal_type(foo.x) # revealed: Unknown | int | None + +foo1 = Foo(1) +reveal_type(foo1) # revealed: Foo +reveal_type(foo1.x) # revealed: Unknown | int | None +``` + +## Version specific + +Function definitions can vary between multiple Python versions. + +### Overload and non-overload (3.9) + +Here, the same function is overloaded in one version and not in another. + +```toml +[environment] +python-version = "3.9" +``` + +```py +import sys +from typing import overload + +if sys.version_info < (3, 10): + def func(x: int) -> int: + return x + +elif sys.version_info <= (3, 12): + @overload + def func() -> None: ... + @overload + def func(x: int) -> int: ... + def func(x: int | None = None) -> int | None: + return x + +reveal_type(func) # revealed: def func(x: int) -> int +func() # error: [missing-argument] +``` + +### Overload and non-overload (3.10) + +```toml +[environment] +python-version = "3.10" +``` + +```py +import sys +from typing import overload + +if sys.version_info < (3, 10): + def func(x: int) -> int: + return x + +elif sys.version_info <= (3, 12): + @overload + def func() -> None: ... + @overload + def func(x: int) -> int: ... + def func(x: int | None = None) -> int | None: + return x + +reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int] +reveal_type(func()) # revealed: None +reveal_type(func(1)) # revealed: int +``` + +### Some overloads are version specific (3.9) + +```toml +[environment] +python-version = "3.9" +``` + +`overloaded.pyi`: + +```pyi +import sys +from typing import overload + +if sys.version_info >= (3, 10): + @overload + def func() -> None: ... + +@overload +def func(x: int) -> int: ... +@overload +def func(x: str) -> str: ... +``` + +`main.py`: + +```py +from overloaded import func + +reveal_type(func) # revealed: Overload[(x: int) -> int, (x: str) -> str] +func() # error: [no-matching-overload] +reveal_type(func(1)) # revealed: int +reveal_type(func("")) # revealed: str +``` + +### Some overloads are version specific (3.10) + +```toml +[environment] +python-version = "3.10" +``` + +`overloaded.pyi`: + +```pyi +import sys +from typing import overload + +@overload +def func() -> None: ... + +if sys.version_info >= (3, 10): + @overload + def func(x: int) -> int: ... + +@overload +def func(x: str) -> str: ... +``` + +`main.py`: + +```py +from overloaded import func + +reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int, (x: str) -> str] +reveal_type(func()) # revealed: None +reveal_type(func(1)) # revealed: int +reveal_type(func("")) # revealed: str +``` + +## Generic + +```toml +[environment] +python-version = "3.12" +``` + +For an overloaded generic function, it's not necessary for all overloads to be generic. + +```py +from typing import overload + +@overload +def func() -> None: ... +@overload +def func[T](x: T) -> T: ... +def func[T](x: T | None = None) -> T | None: + return x + +reveal_type(func) # revealed: Overload[() -> None, (x: T) -> T] +reveal_type(func()) # revealed: None +reveal_type(func(1)) # revealed: Literal[1] +reveal_type(func("")) # revealed: Literal[""] +``` + +## Invalid + +### At least two overloads + +At least two `@overload`-decorated definitions must be present. + +```py +from typing import overload + +# TODO: error +@overload +def func(x: int) -> int: ... +def func(x: int | str) -> int | str: + return x +``` + +### Overload without an implementation + +#### Regular modules + +In regular modules, a series of `@overload`-decorated definitions must be followed by exactly one +non-`@overload`-decorated definition (for the same function/method). + +```py +from typing import overload + +# TODO: error because implementation does not exists +@overload +def func(x: int) -> int: ... +@overload +def func(x: str) -> str: ... + +class Foo: + # TODO: error because implementation does not exists + @overload + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... +``` + +#### Stub files + +Overload definitions within stub files are exempt from this check. + +```pyi +from typing import overload + +@overload +def func(x: int) -> int: ... +@overload +def func(x: str) -> str: ... +``` + +#### Protocols + +Overload definitions within protocols are exempt from this check. + +```py +from typing import Protocol, overload + +class Foo(Protocol): + @overload + def f(self, x: int) -> int: ... + @overload + def f(self, x: str) -> str: ... +``` + +#### Abstract methods + +Overload definitions within abstract base classes are exempt from this check. + +```py +from abc import ABC, abstractmethod +from typing import overload + +class AbstractFoo(ABC): + @overload + @abstractmethod + def f(self, x: int) -> int: ... + @overload + @abstractmethod + def f(self, x: str) -> str: ... +``` + +Using the `@abstractmethod` decorator requires that the class's metaclass is `ABCMeta` or is derived +from it. + +```py +class Foo: + # TODO: Error because implementation does not exists + @overload + @abstractmethod + def f(self, x: int) -> int: ... + @overload + @abstractmethod + def f(self, x: str) -> str: ... +``` + +And, the `@abstractmethod` decorator must be present on all the `@overload`-ed methods. + +```py +class PartialFoo1(ABC): + @overload + @abstractmethod + def f(self, x: int) -> int: ... + @overload + def f(self, x: str) -> str: ... + +class PartialFoo(ABC): + @overload + def f(self, x: int) -> int: ... + @overload + @abstractmethod + def f(self, x: str) -> str: ... +``` + +### Inconsistent decorators + +#### `@staticmethod` / `@classmethod` + +If one overload signature is decorated with `@staticmethod` or `@classmethod`, all overload +signatures must be similarly decorated. The implementation, if present, must also have a consistent +decorator. + +```py +from __future__ import annotations + +from typing import overload + +class CheckStaticMethod: + # TODO: error because `@staticmethod` does not exist on all overloads + @overload + def method1(x: int) -> int: ... + @overload + def method1(x: str) -> str: ... + @staticmethod + def method1(x: int | str) -> int | str: + return x + # TODO: error because `@staticmethod` does not exist on all overloads + @overload + def method2(x: int) -> int: ... + @overload + @staticmethod + def method2(x: str) -> str: ... + @staticmethod + def method2(x: int | str) -> int | str: + return x + # TODO: error because `@staticmethod` does not exist on the implementation + @overload + @staticmethod + def method3(x: int) -> int: ... + @overload + @staticmethod + def method3(x: str) -> str: ... + def method3(x: int | str) -> int | str: + return x + + @overload + @staticmethod + def method4(x: int) -> int: ... + @overload + @staticmethod + def method4(x: str) -> str: ... + @staticmethod + def method4(x: int | str) -> int | str: + return x + +class CheckClassMethod: + def __init__(self, x: int) -> None: + self.x = x + # TODO: error because `@classmethod` does not exist on all overloads + @overload + @classmethod + def try_from1(cls, x: int) -> CheckClassMethod: ... + @overload + def try_from1(cls, x: str) -> None: ... + @classmethod + def try_from1(cls, x: int | str) -> CheckClassMethod | None: + if isinstance(x, int): + return cls(x) + return None + # TODO: error because `@classmethod` does not exist on all overloads + @overload + def try_from2(cls, x: int) -> CheckClassMethod: ... + @overload + @classmethod + def try_from2(cls, x: str) -> None: ... + @classmethod + def try_from2(cls, x: int | str) -> CheckClassMethod | None: + if isinstance(x, int): + return cls(x) + return None + # TODO: error because `@classmethod` does not exist on the implementation + @overload + @classmethod + def try_from3(cls, x: int) -> CheckClassMethod: ... + @overload + @classmethod + def try_from3(cls, x: str) -> None: ... + def try_from3(cls, x: int | str) -> CheckClassMethod | None: + if isinstance(x, int): + return cls(x) + return None + + @overload + @classmethod + def try_from4(cls, x: int) -> CheckClassMethod: ... + @overload + @classmethod + def try_from4(cls, x: str) -> None: ... + @classmethod + def try_from4(cls, x: int | str) -> CheckClassMethod | None: + if isinstance(x, int): + return cls(x) + return None +``` + +#### `@final` / `@override` + +If a `@final` or `@override` decorator is supplied for a function with overloads, the decorator +should be applied only to the overload implementation if it is present. + +```py +from typing_extensions import final, overload, override + +class Foo: + @overload + def method1(self, x: int) -> int: ... + @overload + def method1(self, x: str) -> str: ... + @final + def method1(self, x: int | str) -> int | str: + return x + # TODO: error because `@final` is not on the implementation + @overload + @final + def method2(self, x: int) -> int: ... + @overload + def method2(self, x: str) -> str: ... + def method2(self, x: int | str) -> int | str: + return x + # TODO: error because `@final` is not on the implementation + @overload + def method3(self, x: int) -> int: ... + @overload + @final + def method3(self, x: str) -> str: ... + def method3(self, x: int | str) -> int | str: + return x + +class Base: + @overload + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... + def method(self, x: int | str) -> int | str: + return x + +class Sub1(Base): + @overload + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... + @override + def method(self, x: int | str) -> int | str: + return x + +class Sub2(Base): + # TODO: error because `@override` is not on the implementation + @overload + def method(self, x: int) -> int: ... + @overload + @override + def method(self, x: str) -> str: ... + def method(self, x: int | str) -> int | str: + return x + +class Sub3(Base): + # TODO: error because `@override` is not on the implementation + @overload + @override + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... + def method(self, x: int | str) -> int | str: + return x +``` + +#### `@final` / `@override` in stub files + +If an overload implementation isn’t present (for example, in a stub file), the `@final` or +`@override` decorator should be applied only to the first overload. + +```pyi +from typing_extensions import final, overload, override + +class Foo: + @overload + @final + def method1(self, x: int) -> int: ... + @overload + def method1(self, x: str) -> str: ... + + # TODO: error because `@final` is not on the first overload + @overload + def method2(self, x: int) -> int: ... + @final + @overload + def method2(self, x: str) -> str: ... + +class Base: + @overload + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... + +class Sub1(Base): + @overload + @override + def method(self, x: int) -> int: ... + @overload + def method(self, x: str) -> str: ... + +class Sub2(Base): + # TODO: error because `@override` is not on the first overload + @overload + def method(self, x: int) -> int: ... + @overload + @override + def method(self, x: str) -> str: ... +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md index 255cb6bc9d0f0c..7127263dd4b990 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md @@ -24,8 +24,7 @@ reveal_type(y) # revealed: Unknown def _(n: int): a = b"abcde"[n] - # TODO: Support overloads... Should be `bytes` - reveal_type(a) # revealed: @Todo(return type of overloaded function) + reveal_type(a) # revealed: int ``` ## Slices @@ -43,11 +42,9 @@ b[::0] # error: [zero-stepsize-in-slice] def _(m: int, n: int): byte_slice1 = b[m:n] - # TODO: Support overloads... Should be `bytes` - reveal_type(byte_slice1) # revealed: @Todo(return type of overloaded function) + reveal_type(byte_slice1) # revealed: bytes def _(s: bytes) -> bytes: byte_slice2 = s[0:5] - # TODO: Support overloads... Should be `bytes` - return reveal_type(byte_slice2) # revealed: @Todo(return type of overloaded function) + return reveal_type(byte_slice2) # revealed: bytes ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md index 408a4f43b6dec8..5f9264f6faa164 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md @@ -12,13 +12,13 @@ x = [1, 2, 3] reveal_type(x) # revealed: list # TODO reveal int -reveal_type(x[0]) # revealed: @Todo(return type of overloaded function) +reveal_type(x[0]) # revealed: Unknown | @Todo(Support for `typing.TypeVar` instances in type expressions) # TODO reveal list -reveal_type(x[0:1]) # revealed: @Todo(return type of overloaded function) +reveal_type(x[0:1]) # revealed: @Todo(generics) -# TODO error -reveal_type(x["a"]) # revealed: @Todo(return type of overloaded function) +# error: [call-non-callable] +reveal_type(x["a"]) # revealed: Unknown ``` ## Assignments within list assignment @@ -29,9 +29,11 @@ In assignment, we might also have a named assignment. This should also get type x = [1, 2, 3] x[0 if (y := 2) else 1] = 5 -# TODO error? (indeterminite index type) +# TODO: better error than "method `__getitem__` not callable on type `list`" +# error: [call-non-callable] x["a" if (y := 2) else 1] = 6 -# TODO error (can't index via string) +# TODO: better error than "method `__getitem__` not callable on type `list`" +# error: [call-non-callable] x["a" if (y := 2) else "b"] = 6 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md index 9a875dc3231c1a..469300c0b4eda9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md @@ -21,8 +21,7 @@ reveal_type(b) # revealed: Unknown def _(n: int): a = "abcde"[n] - # TODO: Support overloads... Should be `str` - reveal_type(a) # revealed: @Todo(return type of overloaded function) + reveal_type(a) # revealed: LiteralString ``` ## Slices @@ -75,12 +74,10 @@ def _(m: int, n: int, s2: str): s[::0] # error: [zero-stepsize-in-slice] substring1 = s[m:n] - # TODO: Support overloads... Should be `LiteralString` - reveal_type(substring1) # revealed: @Todo(return type of overloaded function) + reveal_type(substring1) # revealed: LiteralString substring2 = s2[0:5] - # TODO: Support overloads... Should be `str` - reveal_type(substring2) # revealed: @Todo(return type of overloaded function) + reveal_type(substring2) # revealed: str ``` ## Unsupported slice types diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index 8ac7ff25342760..579d7451f66fd1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -69,8 +69,8 @@ def _(m: int, n: int): t[::0] # error: [zero-stepsize-in-slice] tuple_slice = t[m:n] - # TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]` - reveal_type(tuple_slice) # revealed: @Todo(return type of overloaded function) + # TODO: Should be `tuple[Literal[1, 'a', b"b"] | None, ...]` + reveal_type(tuple_slice) # revealed: @Todo(full tuple[...] support) ``` ## Inheritance diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 5ca7c9c6284e5e..fdd7ddbe7c259a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -522,4 +522,35 @@ c: Callable[[Any], str] = A().f c: Callable[[Any], str] = A().g ``` +### Overloads + +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +@overload +def overloaded() -> None: ... +@overload +def overloaded(a: str) -> str: ... +@overload +def overloaded(a: str, b: Any) -> str: ... +``` + +```py +from overloaded import overloaded +from typing import Any, Callable + +c: Callable[[], None] = overloaded +c: Callable[[str], str] = overloaded +c: Callable[[str, Any], Any] = overloaded +c: Callable[..., str] = overloaded + +# error: [invalid-assignment] +c: Callable[..., int] = overloaded + +# error: [invalid-assignment] +c: Callable[[int], str] = overloaded +``` + [typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index 39aa18fc432863..3a234bb8a6cd57 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -254,4 +254,8 @@ from knot_extensions import is_equivalent_to, static_assert static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str | int], None] | int)) ``` +### Overloads + +TODO + [the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md index 8af5af41545ad2..9a44ce090bbd5d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md @@ -99,3 +99,34 @@ static_assert(not is_fully_static(CallableTypeOf[f13])) static_assert(not is_fully_static(CallableTypeOf[f14])) static_assert(not is_fully_static(CallableTypeOf[f15])) ``` + +## Overloads + +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +@overload +def gradual() -> None: ... +@overload +def gradual(a: Any) -> None: ... + +@overload +def static() -> None: ... +@overload +def static(x: int) -> None: ... +@overload +def static(x: str) -> str: ... +``` + +```py +from knot_extensions import CallableTypeOf, TypeOf, is_fully_static, static_assert +from overloaded import gradual, static + +static_assert(is_fully_static(TypeOf[gradual])) +static_assert(is_fully_static(TypeOf[static])) + +static_assert(not is_fully_static(CallableTypeOf[gradual])) +static_assert(is_fully_static(CallableTypeOf[static])) +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md index 43062b1802bb8d..e3e46a96e682ef 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md @@ -157,4 +157,6 @@ def f6(a, /): ... static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6])) ``` +TODO: Overloads + [materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize 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 aa5f025e13d8c4..c61f74859832c8 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 @@ -1153,5 +1153,187 @@ static_assert(not is_subtype_of(TypeOf[A.g], Callable[[], int])) static_assert(is_subtype_of(TypeOf[A.f], Callable[[A, int], int])) ``` +### Overloads + +#### Subtype overloaded + +For `B <: A`, if a callable `B` is overloaded with two or more signatures, it is a subtype of +callable `A` if _at least one_ of the overloaded signatures in `B` is a subtype of `A`. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B: ... +class C: ... + +@overload +def overloaded(x: A) -> None: ... +@overload +def overloaded(x: B) -> None: ... +``` + +```py +from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from overloaded import A, B, C, overloaded + +def accepts_a(x: A) -> None: ... +def accepts_b(x: B) -> None: ... +def accepts_c(x: C) -> None: ... + +static_assert(is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_a])) +static_assert(is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_b])) +static_assert(not is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_c])) +``` + +#### Supertype overloaded + +For `B <: A`, if a callable `A` is overloaded with two or more signatures, callable `B` is a subtype +of `A` if `B` is a subtype of _all_ of the signatures in `A`. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class Grandparent: ... +class Parent(Grandparent): ... +class Child(Parent): ... + +@overload +def overloaded(a: Child) -> None: ... +@overload +def overloaded(a: Parent) -> None: ... +@overload +def overloaded(a: Grandparent) -> None: ... +``` + +```py +from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from overloaded import Grandparent, Parent, Child, overloaded + +# This is a subtype of only the first overload +def child(a: Child) -> None: ... + +# This is a subtype of the first and second overload +def parent(a: Parent) -> None: ... + +# This is the only function that's a subtype of all overloads +def grandparent(a: Grandparent) -> None: ... + +static_assert(not is_subtype_of(CallableTypeOf[child], CallableTypeOf[overloaded])) +static_assert(not is_subtype_of(CallableTypeOf[parent], CallableTypeOf[overloaded])) +static_assert(is_subtype_of(CallableTypeOf[grandparent], CallableTypeOf[overloaded])) +``` + +#### Both overloads + +For `B <: A`, if both `A` and `B` is a callable that's overloaded with two or more signatures, then +`B` is a subtype of `A` if for _every_ signature in `A`, there is _at least one_ signature in `B` +that is a subtype of it. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class Grandparent: ... +class Parent(Grandparent): ... +class Child(Parent): ... +class Other: ... + +@overload +def pg(a: Parent) -> None: ... +@overload +def pg(a: Grandparent) -> None: ... + +@overload +def po(a: Parent) -> None: ... +@overload +def po(a: Other) -> None: ... + +@overload +def go(a: Grandparent) -> None: ... +@overload +def go(a: Other) -> None: ... + +@overload +def cpg(a: Child) -> None: ... +@overload +def cpg(a: Parent) -> None: ... +@overload +def cpg(a: Grandparent) -> None: ... + +@overload +def empty_go() -> Child: ... +@overload +def empty_go(a: Grandparent) -> None: ... +@overload +def empty_go(a: Other) -> Other: ... + +@overload +def empty_cp() -> Parent: ... +@overload +def empty_cp(a: Child) -> None: ... +@overload +def empty_cp(a: Parent) -> None: ... +``` + +```py +from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from overloaded import pg, po, go, cpg, empty_go, empty_cp + +static_assert(is_subtype_of(CallableTypeOf[pg], CallableTypeOf[cpg])) +static_assert(is_subtype_of(CallableTypeOf[cpg], CallableTypeOf[pg])) + +static_assert(not is_subtype_of(CallableTypeOf[po], CallableTypeOf[pg])) +static_assert(not is_subtype_of(CallableTypeOf[pg], CallableTypeOf[po])) + +static_assert(is_subtype_of(CallableTypeOf[go], CallableTypeOf[pg])) +static_assert(not is_subtype_of(CallableTypeOf[pg], CallableTypeOf[go])) + +# Overload 1 in `empty_go` is a subtype of overload 1 in `empty_cp` +# Overload 2 in `empty_go` is a subtype of overload 2 in `empty_cp` +# Overload 2 in `empty_go` is a subtype of overload 3 in `empty_cp` +# +# All overloads in `empty_cp` has a subtype in `empty_go` +static_assert(is_subtype_of(CallableTypeOf[empty_go], CallableTypeOf[empty_cp])) + +static_assert(not is_subtype_of(CallableTypeOf[empty_cp], CallableTypeOf[empty_go])) +``` + +#### Order of overloads + +Order of overloads is irrelevant for subtyping. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B: ... + +@overload +def overload_ab(x: A) -> None: ... +@overload +def overload_ab(x: B) -> None: ... + +@overload +def overload_ba(x: B) -> None: ... +@overload +def overload_ba(x: A) -> None: ... +``` + +```py +from overloaded import overload_ab, overload_ba +from knot_extensions import CallableTypeOf, is_subtype_of, static_assert + +static_assert(is_subtype_of(CallableTypeOf[overload_ab], CallableTypeOf[overload_ba])) +static_assert(is_subtype_of(CallableTypeOf[overload_ba], CallableTypeOf[overload_ab])) +``` + [special case for float and complex]: https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex [typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence diff --git a/crates/red_knot_python_semantic/src/semantic_index/ast_ids.rs b/crates/red_knot_python_semantic/src/semantic_index/ast_ids.rs index 160f3af7b9cd23..bd5e93ada60df0 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/ast_ids.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/ast_ids.rs @@ -58,6 +58,13 @@ pub trait HasScopedUseId { fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId; } +impl HasScopedUseId for ast::Identifier { + fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { + let ast_ids = ast_ids(db, scope); + ast_ids.use_id(self) + } +} + impl HasScopedUseId for ast::ExprName { fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId { let expression_ref = ExprRef::from(self); @@ -157,7 +164,7 @@ impl AstIdsBuilder { } /// Adds `expr` to the use ids map and returns its id. - pub(super) fn record_use(&mut self, expr: &ast::Expr) -> ScopedUseId { + pub(super) fn record_use(&mut self, expr: impl Into) -> ScopedUseId { let use_id = self.uses_map.len().into(); self.uses_map.insert(expr.into(), use_id); @@ -196,4 +203,10 @@ pub(crate) mod node_key { Self(NodeKey::from_node(value)) } } + + impl From<&ast::Identifier> for ExpressionNodeKey { + fn from(value: &ast::Identifier) -> Self { + Self(NodeKey::from_node(value)) + } + } } diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index e4c25f48400b5e..f9d312ab531a52 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -1115,6 +1115,17 @@ where // at the end to match the runtime evaluation of parameter defaults // and return-type annotations. let (symbol, _) = self.add_symbol(name.id.clone()); + + // Record a use of the function name in the scope that it is defined in, so that it + // can be used to find previously defined functions with the same name. This is + // used to collect all the overloaded definitions of a function. This needs to be + // done on the `Identifier` node as opposed to `ExprName` because that's what the + // AST uses. + self.mark_symbol_used(symbol); + let use_id = self.current_ast_ids().record_use(name); + self.current_use_def_map_mut() + .record_use(symbol, use_id, NodeKey::from_node(name)); + self.add_definition(symbol, function_def); } ast::Stmt::ClassDef(class) => { diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 4a2b446d2ee45d..6782de5f69dbd2 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -29,12 +29,14 @@ pub(crate) use self::signatures::{CallableSignature, Signature, Signatures}; pub(crate) use self::subclass_of::SubclassOfType; use crate::module_name::ModuleName; use crate::module_resolver::{file_to_module, resolve_module, KnownModule}; -use crate::semantic_index::ast_ids::HasScopedExpressionId; +use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId}; use crate::semantic_index::definition::Definition; use crate::semantic_index::symbol::ScopeId; use crate::semantic_index::{imported_modules, semantic_index}; use crate::suppression::check_suppressions; -use crate::symbol::{imported_symbol, Boundness, Symbol, SymbolAndQualifiers}; +use crate::symbol::{ + imported_symbol, symbol_from_bindings, Boundness, Symbol, SymbolAndQualifiers, +}; use crate::types::call::{Bindings, CallArgumentTypes, CallableBinding}; pub(crate) use crate::types::class_base::ClassBase; use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; @@ -507,12 +509,14 @@ impl<'db> Type<'db> { .any(|ty| ty.contains_todo(db)), Self::Callable(callable) => { - let signature = callable.signature(db); - signature.parameters().iter().any(|param| { - param - .annotated_type() - .is_some_and(|ty| ty.contains_todo(db)) - }) || signature.return_ty.is_some_and(|ty| ty.contains_todo(db)) + let signatures = callable.signatures(db); + signatures.iter().any(|signature| { + signature.parameters().iter().any(|param| { + param + .annotated_type() + .is_some_and(|ty| ty.contains_todo(db)) + }) || signature.return_ty.is_some_and(|ty| ty.contains_todo(db)) + }) } Self::SubclassOf(subclass_of) => match subclass_of.subclass_of() { @@ -1029,9 +1033,9 @@ impl<'db> Type<'db> { .to_instance(db) .is_subtype_of(db, target), - (Type::Callable(self_callable), Type::Callable(other_callable)) => self_callable - .signature(db) - .is_subtype_of(db, other_callable.signature(db)), + (Type::Callable(self_callable), Type::Callable(other_callable)) => { + self_callable.is_subtype_of(db, other_callable) + } (Type::DataclassDecorator(_), _) => { // TODO: Implement subtyping using an equivalent `Callable` type. @@ -1358,9 +1362,9 @@ impl<'db> Type<'db> { ) } - (Type::Callable(self_callable), Type::Callable(target_callable)) => self_callable - .signature(db) - .is_assignable_to(db, target_callable.signature(db)), + (Type::Callable(self_callable), Type::Callable(target_callable)) => { + self_callable.is_assignable_to(db, target_callable) + } (Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => { self_function_literal @@ -1391,9 +1395,7 @@ impl<'db> Type<'db> { left.is_equivalent_to(db, right) } (Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right), - (Type::Callable(left), Type::Callable(right)) => { - left.signature(db).is_equivalent_to(db, right.signature(db)) - } + (Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right), _ => self == other && self.is_fully_static(db) && other.is_fully_static(db), } } @@ -1454,9 +1456,9 @@ impl<'db> Type<'db> { first.is_gradual_equivalent_to(db, second) } - (Type::Callable(first), Type::Callable(second)) => first - .signature(db) - .is_gradual_equivalent_to(db, second.signature(db)), + (Type::Callable(first), Type::Callable(second)) => { + first.is_gradual_equivalent_to(db, second) + } _ => false, } @@ -1904,7 +1906,7 @@ impl<'db> Type<'db> { .elements(db) .iter() .all(|elem| elem.is_fully_static(db)), - Type::Callable(callable) => callable.signature(db).is_fully_static(db), + Type::Callable(callable) => callable.is_fully_static(db), } } @@ -3141,16 +3143,27 @@ impl<'db> Type<'db> { /// [`CallErrorKind::NotCallable`]. fn signatures(self, db: &'db dyn Db) -> Signatures<'db> { match self { - Type::Callable(callable) => Signatures::single(CallableSignature::single( - self, - callable.signature(db).clone(), - )), + Type::Callable(callable) => { + Signatures::single(match callable.signatures(db).as_ref() { + [signature] => CallableSignature::single(self, signature.clone()), + signatures => { + CallableSignature::from_overloads(self, signatures.iter().cloned()) + } + }) + } Type::BoundMethod(bound_method) => { let signature = bound_method.function(db).signature(db); - let signature = CallableSignature::single(self, signature.clone()) - .with_bound_type(bound_method.self_instance(db)); - Signatures::single(signature) + Signatures::single(match signature { + FunctionSignature::Single(signature) => { + CallableSignature::single(self, signature.clone()) + .with_bound_type(bound_method.self_instance(db)) + } + FunctionSignature::Overloaded(signatures, _) => { + CallableSignature::from_overloads(self, signatures.iter().cloned()) + .with_bound_type(bound_method.self_instance(db)) + } + }) } Type::MethodWrapper( @@ -3497,10 +3510,14 @@ impl<'db> Type<'db> { Signatures::single(signature) } - _ => Signatures::single(CallableSignature::single( - self, - function_type.signature(db).clone(), - )), + _ => Signatures::single(match function_type.signature(db) { + FunctionSignature::Single(signature) => { + CallableSignature::single(self, signature.clone()) + } + FunctionSignature::Overloaded(signatures, _) => { + CallableSignature::from_overloads(self, signatures.iter().cloned()) + } + }), }, Type::ClassLiteral(class) => match class.known(db) { @@ -3692,7 +3709,10 @@ impl<'db> Type<'db> { .with_annotated_type(UnionType::from_elements( db, [ - Type::Callable(CallableType::new(db, getter_signature)), + Type::Callable(CallableType::single( + db, + getter_signature, + )), Type::none(db), ], )) @@ -3701,7 +3721,10 @@ impl<'db> Type<'db> { .with_annotated_type(UnionType::from_elements( db, [ - Type::Callable(CallableType::new(db, setter_signature)), + Type::Callable(CallableType::single( + db, + setter_signature, + )), Type::none(db), ], )) @@ -3710,7 +3733,7 @@ impl<'db> Type<'db> { .with_annotated_type(UnionType::from_elements( db, [ - Type::Callable(CallableType::new( + Type::Callable(CallableType::single( db, deleter_signature, )), @@ -5738,15 +5761,54 @@ bitflags! { } } +/// A function signature, which can be either a single signature or an overloaded signature. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub(crate) enum FunctionSignature<'db> { + /// A single function signature. + Single(Signature<'db>), + + /// An overloaded function signature containing the `@overload`-ed signatures and an optional + /// implementation signature. + Overloaded(Vec>, Option>), +} + +impl<'db> FunctionSignature<'db> { + /// Returns a slice of all signatures. + /// + /// For an overloaded function, this only includes the `@overload`-ed signatures and not the + /// implementation signature. + pub(crate) fn as_slice(&self) -> &[Signature<'db>] { + match self { + Self::Single(signature) => std::slice::from_ref(signature), + Self::Overloaded(signatures, _) => signatures, + } + } + + /// Returns an iterator over the signatures. + pub(crate) fn iter(&self) -> Iter> { + self.as_slice().iter() + } +} + +impl<'db> IntoIterator for &'db FunctionSignature<'db> { + type Item = &'db Signature<'db>; + type IntoIter = Iter<'db, Signature<'db>>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + #[salsa::interned(debug)] pub struct FunctionType<'db> { - /// name of the function at definition + /// Name of the function at definition. #[return_ref] pub name: ast::name::Name, /// Is this a function that we special-case somehow? If so, which one? known: Option, + /// The scope that's created by the function, in which the function body is evaluated. body_scope: ScopeId<'db>, /// A set of special decorators that were applied to this function @@ -5768,10 +5830,11 @@ impl<'db> FunctionType<'db> { } /// Convert the `FunctionType` into a [`Type::Callable`]. - /// - /// This powers the `CallableTypeOf` special form from the `knot_extensions` module. pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> { - Type::Callable(CallableType::new(db, self.signature(db).clone())) + Type::Callable(CallableType::from_overloads( + db, + self.signature(db).iter().cloned(), + )) } /// Returns the [`FileRange`] of the function's name. @@ -5808,18 +5871,55 @@ impl<'db> FunctionType<'db> { /// Were this not a salsa query, then the calling query /// would depend on the function's AST and rerun for every change in that file. #[salsa::tracked(return_ref)] - pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { + pub(crate) fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> { let mut internal_signature = self.internal_signature(db); - if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) { - return Signature::todo("return type of overloaded function"); + if let Some(specialization) = self.specialization(db) { + internal_signature = internal_signature.apply_specialization(db, specialization); } - if let Some(specialization) = self.specialization(db) { - internal_signature.apply_specialization(db, specialization); + // The semantic model records a use for each function on the name node. This is used here + // to get the previous function definition with the same name. + let scope = self.definition(db).scope(db); + let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db)); + let use_id = self + .body_scope(db) + .node(db) + .expect_function() + .name + .scoped_use_id(db, scope); + + if let Symbol::Type(Type::FunctionLiteral(function_literal), Boundness::Bound) = + symbol_from_bindings(db, use_def.bindings_at_use(use_id)) + { + match function_literal.signature(db) { + FunctionSignature::Single(_) => { + debug_assert!( + !function_literal.has_known_decorator(db, FunctionDecorators::OVERLOAD), + "Expected `FunctionSignature::Overloaded` if the previous function was an overload" + ); + } + FunctionSignature::Overloaded(_, Some(_)) => { + // If the previous overloaded function already has an implementation, then this + // new signature completely replaces it. + } + FunctionSignature::Overloaded(signatures, None) => { + return if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) { + let mut signatures = signatures.clone(); + signatures.push(internal_signature); + FunctionSignature::Overloaded(signatures, None) + } else { + FunctionSignature::Overloaded(signatures.clone(), Some(internal_signature)) + }; + } + } } - internal_signature + if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) { + FunctionSignature::Overloaded(vec![internal_signature], None) + } else { + FunctionSignature::Single(internal_signature) + } } /// Typed internally-visible signature for this function. @@ -6013,27 +6113,54 @@ pub struct BoundMethodType<'db> { impl<'db> BoundMethodType<'db> { pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> Type<'db> { - Type::Callable(CallableType::new( + Type::Callable(CallableType::from_overloads( db, - self.function(db).signature(db).bind_self(), + self.function(db) + .signature(db) + .iter() + .map(signatures::Signature::bind_self), )) } } -/// This type represents the set of all callable objects with a certain signature. -/// It can be written in type expressions using `typing.Callable`. -/// `lambda` expressions are inferred directly as `CallableType`s; all function-literal types -/// are subtypes of a `CallableType`. +/// This type represents the set of all callable objects with a certain, possibly overloaded, +/// signature. +/// +/// It can be written in type expressions using `typing.Callable`. `lambda` expressions are +/// inferred directly as `CallableType`s; all function-literal types are subtypes of a +/// `CallableType`. #[salsa::interned(debug)] pub struct CallableType<'db> { #[return_ref] - signature: Signature<'db>, + signatures: Box<[Signature<'db>]>, } impl<'db> CallableType<'db> { + /// Create a non-overloaded callable type with a single signature. + pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> Self { + CallableType::new(db, vec![signature].into_boxed_slice()) + } + + /// Create an overloaded callable type with multiple signatures. + /// + /// # Panics + /// + /// Panics if `overloads` is empty. + pub(crate) fn from_overloads(db: &'db dyn Db, overloads: I) -> Self + where + I: IntoIterator>, + { + let overloads = overloads.into_iter().collect::>().into_boxed_slice(); + assert!( + !overloads.is_empty(), + "CallableType must have at least one signature" + ); + CallableType::new(db, overloads) + } + /// Create a callable type which accepts any parameters and returns an `Unknown` type. pub(crate) fn unknown(db: &'db dyn Db) -> Self { - CallableType::new( + CallableType::single( db, Signature::new(Parameters::unknown(), Some(Type::unknown())), ) @@ -6043,22 +6170,127 @@ impl<'db> CallableType<'db> { /// /// See [`Type::normalized`] for more details. fn normalized(self, db: &'db dyn Db) -> Self { - let signature = self.signature(db); - let parameters = signature - .parameters() - .iter() - .map(|param| param.normalized(db)) - .collect(); - let return_ty = signature - .return_ty - .map(|return_ty| return_ty.normalized(db)); - CallableType::new(db, Signature::new(parameters, return_ty)) + CallableType::from_overloads( + db, + self.signatures(db) + .iter() + .map(|signature| signature.normalized(db)), + ) } + /// Apply a specialization to this callable type. + /// + /// See [`Type::apply_specialization`] for more details. fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { - let mut signature = self.signature(db).clone(); - signature.apply_specialization(db, specialization); - Self::new(db, signature) + CallableType::from_overloads( + db, + self.signatures(db) + .iter() + .map(|signature| signature.apply_specialization(db, specialization)), + ) + } + + /// Check whether this callable type is fully static. + /// + /// See [`Type::is_fully_static`] for more details. + fn is_fully_static(self, db: &'db dyn Db) -> bool { + self.signatures(db) + .iter() + .all(|signature| signature.is_fully_static(db)) + } + + /// Check whether this callable type is a subtype of another callable type. + /// + /// See [`Type::is_subtype_of`] for more details. + fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { + self.is_assignable_to_impl(db, other, &|self_signature, other_signature| { + self_signature.is_subtype_of(db, other_signature) + }) + } + + /// Check whether this callable type is assignable to another callable type. + /// + /// See [`Type::is_assignable_to`] for more details. + fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { + self.is_assignable_to_impl(db, other, &|self_signature, other_signature| { + self_signature.is_assignable_to(db, other_signature) + }) + } + + /// Implementation for the various relation checks between two, possible overloaded, callable + /// types. + /// + /// The `check_signature` closure is used to check the relation between two [`Signature`]s. + fn is_assignable_to_impl(self, db: &'db dyn Db, other: Self, check_signature: &F) -> bool + where + F: Fn(&Signature<'db>, &Signature<'db>) -> bool, + { + match (&**self.signatures(db), &**other.signatures(db)) { + ([self_signature], [other_signature]) => { + // Base case: both callable types contain a single signature. + check_signature(self_signature, other_signature) + } + + // `self` is possibly overloaded while `other` is definitely not overloaded. + (self_signatures, [other_signature]) => { + let other_callable = CallableType::single(db, other_signature.clone()); + self_signatures + .iter() + .map(|self_signature| CallableType::single(db, self_signature.clone())) + .any(|self_callable| { + self_callable.is_assignable_to_impl(db, other_callable, check_signature) + }) + } + + // `self` is definitely not overloaded while `other` is possibly overloaded. + ([self_signature], other_signatures) => { + let self_callable = CallableType::single(db, self_signature.clone()); + other_signatures + .iter() + .map(|other_signature| CallableType::single(db, other_signature.clone())) + .all(|other_callable| { + self_callable.is_assignable_to_impl(db, other_callable, check_signature) + }) + } + + // `self` is definitely overloaded while `other` is possibly overloaded. + (_, other_signatures) => other_signatures + .iter() + .map(|other_signature| CallableType::single(db, other_signature.clone())) + .all(|other_callable| { + self.is_assignable_to_impl(db, other_callable, check_signature) + }), + } + } + + /// Check whether this callable type is equivalent to another callable type. + /// + /// See [`Type::is_equivalent_to`] for more details. + fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + match (&**self.signatures(db), &**other.signatures(db)) { + ([self_signature], [other_signature]) => { + self_signature.is_equivalent_to(db, other_signature) + } + _ => { + // TODO: overloads + false + } + } + } + + /// Check whether this callable type is gradual equivalent to another callable type. + /// + /// See [`Type::is_gradual_equivalent_to`] for more details. + fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + match (&**self.signatures(db), &**other.signatures(db)) { + ([self_signature], [other_signature]) => { + self_signature.is_gradual_equivalent_to(db, other_signature) + } + _ => { + // TODO: overloads + false + } + } } } diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 16f00029e53c1a..7232a08f8daff8 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -19,7 +19,7 @@ use crate::types::diagnostic::{ use crate::types::generics::{Specialization, SpecializationBuilder}; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ - todo_type, BoundMethodType, DataclassMetadata, FunctionDecorators, KnownClass, KnownFunction, + BoundMethodType, DataclassMetadata, FunctionDecorators, KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, UnionType, WrapperDescriptorKind, }; use ruff_db::diagnostic::{Annotation, Severity, Span, SubDiagnostic}; @@ -536,7 +536,11 @@ impl<'db> Bindings<'db> { } Some(KnownFunction::Overload) => { - overload.set_return_type(todo_type!("overload[..] return type")); + // TODO: This can be removed once we understand legacy generics because the + // typeshed definition for `typing.overload` is an identity function. + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(*ty); + } } Some(KnownFunction::GetattrStatic) => { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index d59fa8e0068c55..f22e8a668eac27 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -931,7 +931,7 @@ impl<'db> ClassLiteralType<'db> { let init_signature = Signature::new(Parameters::new(parameters), Some(Type::none(db))); - return Some(Type::Callable(CallableType::new(db, init_signature))); + return Some(Type::Callable(CallableType::single(db, init_signature))); } else if matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") { if metadata.contains(DataclassMetadata::ORDER) { let signature = Signature::new( @@ -943,7 +943,7 @@ impl<'db> ClassLiteralType<'db> { Some(KnownClass::Bool.to_instance(db)), ); - return Some(Type::Callable(CallableType::new(db, signature))); + return Some(Type::Callable(CallableType::single(db, signature))); } } diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 5a6da8013258cc..96c3c5c535fa32 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -11,12 +11,15 @@ use crate::types::class_base::ClassBase; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ - InstanceType, IntersectionType, KnownClass, MethodWrapperKind, StringLiteralType, Type, - TypeVarBoundOrConstraints, TypeVarInstance, UnionType, WrapperDescriptorKind, + FunctionSignature, InstanceType, IntersectionType, KnownClass, MethodWrapperKind, + StringLiteralType, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType, + WrapperDescriptorKind, }; use crate::Db; use rustc_hash::FxHashMap; +use super::CallableType; + impl<'db> Type<'db> { pub fn display(&self, db: &'db dyn Db) -> DisplayType { DisplayType { ty: self, db } @@ -95,32 +98,59 @@ impl Display for DisplayRepresentation<'_> { Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)), Type::FunctionLiteral(function) => { let signature = function.signature(self.db); + // TODO: when generic function types are supported, we should add // the generic type parameters to the signature, i.e. // show `def foo[T](x: T) -> T`. - write!( - f, - // "def {name}{specialization}{signature}", - "def {name}{signature}", - name = function.name(self.db), - signature = signature.display(self.db) - ) + match signature { + FunctionSignature::Single(signature) => { + write!( + f, + // "def {name}{specialization}{signature}", + "def {name}{signature}", + name = function.name(self.db), + signature = signature.display(self.db) + ) + } + FunctionSignature::Overloaded(signatures, _) => { + // TODO: How to display overloads? + f.write_str("Overload[")?; + let mut join = f.join(", "); + for signature in signatures { + join.entry(&signature.display(self.db)); + } + f.write_str("]") + } + } } - Type::Callable(callable) => callable.signature(self.db).display(self.db).fmt(f), + Type::Callable(callable) => callable.display(self.db).fmt(f), Type::BoundMethod(bound_method) => { let function = bound_method.function(self.db); // TODO: use the specialization from the method. Similar to the comment above // about the function specialization, - write!( - f, - "bound method {instance}.{method}{signature}", - method = function.name(self.db), - instance = bound_method.self_instance(self.db).display(self.db), - signature = function.signature(self.db).bind_self().display(self.db) - ) + match function.signature(self.db) { + FunctionSignature::Single(signature) => { + write!( + f, + "bound method {instance}.{method}{signature}", + method = function.name(self.db), + instance = bound_method.self_instance(self.db).display(self.db), + signature = signature.bind_self().display(self.db) + ) + } + FunctionSignature::Overloaded(signatures, _) => { + // TODO: How to display overloads? + f.write_str("Overload[")?; + let mut join = f.join(", "); + for signature in signatures { + join.entry(&signature.bind_self().display(self.db)); + } + f.write_str("]") + } + } } Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { write!( @@ -355,8 +385,40 @@ impl Display for DisplaySpecialization<'_> { } } +impl<'db> CallableType<'db> { + pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayCallableType<'db> { + DisplayCallableType { + signatures: self.signatures(db), + db, + } + } +} + +pub(crate) struct DisplayCallableType<'db> { + signatures: &'db [Signature<'db>], + db: &'db dyn Db, +} + +impl Display for DisplayCallableType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.signatures { + [signature] => write!(f, "{}", signature.display(self.db)), + signatures => { + // TODO: How to display overloads? + f.write_str("Overload[")?; + let mut join = f.join(", "); + for signature in signatures { + join.entry(&signature.display(self.db)); + } + join.finish()?; + f.write_char(']') + } + } + } +} + impl<'db> Signature<'db> { - fn display(&'db self, db: &'db dyn Db) -> DisplaySignature<'db> { + pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplaySignature<'db> { DisplaySignature { parameters: self.parameters(), return_ty: self.return_ty, @@ -365,7 +427,7 @@ impl<'db> Signature<'db> { } } -struct DisplaySignature<'db> { +pub(crate) struct DisplaySignature<'db> { parameters: &'db Parameters<'db>, return_ty: Option>, db: &'db dyn Db, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 59248f9f7bc2cd..29a92c058d72bf 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -4133,7 +4133,7 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO: Useful inference of a lambda's return type will require a different approach, // which does the inference of the body expression based on arguments at each call site, // rather than eagerly computing a return type without knowing the argument types. - Type::Callable(CallableType::new( + Type::Callable(CallableType::single( self.db(), Signature::new(parameters, Some(Type::unknown())), )) @@ -7305,7 +7305,7 @@ impl<'db> TypeInferenceBuilder<'db> { let callable_type = if let (Some(parameters), Some(return_type), true) = (parameters, return_type, correct_argument_number) { - CallableType::new(db, Signature::new(parameters, Some(return_type))) + CallableType::single(db, Signature::new(parameters, Some(return_type))) } else { CallableType::unknown(db) }; @@ -7386,8 +7386,22 @@ impl<'db> TypeInferenceBuilder<'db> { let argument_type = self.infer_expression(arguments_slice); let signatures = argument_type.signatures(db); - // TODO overloads - let Some(signature) = signatures.iter().flatten().next() else { + // SAFETY: This is enforced by the constructor methods on `Signatures` even in + // the case of a non-callable union. + let callable_signature = signatures + .iter() + .next() + .expect("`Signatures` should have at least one `CallableSignature`"); + + let mut signature_iter = callable_signature.iter().map(|signature| { + if argument_type.is_bound_method() { + signature.bind_self() + } else { + signature.clone() + } + }); + + let Some(signature) = signature_iter.next() else { self.context.report_lint_old( &INVALID_TYPE_FORM, arguments_slice, @@ -7400,13 +7414,10 @@ impl<'db> TypeInferenceBuilder<'db> { return Type::unknown(); }; - let revealed_signature = if argument_type.is_bound_method() { - signature.bind_self() - } else { - signature.clone() - }; - - Type::Callable(CallableType::new(db, revealed_signature)) + Type::Callable(CallableType::from_overloads( + db, + std::iter::once(signature).chain(signature_iter), + )) } }, diff --git a/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs b/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs index 807219015961ae..470f3e07789550 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs @@ -188,7 +188,7 @@ impl Ty { create_bound_method(db, function, builtins_class) } - Ty::Callable { params, returns } => Type::Callable(CallableType::new( + Ty::Callable { params, returns } => Type::Callable(CallableType::single( db, Signature::new( params.into_parameters(db), diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 50c022e8d6de23..52b1ab827ff387 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -250,16 +250,6 @@ impl<'db> Signature<'db> { } } - /// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo - #[allow(unused_variables)] // 'reason' only unused in debug builds - pub(crate) fn todo(reason: &'static str) -> Self { - Signature { - generic_context: None, - parameters: Parameters::todo(), - return_ty: Some(todo_type!(reason)), - } - } - /// Return a typed signature from a function definition. pub(super) fn from_function( db: &'db dyn Db, @@ -286,15 +276,30 @@ impl<'db> Signature<'db> { } } + pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self { + Self { + generic_context: self.generic_context, + parameters: self + .parameters + .iter() + .map(|param| param.normalized(db)) + .collect(), + return_ty: self.return_ty.map(|return_ty| return_ty.normalized(db)), + } + } + pub(crate) fn apply_specialization( - &mut self, + &self, db: &'db dyn Db, specialization: Specialization<'db>, - ) { - self.parameters.apply_specialization(db, specialization); - self.return_ty = self - .return_ty - .map(|ty| ty.apply_specialization(db, specialization)); + ) -> Self { + Self { + generic_context: self.generic_context, + parameters: self.parameters.apply_specialization(db, specialization), + return_ty: self + .return_ty + .map(|ty| ty.apply_specialization(db, specialization)), + } } /// Return the parameters in this signature. @@ -995,10 +1000,15 @@ impl<'db> Parameters<'db> { ) } - fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) { - self.value - .iter_mut() - .for_each(|param| param.apply_specialization(db, specialization)); + fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + Self { + value: self + .value + .iter() + .map(|param| param.apply_specialization(db, specialization)) + .collect(), + is_gradual: self.is_gradual, + } } pub(crate) fn len(&self) -> usize { @@ -1162,11 +1172,14 @@ impl<'db> Parameter<'db> { self } - fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) { - self.annotated_type = self - .annotated_type - .map(|ty| ty.apply_specialization(db, specialization)); - self.kind.apply_specialization(db, specialization); + fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + Self { + annotated_type: self + .annotated_type + .map(|ty| ty.apply_specialization(db, specialization)), + kind: self.kind.apply_specialization(db, specialization), + form: self.form, + } } /// Strip information from the parameter so that two equivalent parameters compare equal. @@ -1356,14 +1369,27 @@ pub(crate) enum ParameterKind<'db> { } impl<'db> ParameterKind<'db> { - fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) { + fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { match self { - Self::PositionalOnly { default_type, .. } - | Self::PositionalOrKeyword { default_type, .. } - | Self::KeywordOnly { default_type, .. } => { - *default_type = default_type.map(|ty| ty.apply_specialization(db, specialization)); - } - Self::Variadic { .. } | Self::KeywordVariadic { .. } => {} + Self::PositionalOnly { default_type, name } => Self::PositionalOnly { + default_type: default_type + .as_ref() + .map(|ty| ty.apply_specialization(db, specialization)), + name: name.clone(), + }, + Self::PositionalOrKeyword { default_type, name } => Self::PositionalOrKeyword { + default_type: default_type + .as_ref() + .map(|ty| ty.apply_specialization(db, specialization)), + name: name.clone(), + }, + Self::KeywordOnly { default_type, name } => Self::KeywordOnly { + default_type: default_type + .as_ref() + .map(|ty| ty.apply_specialization(db, specialization)), + name: name.clone(), + }, + Self::Variadic { .. } | Self::KeywordVariadic { .. } => self.clone(), } } } @@ -1380,7 +1406,7 @@ mod tests { use super::*; use crate::db::tests::{setup_db, TestDb}; use crate::symbol::global_symbol; - use crate::types::{FunctionType, KnownClass}; + use crate::types::{FunctionSignature, FunctionType, KnownClass}; use ruff_db::system::DbWithWritableSystem as _; #[track_caller] @@ -1627,6 +1653,9 @@ mod tests { let expected_sig = func.internal_signature(&db); // With no decorators, internal and external signature are the same - assert_eq!(func.signature(&db), &expected_sig); + assert_eq!( + func.signature(&db), + &FunctionSignature::Single(expected_sig) + ); } } From 1918c616237d792c242e28aeb66d9e6836f3dfa0 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Apr 2025 06:46:21 -0700 Subject: [PATCH 0010/1161] [red-knot] class bases are not affected by __future__.annotations (#17456) ## Summary We were over-conflating the conditions for deferred name resolution. `from __future__ import annotations` defers annotations, but not class bases. In stub files, class bases are also deferred. Modeling this correctly also reduces likelihood of cycles in Python files using `from __future__ import annotations` (since deferred resolution is inherently cycle-prone). The same cycles are still possible in `.pyi` files, but much less likely, since typically there isn't anything in a `pyi` file that would cause an early return from a scope, or otherwise cause visibility constraints to persist to end of scope. Usually there is only code at module global scope and class scope, which can't have `return` statements, and `raise` or `assert` statements in a stub file would be very strange. (Technically according to the spec we'd be within our rights to just forbid a whole bunch of syntax outright in a stub file, but I kinda like minimizing unnecessary differences between the handling of Python files and stub files.) ## Test Plan Added mdtests. --- .../resources/mdtest/annotations/deferred.md | 21 +++++++++++++++++++ .../src/types/infer.rs | 12 +++++------ 2 files changed, 26 insertions(+), 7 deletions(-) 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 7ee6e84060cab2..8db8d9040920a1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md @@ -156,3 +156,24 @@ def _(): def f(self) -> C: return self ``` + +## Base class references + +### Not deferred by __future__.annotations + +```py +from __future__ import annotations + +class A(B): # error: [unresolved-reference] + pass + +class B: + pass +``` + +### Deferred in stub files + +```pyi +class A(B): ... +class B: ... +``` diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 29a92c058d72bf..c2d5467c4cbc02 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -585,8 +585,8 @@ impl<'db> TypeInferenceBuilder<'db> { /// Are we currently inferring types in file with deferred types? /// This is true for stub files and files with `__future__.annotations` - fn are_all_types_deferred(&self) -> bool { - self.index.has_future_annotations() || self.file().is_stub(self.db().upcast()) + fn defer_annotations(&self) -> bool { + self.index.has_future_annotations() || self.in_stub() } /// Are we currently inferring deferred types? @@ -1467,7 +1467,7 @@ impl<'db> TypeInferenceBuilder<'db> { // If there are type params, parameters and returns are evaluated in that scope, that is, in // `infer_function_type_params`, rather than here. if type_params.is_none() { - if self.are_all_types_deferred() { + if self.defer_annotations() { self.types.deferred.insert(definition); } else { self.infer_optional_annotation_expression( @@ -1791,9 +1791,7 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO: Only defer the references that are actually string literals, instead of // deferring the entire class definition if a string literal occurs anywhere in the // base class list. - if self.are_all_types_deferred() - || class_node.bases().iter().any(contains_string_literal) - { + if self.in_stub() || class_node.bases().iter().any(contains_string_literal) { self.types.deferred.insert(definition); } else { for base in class_node.bases() { @@ -2919,7 +2917,7 @@ impl<'db> TypeInferenceBuilder<'db> { let mut declared_ty = self.infer_annotation_expression( annotation, - DeferredExpressionState::from(self.are_all_types_deferred()), + DeferredExpressionState::from(self.defer_annotations()), ); if target From e4e405d2a107827941d5e2099af42ff3e5e5f21b Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Apr 2025 08:11:07 -0700 Subject: [PATCH 0011/1161] [red-knot] Type narrowing for assertions (take 2) (#17345) ## Summary Fixes #17147. This was landed in #17149 and then reverted in #17335 because it caused cycle panics in checking pybind11. #17456 fixed the cause of that panic. ## Test Plan Add new narrow/assert.md test file Co-authored-by: Matthew Mckee --- .../resources/mdtest/narrow/assert.md | 53 +++++++++++++++++++ .../src/semantic_index/builder.rs | 12 ++++- .../src/types/infer.rs | 2 +- 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md new file mode 100644 index 00000000000000..1fad9290a51155 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md @@ -0,0 +1,53 @@ +# Narrowing with assert statements + +## `assert` a value `is None` or `is not None` + +```py +def _(x: str | None, y: str | None): + assert x is not None + reveal_type(x) # revealed: str + assert y is None + reveal_type(y) # revealed: None +``` + +## `assert` a value is truthy or falsy + +```py +def _(x: bool, y: bool): + assert x + reveal_type(x) # revealed: Literal[True] + assert not y + reveal_type(y) # revealed: Literal[False] +``` + +## `assert` with `is` and `==` for literals + +```py +from typing import Literal + +def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]): + assert x is 2 + reveal_type(x) # revealed: Literal[2] + assert y == 2 + reveal_type(y) # revealed: Literal[1, 2, 3] +``` + +## `assert` with `isinstance` + +```py +def _(x: int | str): + assert isinstance(x, int) + reveal_type(x) # revealed: int +``` + +## `assert` a value `in` a tuple + +```py +from typing import Literal + +def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]): + assert x in (1, 2) + reveal_type(x) # revealed: Literal[1, 2] + assert y not in (1, 2) + reveal_type(y) # revealed: Literal[3] +``` diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index f9d312ab531a52..e1543428dc71ac 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -569,7 +569,6 @@ impl<'db> SemanticIndexBuilder<'db> { } /// Records a visibility constraint by applying it to all live bindings and declarations. - #[must_use = "A visibility constraint must always be negated after it is added"] fn record_visibility_constraint( &mut self, predicate: Predicate<'db>, @@ -1323,6 +1322,17 @@ where ); } } + + ast::Stmt::Assert(node) => { + self.visit_expr(&node.test); + let predicate = self.record_expression_narrowing_constraint(&node.test); + self.record_visibility_constraint(predicate); + + if let Some(msg) = &node.msg { + self.visit_expr(msg); + } + } + ast::Stmt::Assign(node) => { debug_assert_eq!(&self.current_assignments, &[]); diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index c2d5467c4cbc02..a81c97c9ce2f8f 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -3294,7 +3294,7 @@ impl<'db> TypeInferenceBuilder<'db> { msg, } = assert; - let test_ty = self.infer_expression(test); + let test_ty = self.infer_standalone_expression(test); if let Err(err) = test_ty.try_bool(self.db()) { err.report_diagnostic(&self.context, &**test); From 84d064a14c29c36ae35f83cf561f30c1265e6ee9 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Apr 2025 08:20:03 -0700 Subject: [PATCH 0012/1161] [red-knot] fix building unions with literals and AlwaysTruthy/AlwaysFalsy (#17451) In #17403 I added a comment asserting that all same-kind literal types share all the same super-types. This is true, with two notable exceptions: the types `AlwaysTruthy` and `AlwaysFalsy`. These two types are super-types of some literal types within a given kind and not others: `Literal[0]`, `Literal[""]`, and `Literal[b""]` inhabit `AlwaysFalsy`, while other literals inhabit `AlwaysTruthy`. This PR updates the literal-unions optimization to handle these types correctly. Fixes https://github.com/astral-sh/ruff/issues/17447 Verified locally that `QUICKCHECK_TESTS=100000 cargo test -p red_knot_python_semantic -- --ignored types::property_tests::stable` now passes again. --- .../resources/mdtest/union_types.md | 43 ++++++++ .../src/types/builder.rs | 99 ++++++++++++++----- 2 files changed, 118 insertions(+), 24 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/union_types.md b/crates/red_knot_python_semantic/resources/mdtest/union_types.md index 44d4d93d1d1780..45bbf07fac20c6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/union_types.md +++ b/crates/red_knot_python_semantic/resources/mdtest/union_types.md @@ -166,3 +166,46 @@ def _( reveal_type(i1) # revealed: P & Q reveal_type(i2) # revealed: P & Q ``` + +## Unions of literals with `AlwaysTruthy` and `AlwaysFalsy` + +```py +from typing import Literal +from knot_extensions import AlwaysTruthy, AlwaysFalsy + +type strings = Literal["foo", ""] +type ints = Literal[0, 1] +type bytes = Literal[b"foo", b""] + +def _( + strings_or_truthy: strings | AlwaysTruthy, + truthy_or_strings: AlwaysTruthy | strings, + strings_or_falsy: strings | AlwaysFalsy, + falsy_or_strings: AlwaysFalsy | strings, + ints_or_truthy: ints | AlwaysTruthy, + truthy_or_ints: AlwaysTruthy | ints, + ints_or_falsy: ints | AlwaysFalsy, + falsy_or_ints: AlwaysFalsy | ints, + bytes_or_truthy: bytes | AlwaysTruthy, + truthy_or_bytes: AlwaysTruthy | bytes, + bytes_or_falsy: bytes | AlwaysFalsy, + falsy_or_bytes: AlwaysFalsy | bytes, +): + reveal_type(strings_or_truthy) # revealed: Literal[""] | AlwaysTruthy + reveal_type(truthy_or_strings) # revealed: AlwaysTruthy | Literal[""] + + reveal_type(strings_or_falsy) # revealed: Literal["foo"] | AlwaysFalsy + reveal_type(falsy_or_strings) # revealed: AlwaysFalsy | Literal["foo"] + + reveal_type(ints_or_truthy) # revealed: Literal[0] | AlwaysTruthy + reveal_type(truthy_or_ints) # revealed: AlwaysTruthy | Literal[0] + + reveal_type(ints_or_falsy) # revealed: Literal[1] | AlwaysFalsy + reveal_type(falsy_or_ints) # revealed: AlwaysFalsy | Literal[1] + + reveal_type(bytes_or_truthy) # revealed: Literal[b""] | AlwaysTruthy + reveal_type(truthy_or_bytes) # revealed: AlwaysTruthy | Literal[b""] + + reveal_type(bytes_or_falsy) # revealed: Literal[b"foo"] | AlwaysFalsy + reveal_type(falsy_or_bytes) # revealed: AlwaysFalsy | Literal[b"foo"] +``` diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 411c0802578430..f51f59b8718aa3 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -51,6 +51,67 @@ enum UnionElement<'db> { Type(Type<'db>), } +impl<'db> UnionElement<'db> { + /// Try reducing this `UnionElement` given the presence in the same union of `other_type`. + /// + /// If this `UnionElement` is a group of literals, filter the literals present if needed and + /// return `ReduceResult::KeepIf` with a boolean value indicating whether the remaining group + /// of literals should be kept in the union + /// + /// If this `UnionElement` is some other type, return `ReduceResult::Type` so `UnionBuilder` + /// can perform more complex checks on it. + fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> { + // `AlwaysTruthy` and `AlwaysFalsy` are the only types which can be a supertype of only + // _some_ literals of the same kind, so we need to walk the full set in this case. + let needs_filter = matches!(other_type, Type::AlwaysTruthy | Type::AlwaysFalsy); + match self { + UnionElement::IntLiterals(literals) => { + ReduceResult::KeepIf(if needs_filter { + literals.retain(|literal| { + !Type::IntLiteral(*literal).is_subtype_of(db, other_type) + }); + !literals.is_empty() + } else { + // SAFETY: All `UnionElement` literal kinds must always be non-empty + !Type::IntLiteral(literals[0]).is_subtype_of(db, other_type) + }) + } + UnionElement::StringLiterals(literals) => { + ReduceResult::KeepIf(if needs_filter { + literals.retain(|literal| { + !Type::StringLiteral(*literal).is_subtype_of(db, other_type) + }); + !literals.is_empty() + } else { + // SAFETY: All `UnionElement` literal kinds must always be non-empty + !Type::StringLiteral(literals[0]).is_subtype_of(db, other_type) + }) + } + UnionElement::BytesLiterals(literals) => { + ReduceResult::KeepIf(if needs_filter { + literals.retain(|literal| { + !Type::BytesLiteral(*literal).is_subtype_of(db, other_type) + }); + !literals.is_empty() + } else { + // SAFETY: All `UnionElement` literal kinds must always be non-empty + !Type::BytesLiteral(literals[0]).is_subtype_of(db, other_type) + }) + } + UnionElement::Type(existing) => ReduceResult::Type(*existing), + } + } +} + +enum ReduceResult<'db> { + /// Reduction of this `UnionElement` is complete; keep it in the union if the nested + /// boolean is true, eliminate it from the union if false. + KeepIf(bool), + /// The given `Type` can stand-in for the entire `UnionElement` for further union + /// simplification checks. + Type(Type<'db>), +} + // TODO increase this once we extend `UnionElement` throughout all union/intersection // representations, so that we can make large unions of literals fast in all operations. const MAX_UNION_LITERALS: usize = 200; @@ -197,27 +258,17 @@ impl<'db> UnionBuilder<'db> { let mut to_remove = SmallVec::<[usize; 2]>::new(); let ty_negated = ty.negate(self.db); - for (index, element) in self - .elements - .iter() - .map(|element| { - // For literals, the first element in the set can stand in for all the rest, - // since they all have the same super-types. SAFETY: a `UnionElement` of - // literal kind must always have at least one element in it. - match element { - UnionElement::IntLiterals(literals) => Type::IntLiteral(literals[0]), - UnionElement::StringLiterals(literals) => { - Type::StringLiteral(literals[0]) + for (index, element) in self.elements.iter_mut().enumerate() { + let element_type = match element.try_reduce(self.db, ty) { + ReduceResult::KeepIf(keep) => { + if !keep { + to_remove.push(index); } - UnionElement::BytesLiterals(literals) => { - Type::BytesLiteral(literals[0]) - } - UnionElement::Type(ty) => *ty, + continue; } - }) - .enumerate() - { - if Some(element) == bool_pair { + ReduceResult::Type(ty) => ty, + }; + if Some(element_type) == bool_pair { to_add = KnownClass::Bool.to_instance(self.db); to_remove.push(index); // The type we are adding is a BooleanLiteral, which doesn't have any @@ -227,14 +278,14 @@ impl<'db> UnionBuilder<'db> { break; } - if ty.is_same_gradual_form(element) - || ty.is_subtype_of(self.db, element) - || element.is_object(self.db) + if ty.is_same_gradual_form(element_type) + || ty.is_subtype_of(self.db, element_type) + || element_type.is_object(self.db) { return; - } else if element.is_subtype_of(self.db, ty) { + } else if element_type.is_subtype_of(self.db, ty) { to_remove.push(index); - } else if ty_negated.is_subtype_of(self.db, element) { + } else if ty_negated.is_subtype_of(self.db, element_type) { // We add `ty` to the union. We just checked that `~ty` is a subtype of an existing `element`. // This also means that `~ty | ty` is a subtype of `element | ty`, because both elements in the // first union are subtypes of the corresponding elements in the second union. But `~ty | ty` is From 5853eb28dd56873f9110ad0b84201b73c47dbbeb Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Fri, 18 Apr 2025 16:46:15 +0100 Subject: [PATCH 0013/1161] [red-knot] allow assignment expression in call compare narrowing (#17461) ## Summary There was some narrowing constraints not covered from the previous PR ```py def _(x: object): if (type(y := x)) is bool: reveal_type(y) # revealed: bool ``` Also, refactored a bit ## Test Plan Update type_api.md --- .../resources/mdtest/narrow/type.md | 10 ++++ .../src/types/narrow.rs | 51 ++++++++----------- 2 files changed, 32 insertions(+), 29 deletions(-) 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 927670b5081af9..602265039db275 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md @@ -144,3 +144,13 @@ def _(x: Base): # express a constraint like `Base & ~ProperSubtypeOf[Base]`. reveal_type(x) # revealed: Base ``` + +## Assignment expressions + +```py +def _(x: object): + if (y := type(x)) is bool: + reveal_type(y) # revealed: Literal[bool] + if (type(y := x)) is bool: + reveal_type(y) # revealed: bool +``` diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index 50cfb9b931b842..b884d1da850ac9 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -238,6 +238,17 @@ fn negate_if<'db>(constraints: &mut NarrowingConstraints<'db>, db: &'db dyn Db, } } +fn expr_name(expr: &ast::Expr) -> Option<&ast::name::Name> { + match expr { + ast::Expr::Named(ast::ExprNamed { target, .. }) => match target.as_ref() { + ast::Expr::Name(ast::ExprName { id, .. }) => Some(id), + _ => None, + }, + ast::Expr::Name(ast::ExprName { id, .. }) => Some(id), + _ => None, + } +} + struct NarrowingConstraintsBuilder<'db> { db: &'db dyn Db, predicate: PredicateNode<'db>, @@ -497,27 +508,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> { last_rhs_ty = Some(rhs_ty); match left { - ast::Expr::Name(ast::ExprName { - range: _, - id, - ctx: _, - }) => { - let symbol = self.expect_expr_name_symbol(id); - - let op = if is_positive { *op } else { op.negate() }; - - if let Some(ty) = self.evaluate_expr_compare_op(lhs_ty, rhs_ty, op) { - constraints.insert(symbol, ty); - } - } - ast::Expr::Named(ast::ExprNamed { - range: _, - target, - value: _, - }) => { - if let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { + ast::Expr::Name(_) | ast::Expr::Named(_) => { + if let Some(id) = expr_name(left) { let symbol = self.expect_expr_name_symbol(id); - let op = if is_positive { *op } else { op.negate() }; if let Some(ty) = self.evaluate_expr_compare_op(lhs_ty, rhs_ty, op) { @@ -545,8 +538,12 @@ impl<'db> NarrowingConstraintsBuilder<'db> { } }; - let [ast::Expr::Name(ast::ExprName { id, .. })] = &**args else { - continue; + let id = match &**args { + [first] => match expr_name(first) { + Some(id) => id, + None => continue, + }, + _ => continue, }; let is_valid_constraint = if is_positive { @@ -598,13 +595,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> { let function = function_type.known(self.db)?.into_constraint_function()?; let (id, class_info) = match &*expr_call.arguments.args { - [first, class_info] => match first { - ast::Expr::Named(ast::ExprNamed { target, .. }) => match target.as_ref() { - ast::Expr::Name(ast::ExprName { id, .. }) => (id, class_info), - _ => return None, - }, - ast::Expr::Name(ast::ExprName { id, .. }) => (id, class_info), - _ => return None, + [first, class_info] => match expr_name(first) { + Some(id) => (id, class_info), + None => return None, }, _ => return None, }; From 787bcd1c6a30d61ae7d2e59c1f13743f6feca1b7 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 18 Apr 2025 11:49:22 -0400 Subject: [PATCH 0014/1161] [red-knot] Handle explicit class specialization in type expressions (#17434) You can now use subscript expressions in a type expression to explicitly specialize generic classes, just like you could already do in value expressions. This still does not implement bidirectional checking, so a type annotation on an assignment does not influence how we infer a specialization for a (not explicitly specialized) constructor call. You might get an `invalid-assignment` error if (a) we cannot infer a class specialization from the constructor call (in which case you end up e.g. trying to assign `C[Unknown]` to `C[int]`) or if (b) we can infer a specialization, but it doesn't match the annotation. Closes https://github.com/astral-sh/ruff/issues/17432 --- .../resources/mdtest/annotations/invalid.md | 2 +- .../resources/mdtest/annotations/literal.md | 2 +- .../mdtest/assignment/annotations.md | 2 +- .../resources/mdtest/attributes.md | 5 +- .../resources/mdtest/call/methods.md | 2 +- .../resources/mdtest/decorators.md | 4 +- .../resources/mdtest/directives/cast.md | 2 +- .../resources/mdtest/generics/classes.md | 20 +- .../resources/mdtest/generics/pep695.md | 40 +-- .../resources/mdtest/generics/scoping.md | 10 +- .../resources/mdtest/generics/variance.md | 277 ++++++++++++++++++ .../resources/mdtest/protocols.md | 6 +- .../mdtest/scopes/moduletype_attrs.md | 8 +- .../resources/mdtest/subscript/lists.md | 2 +- .../resources/mdtest/union_types.md | 5 + crates/red_knot_python_semantic/src/types.rs | 11 +- .../src/types/class.rs | 114 ++++++- .../src/types/generics.rs | 112 +++++++ .../src/types/infer.rs | 26 +- 19 files changed, 588 insertions(+), 62 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/generics/variance.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md index a5154b450a64b9..9c91adb52e2eb2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md @@ -89,7 +89,7 @@ def _( reveal_type(k) # revealed: Unknown reveal_type(p) # revealed: Unknown reveal_type(q) # revealed: int | Unknown - reveal_type(r) # revealed: @Todo(generics) + reveal_type(r) # revealed: @Todo(unknown type subscript) ``` ## Invalid Collection based AST nodes diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md index 138478dbdf6771..865bf073f1fdde 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md @@ -137,7 +137,7 @@ from other import Literal a1: Literal[26] def f(): - reveal_type(a1) # revealed: @Todo(generics) + reveal_type(a1) # revealed: @Todo(unknown type subscript) ``` ## Detecting typing_extensions.Literal 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 171ade213e99b3..52c51ea490c758 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md @@ -61,7 +61,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]] reveal_type(e) # revealed: @Todo(full tuple[...] support) reveal_type(f) # revealed: @Todo(full tuple[...] support) reveal_type(g) # revealed: @Todo(full tuple[...] support) -reveal_type(h) # revealed: tuple[@Todo(generics), @Todo(generics)] +reveal_type(h) # revealed: tuple[@Todo(specialized non-generic class), @Todo(specialized non-generic class)] reveal_type(i) # revealed: tuple[str | int, str | int] reveal_type(j) # revealed: tuple[str | int] diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 3f6c5321c0349b..57f385fc917ac7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1665,7 +1665,7 @@ functions are instances of that class: def f(): ... reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None -reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None +reveal_type(f.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None ``` Some attributes are special-cased, however: @@ -1716,7 +1716,8 @@ reveal_type(False.real) # revealed: Literal[0] All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`: ```py -reveal_type(b"foo".join) # revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(generics), /) -> bytes +# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(specialized non-generic class), /) -> bytes +reveal_type(b"foo".join) # revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool reveal_type(b"foo".endswith) ``` 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 c7a63beff2e38d..9ca10a05c7a0ad 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -94,7 +94,7 @@ function object. We model this explicitly, which means that we can access `__kwd methods, even though it is not available on `types.MethodType`: ```py -reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None +reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None ``` ## Basic method calls on class objects and instances diff --git a/crates/red_knot_python_semantic/resources/mdtest/decorators.md b/crates/red_knot_python_semantic/resources/mdtest/decorators.md index 3ba75ec10b0d03..b1233f3d0f2109 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/decorators.md +++ b/crates/red_knot_python_semantic/resources/mdtest/decorators.md @@ -145,10 +145,10 @@ def f(x: int) -> int: return x**2 # TODO: Should be `_lru_cache_wrapper[int]` -reveal_type(f) # revealed: @Todo(generics) +reveal_type(f) # revealed: @Todo(specialized non-generic class) # TODO: Should be `int` -reveal_type(f(1)) # revealed: @Todo(generics) +reveal_type(f(1)) # revealed: @Todo(specialized non-generic class) ``` ## Lambdas as decorators diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md index 96a01d90d9e5fd..8ff82e29dfef4e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md @@ -61,7 +61,7 @@ from knot_extensions import Unknown def f(x: Any, y: Unknown, z: Any | str | int): a = cast(dict[str, Any], x) - reveal_type(a) # revealed: @Todo(generics) + reveal_type(a) # revealed: @Todo(specialized non-generic class) b = cast(Any, y) reveal_type(b) # revealed: Any 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 9421d16c3622b3..737e3455e0ed75 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -164,12 +164,12 @@ consistent with each other. ```py class C[T]: - def __new__(cls, x: T) -> "C"[T]: + def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) reveal_type(C(1)) # revealed: C[Literal[1]] -# TODO: error: [invalid-argument-type] +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") ``` @@ -181,7 +181,7 @@ class C[T]: reveal_type(C(1)) # revealed: C[Literal[1]] -# TODO: error: [invalid-argument-type] +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") ``` @@ -189,14 +189,14 @@ wrong_innards: C[int] = C("five") ```py class C[T]: - def __new__(cls, x: T) -> "C"[T]: + def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) def __init__(self, x: T) -> None: ... reveal_type(C(1)) # revealed: C[Literal[1]] -# TODO: error: [invalid-argument-type] +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") ``` @@ -204,25 +204,25 @@ wrong_innards: C[int] = C("five") ```py class C[T]: - def __new__(cls, *args, **kwargs) -> "C"[T]: + def __new__(cls, *args, **kwargs) -> "C[T]": return object.__new__(cls) def __init__(self, x: T) -> None: ... reveal_type(C(1)) # revealed: C[Literal[1]] -# TODO: error: [invalid-argument-type] +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") class D[T]: - def __new__(cls, x: T) -> "D"[T]: + def __new__(cls, x: T) -> "D[T]": return object.__new__(cls) def __init__(self, *args, **kwargs) -> None: ... reveal_type(D(1)) # revealed: D[Literal[1]] -# TODO: error: [invalid-argument-type] +# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`" wrong_innards: D[int] = D("five") ``` @@ -247,7 +247,7 @@ reveal_type(C(1, "string")) # revealed: C[Unknown] # error: [invalid-argument-type] reveal_type(C(1, True)) # revealed: C[Unknown] -# TODO: error for the correct reason +# TODO: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `S`, found `Literal[1]`" wrong_innards: C[int] = C("five", 1) ``` 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 7870e8b62f0ff8..67e69940f3129a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -64,19 +64,19 @@ is.) from knot_extensions import is_fully_static, static_assert from typing import Any -def unbounded_unconstrained[T](t: list[T]) -> None: +def unbounded_unconstrained[T](t: T) -> None: static_assert(is_fully_static(T)) -def bounded[T: int](t: list[T]) -> None: +def bounded[T: int](t: T) -> None: static_assert(is_fully_static(T)) -def bounded_by_gradual[T: Any](t: list[T]) -> None: +def bounded_by_gradual[T: Any](t: T) -> None: static_assert(not is_fully_static(T)) -def constrained[T: (int, str)](t: list[T]) -> None: +def constrained[T: (int, str)](t: T) -> None: static_assert(is_fully_static(T)) -def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None: +def constrained_by_gradual[T: (int, Any)](t: T) -> None: static_assert(not is_fully_static(T)) ``` @@ -99,7 +99,7 @@ class Base(Super): ... class Sub(Base): ... class Unrelated: ... -def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None: +def unbounded_unconstrained[T, U](t: T, u: U) -> None: static_assert(is_assignable_to(T, T)) static_assert(is_assignable_to(T, object)) static_assert(not is_assignable_to(T, Super)) @@ -129,7 +129,7 @@ is a final class, since the typevar can still be specialized to `Never`.) from typing import Any from typing_extensions import final -def bounded[T: Super](t: list[T]) -> None: +def bounded[T: Super](t: T) -> None: static_assert(is_assignable_to(T, Super)) static_assert(not is_assignable_to(T, Sub)) static_assert(not is_assignable_to(Super, T)) @@ -140,7 +140,7 @@ def bounded[T: Super](t: list[T]) -> None: static_assert(not is_subtype_of(Super, T)) static_assert(not is_subtype_of(Sub, T)) -def bounded_by_gradual[T: Any](t: list[T]) -> None: +def bounded_by_gradual[T: Any](t: T) -> None: static_assert(is_assignable_to(T, Any)) static_assert(is_assignable_to(Any, T)) static_assert(is_assignable_to(T, Super)) @@ -158,7 +158,7 @@ def bounded_by_gradual[T: Any](t: list[T]) -> None: @final class FinalClass: ... -def bounded_final[T: FinalClass](t: list[T]) -> None: +def bounded_final[T: FinalClass](t: T) -> None: static_assert(is_assignable_to(T, FinalClass)) static_assert(not is_assignable_to(FinalClass, T)) @@ -172,14 +172,14 @@ true even if both typevars are bounded by the same final class, since you can sp typevars to `Never` in addition to that final class. ```py -def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None: +def two_bounded[T: Super, U: Super](t: T, u: U) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) static_assert(not is_subtype_of(T, U)) static_assert(not is_subtype_of(U, T)) -def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None: +def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -194,7 +194,7 @@ intersection of all of its constraints is a subtype of the typevar. ```py from knot_extensions import Intersection -def constrained[T: (Base, Unrelated)](t: list[T]) -> None: +def constrained[T: (Base, Unrelated)](t: T) -> None: static_assert(not is_assignable_to(T, Super)) static_assert(not is_assignable_to(T, Base)) static_assert(not is_assignable_to(T, Sub)) @@ -219,7 +219,7 @@ def constrained[T: (Base, Unrelated)](t: list[T]) -> None: static_assert(not is_subtype_of(Super | Unrelated, T)) static_assert(is_subtype_of(Intersection[Base, Unrelated], T)) -def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None: +def constrained_by_gradual[T: (Base, Any)](t: T) -> None: static_assert(is_assignable_to(T, Super)) static_assert(is_assignable_to(T, Base)) static_assert(not is_assignable_to(T, Sub)) @@ -261,7 +261,7 @@ distinct constraints, meaning that there is (still) no guarantee that they will the same type. ```py -def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None: +def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -271,7 +271,7 @@ def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> Non @final class AnotherFinalClass: ... -def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None: +def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None: static_assert(not is_assignable_to(T, U)) static_assert(not is_assignable_to(U, T)) @@ -290,7 +290,7 @@ non-singleton type. ```py from knot_extensions import is_singleton, is_single_valued, static_assert -def unbounded_unconstrained[T](t: list[T]) -> None: +def unbounded_unconstrained[T](t: T) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) ``` @@ -299,7 +299,7 @@ A bounded typevar is not a singleton, even if its bound is a singleton, since it specialized to `Never`. ```py -def bounded[T: None](t: list[T]) -> None: +def bounded[T: None](t: T) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) ``` @@ -310,14 +310,14 @@ specialize a constrained typevar to a subtype of a constraint.) ```py from typing_extensions import Literal -def constrained_non_singletons[T: (int, str)](t: list[T]) -> None: +def constrained_non_singletons[T: (int, str)](t: T) -> None: static_assert(not is_singleton(T)) static_assert(not is_single_valued(T)) -def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None: +def constrained_singletons[T: (Literal[True], Literal[False])](t: T) -> None: static_assert(is_singleton(T)) -def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None: +def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None: static_assert(is_single_valued(T)) ``` 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 da09b7c7bc8f62..0d99019894009d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -174,13 +174,13 @@ S = TypeVar("S") def f(x: T) -> None: x: list[T] = [] - # TODO: error + # TODO: invalid-assignment error y: list[S] = [] # TODO: no error # error: [invalid-base] class C(Generic[T]): - # TODO: error + # TODO: error: cannot use S if it's not in the current generic context x: list[S] = [] # This is not an error, as shown in the previous test @@ -200,11 +200,11 @@ S = TypeVar("S") def f[T](x: T) -> None: x: list[T] = [] - # TODO: error + # TODO: invalid assignment error y: list[S] = [] class C[T]: - # TODO: error + # TODO: error: cannot use S if it's not in the current generic context x: list[S] = [] def m1(self, x: S) -> S: @@ -288,7 +288,7 @@ class C[T]: ok1: list[T] = [] class Bad: - # TODO: error + # TODO: error: cannot refer to T in nested scope bad: list[T] = [] class Inner[S]: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/variance.md b/crates/red_knot_python_semantic/resources/mdtest/generics/variance.md new file mode 100644 index 00000000000000..1541a46fbca738 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/variance.md @@ -0,0 +1,277 @@ +# Variance + +```toml +[environment] +python-version = "3.12" +``` + +Type variables have a property called _variance_ that affects the subtyping and assignability +relations. Much more detail can be found in the [spec]. To summarize, each typevar is either +**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not +currently mentioned in the typing spec, but is a fourth case that we must consider.) + +For all of the examples below, we will consider a typevar `T`, a generic class using that typevar +`C[T]`, and two types `A` and `B`. + +## Covariance + +With a covariant typevar, subtyping is in "alignment": if `A <: B`, then `C[A] <: C[B]`. + +Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of +`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would +get from the sequence is a valid `int`. + +```py +from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + def receive(self) -> T: + raise ValueError + +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[B], C[A])) +static_assert(not is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[B], C[A])) +static_assert(not is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(is_gradual_equivalent_to(C[A], C[A])) +static_assert(is_gradual_equivalent_to(C[B], C[B])) +static_assert(is_gradual_equivalent_to(C[Any], C[Any])) +static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) +static_assert(not is_gradual_equivalent_to(C[B], C[A])) +static_assert(not is_gradual_equivalent_to(C[A], C[B])) +static_assert(not is_gradual_equivalent_to(C[A], C[Any])) +static_assert(not is_gradual_equivalent_to(C[B], C[Any])) +static_assert(not is_gradual_equivalent_to(C[Any], C[A])) +static_assert(not is_gradual_equivalent_to(C[Any], C[B])) +``` + +## Contravariance + +With a contravariant typevar, subtyping is in "opposition": if `A <: B`, then `C[B] <: C[A]`. + +Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives +`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool` +that you pass into the consumer is a valid `int`. + +```py +from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + def send(self, value: T): ... + +static_assert(not is_assignable_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +static_assert(not is_subtype_of(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(is_gradual_equivalent_to(C[A], C[A])) +static_assert(is_gradual_equivalent_to(C[B], C[B])) +static_assert(is_gradual_equivalent_to(C[Any], C[Any])) +static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) +static_assert(not is_gradual_equivalent_to(C[B], C[A])) +static_assert(not is_gradual_equivalent_to(C[A], C[B])) +static_assert(not is_gradual_equivalent_to(C[A], C[Any])) +static_assert(not is_gradual_equivalent_to(C[B], C[Any])) +static_assert(not is_gradual_equivalent_to(C[Any], C[A])) +static_assert(not is_gradual_equivalent_to(C[Any], C[B])) +``` + +## Invariance + +With an invariant typevar, _no_ specializations of the generic class are subtypes of each other. + +This often occurs for types that are both producers _and_ consumers, like a mutable `list`. +Iterating over the elements in a list would work with a covariant typevar, just like with the +"producer" type above. Appending elements to a list would work with a contravariant typevar, just +like with the "consumer" type above. However, a typevar cannot be both covariant and contravariant +at the same time! + +If you expect a mutable list of `int`s, it's not safe for someone to provide you with a mutable list +of `bool`s, since you might try to add an element to the list: if you try to add an `int`, the list +would no longer only contain elements that are subtypes of `bool`. + +Conversely, if you expect a mutable list of `bool`s, it's not safe for someone to provide you with a +mutable list of `int`s, since you might try to extract elements from the list: you expect every +element that you extract to be a subtype of `bool`, but the list can contain any `int`. + +In the end, if you expect a mutable list, you must always be given a list of exactly that type, +since we can't know in advance which of the allowed methods you'll want to use. + +```py +from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + def send(self, value: T): ... + def receive(self) -> T: + raise ValueError + +static_assert(not is_assignable_to(C[B], C[A])) +static_assert(not is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +static_assert(not is_subtype_of(C[B], C[A])) +static_assert(not is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +static_assert(not is_equivalent_to(C[B], C[A])) +static_assert(not is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(is_gradual_equivalent_to(C[A], C[A])) +static_assert(is_gradual_equivalent_to(C[B], C[B])) +static_assert(is_gradual_equivalent_to(C[Any], C[Any])) +static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) +static_assert(not is_gradual_equivalent_to(C[B], C[A])) +static_assert(not is_gradual_equivalent_to(C[A], C[B])) +static_assert(not is_gradual_equivalent_to(C[A], C[Any])) +static_assert(not is_gradual_equivalent_to(C[B], C[Any])) +static_assert(not is_gradual_equivalent_to(C[Any], C[A])) +static_assert(not is_gradual_equivalent_to(C[Any], C[B])) +``` + +## Bivariance + +With a bivariant typevar, _all_ specializations of the generic class are subtypes of (and in fact, +equivalent to) each other. + +This is a bit of pathological case, which really only happens when the class doesn't use the typevar +at all. (If it did, it would have to be covariant, contravariant, or invariant, depending on _how_ +the typevar was used.) + +```py +from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from typing import Any + +class A: ... +class B(A): ... + +class C[T]: + pass + +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_assignable_to(C[A], C[B])) +static_assert(is_assignable_to(C[A], C[Any])) +static_assert(is_assignable_to(C[B], C[Any])) +static_assert(is_assignable_to(C[Any], C[A])) +static_assert(is_assignable_to(C[Any], C[B])) + +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[Any])) +static_assert(not is_subtype_of(C[B], C[Any])) +static_assert(not is_subtype_of(C[Any], C[A])) +static_assert(not is_subtype_of(C[Any], C[B])) + +static_assert(is_equivalent_to(C[A], C[A])) +static_assert(is_equivalent_to(C[B], C[B])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[A], C[B])) +static_assert(not is_equivalent_to(C[A], C[Any])) +static_assert(not is_equivalent_to(C[B], C[Any])) +static_assert(not is_equivalent_to(C[Any], C[A])) +static_assert(not is_equivalent_to(C[Any], C[B])) + +static_assert(is_gradual_equivalent_to(C[A], C[A])) +static_assert(is_gradual_equivalent_to(C[B], C[B])) +static_assert(is_gradual_equivalent_to(C[Any], C[Any])) +static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[B], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[A], C[B])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[A], C[Any])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[B], C[Any])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[Any], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_gradual_equivalent_to(C[Any], C[B])) +``` + +[spec]: https://typing.python.org/en/latest/spec/generics.html#variance diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index f8620997512994..2ddb0f404da89e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -344,7 +344,7 @@ class Foo(Protocol): # `tuple[Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]` # # `frozenset[Literal["x", "y", "z", "method_member"]]` -reveal_type(get_protocol_members(Foo)) # revealed: @Todo(generics) +reveal_type(get_protocol_members(Foo)) # revealed: @Todo(specialized non-generic class) ``` Calling `get_protocol_members` on a non-protocol class raises an error at runtime: @@ -353,7 +353,7 @@ Calling `get_protocol_members` on a non-protocol class raises an error at runtim class NotAProtocol: ... # TODO: should emit `[invalid-protocol]` error, should reveal `Unknown` -reveal_type(get_protocol_members(NotAProtocol)) # revealed: @Todo(generics) +reveal_type(get_protocol_members(NotAProtocol)) # revealed: @Todo(specialized non-generic class) ``` Certain special attributes and methods are not considered protocol members at runtime, and should @@ -372,7 +372,7 @@ class Lumberjack(Protocol): self.x = x # TODO: `tuple[Literal["x"]]` or `frozenset[Literal["x"]]` -reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(generics) +reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(specialized non-generic class) ``` ## Subtyping of protocols with attribute members diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md index 65d9a930aff1ea..8cbe44952647b3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -14,7 +14,7 @@ reveal_type(__package__) # revealed: str | None reveal_type(__doc__) # revealed: str | None reveal_type(__spec__) # revealed: ModuleSpec | None -reveal_type(__path__) # revealed: @Todo(generics) +reveal_type(__path__) # revealed: @Todo(specialized non-generic class) class X: reveal_type(__name__) # revealed: str @@ -59,7 +59,7 @@ reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: ob reveal_type(typing.__class__) # revealed: Literal[ModuleType] # TODO: needs support generics; should be `dict[str, Any]`: -reveal_type(typing.__dict__) # revealed: @Todo(generics) +reveal_type(typing.__dict__) # revealed: @Todo(specialized non-generic class) ``` Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with @@ -92,8 +92,8 @@ import foo from foo import __dict__ as foo_dict # TODO: needs support generics; should be `dict[str, Any]` for both of these: -reveal_type(foo.__dict__) # revealed: @Todo(generics) -reveal_type(foo_dict) # revealed: @Todo(generics) +reveal_type(foo.__dict__) # revealed: @Todo(specialized non-generic class) +reveal_type(foo_dict) # revealed: @Todo(specialized non-generic class) ``` ## Conditionally global or `ModuleType` attribute diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md index 5f9264f6faa164..9bb53d4e672259 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md @@ -15,7 +15,7 @@ reveal_type(x) # revealed: list reveal_type(x[0]) # revealed: Unknown | @Todo(Support for `typing.TypeVar` instances in type expressions) # TODO reveal list -reveal_type(x[0:1]) # revealed: @Todo(generics) +reveal_type(x[0:1]) # revealed: @Todo(specialized non-generic class) # error: [call-non-callable] reveal_type(x["a"]) # revealed: Unknown diff --git a/crates/red_knot_python_semantic/resources/mdtest/union_types.md b/crates/red_knot_python_semantic/resources/mdtest/union_types.md index 45bbf07fac20c6..398847ecbe0e0e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/union_types.md +++ b/crates/red_knot_python_semantic/resources/mdtest/union_types.md @@ -169,6 +169,11 @@ def _( ## Unions of literals with `AlwaysTruthy` and `AlwaysFalsy` +```toml +[environment] +python-version = "3.12" +``` + ```py from typing import Literal from knot_extensions import AlwaysTruthy, AlwaysFalsy diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 6782de5f69dbd2..68fbc1d2fd274f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1362,6 +1362,10 @@ impl<'db> Type<'db> { ) } + (Type::Instance(self_instance), Type::Instance(target_instance)) => { + self_instance.is_assignable_to(db, target_instance) + } + (Type::Callable(self_callable), Type::Callable(target_callable)) => { self_callable.is_assignable_to(db, target_callable) } @@ -1376,7 +1380,7 @@ impl<'db> Type<'db> { .into_callable_type(db) .is_assignable_to(db, target), - // TODO other types containing gradual forms (e.g. generics containing Any/Unknown) + // TODO other types containing gradual forms _ => self.is_subtype_of(db, target), } } @@ -1396,6 +1400,7 @@ impl<'db> Type<'db> { } (Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right), (Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right), + (Type::Instance(left), Type::Instance(right)) => left.is_equivalent_to(db, right), _ => self == other && self.is_fully_static(db) && other.is_fully_static(db), } } @@ -1448,6 +1453,10 @@ impl<'db> Type<'db> { (Type::TypeVar(first), Type::TypeVar(second)) => first == second, + (Type::Instance(first), Type::Instance(second)) => { + first.is_gradual_equivalent_to(db, second) + } + (Type::Tuple(first), Type::Tuple(second)) => first.is_gradual_equivalent_to(db, second), (Type::Union(first), Type::Union(second)) => first.is_gradual_equivalent_to(db, second), diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index f22e8a668eac27..e8e53a2e3f561c 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -257,11 +257,107 @@ impl<'db> ClassType<'db> { class_literal.is_final(db) } + /// If `self` and `other` are generic aliases of the same generic class, returns their + /// corresponding specializations. + fn compatible_specializations( + self, + db: &'db dyn Db, + other: ClassType<'db>, + ) -> Option<(Specialization<'db>, Specialization<'db>)> { + match (self, other) { + (ClassType::Generic(self_generic), ClassType::Generic(other_generic)) => { + if self_generic.origin(db) == other_generic.origin(db) { + Some(( + self_generic.specialization(db), + other_generic.specialization(db), + )) + } else { + None + } + } + _ => None, + } + } + /// Return `true` if `other` is present in this class's MRO. pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { // `is_subclass_of` is checking the subtype relation, in which gradual types do not // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. - self.iter_mro(db).contains(&ClassBase::Class(other)) + if self.iter_mro(db).contains(&ClassBase::Class(other)) { + return true; + } + + // `self` is a subclass of `other` if they are both generic aliases of the same generic + // class, and their specializations are compatible, taking into account the variance of the + // class's typevars. + if let Some((self_specialization, other_specialization)) = + self.compatible_specializations(db, other) + { + if self_specialization.is_subtype_of(db, other_specialization) { + return true; + } + } + + false + } + + pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + if self == other { + return true; + } + + // `self` is equivalent to `other` if they are both generic aliases of the same generic + // class, and their specializations are compatible, taking into account the variance of the + // class's typevars. + if let Some((self_specialization, other_specialization)) = + self.compatible_specializations(db, other) + { + if self_specialization.is_equivalent_to(db, other_specialization) { + return true; + } + } + + false + } + + pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + // `is_subclass_of` is checking the subtype relation, in which gradual types do not + // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. + if self.is_subclass_of(db, other) { + return true; + } + + // `self` is assignable to `other` if they are both generic aliases of the same generic + // class, and their specializations are compatible, taking into account the variance of the + // class's typevars. + if let Some((self_specialization, other_specialization)) = + self.compatible_specializations(db, other) + { + if self_specialization.is_assignable_to(db, other_specialization) { + return true; + } + } + + false + } + + pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { + if self == other { + return true; + } + + // `self` is equivalent to `other` if they are both generic aliases of the same generic + // class, and their specializations are compatible, taking into account the variance of the + // class's typevars. + if let Some((self_specialization, other_specialization)) = + self.compatible_specializations(db, other) + { + if self_specialization.is_gradual_equivalent_to(db, other_specialization) { + return true; + } + } + + false } /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. @@ -1516,6 +1612,22 @@ impl<'db> InstanceType<'db> { // N.B. The subclass relation is fully static self.class.is_subclass_of(db, other.class) } + + pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { + self.class.is_equivalent_to(db, other.class) + } + + pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { + self.class.is_assignable_to(db, other.class) + } + + pub(super) fn is_gradual_equivalent_to( + self, + db: &'db dyn Db, + other: InstanceType<'db>, + ) -> bool { + self.class.is_gradual_equivalent_to(db, other.class) + } } impl<'db> From> for Type<'db> { diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 3e350112f9abfa..ef5d66a772eed5 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -181,6 +181,118 @@ impl<'db> Specialization<'db> { .find(|(var, _)| **var == typevar) .map(|(_, ty)| *ty) } + + pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Specialization<'db>) -> bool { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return false; + } + + for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) + .zip(self.types(db)) + .zip(other.types(db)) + { + if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) { + return false; + } + + // TODO: We currently treat all typevars as invariant. Once we track the actual + // variance of each typevar, these checks should change: + // - covariant: verify that self_type <: other_type + // - contravariant: verify that other_type <: self_type + // - invariant: verify that self_type == other_type + // - bivariant: skip, can't make subtyping false + if !self_type.is_equivalent_to(db, *other_type) { + return false; + } + } + + true + } + + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return false; + } + + for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) + .zip(self.types(db)) + .zip(other.types(db)) + { + if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) { + return false; + } + + // TODO: We currently treat all typevars as invariant. Once we track the actual + // variance of each typevar, these checks should change: + // - covariant: verify that self_type == other_type + // - contravariant: verify that other_type == self_type + // - invariant: verify that self_type == other_type + // - bivariant: skip, can't make equivalence false + if !self_type.is_equivalent_to(db, *other_type) { + return false; + } + } + + true + } + + pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Specialization<'db>) -> bool { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return false; + } + + for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) + .zip(self.types(db)) + .zip(other.types(db)) + { + if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) { + continue; + } + + // TODO: We currently treat all typevars as invariant. Once we track the actual + // variance of each typevar, these checks should change: + // - covariant: verify that self_type <: other_type + // - contravariant: verify that other_type <: self_type + // - invariant: verify that self_type == other_type + // - bivariant: skip, can't make assignability false + if !self_type.is_gradual_equivalent_to(db, *other_type) { + return false; + } + } + + true + } + + pub(crate) fn is_gradual_equivalent_to( + self, + db: &'db dyn Db, + other: Specialization<'db>, + ) -> bool { + let generic_context = self.generic_context(db); + if generic_context != other.generic_context(db) { + return false; + } + + for ((_typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) + .zip(self.types(db)) + .zip(other.types(db)) + { + // TODO: We currently treat all typevars as invariant. Once we track the actual + // variance of each typevar, these checks should change: + // - covariant: verify that self_type == other_type + // - contravariant: verify that other_type == self_type + // - invariant: verify that self_type == other_type + // - bivariant: skip, can't make equivalence false + if !self_type.is_gradual_equivalent_to(db, *other_type) { + return false; + } + } + + true + } } /// Performs type inference between parameter annotations and argument types, producing a diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index a81c97c9ce2f8f..4107b397753242 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -6044,12 +6044,7 @@ impl<'db> TypeInferenceBuilder<'db> { // special cases, too. let value_ty = self.infer_expression(value); if let Type::ClassLiteral(ClassLiteralType::Generic(generic_class)) = value_ty { - return self.infer_explicit_class_specialization( - subscript, - value_ty, - generic_class, - slice, - ); + return self.infer_explicit_class_specialization(subscript, value_ty, generic_class); } let slice_ty = self.infer_expression(slice); @@ -6061,8 +6056,8 @@ impl<'db> TypeInferenceBuilder<'db> { subscript: &ast::ExprSubscript, value_ty: Type<'db>, generic_class: GenericClass<'db>, - slice_node: &ast::Expr, ) -> Type<'db> { + let slice_node = subscript.slice.as_ref(); let mut call_argument_types = match slice_node { ast::Expr::Tuple(tuple) => CallArgumentTypes::positional( tuple.elts.iter().map(|elt| self.infer_type_expression(elt)), @@ -7183,9 +7178,24 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_type_expression(slice); value_ty } + Type::ClassLiteral(ClassLiteralType::Generic(generic_class)) => { + let specialized_class = + self.infer_explicit_class_specialization(subscript, value_ty, generic_class); + specialized_class + .in_type_expression(self.db()) + .unwrap_or(Type::unknown()) + } + Type::ClassLiteral(ClassLiteralType::NonGeneric(_)) => { + // TODO: Once we know that e.g. `list` is generic, emit a diagnostic if you try to + // specialize a non-generic class. + self.infer_type_expression(slice); + todo_type!("specialized non-generic class") + } _ => { + // TODO: Emit a diagnostic once we've implemented all valid subscript type + // expressions. self.infer_type_expression(slice); - todo_type!("generics") + todo_type!("unknown type subscript") } } } From 5fec1039ed78ceeb62f4e142046c5233727f43c0 Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Fri, 18 Apr 2025 18:49:01 +0200 Subject: [PATCH 0015/1161] [`pylint`] Make fix unsafe if it deletes comments (`PLR1730`) (#17459) The PR addresses issue #17311 --------- Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../test/fixtures/pylint/if_stmt_min_max.py | 12 ++++ .../src/rules/pylint/rules/if_stmt_min_max.rs | 30 ++++++++-- ...nt__tests__PLR1730_if_stmt_min_max.py.snap | 55 +++++++++++++++++-- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/if_stmt_min_max.py b/crates/ruff_linter/resources/test/fixtures/pylint/if_stmt_min_max.py index e316c3383ca9f1..d5fa9d34c3e9f0 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/if_stmt_min_max.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/if_stmt_min_max.py @@ -237,3 +237,15 @@ def foo(self, value) -> None: # case 8: counter["a"] = max(counter["b"], counter["a"]) if counter["a"] > counter["b"]: counter["b"] = counter["a"] + +# https://github.com/astral-sh/ruff/issues/17311 + +# fix marked unsafe as delete comments +a, b = [], [] +if a >= b: + # very important comment + a = b + +# fix marked safe as preserve comments +if a >= b: + a = b # very important comment diff --git a/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs b/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs index 4e64179d1e373c..4e3e923b62bccf 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/if_stmt_min_max.rs @@ -1,4 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::parenthesize::parenthesized_range; @@ -29,6 +29,19 @@ use crate::fix::snippet::SourceCodeSnippet; /// highest_score = max(highest_score, score) /// ``` /// +/// ## Fix safety +/// This fix is marked unsafe if it would delete any comments within the replacement range. +/// +/// An example to illustrate where comments are preserved and where they are not: +/// +/// ```py +/// a, b = 0, 10 +/// +/// if a >= b: # deleted comment +/// # deleted comment +/// a = b # preserved comment +/// ``` +/// /// ## References /// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max) /// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min) @@ -169,11 +182,18 @@ pub(crate) fn if_stmt_min_max(checker: &Checker, stmt_if: &ast::StmtIf) { stmt_if.range(), ); + let range_replacement = stmt_if.range(); + let applicability = if checker.comment_ranges().intersects(range_replacement) { + Applicability::Unsafe + } else { + Applicability::Safe + }; + if checker.semantic().has_builtin_binding(min_max.as_str()) { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - replacement, - stmt_if.range(), - ))); + diagnostic.set_fix(Fix::applicable_edit( + Edit::range_replacement(replacement, range_replacement), + applicability, + )); } checker.report_diagnostic(diagnostic); diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1730_if_stmt_min_max.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1730_if_stmt_min_max.py.snap index 94b7385d0a67e4..89aec8775ee6c0 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1730_if_stmt_min_max.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1730_if_stmt_min_max.py.snap @@ -359,7 +359,7 @@ if_stmt_min_max.py:119:1: PLR1730 [*] Replace `if` statement with `A2 = max(A2, | = help: Replace with `A2 = max(A2, A1)` -ℹ Safe fix +ℹ Unsafe fix 116 116 | A1 = AA(0) 117 117 | A2 = AA(3) 118 118 | @@ -382,7 +382,7 @@ if_stmt_min_max.py:122:1: PLR1730 [*] Replace `if` statement with `A2 = max(A1, | = help: Replace with `A2 = max(A1, A2)` -ℹ Safe fix +ℹ Unsafe fix 119 119 | if A2 < A1: # [max-instead-of-if] 120 120 | A2 = A1 121 121 | @@ -405,7 +405,7 @@ if_stmt_min_max.py:125:1: PLR1730 [*] Replace `if` statement with `A2 = min(A2, | = help: Replace with `A2 = min(A2, A1)` -ℹ Safe fix +ℹ Unsafe fix 122 122 | if A2 <= A1: # [max-instead-of-if] 123 123 | A2 = A1 124 124 | @@ -428,7 +428,7 @@ if_stmt_min_max.py:128:1: PLR1730 [*] Replace `if` statement with `A2 = min(A1, | = help: Replace with `A2 = min(A1, A2)` -ℹ Safe fix +ℹ Unsafe fix 125 125 | if A2 > A1: # [min-instead-of-if] 126 126 | A2 = A1 127 127 | @@ -721,6 +721,8 @@ if_stmt_min_max.py:238:1: PLR1730 [*] Replace `if` statement with `counter["b"] 238 | / if counter["a"] > counter["b"]: 239 | | counter["b"] = counter["a"] | |_______________________________^ PLR1730 +240 | +241 | # https://github.com/astral-sh/ruff/issues/17311 | = help: Replace with `counter["b"] = max(counter["b"], counter["a"])` @@ -731,3 +733,48 @@ if_stmt_min_max.py:238:1: PLR1730 [*] Replace `if` statement with `counter["b"] 238 |-if counter["a"] > counter["b"]: 239 |- counter["b"] = counter["a"] 238 |+counter["b"] = max(counter["b"], counter["a"]) +240 239 | +241 240 | # https://github.com/astral-sh/ruff/issues/17311 +242 241 | + +if_stmt_min_max.py:245:1: PLR1730 [*] Replace `if` statement with `a = min(b, a)` + | +243 | # fix marked unsafe as delete comments +244 | a, b = [], [] +245 | / if a >= b: +246 | | # very important comment +247 | | a = b + | |_________^ PLR1730 +248 | +249 | # fix marked safe as preserve comments + | + = help: Replace with `a = min(b, a)` + +ℹ Unsafe fix +242 242 | +243 243 | # fix marked unsafe as delete comments +244 244 | a, b = [], [] +245 |-if a >= b: +246 |- # very important comment +247 |- a = b + 245 |+a = min(b, a) +248 246 | +249 247 | # fix marked safe as preserve comments +250 248 | if a >= b: + +if_stmt_min_max.py:250:1: PLR1730 [*] Replace `if` statement with `a = min(b, a)` + | +249 | # fix marked safe as preserve comments +250 | / if a >= b: +251 | | a = b # very important comment + | |_________^ PLR1730 + | + = help: Replace with `a = min(b, a)` + +ℹ Safe fix +247 247 | a = b +248 248 | +249 249 | # fix marked safe as preserve comments +250 |-if a >= b: +251 |- a = b # very important comment + 250 |+a = min(b, a) # very important comment From 08221454f628b1b11150b1d4f0638f2d1601350c Mon Sep 17 00:00:00 2001 From: w0nder1ng <94079074+w0nder1ng@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:10:40 -0400 Subject: [PATCH 0016/1161] [`perflint`] Implement fix for `manual-dict-comprehension` (`PERF403`) (#16719) ## Summary This change adds an auto-fix for manual dict comprehensions. It also copies many of the improvements from #13919 (and associated PRs fixing issues with it), and moves some of the utility functions from `manual_list_comprehension.rs` into a separate `helpers.rs` to be used in both. ## Test Plan I added a preview test case to showcase the new fix and added a test case in `PERF403.py` to make sure lines with semicolons function. I didn't yet make similar tests to the ones I added earlier to `PERF401.py`, but the logic is the same, so it might be good to add those to make sure they work. --- .../test/fixtures/perflint/PERF403.py | 64 +++- .../ast/analyze/deferred_for_loops.rs | 3 + .../src/checkers/ast/analyze/statement.rs | 5 +- .../ruff_linter/src/rules/perflint/helpers.rs | 98 ++++++ crates/ruff_linter/src/rules/perflint/mod.rs | 5 +- .../rules/manual_dict_comprehension.rs | 326 +++++++++++++++++- .../rules/manual_list_comprehension.rs | 104 ++---- ...__perflint__tests__PERF403_PERF403.py.snap | 113 ++++-- ...t__tests__preview__PERF401_PERF401.py.snap | 8 +- ...t__tests__preview__PERF403_PERF403.py.snap | 307 +++++++++++++++++ 10 files changed, 909 insertions(+), 124 deletions(-) create mode 100644 crates/ruff_linter/src/rules/perflint/helpers.rs create mode 100644 crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py b/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py index 8ef430c8fc9f33..89afca39ffd7e6 100644 --- a/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py +++ b/crates/ruff_linter/resources/test/fixtures/perflint/PERF403.py @@ -18,7 +18,9 @@ def foo(): result = {} for idx, name in enumerate(fruit): if idx % 2: - result[idx] = name # Ok (false negative: edge case where `else` is same as `if`) + result[idx] = ( + name # Ok (false negative: edge case where `else` is same as `if`) + ) else: result[idx] = name @@ -85,7 +87,67 @@ def foo(): def foo(): from builtins import dict as SneakyDict + fruit = ["apple", "pear", "orange"] result = SneakyDict() for idx, name in enumerate(fruit): result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + result: dict[str, int] = { + # comment 1 + } + for idx, name in enumerate( + fruit # comment 2 + ): + # comment 3 + result[ + name # comment 4 + ] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + a = 1; result = {}; b = 2 + for idx, name in enumerate(fruit): + result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + result = {"kiwi": 3} + for idx, name in enumerate(fruit): + result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + (_, result) = (None, {"kiwi": 3}) + for idx, name in enumerate(fruit): + result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + result = {} + print(len(result)) + for idx, name in enumerate(fruit): + result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + result = {} + for idx, name in enumerate(fruit): + if last_idx := idx % 3: + result[name] = idx # PERF403 + + +def foo(): + fruit = ["apple", "pear", "orange"] + indices = [0, 1, 2] + result = {} + for idx, name in indices, fruit: + result[name] = idx # PERF403 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs index 7bc5cf097f9e37..abb9110b6ea8b3 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs @@ -35,6 +35,9 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) { if checker.enabled(Rule::DictIndexMissingItems) { pylint::rules::dict_index_missing_items(checker, stmt_for); } + if checker.enabled(Rule::ManualDictComprehension) { + perflint::rules::manual_dict_comprehension(checker, stmt_for); + } if checker.enabled(Rule::ManualListComprehension) { perflint::rules::manual_list_comprehension(checker, stmt_for); } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index e503e7df1abf9f..84563726a21ee0 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1319,6 +1319,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { Rule::UnnecessaryEnumerate, Rule::UnusedLoopControlVariable, Rule::YieldInForLoop, + Rule::ManualDictComprehension, Rule::ManualListComprehension, ]) { checker.analyze.for_loops.push(checker.semantic.snapshot()); @@ -1347,9 +1348,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::ManualListCopy) { perflint::rules::manual_list_copy(checker, for_stmt); } - if checker.enabled(Rule::ManualDictComprehension) { - perflint::rules::manual_dict_comprehension(checker, target, body); - } + if checker.enabled(Rule::ModifiedIteratingSet) { pylint::rules::modified_iterating_set(checker, for_stmt); } diff --git a/crates/ruff_linter/src/rules/perflint/helpers.rs b/crates/ruff_linter/src/rules/perflint/helpers.rs new file mode 100644 index 00000000000000..271975246c14e6 --- /dev/null +++ b/crates/ruff_linter/src/rules/perflint/helpers.rs @@ -0,0 +1,98 @@ +use ruff_python_trivia::{ + BackwardsTokenizer, PythonWhitespace, SimpleToken, SimpleTokenKind, SimpleTokenizer, +}; +use ruff_source_file::LineRanges; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::Checker; + +pub(super) fn comment_strings_in_range<'a>( + checker: &'a Checker, + range: TextRange, + ranges_to_ignore: &[TextRange], +) -> Vec<&'a str> { + checker + .comment_ranges() + .comments_in_range(range) + .iter() + // Ignore comments inside of the append or iterator, since these are preserved + .filter(|comment| { + !ranges_to_ignore + .iter() + .any(|to_ignore| to_ignore.contains_range(**comment)) + }) + .map(|range| checker.locator().slice(range).trim_whitespace_start()) + .collect() +} + +fn semicolon_before_and_after( + checker: &Checker, + statement: TextRange, +) -> (Option, Option) { + // determine whether there's a semicolon either before or after the binding statement. + // Since it's a binding statement, we can just check whether there's a semicolon immediately + // after the whitespace in front of or behind it + let mut after_tokenizer = + SimpleTokenizer::starts_at(statement.end(), checker.locator().contents()).skip_trivia(); + + let after_semicolon = if after_tokenizer + .next() + .is_some_and(|token| token.kind() == SimpleTokenKind::Semi) + { + after_tokenizer.next() + } else { + None + }; + + let semicolon_before = BackwardsTokenizer::up_to( + statement.start(), + checker.locator().contents(), + checker.comment_ranges(), + ) + .skip_trivia() + .next() + .filter(|token| token.kind() == SimpleTokenKind::Semi); + + (semicolon_before, after_semicolon) +} + +/// Finds the range necessary to delete a statement (including any semicolons around it). +/// Returns the range and whether there were multiple statements on the line +pub(super) fn statement_deletion_range( + checker: &Checker, + statement_range: TextRange, +) -> (TextRange, bool) { + let locator = checker.locator(); + // If the binding has multiple statements on its line, the fix would be substantially more complicated + let (semicolon_before, after_semicolon) = semicolon_before_and_after(checker, statement_range); + + // If there are multiple binding statements in one line, we don't want to accidentally delete them + // Instead, we just delete the binding statement and leave any comments where they are + + match (semicolon_before, after_semicolon) { + // ```python + // a = [] + // ``` + (None, None) => (locator.full_lines_range(statement_range), false), + + // ```python + // a = 1; b = [] + // ^^^^^^^^ + // a = 1; b = []; c = 3 + // ^^^^^^^^ + // ``` + (Some(semicolon_before), Some(_) | None) => ( + TextRange::new(semicolon_before.start(), statement_range.end()), + true, + ), + + // ```python + // a = []; b = 3 + // ^^^^^^^ + // ``` + (None, Some(after_semicolon)) => ( + TextRange::new(statement_range.start(), after_semicolon.start()), + true, + ), + } +} diff --git a/crates/ruff_linter/src/rules/perflint/mod.rs b/crates/ruff_linter/src/rules/perflint/mod.rs index 07d67d8363bfd6..74baf85e652b0e 100644 --- a/crates/ruff_linter/src/rules/perflint/mod.rs +++ b/crates/ruff_linter/src/rules/perflint/mod.rs @@ -1,6 +1,6 @@ //! Rules from [perflint](https://pypi.org/project/perflint/). +mod helpers; pub(crate) mod rules; - #[cfg(test)] mod tests { use std::path::Path; @@ -31,7 +31,8 @@ mod tests { Ok(()) } - // TODO: remove this test case when the fix for `perf401` is stabilized + // TODO: remove this test case when the fixes for `perf401` and `perf403` are stabilized + #[test_case(Rule::ManualDictComprehension, Path::new("PERF403.py"))] #[test_case(Rule::ManualListComprehension, Path::new("PERF401.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs index 97f53cb55ce7a8..c2b87b6efe2a8f 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs @@ -1,11 +1,14 @@ -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::helpers::any_over_expr; -use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_python_semantic::analyze::typing::is_dict; +use ruff_python_ast::{ + self as ast, comparable::ComparableExpr, helpers::any_over_expr, Expr, Stmt, +}; +use ruff_python_semantic::{analyze::typing::is_dict, Binding}; +use ruff_source_file::LineRanges; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::rules::perflint::helpers::{comment_strings_in_range, statement_deletion_range}; /// ## What it does /// Checks for `for` loops that can be replaced by a dictionary comprehension. @@ -42,17 +45,45 @@ use crate::checkers::ast::Checker; /// result.update({x: y for x, y in pairs if y % 2}) /// ``` #[derive(ViolationMetadata)] -pub(crate) struct ManualDictComprehension; +pub(crate) struct ManualDictComprehension { + fix_type: DictComprehensionType, + is_async: bool, +} impl Violation for ManualDictComprehension { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { - "Use a dictionary comprehension instead of a for-loop".to_string() + let modifier = if self.is_async { "an async" } else { "a" }; + + match self.fix_type { + DictComprehensionType::Comprehension => { + format!("Use a dictionary comprehension instead of {modifier} for-loop") + } + DictComprehensionType::Update => { + format!("Use `dict.update` instead of {modifier} for-loop") + } + } + } + fn fix_title(&self) -> Option { + let modifier = if self.is_async { "async " } else { "" }; + match self.fix_type { + DictComprehensionType::Comprehension => Some(format!( + "Replace {modifier}for loop with dict comprehension" + )), + DictComprehensionType::Update => { + Some(format!("Replace {modifier}for loop with `dict.update`")) + } + } } } /// PERF403 -pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body: &[Stmt]) { +pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtFor) { + let ast::StmtFor { body, target, .. } = for_stmt; + let body = body.as_slice(); + let target = target.as_ref(); let (stmt, if_test) = match body { // ```python // for idx, name in enumerate(names): @@ -94,18 +125,42 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body: let [Expr::Subscript(ast::ExprSubscript { value: subscript_value, - slice, + slice: key, .. })] = targets.as_slice() else { return; }; + // If any references to a target variable are after the loop, + // then removing the loop would cause a NameError + let any_references_after_for_loop = |target: &Expr| { + let target_binding = checker + .semantic() + .bindings + .iter() + .find(|binding| target.range() == binding.range); + debug_assert!( + target_binding.is_some(), + "for-loop target binding must exist" + ); + + let Some(target_binding) = target_binding else { + // All uses of this function will early-return if this returns true, so this must early-return the rule + return true; + }; + + target_binding + .references() + .map(|reference| checker.semantic().reference(reference)) + .any(|other_reference| other_reference.start() > for_stmt.end()) + }; + match target { Expr::Tuple(tuple) => { if !tuple .iter() - .any(|element| ComparableExpr::from(slice) == ComparableExpr::from(element)) + .any(|element| ComparableExpr::from(key) == ComparableExpr::from(element)) { return; } @@ -115,14 +170,24 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body: { return; } + // Make sure none of the variables are used outside the for loop + if tuple.iter().any(any_references_after_for_loop) { + return; + } } Expr::Name(_) => { - if ComparableExpr::from(slice) != ComparableExpr::from(target) { + if ComparableExpr::from(key) != ComparableExpr::from(target) { return; } if ComparableExpr::from(value) != ComparableExpr::from(target) { return; } + + // We know that `target` contains an ExprName, but closures can't take `&impl Ranged`, + // so we pass `target` itself instead of the inner ExprName + if any_references_after_for_loop(target) { + return; + } } _ => return, } @@ -164,5 +229,242 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body: return; } - checker.report_diagnostic(Diagnostic::new(ManualDictComprehension, *range)); + if checker.settings.preview.is_enabled() { + let binding_stmt = binding.statement(checker.semantic()); + let binding_value = binding_stmt.and_then(|binding_stmt| match binding_stmt { + ast::Stmt::AnnAssign(assign) => assign.value.as_deref(), + ast::Stmt::Assign(assign) => Some(&assign.value), + _ => None, + }); + + // If the variable is an empty dict literal, then we might be able to replace it with a full dict comprehension. + // otherwise, it has to be replaced with a `dict.update` + let binding_is_empty_dict = + binding_value.is_some_and(|binding_value| match binding_value { + // value = {} + Expr::Dict(dict_expr) => dict_expr.is_empty(), + // value = dict() + Expr::Call(call) => { + checker + .semantic() + .resolve_builtin_symbol(&call.func) + .is_some_and(|name| name == "dict") + && call.arguments.is_empty() + } + _ => false, + }); + + let assignment_in_same_statement = binding.source.is_some_and(|binding_source| { + let for_loop_parent = checker.semantic().current_statement_parent_id(); + let binding_parent = checker.semantic().parent_statement_id(binding_source); + for_loop_parent == binding_parent + }); + // If the binding is not a single name expression, it could be replaced with a dict comprehension, + // but not necessarily, so this needs to be manually fixed. This does not apply when using an update. + let binding_has_one_target = binding_stmt.is_some_and(|binding_stmt| match binding_stmt { + ast::Stmt::AnnAssign(_) => true, + ast::Stmt::Assign(assign) => assign.targets.len() == 1, + _ => false, + }); + // If the binding gets used in between the assignment and the for loop, a comprehension is no longer safe + + // If the binding is after the for loop, then it can't be fixed, and this check would panic, + // so we check that they are in the same statement first + let binding_unused_between = assignment_in_same_statement + && binding_stmt.is_some_and(|binding_stmt| { + let from_assign_to_loop = TextRange::new(binding_stmt.end(), for_stmt.start()); + // Test if there's any reference to the result dictionary between its definition and the for loop. + // If there's at least one, then it's been accessed in the middle somewhere, so it's not safe to change into a comprehension + !binding + .references() + .map(|ref_id| checker.semantic().reference(ref_id).range()) + .any(|text_range| from_assign_to_loop.contains_range(text_range)) + }); + // A dict update works in every context, while a dict comprehension only works when all the criteria are true + let fix_type = if binding_is_empty_dict + && assignment_in_same_statement + && binding_has_one_target + && binding_unused_between + { + DictComprehensionType::Comprehension + } else { + DictComprehensionType::Update + }; + + let mut diagnostic = Diagnostic::new( + ManualDictComprehension { + fix_type, + is_async: for_stmt.is_async, + }, + *range, + ); + diagnostic.try_set_optional_fix(|| { + Ok(convert_to_dict_comprehension( + fix_type, + binding, + for_stmt, + if_test.map(std::convert::AsRef::as_ref), + key.as_ref(), + value.as_ref(), + checker, + )) + }); + + checker.report_diagnostic(diagnostic); + } else { + checker.report_diagnostic(Diagnostic::new( + ManualDictComprehension { + fix_type: DictComprehensionType::Comprehension, + is_async: for_stmt.is_async, + }, + *range, + )); + } +} + +fn convert_to_dict_comprehension( + fix_type: DictComprehensionType, + binding: &Binding, + for_stmt: &ast::StmtFor, + if_test: Option<&ast::Expr>, + key: &Expr, + value: &Expr, + checker: &Checker, +) -> Option { + let locator = checker.locator(); + + let if_str = match if_test { + Some(test) => { + // If the test is an assignment expression, + // we must parenthesize it when it appears + // inside the comprehension to avoid a syntax error. + // + // Notice that we do not need `any_over_expr` here, + // since if the assignment expression appears + // internally (e.g. as an operand in a boolean + // operation) then it will already be parenthesized. + if test.is_named_expr() { + format!(" if ({})", locator.slice(test.range())) + } else { + format!(" if {}", locator.slice(test.range())) + } + } + None => String::new(), + }; + + // if the loop target was an implicit tuple, add parentheses around it + // ```python + // for i in a, b: + // ... + // ``` + // becomes + // {... for i in (a, b)} + let iter_str = if let Expr::Tuple(ast::ExprTuple { + parenthesized: false, + .. + }) = &*for_stmt.iter + { + format!("({})", locator.slice(for_stmt.iter.range())) + } else { + locator.slice(for_stmt.iter.range()).to_string() + }; + + let target_str = locator.slice(for_stmt.target.range()); + let for_type = if for_stmt.is_async { + "async for" + } else { + "for" + }; + let elt_str = format!( + "{}: {}", + locator.slice(key.range()), + locator.slice(value.range()) + ); + + let comprehension_str = format!("{{{elt_str} {for_type} {target_str} in {iter_str}{if_str}}}"); + + let for_loop_inline_comments = comment_strings_in_range( + checker, + for_stmt.range, + &[key.range(), value.range(), for_stmt.iter.range()], + ); + + let newline = checker.stylist().line_ending().as_str(); + + let indent = locator.slice(TextRange::new( + locator.line_start(for_stmt.range.start()), + for_stmt.range.start(), + )); + + let variable_name = locator.slice(binding); + match fix_type { + DictComprehensionType::Update => { + let indentation = if for_loop_inline_comments.is_empty() { + String::new() + } else { + format!("{newline}{indent}") + }; + + let comprehension_body = format!("{variable_name}.update({comprehension_str})"); + + let text_to_replace = format!( + "{}{indentation}{comprehension_body}", + for_loop_inline_comments.join(&indentation) + ); + + Some(Fix::unsafe_edit(Edit::range_replacement( + text_to_replace, + for_stmt.range, + ))) + } + DictComprehensionType::Comprehension => { + let binding_stmt = binding.statement(checker.semantic()); + debug_assert!( + binding_stmt.is_some(), + "must be passed a binding with a statement" + ); + let binding_stmt = binding_stmt?; + + let binding_stmt_range = binding_stmt.range(); + + let annotations = match binding_stmt.as_ann_assign_stmt() { + Some(assign) => format!(": {}", locator.slice(assign.annotation.range())), + None => String::new(), + }; + + // If there are multiple binding statements in one line, we don't want to accidentally delete them + // Instead, we just delete the binding statement and leave any comments where they are + let (binding_stmt_deletion_range, binding_is_multiple_stmts) = + statement_deletion_range(checker, binding_stmt_range); + + let comments_to_move = if binding_is_multiple_stmts { + for_loop_inline_comments + } else { + let mut new_comments = + comment_strings_in_range(checker, binding_stmt_deletion_range, &[]); + new_comments.extend(for_loop_inline_comments); + new_comments + }; + + let indentation = if comments_to_move.is_empty() { + String::new() + } else { + format!("{newline}{indent}") + }; + let leading_comments = format!("{}{indentation}", comments_to_move.join(&indentation)); + + let comprehension_body = + format!("{leading_comments}{variable_name}{annotations} = {comprehension_str}"); + Some(Fix::unsafe_edits( + Edit::range_deletion(binding_stmt_deletion_range), + [Edit::range_replacement(comprehension_body, for_stmt.range)], + )) + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DictComprehensionType { + Update, + Comprehension, } diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs index 752678546b4d69..35a0cd2d3e3394 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs @@ -1,16 +1,15 @@ use ruff_python_ast::{self as ast, Arguments, Expr}; -use crate::checkers::ast::Checker; +use crate::{checkers::ast::Checker, rules::perflint::helpers::statement_deletion_range}; use anyhow::{anyhow, Result}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use crate::rules::perflint::helpers::comment_strings_in_range; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::helpers::any_over_expr; use ruff_python_semantic::{analyze::typing::is_list, Binding}; -use ruff_python_trivia::{BackwardsTokenizer, PythonWhitespace, SimpleTokenKind, SimpleTokenizer}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; - /// ## What it does /// Checks for `for` loops that can be replaced by a list comprehension. /// @@ -264,12 +263,14 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF ast::Stmt::Assign(assign) => Some(&assign.value), _ => None, }); + // If the variable is an empty list literal, then we might be able to replace it with a full list comprehension - // otherwise, it has to be replaced with a `list.extend` + // otherwise, it has to be replaced with a `list.extend`. let binding_is_empty_list = - list_binding_value.is_some_and(|binding_value| match binding_value.as_list_expr() { - Some(list_expr) => list_expr.elts.is_empty(), - None => false, + list_binding_value.is_some_and(|binding_value| match binding_value { + // `value = []` + Expr::List(list_expr) => list_expr.is_empty(), + _ => false, }); // If the for loop does not have the same parent element as the binding, then it cannot always be @@ -397,22 +398,12 @@ fn convert_to_list_extend( let elt_str = locator.slice(to_append); let generator_str = format!("{elt_str} {for_type} {target_str} in {for_iter_str}{if_str}"); - let comment_strings_in_range = |range| { - checker - .comment_ranges() - .comments_in_range(range) - .iter() - // Ignore comments inside of the append or iterator, since these are preserved - .filter(|comment| { - !to_append.range().contains_range(**comment) - && !for_stmt.iter.range().contains_range(**comment) - }) - .map(|range| locator.slice(range).trim_whitespace_start()) - .collect() - }; - let variable_name = locator.slice(binding); - let for_loop_inline_comments: Vec<&str> = comment_strings_in_range(for_stmt.range); + let for_loop_inline_comments = comment_strings_in_range( + checker, + for_stmt.range, + &[to_append.range(), for_stmt.iter.range()], + ); let newline = checker.stylist().line_ending().as_str(); @@ -457,74 +448,25 @@ fn convert_to_list_extend( .ok_or(anyhow!( "Binding must have a statement to convert into a list comprehension" ))?; - // If the binding has multiple statements on its line, the fix would be substantially more complicated - let (semicolon_before, after_semicolon) = { - // determine whether there's a semicolon either before or after the binding statement. - // Since it's a binding statement, we can just check whether there's a semicolon immediately - // after the whitespace in front of or behind it - let mut after_tokenizer = - SimpleTokenizer::starts_at(binding_stmt_range.end(), locator.contents()) - .skip_trivia(); - - let after_semicolon = if after_tokenizer - .next() - .is_some_and(|token| token.kind() == SimpleTokenKind::Semi) - { - after_tokenizer.next() - } else { - None - }; - - let semicolon_before = BackwardsTokenizer::up_to( - binding_stmt_range.start(), - locator.contents(), - checker.comment_ranges(), - ) - .skip_trivia() - .next() - .filter(|token| token.kind() == SimpleTokenKind::Semi); - - (semicolon_before, after_semicolon) - }; + // If there are multiple binding statements in one line, we don't want to accidentally delete them // Instead, we just delete the binding statement and leave any comments where they are let (binding_stmt_deletion_range, binding_is_multiple_stmts) = - match (semicolon_before, after_semicolon) { - // ```python - // a = [] - // ``` - (None, None) => (locator.full_lines_range(binding_stmt_range), false), - - // ```python - // a = 1; b = [] - // ^^^^^^^^ - // a = 1; b = []; c = 3 - // ^^^^^^^^ - // ``` - (Some(semicolon_before), Some(_) | None) => ( - TextRange::new(semicolon_before.start(), binding_stmt_range.end()), - true, - ), - - // ```python - // a = []; b = 3 - // ^^^^^^^ - // ``` - (None, Some(after_semicolon)) => ( - TextRange::new(binding_stmt_range.start(), after_semicolon.start()), - true, - ), - }; + statement_deletion_range(checker, binding_stmt_range); let annotations = match binding_stmt.and_then(|stmt| stmt.as_ann_assign_stmt()) { Some(assign) => format!(": {}", locator.slice(assign.annotation.range())), None => String::new(), }; - let mut comments_to_move = for_loop_inline_comments; - if !binding_is_multiple_stmts { - comments_to_move.extend(comment_strings_in_range(binding_stmt_deletion_range)); - } + let comments_to_move = if binding_is_multiple_stmts { + for_loop_inline_comments + } else { + let mut new_comments = + comment_strings_in_range(checker, binding_stmt_deletion_range, &[]); + new_comments.extend(for_loop_inline_comments); + new_comments + }; let indentation = if comments_to_move.is_empty() { String::new() diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap index 6f26d4eeeed099..2615a1772d1b0b 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF403_PERF403.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/perflint/mod.rs -snapshot_kind: text --- PERF403.py:5:9: PERF403 Use a dictionary comprehension instead of a for-loop | @@ -9,6 +8,7 @@ PERF403.py:5:9: PERF403 Use a dictionary comprehension instead of a for-loop 5 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension PERF403.py:13:13: PERF403 Use a dictionary comprehension instead of a for-loop | @@ -17,43 +17,114 @@ PERF403.py:13:13: PERF403 Use a dictionary comprehension instead of a for-loop 13 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension -PERF403.py:31:13: PERF403 Use a dictionary comprehension instead of a for-loop +PERF403.py:33:13: PERF403 Use a dictionary comprehension instead of a for-loop | -29 | for idx, name in enumerate(fruit): -30 | if idx % 2: -31 | result[idx] = name # PERF403 +31 | for idx, name in enumerate(fruit): +32 | if idx % 2: +33 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension -PERF403.py:61:13: PERF403 Use a dictionary comprehension instead of a for-loop +PERF403.py:63:13: PERF403 Use a dictionary comprehension instead of a for-loop | -59 | for idx, name in enumerate(fruit): -60 | if idx % 2: -61 | result[idx] = name # PERF403 +61 | for idx, name in enumerate(fruit): +62 | if idx % 2: +63 | result[idx] = name # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension -PERF403.py:76:9: PERF403 Use a dictionary comprehension instead of a for-loop +PERF403.py:78:9: PERF403 Use a dictionary comprehension instead of a for-loop | -74 | result = {} -75 | for name in fruit: -76 | result[name] = name # PERF403 +76 | result = {} +77 | for name in fruit: +78 | result[name] = name # PERF403 | ^^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension -PERF403.py:83:9: PERF403 Use a dictionary comprehension instead of a for-loop +PERF403.py:85:9: PERF403 Use a dictionary comprehension instead of a for-loop | -81 | result = {} -82 | for idx, name in enumerate(fruit): -83 | result[name] = idx # PERF403 +83 | result = {} +84 | for idx, name in enumerate(fruit): +85 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension -PERF403.py:91:9: PERF403 Use a dictionary comprehension instead of a for-loop +PERF403.py:94:9: PERF403 Use a dictionary comprehension instead of a for-loop | -89 | result = SneakyDict() -90 | for idx, name in enumerate(fruit): -91 | result[name] = idx # PERF403 +92 | result = SneakyDict() +93 | for idx, name in enumerate(fruit): +94 | result[name] = idx # PERF403 | ^^^^^^^^^^^^^^^^^^ PERF403 | + = help: Replace for loop with dict comprehension + +PERF403.py:106:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +104 | ): +105 | # comment 3 +106 | / result[ +107 | | name # comment 4 +108 | | ] = idx # PERF403 + | |_______________^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:115:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +113 | a = 1; result = {}; b = 2 +114 | for idx, name in enumerate(fruit): +115 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:122:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +120 | result = {"kiwi": 3} +121 | for idx, name in enumerate(fruit): +122 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:129:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +127 | (_, result) = (None, {"kiwi": 3}) +128 | for idx, name in enumerate(fruit): +129 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:137:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +135 | print(len(result)) +136 | for idx, name in enumerate(fruit): +137 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:145:13: PERF403 Use a dictionary comprehension instead of a for-loop + | +143 | for idx, name in enumerate(fruit): +144 | if last_idx := idx % 3: +145 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +PERF403.py:153:9: PERF403 Use a dictionary comprehension instead of a for-loop + | +151 | result = {} +152 | for idx, name in indices, fruit: +153 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap index ff568d18219eb3..8bcbb47d03a138 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap @@ -169,10 +169,10 @@ PERF401.py:119:13: PERF401 [*] Use a list comprehension to create a transformed 117 |- # single-line comment 2 should be protected 118 |- if i % 2: # single-line comment 3 should be protected 119 |- result.append(i) # PERF401 - 115 |+ # single-line comment 1 should be protected - 116 |+ # single-line comment 2 should be protected - 117 |+ # single-line comment 3 should be protected - 118 |+ # comment after assignment should be protected + 115 |+ # comment after assignment should be protected + 116 |+ # single-line comment 1 should be protected + 117 |+ # single-line comment 2 should be protected + 118 |+ # single-line comment 3 should be protected 119 |+ result = [i for i in range(10) if i % 2] # PERF401 120 120 | 121 121 | diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap new file mode 100644 index 00000000000000..9e01e0db30b486 --- /dev/null +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF403_PERF403.py.snap @@ -0,0 +1,307 @@ +--- +source: crates/ruff_linter/src/rules/perflint/mod.rs +--- +PERF403.py:5:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +3 | result = {} +4 | for idx, name in enumerate(fruit): +5 | result[idx] = name # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +1 1 | def foo(): +2 2 | fruit = ["apple", "pear", "orange"] +3 |- result = {} +4 |- for idx, name in enumerate(fruit): +5 |- result[idx] = name # PERF403 + 3 |+ result = {idx: name for idx, name in enumerate(fruit)} # PERF403 +6 4 | +7 5 | +8 6 | def foo(): + +PERF403.py:13:13: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +11 | for idx, name in enumerate(fruit): +12 | if idx % 2: +13 | result[idx] = name # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +7 7 | +8 8 | def foo(): +9 9 | fruit = ["apple", "pear", "orange"] +10 |- result = {} +11 |- for idx, name in enumerate(fruit): +12 |- if idx % 2: +13 |- result[idx] = name # PERF403 + 10 |+ result = {idx: name for idx, name in enumerate(fruit) if idx % 2} # PERF403 +14 11 | +15 12 | +16 13 | def foo(): + +PERF403.py:33:13: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +31 | for idx, name in enumerate(fruit): +32 | if idx % 2: +33 | result[idx] = name # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +26 26 | +27 27 | +28 28 | def foo(): +29 |- result = {} +30 29 | fruit = ["apple", "pear", "orange"] +31 |- for idx, name in enumerate(fruit): +32 |- if idx % 2: +33 |- result[idx] = name # PERF403 + 30 |+ result = {idx: name for idx, name in enumerate(fruit) if idx % 2} # PERF403 +34 31 | +35 32 | +36 33 | def foo(): + +PERF403.py:63:13: PERF403 [*] Use `dict.update` instead of a for-loop + | +61 | for idx, name in enumerate(fruit): +62 | if idx % 2: +63 | result[idx] = name # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with `dict.update` + +ℹ Unsafe fix +58 58 | def foo(): +59 59 | result = {1: "banana"} +60 60 | fruit = ["apple", "pear", "orange"] +61 |- for idx, name in enumerate(fruit): +62 |- if idx % 2: +63 |- result[idx] = name # PERF403 + 61 |+ result.update({idx: name for idx, name in enumerate(fruit) if idx % 2}) # PERF403 +64 62 | +65 63 | +66 64 | def foo(): + +PERF403.py:78:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +76 | result = {} +77 | for name in fruit: +78 | result[name] = name # PERF403 + | ^^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +73 73 | +74 74 | def foo(): +75 75 | fruit = ["apple", "pear", "orange"] +76 |- result = {} +77 |- for name in fruit: +78 |- result[name] = name # PERF403 + 76 |+ result = {name: name for name in fruit} # PERF403 +79 77 | +80 78 | +81 79 | def foo(): + +PERF403.py:85:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +83 | result = {} +84 | for idx, name in enumerate(fruit): +85 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +80 80 | +81 81 | def foo(): +82 82 | fruit = ["apple", "pear", "orange"] +83 |- result = {} +84 |- for idx, name in enumerate(fruit): +85 |- result[name] = idx # PERF403 + 83 |+ result = {name: idx for idx, name in enumerate(fruit)} # PERF403 +86 84 | +87 85 | +88 86 | def foo(): + +PERF403.py:94:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +92 | result = SneakyDict() +93 | for idx, name in enumerate(fruit): +94 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +89 89 | from builtins import dict as SneakyDict +90 90 | +91 91 | fruit = ["apple", "pear", "orange"] +92 |- result = SneakyDict() +93 |- for idx, name in enumerate(fruit): +94 |- result[name] = idx # PERF403 + 92 |+ result = {name: idx for idx, name in enumerate(fruit)} # PERF403 +95 93 | +96 94 | +97 95 | def foo(): + +PERF403.py:106:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +104 | ): +105 | # comment 3 +106 | / result[ +107 | | name # comment 4 +108 | | ] = idx # PERF403 + | |_______________^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +96 96 | +97 97 | def foo(): +98 98 | fruit = ["apple", "pear", "orange"] +99 |- result: dict[str, int] = { +100 |- # comment 1 +101 |- } +102 |- for idx, name in enumerate( + 99 |+ # comment 1 + 100 |+ # comment 3 + 101 |+ # comment 4 + 102 |+ result: dict[str, int] = {name: idx for idx, name in enumerate( +103 103 | fruit # comment 2 +104 |- ): +105 |- # comment 3 +106 |- result[ +107 |- name # comment 4 +108 |- ] = idx # PERF403 + 104 |+ )} # PERF403 +109 105 | +110 106 | +111 107 | def foo(): + +PERF403.py:115:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +113 | a = 1; result = {}; b = 2 +114 | for idx, name in enumerate(fruit): +115 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +110 110 | +111 111 | def foo(): +112 112 | fruit = ["apple", "pear", "orange"] +113 |- a = 1; result = {}; b = 2 +114 |- for idx, name in enumerate(fruit): +115 |- result[name] = idx # PERF403 + 113 |+ a = 1; b = 2 + 114 |+ result = {name: idx for idx, name in enumerate(fruit)} # PERF403 +116 115 | +117 116 | +118 117 | def foo(): + +PERF403.py:122:9: PERF403 [*] Use `dict.update` instead of a for-loop + | +120 | result = {"kiwi": 3} +121 | for idx, name in enumerate(fruit): +122 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with `dict.update` + +ℹ Unsafe fix +118 118 | def foo(): +119 119 | fruit = ["apple", "pear", "orange"] +120 120 | result = {"kiwi": 3} +121 |- for idx, name in enumerate(fruit): +122 |- result[name] = idx # PERF403 + 121 |+ result.update({name: idx for idx, name in enumerate(fruit)}) # PERF403 +123 122 | +124 123 | +125 124 | def foo(): + +PERF403.py:129:9: PERF403 [*] Use `dict.update` instead of a for-loop + | +127 | (_, result) = (None, {"kiwi": 3}) +128 | for idx, name in enumerate(fruit): +129 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with `dict.update` + +ℹ Unsafe fix +125 125 | def foo(): +126 126 | fruit = ["apple", "pear", "orange"] +127 127 | (_, result) = (None, {"kiwi": 3}) +128 |- for idx, name in enumerate(fruit): +129 |- result[name] = idx # PERF403 + 128 |+ result.update({name: idx for idx, name in enumerate(fruit)}) # PERF403 +130 129 | +131 130 | +132 131 | def foo(): + +PERF403.py:137:9: PERF403 [*] Use `dict.update` instead of a for-loop + | +135 | print(len(result)) +136 | for idx, name in enumerate(fruit): +137 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with `dict.update` + +ℹ Unsafe fix +133 133 | fruit = ["apple", "pear", "orange"] +134 134 | result = {} +135 135 | print(len(result)) +136 |- for idx, name in enumerate(fruit): +137 |- result[name] = idx # PERF403 + 136 |+ result.update({name: idx for idx, name in enumerate(fruit)}) # PERF403 +138 137 | +139 138 | +140 139 | def foo(): + +PERF403.py:145:13: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +143 | for idx, name in enumerate(fruit): +144 | if last_idx := idx % 3: +145 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +139 139 | +140 140 | def foo(): +141 141 | fruit = ["apple", "pear", "orange"] +142 |- result = {} +143 |- for idx, name in enumerate(fruit): +144 |- if last_idx := idx % 3: +145 |- result[name] = idx # PERF403 + 142 |+ result = {name: idx for idx, name in enumerate(fruit) if (last_idx := idx % 3)} # PERF403 +146 143 | +147 144 | +148 145 | def foo(): + +PERF403.py:153:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop + | +151 | result = {} +152 | for idx, name in indices, fruit: +153 | result[name] = idx # PERF403 + | ^^^^^^^^^^^^^^^^^^ PERF403 + | + = help: Replace for loop with dict comprehension + +ℹ Unsafe fix +148 148 | def foo(): +149 149 | fruit = ["apple", "pear", "orange"] +150 150 | indices = [0, 1, 2] +151 |- result = {} +152 |- for idx, name in indices, fruit: +153 |- result[name] = idx # PERF403 + 151 |+ result = {name: idx for idx, name in (indices, fruit)} # PERF403 From 27a315b74095fa0d4dd83749b8a5ff567e06a38b Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Apr 2025 10:20:03 -0700 Subject: [PATCH 0017/1161] [red-knot] add fixpoint iteration for Type::member_lookup_with_policy (#17464) ## Summary Member lookup can be cyclic, with type inference of implicit members. A sample case is shown in the added mdtest. There's no clear way to handle such cases other than to fixpoint-iterate the cycle. Fixes #17457. ## Test Plan Added test. --- .../resources/mdtest/attributes.md | 72 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 30 ++++++-- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 57f385fc917ac7..ffec37148dcb63 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1820,6 +1820,78 @@ def f(never: Never): never.another_attribute = never ``` +### Cyclic implicit attributes + +Inferring types for undeclared implicit attributes can be cyclic: + +```py +class C: + def __init__(self): + self.x = 1 + + def copy(self, other: "C"): + self.x = other.x + +reveal_type(C().x) # revealed: Unknown | Literal[1] +``` + +If the only assignment to a name is cyclic, we just infer `Unknown` for that attribute: + +```py +class D: + def copy(self, other: "D"): + self.x = other.x + +reveal_type(D().x) # revealed: Unknown +``` + +If there is an annotation for a name, we don't try to infer any type from the RHS of assignments to +that name, so these cases don't trigger any cycle: + +```py +class E: + def __init__(self): + self.x: int = 1 + + def copy(self, other: "E"): + self.x = other.x + +reveal_type(E().x) # revealed: int + +class F: + def __init__(self): + self.x = 1 + + def copy(self, other: "F"): + self.x: int = other.x + +reveal_type(F().x) # revealed: int + +class G: + def copy(self, other: "G"): + self.x: int = other.x + +reveal_type(G().x) # revealed: int +``` + +We can even handle cycles involving multiple classes: + +```py +class A: + def __init__(self): + self.x = 1 + + def copy(self, other: "B"): + self.x = other.x + +class B: + def copy(self, other: "A"): + self.x = other.x + +reveal_type(B().x) # revealed: Unknown | Literal[1] +reveal_type(A().x) # revealed: Unknown | Literal[1] +``` + ### Builtin types attributes This test can probably be removed eventually, but we currently include it because we do not yet diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 68fbc1d2fd274f..52c9f934e81a0d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -147,6 +147,12 @@ enum AttributeKind { NormalOrNonDataDescriptor, } +impl AttributeKind { + const fn is_data(self) -> bool { + matches!(self, Self::DataDescriptor) + } +} + /// This enum is used to control the behavior of the descriptor protocol implementation. /// When invoked on a class object, the fallback type (a class attribute) can shadow a /// non-data descriptor of the meta-type (the class's metaclass). However, this is not @@ -217,10 +223,24 @@ impl Default for MemberLookupPolicy { } } -impl AttributeKind { - const fn is_data(self) -> bool { - matches!(self, Self::DataDescriptor) - } +fn member_lookup_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &SymbolAndQualifiers<'db>, + _count: u32, + _self: Type<'db>, + _name: Name, + _policy: MemberLookupPolicy, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn member_lookup_cycle_initial<'db>( + _db: &'db dyn Db, + _self: Type<'db>, + _name: Name, + _policy: MemberLookupPolicy, +) -> SymbolAndQualifiers<'db> { + Symbol::bound(Type::Never).into() } /// Meta data for `Type::Todo`, which represents a known limitation in red-knot. @@ -2631,7 +2651,7 @@ impl<'db> Type<'db> { /// Similar to [`Type::member`], but allows the caller to specify what policy should be used /// when looking up attributes. See [`MemberLookupPolicy`] for more information. - #[salsa::tracked] + #[salsa::tracked(cycle_fn=member_lookup_cycle_recover, cycle_initial=member_lookup_cycle_initial)] fn member_lookup_with_policy( self, db: &'db dyn Db, From f8061e8b99585fb9affe02ec23c193f920fc96ca Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Fri, 18 Apr 2025 19:46:01 +0200 Subject: [PATCH 0018/1161] [`refurb`] Mark the `FURB161` fix unsafe except for integers and booleans (#17240) The PR fixes #16457 . Specifically, `FURB161` is marked safe, but the rule generates safe fixes only in specific cases. Therefore, we attempt to mark the fix as unsafe when we are not in one of these cases. For instances, the fix is marked as aunsafe just in case of strings (as pointed out in the issue). Let me know if I should change something. --------- Co-authored-by: Brent Westbrook --- .../resources/test/fixtures/refurb/FURB161.py | 1 + .../src/rules/refurb/rules/bit_count.rs | 25 ++++++++--- ...es__refurb__tests__FURB161_FURB161.py.snap | 43 ++++++++++++++----- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB161.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB161.py index 0736b9dc3acaca..6dda8a11c9457d 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB161.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB161.py @@ -12,6 +12,7 @@ def ten() -> int: count = bin(ten()).count("1") # FURB161 count = bin((10)).count("1") # FURB161 count = bin("10" "15").count("1") # FURB161 +count = bin("123").count("1") # FURB161 count = x.bit_count() # OK count = (10).bit_count() # OK diff --git a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs index 863ea3cdd4c08a..4de3c44a96c4c3 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs @@ -1,7 +1,8 @@ -use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::PythonVersion; use ruff_python_ast::{self as ast, Expr, ExprAttribute, ExprCall}; +use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -27,6 +28,11 @@ use crate::fix::snippet::SourceCodeSnippet; /// y = 0b1111011.bit_count() /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe unless the argument to `bin` can be inferred as +/// an instance of a type that implements the `__index__` and `bit_count` methods because this can +/// change the exception raised at runtime for an invalid argument. +/// /// ## Options /// - `target-version` /// @@ -163,6 +169,14 @@ pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) { | Expr::Subscript(_) => false, }; + // check if the fix is safe or not + let applicability: Applicability = match ResolvedPythonType::from(arg) { + ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer | NumberLike::Bool)) => { + Applicability::Safe + } + _ => Applicability::Unsafe, + }; + let replacement = if parenthesize { format!("({literal_text}).bit_count()") } else { @@ -176,11 +190,10 @@ pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) { }, call.range(), ); - - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - replacement, - call.range(), - ))); + diagnostic.set_fix(Fix::applicable_edit( + Edit::range_replacement(replacement, call.range()), + applicability, + )); checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB161_FURB161.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB161_FURB161.py.snap index 7888f39f7dee03..2bff8222284779 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB161_FURB161.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB161_FURB161.py.snap @@ -12,7 +12,7 @@ FURB161.py:6:9: FURB161 [*] Use of `bin(x).count('1')` | = help: Replace with `(x).bit_count()` -ℹ Safe fix +ℹ Unsafe fix 3 3 | def ten() -> int: 4 4 | return 10 5 5 | @@ -137,7 +137,7 @@ FURB161.py:12:9: FURB161 [*] Use of `bin(ten()).count('1')` | = help: Replace with `ten().bit_count()` -ℹ Safe fix +ℹ Unsafe fix 9 9 | count = bin(0xA).count("1") # FURB161 10 10 | count = bin(0o12).count("1") # FURB161 11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161 @@ -145,7 +145,7 @@ FURB161.py:12:9: FURB161 [*] Use of `bin(ten()).count('1')` 12 |+count = ten().bit_count() # FURB161 13 13 | count = bin((10)).count("1") # FURB161 14 14 | count = bin("10" "15").count("1") # FURB161 -15 15 | +15 15 | count = bin("123").count("1") # FURB161 FURB161.py:13:9: FURB161 [*] Use of `bin(10).count('1')` | @@ -154,6 +154,7 @@ FURB161.py:13:9: FURB161 [*] Use of `bin(10).count('1')` 13 | count = bin((10)).count("1") # FURB161 | ^^^^^^^^^^^^^^^^^^^^ FURB161 14 | count = bin("10" "15").count("1") # FURB161 +15 | count = bin("123").count("1") # FURB161 | = help: Replace with `(10).bit_count()` @@ -164,8 +165,8 @@ FURB161.py:13:9: FURB161 [*] Use of `bin(10).count('1')` 13 |-count = bin((10)).count("1") # FURB161 13 |+count = (10).bit_count() # FURB161 14 14 | count = bin("10" "15").count("1") # FURB161 -15 15 | -16 16 | count = x.bit_count() # OK +15 15 | count = bin("123").count("1") # FURB161 +16 16 | FURB161.py:14:9: FURB161 [*] Use of `bin("10" "15").count('1')` | @@ -173,17 +174,37 @@ FURB161.py:14:9: FURB161 [*] Use of `bin("10" "15").count('1')` 13 | count = bin((10)).count("1") # FURB161 14 | count = bin("10" "15").count("1") # FURB161 | ^^^^^^^^^^^^^^^^^^^^^^^^^ FURB161 -15 | -16 | count = x.bit_count() # OK +15 | count = bin("123").count("1") # FURB161 | = help: Replace with `("10" "15").bit_count()` -ℹ Safe fix +ℹ Unsafe fix 11 11 | count = bin(0x10 + 0x1000).count("1") # FURB161 12 12 | count = bin(ten()).count("1") # FURB161 13 13 | count = bin((10)).count("1") # FURB161 14 |-count = bin("10" "15").count("1") # FURB161 14 |+count = ("10" "15").bit_count() # FURB161 -15 15 | -16 16 | count = x.bit_count() # OK -17 17 | count = (10).bit_count() # OK +15 15 | count = bin("123").count("1") # FURB161 +16 16 | +17 17 | count = x.bit_count() # OK + +FURB161.py:15:9: FURB161 [*] Use of `bin("123").count('1')` + | +13 | count = bin((10)).count("1") # FURB161 +14 | count = bin("10" "15").count("1") # FURB161 +15 | count = bin("123").count("1") # FURB161 + | ^^^^^^^^^^^^^^^^^^^^^ FURB161 +16 | +17 | count = x.bit_count() # OK + | + = help: Replace with `"123".bit_count()` + +ℹ Unsafe fix +12 12 | count = bin(ten()).count("1") # FURB161 +13 13 | count = bin((10)).count("1") # FURB161 +14 14 | count = bin("10" "15").count("1") # FURB161 +15 |-count = bin("123").count("1") # FURB161 + 15 |+count = "123".bit_count() # FURB161 +16 16 | +17 17 | count = x.bit_count() # OK +18 18 | count = (10).bit_count() # OK From c550b4d5654db33e752ce1471572e497dcd73c34 Mon Sep 17 00:00:00 2001 From: Hans Date: Sat, 19 Apr 2025 01:48:13 +0800 Subject: [PATCH 0019/1161] [`pyupgrade`] Add fix safety section to docs (`UP008`, `UP022`) (#17441) ## Summary add fix safety section to replace_stdout_stderr and super_call_with_parameters, for #15584 I checked the behavior and found that these two files could only potentially delete the appended comments, so I submitted them as a PR. --- .../src/rules/pyupgrade/rules/replace_stdout_stderr.rs | 6 ++++++ .../src/rules/pyupgrade/rules/super_call_with_parameters.rs | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs index 9a752241d0000b..ea1e820df93105 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -33,6 +33,12 @@ use crate::fix::edits::{remove_argument, Parentheses}; /// subprocess.run(["foo"], capture_output=True) /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe because replacing `stdout=subprocess.PIPE` and +/// `stderr=subprocess.PIPE` with `capture_output=True` may delete comments attached +/// to the original arguments. +/// /// ## References /// - [Python 3.7 release notes](https://docs.python.org/3/whatsnew/3.7.html#subprocess) /// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs index ba11eb64c5505c..e64fb041c62cdd 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs @@ -40,6 +40,11 @@ use crate::checkers::ast::Checker; /// super().foo() /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe because removing the arguments from a call +/// may delete comments that are attached to the arguments. +/// /// ## References /// - [Python documentation: `super`](https://docs.python.org/3/library/functions.html#super) /// - [super/MRO, Python's most misunderstood feature.](https://www.youtube.com/watch?v=X1PQ7zzltz4) From fd3fc34a9e5115659c3806f736fef8e7404a9daa Mon Sep 17 00:00:00 2001 From: Hans Date: Sat, 19 Apr 2025 02:27:40 +0800 Subject: [PATCH 0020/1161] [`pyflakes`] Add fix safety section to docs (`F601`, `F602`) (#17440) ## Summary add fix safety section to repeated_keys_docs, for #15584 --------- Co-authored-by: Brent Westbrook --- .../src/rules/pyflakes/rules/repeated_keys.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs index f29d1dd3399cbc..ac71ff477dd55b 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs @@ -40,6 +40,12 @@ use crate::registry::Rule; /// foo["baz"] # 2 /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe because removing a repeated dictionary key +/// may delete comments that are attached to the removed key-value pair. This can also change +/// the program's behavior if the value expressions have side effects. +/// /// ## References /// - [Python documentation: Dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) #[derive(ViolationMetadata)] @@ -106,6 +112,12 @@ impl Violation for MultiValueRepeatedKeyLiteral { /// foo[baz] # 2 /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is marked as unsafe because removing a repeated dictionary key +/// may delete comments that are attached to the removed key-value pair. This can also change +/// the program's behavior if the value expressions have side effects. +/// /// ## References /// - [Python documentation: Dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) #[derive(ViolationMetadata)] From 454ad15aee13e5ac2fc9b67ff5907658764207df Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 18 Apr 2025 20:55:53 +0100 Subject: [PATCH 0021/1161] [red-knot] Fix MRO inference for protocol classes; allow inheritance from subscripted `Generic[]`; forbid subclassing unsubscripted `Generic` (#17452) --- .../annotations/stdlib_typing_aliases.md | 19 ++-- .../annotations/unsupported_special_forms.md | 6 +- .../resources/mdtest/generics/classes.md | 2 - .../resources/mdtest/generics/scoping.md | 10 +- .../resources/mdtest/protocols.md | 24 ++--- .../resources/mdtest/subscript/tuple.md | 3 +- crates/red_knot_python_semantic/src/types.rs | 101 ++++++++++++------ .../src/types/class.rs | 43 ++++++-- .../src/types/class_base.rs | 83 ++++++++++---- .../src/types/display.rs | 9 +- .../src/types/infer.rs | 65 +++++++---- .../src/types/subclass_of.rs | 93 +++++++++++++--- .../src/types/type_ordering.rs | 26 +++-- 13 files changed, 345 insertions(+), 139 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md index d41d1276d6ac79..7426e4c8875733 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -67,21 +67,24 @@ import typing #################### ### Built-ins +#################### class ListSubclass(typing.List): ... -# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(ListSubclass.__mro__) class DictSubclass(typing.Dict): ... -# TODO: should have `Generic`, should not have `Unknown` -# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[DictSubclass], Literal[dict], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(DictSubclass.__mro__) class SetSubclass(typing.Set): ... -# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(SetSubclass.__mro__) class FrozenSetSubclass(typing.FrozenSet): ... @@ -92,11 +95,12 @@ reveal_type(FrozenSetSubclass.__mro__) #################### ### `collections` +#################### class ChainMapSubclass(typing.ChainMap): ... -# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(ChainMapSubclass.__mro__) class CounterSubclass(typing.Counter): ... @@ -113,7 +117,8 @@ reveal_type(DefaultDictSubclass.__mro__) class DequeSubclass(typing.Deque): ... -# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(DequeSubclass.__mro__) class OrderedDictSubclass(typing.OrderedDict): ... 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 80161644ab963e..3d76ece35d281f 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 @@ -41,7 +41,7 @@ class Foo: One thing that is supported is error messages for using special forms in type expressions. ```py -from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec +from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, Generic def _( a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression" @@ -49,6 +49,7 @@ def _( c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression" d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression" e: ParamSpec, + f: Generic, # error: [invalid-type-form] "`typing.Generic` is not allowed in type expressions" ) -> None: reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -65,7 +66,7 @@ You can't inherit from most of these. `typing.Callable` is an exception. ```py from typing import Callable -from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate +from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate, Generic class A(Self): ... # error: [invalid-base] class B(Unpack): ... # error: [invalid-base] @@ -73,6 +74,7 @@ class C(TypeGuard): ... # error: [invalid-base] class D(TypeIs): ... # error: [invalid-base] class E(Concatenate): ... # error: [invalid-base] class F(Callable): ... +class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`" reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]] ``` 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 737e3455e0ed75..63599786601160 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -45,8 +45,6 @@ from typing import Generic, TypeVar T = TypeVar("T") -# TODO: no error -# error: [invalid-base] class C(Generic[T]): ... ``` 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 0d99019894009d..9712be7213f41c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -137,8 +137,6 @@ from typing import TypeVar, Generic T = TypeVar("T") S = TypeVar("S") -# TODO: no error -# error: [invalid-base] class Legacy(Generic[T]): def m(self, x: T, y: S) -> S: return y @@ -177,8 +175,6 @@ def f(x: T) -> None: # TODO: invalid-assignment error y: list[S] = [] -# TODO: no error -# error: [invalid-base] class C(Generic[T]): # TODO: error: cannot use S if it's not in the current generic context x: list[S] = [] @@ -259,8 +255,7 @@ def f[T](x: T, y: T) -> None: class Ok[S]: ... # TODO: error for reuse of typevar class Bad1[T]: ... - # TODO: no non-subscriptable error, error for reuse of typevar - # error: [non-subscriptable] + # TODO: error for reuse of typevar class Bad2(Iterable[T]): ... ``` @@ -273,8 +268,7 @@ class C[T]: class Ok1[S]: ... # TODO: error for reuse of typevar class Bad1[T]: ... - # TODO: no non-subscriptable error, error for reuse of typevar - # error: [non-subscriptable] + # TODO: error for reuse of typevar class Bad2(Iterable[T]): ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 2ddb0f404da89e..ee375cd0cb3dae 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -28,8 +28,7 @@ from typing import Protocol class MyProtocol(Protocol): ... -# TODO: at runtime this is `(, , , )` -reveal_type(MyProtocol.__mro__) # revealed: tuple[Literal[MyProtocol], @Todo(protocol), Literal[object]] +reveal_type(MyProtocol.__mro__) # revealed: tuple[Literal[MyProtocol], typing.Protocol, typing.Generic, Literal[object]] ``` Just like for any other class base, it is an error for `Protocol` to appear multiple times in a @@ -72,8 +71,7 @@ it is not sufficient for it to have `Protocol` in its MRO. ```py class SubclassOfMyProtocol(MyProtocol): ... -# TODO -# revealed: tuple[Literal[SubclassOfMyProtocol], Literal[MyProtocol], @Todo(protocol), Literal[object]] +# revealed: tuple[Literal[SubclassOfMyProtocol], Literal[MyProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(SubclassOfMyProtocol.__mro__) # TODO: should be `Literal[False]` @@ -94,8 +92,7 @@ class OtherProtocol(Protocol): class ComplexInheritance(SubProtocol, OtherProtocol, Protocol): ... -# TODO -# revealed: tuple[Literal[ComplexInheritance], Literal[SubProtocol], Literal[MyProtocol], Literal[OtherProtocol], @Todo(protocol), Literal[object]] +# revealed: tuple[Literal[ComplexInheritance], Literal[SubProtocol], Literal[MyProtocol], Literal[OtherProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(ComplexInheritance.__mro__) # TODO: should be `Literal[True]` @@ -109,15 +106,13 @@ or `TypeError` is raised at runtime when the class is created. # TODO: should emit `[invalid-protocol]` class Invalid(NotAProtocol, Protocol): ... -# TODO -# revealed: tuple[Literal[Invalid], Literal[NotAProtocol], @Todo(protocol), Literal[object]] +# revealed: tuple[Literal[Invalid], Literal[NotAProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(Invalid.__mro__) # TODO: should emit an `[invalid-protocol`] error class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ... -# TODO -# revealed: tuple[Literal[AlsoInvalid], Literal[MyProtocol], Literal[OtherProtocol], Literal[NotAProtocol], @Todo(protocol), Literal[object]] +# revealed: tuple[Literal[AlsoInvalid], Literal[MyProtocol], Literal[OtherProtocol], Literal[NotAProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(AlsoInvalid.__mro__) ``` @@ -135,11 +130,9 @@ T = TypeVar("T") # type checkers. class Fine(Protocol, object): ... -# TODO -reveal_type(Fine.__mro__) # revealed: tuple[Literal[Fine], @Todo(protocol), Literal[object]] +reveal_type(Fine.__mro__) # revealed: tuple[Literal[Fine], typing.Protocol, typing.Generic, Literal[object]] -# TODO: should not error -class StillFine(Protocol, Generic[T], object): ... # error: [invalid-base] +class StillFine(Protocol, Generic[T], object): ... class EvenThis[T](Protocol, object): ... ``` @@ -149,8 +142,7 @@ And multiple inheritance from a mix of protocol and non-protocol classes is fine ```py class FineAndDandy(MyProtocol, OtherProtocol, NotAProtocol): ... -# TODO -# revealed: tuple[Literal[FineAndDandy], Literal[MyProtocol], Literal[OtherProtocol], @Todo(protocol), Literal[NotAProtocol], Literal[object]] +# revealed: tuple[Literal[FineAndDandy], Literal[MyProtocol], Literal[OtherProtocol], typing.Protocol, typing.Generic, Literal[NotAProtocol], Literal[object]] reveal_type(FineAndDandy.__mro__) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index 579d7451f66fd1..005e3da1ea87ff 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -117,6 +117,7 @@ from typing import Tuple class C(Tuple): ... -# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(C.__mro__) ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 52c9f934e81a0d..aeb0c67b8e747c 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -26,7 +26,7 @@ pub(crate) use self::infer::{ }; pub(crate) use self::narrow::KnownConstraintFunction; pub(crate) use self::signatures::{CallableSignature, Signature, Signatures}; -pub(crate) use self::subclass_of::SubclassOfType; +pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; use crate::module_name::ModuleName; use crate::module_resolver::{file_to_module, resolve_module, KnownModule}; use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId}; @@ -499,7 +499,11 @@ impl<'db> Type<'db> { pub fn contains_todo(&self, db: &'db dyn Db) -> bool { match self { - Self::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) => true, + Self::Dynamic( + DynamicType::Todo(_) + | DynamicType::SubscriptedProtocol + | DynamicType::SubscriptedGeneric, + ) => true, Self::AlwaysFalsy | Self::AlwaysTruthy @@ -540,9 +544,13 @@ impl<'db> Type<'db> { } Self::SubclassOf(subclass_of) => match subclass_of.subclass_of() { - ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) => true, - ClassBase::Dynamic(DynamicType::Unknown | DynamicType::Any) => false, - ClassBase::Class(_) => false, + SubclassOfInner::Dynamic( + DynamicType::Todo(_) + | DynamicType::SubscriptedProtocol + | DynamicType::SubscriptedGeneric, + ) => true, + SubclassOfInner::Dynamic(DynamicType::Unknown | DynamicType::Any) => false, + SubclassOfInner::Class(_) => false, }, Self::TypeVar(typevar) => match typevar.bound_or_constraints(db) { @@ -557,10 +565,18 @@ impl<'db> Type<'db> { Self::BoundSuper(bound_super) => { matches!( bound_super.pivot_class(db), - ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) + ClassBase::Dynamic( + DynamicType::Todo(_) + | DynamicType::SubscriptedGeneric + | DynamicType::SubscriptedProtocol + ) ) || matches!( bound_super.owner(db), - SuperOwnerKind::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) + SuperOwnerKind::Dynamic( + DynamicType::Todo(_) + | DynamicType::SubscriptedGeneric + | DynamicType::SubscriptedProtocol + ) ) } @@ -1466,7 +1482,7 @@ impl<'db> Type<'db> { (Type::SubclassOf(first), Type::SubclassOf(second)) => { match (first.subclass_of(), second.subclass_of()) { (first, second) if first == second => true, - (ClassBase::Dynamic(_), ClassBase::Dynamic(_)) => true, + (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => true, _ => false, } } @@ -1633,16 +1649,16 @@ impl<'db> Type<'db> { (Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b)) | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => false, - ClassBase::Class(class_a) => !class_b.is_subclass_of(db, None, class_a), + SubclassOfInner::Dynamic(_) => false, + SubclassOfInner::Class(class_a) => !class_b.is_subclass_of(db, None, class_a), } } (Type::SubclassOf(subclass_of_ty), Type::GenericAlias(alias_b)) | (Type::GenericAlias(alias_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => false, - ClassBase::Class(class_a) => { + SubclassOfInner::Dynamic(_) => false, + SubclassOfInner::Class(class_a) => { !ClassType::from(alias_b).is_subclass_of(db, class_a) } } @@ -1691,10 +1707,10 @@ impl<'db> Type<'db> { // so although the type is dynamic we can still determine disjointedness in some situations (Type::SubclassOf(subclass_of_ty), other) | (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => { + SubclassOfInner::Dynamic(_) => { KnownClass::Type.to_instance(db).is_disjoint_from(db, other) } - ClassBase::Class(class) => class + SubclassOfInner::Class(class) => class .metaclass_instance_type(db) .is_disjoint_from(db, other), }, @@ -3073,8 +3089,8 @@ impl<'db> Type<'db> { .try_bool_impl(db, allow_short_circuit)?, Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => Truthiness::Ambiguous, - ClassBase::Class(class) => { + SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, + SubclassOfInner::Class(class) => { Type::from(class).try_bool_impl(db, allow_short_circuit)? } }, @@ -3812,12 +3828,14 @@ impl<'db> Type<'db> { } Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).signatures(db), + SubclassOfInner::Dynamic(dynamic_type) => { + Type::Dynamic(dynamic_type).signatures(db) + } // Most type[] constructor calls are handled by `try_call_constructor` and not via // getting the signature here. This signature can still be used in some cases (e.g. // evaluating callable subtyping). TODO improve this definition (intersection of // `__new__` and `__init__` signatures? and respect metaclass `__call__`). - ClassBase::Class(class) => Type::from(class).signatures(db), + SubclassOfInner::Class(class) => Type::from(class).signatures(db), }, Type::Instance(_) => { @@ -4380,6 +4398,10 @@ impl<'db> Type<'db> { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol], fallback_type: Type::unknown(), }), + KnownInstanceType::Generic => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Generic], + fallback_type: Type::unknown(), + }), KnownInstanceType::Literal | KnownInstanceType::Union @@ -4564,21 +4586,21 @@ impl<'db> Type<'db> { Type::ClassLiteral(class) => class.metaclass(db), Type::GenericAlias(alias) => ClassType::from(*alias).metaclass(db), Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => *self, - ClassBase::Class(class) => SubclassOfType::from( + SubclassOfInner::Dynamic(_) => *self, + SubclassOfInner::Class(class) => SubclassOfType::from( db, - ClassBase::try_from_type(db, class.metaclass(db)) - .unwrap_or(ClassBase::unknown()), + SubclassOfInner::try_from_type(db, class.metaclass(db)) + .unwrap_or(SubclassOfInner::unknown()), ), }, Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class_literal(db), - Type::Dynamic(dynamic) => SubclassOfType::from(db, ClassBase::Dynamic(*dynamic)), + Type::Dynamic(dynamic) => SubclassOfType::from(db, SubclassOfInner::Dynamic(*dynamic)), // TODO intersections Type::Intersection(_) => SubclassOfType::from( db, - ClassBase::try_from_type(db, todo_type!("Intersection meta-type")) - .expect("Type::Todo should be a valid ClassBase"), + SubclassOfInner::try_from_type(db, todo_type!("Intersection meta-type")) + .expect("Type::Todo should be a valid `SubclassOfInner`"), ), Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), @@ -4780,8 +4802,8 @@ impl<'db> Type<'db> { }, Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - ClassBase::Class(class) => Some(TypeDefinition::Class(class.definition(db))), - ClassBase::Dynamic(_) => None, + SubclassOfInner::Class(class) => Some(TypeDefinition::Class(class.definition(db))), + SubclassOfInner::Dynamic(_) => None, }, Self::StringLiteral(_) @@ -4833,9 +4855,12 @@ pub enum DynamicType { /// /// This variant should be created with the `todo_type!` macro. Todo(TodoType), - /// Temporary type until we support protocols. We use a separate variant (instead of `Todo(…)`) - /// in order to be able to match on them explicitly. - TodoProtocol, + /// Temporary type until we support generic protocols. + /// We use a separate variant (instead of `Todo(…)`) in order to be able to match on them explicitly. + SubscriptedProtocol, + /// Temporary type until we support old-style generics. + /// We use a separate variant (instead of `Todo(…)`) in order to be able to match on them explicitly. + SubscriptedGeneric, } impl std::fmt::Display for DynamicType { @@ -4846,8 +4871,13 @@ impl std::fmt::Display for DynamicType { // `DynamicType::Todo`'s display should be explicit that is not a valid display of // any other type DynamicType::Todo(todo) => write!(f, "@Todo{todo}"), - DynamicType::TodoProtocol => f.write_str(if cfg!(debug_assertions) { - "@Todo(protocol)" + DynamicType::SubscriptedProtocol => f.write_str(if cfg!(debug_assertions) { + "@Todo(`Protocol[]` subscript)" + } else { + "@Todo" + }), + DynamicType::SubscriptedGeneric => f.write_str(if cfg!(debug_assertions) { + "@Todo(`Generic[]` subscript)" } else { "@Todo" }), @@ -4959,8 +4989,10 @@ enum InvalidTypeExpression<'db> { RequiresArguments(Type<'db>), /// Some types always require at least two arguments when used in a type expression RequiresTwoArguments(Type<'db>), - /// The `Protocol` type is invalid in type expressions + /// The `Protocol` class is invalid in type expressions Protocol, + /// Same for `Generic` + Generic, /// Type qualifiers are always invalid in *type expressions*, /// but these ones are okay with 0 arguments in *annotation expressions* TypeQualifier(KnownInstanceType<'db>), @@ -4999,6 +5031,9 @@ impl<'db> InvalidTypeExpression<'db> { InvalidTypeExpression::Protocol => f.write_str( "`typing.Protocol` is not allowed in type expressions" ), + InvalidTypeExpression::Generic => f.write_str( + "`typing.Generic` is not allowed in type expressions" + ), InvalidTypeExpression::TypeQualifier(qualifier) => write!( f, "Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions)", diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index e8e53a2e3f561c..2f57261650e3c6 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -246,7 +246,7 @@ impl<'db> ClassType<'db> { /// cases rather than simply iterating over the inferred resolution order for the class. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(super) fn iter_mro(self, db: &'db dyn Db) -> impl Iterator> { + pub(super) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { let (class_literal, specialization) = self.class_literal(db); class_literal.iter_mro(db, specialization) } @@ -645,7 +645,7 @@ impl<'db> ClassLiteralType<'db> { self, db: &'db dyn Db, specialization: Option>, - ) -> impl Iterator> { + ) -> MroIterator<'db> { MroIterator::new(db, self, specialization) } @@ -846,7 +846,11 @@ impl<'db> ClassLiteralType<'db> { for superclass in mro_iter { match superclass { - ClassBase::Dynamic(DynamicType::TodoProtocol) => { + ClassBase::Dynamic( + DynamicType::SubscriptedGeneric | DynamicType::SubscriptedProtocol, + ) + | ClassBase::Generic + | ClassBase::Protocol => { // TODO: We currently skip `Protocol` when looking up class members, in order to // avoid creating many dynamic types in our test suite that would otherwise // result from looking up attributes on builtin types like `str`, `list`, `tuple` @@ -865,7 +869,12 @@ impl<'db> ClassLiteralType<'db> { continue; } - if class.is_known(db, KnownClass::Type) && policy.meta_class_no_type_fallback() + // HACK: we should implement some more general logic here that supports arbitrary custom + // metaclasses, not just `type` and `ABCMeta`. + if matches!( + class.known(db), + Some(KnownClass::Type | KnownClass::ABCMeta) + ) && policy.meta_class_no_type_fallback() { continue; } @@ -1158,8 +1167,12 @@ impl<'db> ClassLiteralType<'db> { for superclass in self.iter_mro(db, specialization) { match superclass { - ClassBase::Dynamic(DynamicType::TodoProtocol) => { - // TODO: We currently skip `Protocol` when looking up instance members, in order to + ClassBase::Dynamic( + DynamicType::SubscriptedProtocol | DynamicType::SubscriptedGeneric, + ) + | ClassBase::Generic + | ClassBase::Protocol => { + // TODO: We currently skip these when looking up instance members, in order to // avoid creating many dynamic types in our test suite that would otherwise // result from looking up attributes on builtin types like `str`, `list`, `tuple` } @@ -1671,6 +1684,8 @@ pub(crate) enum KnownClass { Super, // enum Enum, + // abc + ABCMeta, // Types GenericAlias, ModuleType, @@ -1780,6 +1795,7 @@ impl<'db> KnownClass { | Self::Float | Self::Sized | Self::Enum + | Self::ABCMeta // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 // and raises a `TypeError` in Python >=3.14 // (see https://docs.python.org/3/library/constants.html#NotImplemented) @@ -1836,6 +1852,7 @@ impl<'db> KnownClass { Self::Sized => "Sized", Self::OrderedDict => "OrderedDict", Self::Enum => "Enum", + Self::ABCMeta => "ABCMeta", Self::Super => "super", // For example, `typing.List` is defined as `List = _Alias()` in typeshed Self::StdlibAlias => "_Alias", @@ -1992,6 +2009,7 @@ impl<'db> KnownClass { | Self::Super | Self::Property => KnownModule::Builtins, Self::VersionInfo => KnownModule::Sys, + Self::ABCMeta => KnownModule::Abc, Self::Enum => KnownModule::Enum, Self::GenericAlias | Self::ModuleType @@ -2096,6 +2114,7 @@ impl<'db> KnownClass { | Self::TypeVarTuple | Self::Sized | Self::Enum + | Self::ABCMeta | Self::Super | Self::NewType => false, } @@ -2155,6 +2174,7 @@ impl<'db> KnownClass { | Self::TypeVarTuple | Self::Sized | Self::Enum + | Self::ABCMeta | Self::Super | Self::UnionType | Self::NewType => false, @@ -2216,6 +2236,7 @@ impl<'db> KnownClass { "SupportsIndex" => Self::SupportsIndex, "Sized" => Self::Sized, "Enum" => Self::Enum, + "ABCMeta" => Self::ABCMeta, "super" => Self::Super, "_version_info" => Self::VersionInfo, "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { @@ -2271,6 +2292,7 @@ impl<'db> KnownClass { | Self::MethodType | Self::MethodWrapperType | Self::Enum + | Self::ABCMeta | Self::Super | Self::NotImplementedType | Self::UnionType @@ -2393,6 +2415,8 @@ pub enum KnownInstanceType<'db> { OrderedDict, /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) Protocol, + /// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`) + Generic, /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) Type, /// A single instance of `typing.TypeVar` @@ -2467,6 +2491,7 @@ impl<'db> KnownInstanceType<'db> { | Self::ChainMap | Self::OrderedDict | Self::Protocol + | Self::Generic | Self::ReadOnly | Self::TypeAliasType(_) | Self::Unknown @@ -2513,6 +2538,7 @@ impl<'db> KnownInstanceType<'db> { Self::ChainMap => "typing.ChainMap", Self::OrderedDict => "typing.OrderedDict", Self::Protocol => "typing.Protocol", + Self::Generic => "typing.Generic", Self::ReadOnly => "typing.ReadOnly", Self::TypeVar(typevar) => typevar.name(db), Self::TypeAliasType(_) => "typing.TypeAliasType", @@ -2560,7 +2586,8 @@ impl<'db> KnownInstanceType<'db> { Self::Deque => KnownClass::StdlibAlias, Self::ChainMap => KnownClass::StdlibAlias, Self::OrderedDict => KnownClass::StdlibAlias, - Self::Protocol => KnownClass::SpecialForm, + Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says + Self::Generic => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says Self::TypeVar(_) => KnownClass::TypeVar, Self::TypeAliasType(_) => KnownClass::TypeAliasType, Self::TypeOf => KnownClass::SpecialForm, @@ -2604,6 +2631,7 @@ impl<'db> KnownInstanceType<'db> { "Counter" => Self::Counter, "ChainMap" => Self::ChainMap, "OrderedDict" => Self::OrderedDict, + "Generic" => Self::Generic, "Protocol" => Self::Protocol, "Optional" => Self::Optional, "Union" => Self::Union, @@ -2662,6 +2690,7 @@ impl<'db> KnownInstanceType<'db> { | Self::NoReturn | Self::Tuple | Self::Type + | Self::Generic | Self::Callable => module.is_typing(), Self::Annotated | Self::Protocol diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 4b33c60b8ccf1c..5f342a3f3a979f 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -1,11 +1,12 @@ -use crate::types::{todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, Type}; +use crate::types::{ + todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, MroIterator, Type, +}; use crate::Db; -use itertools::Either; /// Enumeration of the possible kinds of types we allow in class bases. /// /// This is much more limited than the [`Type`] enum: all types that would be invalid to have as a -/// class base are transformed into [`ClassBase::unknown`] +/// class base are transformed into [`ClassBase::unknown()`] /// /// Note that a non-specialized generic class _cannot_ be a class base. When we see a /// non-specialized generic class in any type expression (including the list of base classes), we @@ -14,6 +15,13 @@ use itertools::Either; pub enum ClassBase<'db> { Dynamic(DynamicType), Class(ClassType<'db>), + /// Although `Protocol` is not a class in typeshed's stubs, it is at runtime, + /// and can appear in the MRO of a class. + Protocol, + /// Bare `Generic` cannot be subclassed directly in user code, + /// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`, + /// `Protocol[T]`, or bare `Protocol`. + Generic, } impl<'db> ClassBase<'db> { @@ -25,13 +33,6 @@ impl<'db> ClassBase<'db> { Self::Dynamic(DynamicType::Unknown) } - pub(crate) const fn is_dynamic(self) -> bool { - match self { - ClassBase::Dynamic(_) => true, - ClassBase::Class(_) => false, - } - } - pub(crate) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { struct Display<'db> { base: ClassBase<'db>, @@ -48,6 +49,8 @@ impl<'db> ClassBase<'db> { ClassBase::Class(ClassType::Generic(alias)) => { write!(f, "", alias.display(self.db)) } + ClassBase::Protocol => f.write_str("typing.Protocol"), + ClassBase::Generic => f.write_str("typing.Generic"), } } } @@ -165,7 +168,8 @@ impl<'db> ClassBase<'db> { KnownInstanceType::Callable => { Self::try_from_type(db, todo_type!("Support for Callable as a base class")) } - KnownInstanceType::Protocol => Some(ClassBase::Dynamic(DynamicType::TodoProtocol)), + KnownInstanceType::Protocol => Some(ClassBase::Protocol), + KnownInstanceType::Generic => Some(ClassBase::Generic), }, } } @@ -173,18 +177,21 @@ impl<'db> ClassBase<'db> { pub(super) fn into_class(self) -> Option> { match self { Self::Class(class) => Some(class), - Self::Dynamic(_) => None, + Self::Dynamic(_) | Self::Generic | Self::Protocol => None, } } /// Iterate over the MRO of this base - pub(super) fn mro( - self, - db: &'db dyn Db, - ) -> Either>, impl Iterator>> { + pub(super) fn mro(self, db: &'db dyn Db) -> impl Iterator> { match self { - ClassBase::Dynamic(_) => Either::Left([self, ClassBase::object(db)].into_iter()), - ClassBase::Class(class) => Either::Right(class.iter_mro(db)), + ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic), + ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => ClassBaseMroIterator::length_3( + db, + self, + ClassBase::Dynamic(DynamicType::SubscriptedGeneric), + ), + ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self), + ClassBase::Class(class) => ClassBaseMroIterator::from_class(db, class), } } } @@ -200,6 +207,8 @@ impl<'db> From> for Type<'db> { match value { ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic), ClassBase::Class(class) => class.into(), + ClassBase::Protocol => Type::KnownInstance(KnownInstanceType::Protocol), + ClassBase::Generic => Type::KnownInstance(KnownInstanceType::Generic), } } } @@ -209,3 +218,41 @@ impl<'db> From<&ClassBase<'db>> for Type<'db> { Self::from(*value) } } + +/// An iterator over the MRO of a class base. +enum ClassBaseMroIterator<'db> { + Length2(core::array::IntoIter, 2>), + Length3(core::array::IntoIter, 3>), + FromClass(MroIterator<'db>), +} + +impl<'db> ClassBaseMroIterator<'db> { + /// Iterate over an MRO of length 2 that consists of `first_element` and then `object`. + fn length_2(db: &'db dyn Db, first_element: ClassBase<'db>) -> Self { + ClassBaseMroIterator::Length2([first_element, ClassBase::object(db)].into_iter()) + } + + /// Iterate over an MRO of length 3 that consists of `first_element`, then `second_element`, then `object`. + fn length_3(db: &'db dyn Db, element_1: ClassBase<'db>, element_2: ClassBase<'db>) -> Self { + ClassBaseMroIterator::Length3([element_1, element_2, ClassBase::object(db)].into_iter()) + } + + /// Iterate over the MRO of an arbitrary class. The MRO may be of any length. + fn from_class(db: &'db dyn Db, class: ClassType<'db>) -> Self { + ClassBaseMroIterator::FromClass(class.iter_mro(db)) + } +} + +impl<'db> Iterator for ClassBaseMroIterator<'db> { + type Item = ClassBase<'db>; + + fn next(&mut self) -> Option { + match self { + Self::Length2(iter) => iter.next(), + Self::Length3(iter) => iter.next(), + Self::FromClass(iter) => iter.next(), + } + } +} + +impl std::iter::FusedIterator for ClassBaseMroIterator<'_> {} diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 96c3c5c535fa32..b420d3ef72c58d 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -7,13 +7,12 @@ use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_literal::escape::AsciiEscape; use crate::types::class::{ClassType, GenericAlias, GenericClass}; -use crate::types::class_base::ClassBase; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ FunctionSignature, InstanceType, IntersectionType, KnownClass, MethodWrapperKind, - StringLiteralType, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType, - WrapperDescriptorKind, + StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, + UnionType, WrapperDescriptorKind, }; use crate::Db; use rustc_hash::FxHashMap; @@ -92,8 +91,8 @@ impl Display for DisplayRepresentation<'_> { Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { // Only show the bare class name here; ClassBase::display would render this as // type[] instead of type[Foo]. - ClassBase::Class(class) => write!(f, "type[{}]", class.name(self.db)), - ClassBase::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), + SubclassOfInner::Class(class) => write!(f, "type[{}]", class.name(self.db)), + SubclassOfInner::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), }, Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)), Type::FunctionLiteral(function) => { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 4107b397753242..e8f4ac2c6e4bb3 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -94,7 +94,6 @@ use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; use crate::Db; -use super::class_base::ClassBase; use super::context::{InNoTypeCheck, InferContext}; use super::diagnostic::{ report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause, @@ -107,7 +106,8 @@ use super::slots::check_class_slots; use super::string_annotation::{ parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, }; -use super::{BoundSuperError, BoundSuperType}; +use super::subclass_of::SubclassOfInner; +use super::{BoundSuperError, BoundSuperType, ClassBase}; /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. /// Use when checking a scope, or needing to provide a type for an arbitrary expression in the @@ -763,12 +763,25 @@ impl<'db> TypeInferenceBuilder<'db> { continue; } - // (2) Check for classes that inherit from `@final` classes + // (2) Check for inheritance from plain `Generic`, + // and from classes that inherit from `@final` classes for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() { - // dynamic/unknown bases are never `@final` - let Some(base_class) = base_class.into_class_literal() else { - continue; + let base_class = match base_class { + Type::KnownInstance(KnownInstanceType::Generic) => { + // `Generic` can appear in the MRO of many classes, + // but it is never valid as an explicit base class in user code. + self.context.report_lint_old( + &INVALID_BASE, + &class_node.bases()[i], + format_args!("Cannot inherit from plain `Generic`",), + ); + continue; + } + Type::ClassLiteral(class) => class, + // dynamic/unknown bases are never `@final` + _ => continue, }; + if !base_class.is_final(self.db()) { continue; } @@ -4736,10 +4749,10 @@ impl<'db> TypeInferenceBuilder<'db> { } Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => { match subclass_of.subclass_of() { - ClassBase::Class(class) => { + SubclassOfInner::Class(class) => { !class.instance_member(db, attr).symbol.is_unbound() } - ClassBase::Dynamic(_) => unreachable!( + SubclassOfInner::Dynamic(_) => unreachable!( "Attribute lookup on a dynamic `SubclassOf` type should always return a bound symbol" ), } @@ -4981,8 +4994,10 @@ impl<'db> TypeInferenceBuilder<'db> { | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), (todo @ Type::Dynamic(DynamicType::Todo(_)), _, _) | (_, todo @ Type::Dynamic(DynamicType::Todo(_)), _) => Some(todo), - (todo @ Type::Dynamic(DynamicType::TodoProtocol), _, _) - | (_, todo @ Type::Dynamic(DynamicType::TodoProtocol), _) => Some(todo), + (todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _, _) + | (_, todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _) => Some(todo), + (todo @ Type::Dynamic(DynamicType::SubscriptedGeneric), _, _) + | (_, todo @ Type::Dynamic(DynamicType::SubscriptedGeneric), _) => Some(todo), (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( @@ -6224,7 +6239,10 @@ impl<'db> TypeInferenceBuilder<'db> { Type::IntLiteral(i64::from(bool)), ), (Type::KnownInstance(KnownInstanceType::Protocol), _) => { - Type::Dynamic(DynamicType::TodoProtocol) + Type::Dynamic(DynamicType::SubscriptedProtocol) + } + (Type::KnownInstance(KnownInstanceType::Generic), _) => { + Type::Dynamic(DynamicType::SubscriptedGeneric) } (Type::KnownInstance(known_instance), _) if known_instance.class().is_special_form() => @@ -6331,12 +6349,19 @@ impl<'db> TypeInferenceBuilder<'db> { } } - report_non_subscriptable( - &self.context, - value_node.into(), - value_ty, - "__class_getitem__", - ); + // TODO: properly handle old-style generics; get rid of this temporary hack + if !value_ty.into_class_literal().is_some_and(|class| { + class + .iter_mro(self.db(), None) + .contains(&ClassBase::Dynamic(DynamicType::SubscriptedGeneric)) + }) { + report_non_subscriptable( + &self.context, + value_node.into(), + value_ty, + "__class_getitem__", + ); + } } else { report_non_subscriptable( &self.context, @@ -7508,7 +7533,11 @@ impl<'db> TypeInferenceBuilder<'db> { } KnownInstanceType::Protocol => { self.infer_type_expression(arguments_slice); - Type::Dynamic(DynamicType::TodoProtocol) + Type::Dynamic(DynamicType::SubscriptedProtocol) + } + KnownInstanceType::Generic => { + self.infer_type_expression(arguments_slice); + Type::Dynamic(DynamicType::SubscriptedGeneric) } KnownInstanceType::NoReturn | KnownInstanceType::Never diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/red_knot_python_semantic/src/types/subclass_of.rs index 49c1d168fc63ce..52dd82aaa665ff 100644 --- a/crates/red_knot_python_semantic/src/types/subclass_of.rs +++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs @@ -1,12 +1,12 @@ use crate::symbol::SymbolAndQualifiers; -use super::{ClassBase, Db, KnownClass, MemberLookupPolicy, Type}; +use super::{ClassType, Db, DynamicType, KnownClass, MemberLookupPolicy, Type}; /// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct SubclassOfType<'db> { // Keep this field private, so that the only way of constructing the struct is through the `from` method. - subclass_of: ClassBase<'db>, + subclass_of: SubclassOfInner<'db>, } impl<'db> SubclassOfType<'db> { @@ -21,11 +21,11 @@ impl<'db> SubclassOfType<'db> { /// /// The eager normalization here means that we do not need to worry elsewhere about distinguishing /// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants. - pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into>) -> Type<'db> { + pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into>) -> Type<'db> { let subclass_of = subclass_of.into(); match subclass_of { - ClassBase::Dynamic(_) => Type::SubclassOf(Self { subclass_of }), - ClassBase::Class(class) => { + SubclassOfInner::Dynamic(_) => Type::SubclassOf(Self { subclass_of }), + SubclassOfInner::Class(class) => { if class.is_final(db) { Type::from(class) } else if class.is_object(db) { @@ -40,19 +40,19 @@ impl<'db> SubclassOfType<'db> { /// Return a [`Type`] instance representing the type `type[Unknown]`. pub(crate) const fn subclass_of_unknown() -> Type<'db> { Type::SubclassOf(SubclassOfType { - subclass_of: ClassBase::unknown(), + subclass_of: SubclassOfInner::unknown(), }) } /// Return a [`Type`] instance representing the type `type[Any]`. pub(crate) const fn subclass_of_any() -> Type<'db> { Type::SubclassOf(SubclassOfType { - subclass_of: ClassBase::any(), + subclass_of: SubclassOfInner::Dynamic(DynamicType::Any), }) } - /// Return the inner [`ClassBase`] value wrapped by this `SubclassOfType`. - pub(crate) const fn subclass_of(self) -> ClassBase<'db> { + /// Return the inner [`SubclassOfInner`] value wrapped by this `SubclassOfType`. + pub(crate) const fn subclass_of(self) -> SubclassOfInner<'db> { self.subclass_of } @@ -77,17 +77,17 @@ impl<'db> SubclassOfType<'db> { /// Return `true` if `self` is a subtype of `other`. /// - /// This can only return `true` if `self.subclass_of` is a [`ClassBase::Class`] variant; + /// This can only return `true` if `self.subclass_of` is a [`SubclassOfInner::Class`] variant; /// only fully static types participate in subtyping. pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool { match (self.subclass_of, other.subclass_of) { // Non-fully-static types do not participate in subtyping - (ClassBase::Dynamic(_), _) | (_, ClassBase::Dynamic(_)) => false, + (SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => false, // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, // and `type[int]` describes all possible runtime subclasses of the class `int`. // The first set is a subset of the second set, because `bool` is itself a subclass of `int`. - (ClassBase::Class(self_class), ClassBase::Class(other_class)) => { + (SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => { // N.B. The subclass relation is fully static self_class.is_subclass_of(db, other_class) } @@ -96,8 +96,73 @@ impl<'db> SubclassOfType<'db> { pub(crate) fn to_instance(self) -> Type<'db> { match self.subclass_of { - ClassBase::Class(class) => Type::instance(class), - ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), + SubclassOfInner::Class(class) => Type::instance(class), + SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), + } + } +} + +/// An enumeration of the different kinds of `type[]` types that a [`SubclassOfType`] can represent: +/// +/// 1. A "subclass of a class": `type[C]` for any class object `C` +/// 2. A "subclass of a dynamic type": `type[Any]`, `type[Unknown]` and `type[@Todo]` +/// +/// In the long term, we may want to implement . +/// Doing this would allow us to get rid of this enum, +/// since `type[Any]` would be represented as `type & Any` +/// rather than using the [`Type::SubclassOf`] variant at all; +/// [`SubclassOfType`] would then be a simple wrapper around [`ClassType`]. +/// +/// Note that this enum is similar to the [`super::ClassBase`] enum, +/// but does not include the `ClassBase::Protocol` and `ClassBase::Generic` variants +/// (`type[Protocol]` and `type[Generic]` are not valid types). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub(crate) enum SubclassOfInner<'db> { + Class(ClassType<'db>), + Dynamic(DynamicType), +} + +impl<'db> SubclassOfInner<'db> { + pub(crate) const fn unknown() -> Self { + Self::Dynamic(DynamicType::Unknown) + } + + pub(crate) const fn is_dynamic(self) -> bool { + matches!(self, Self::Dynamic(_)) + } + + pub(crate) const fn into_class(self) -> Option> { + match self { + Self::Class(class) => Some(class), + Self::Dynamic(_) => None, + } + } + + pub(crate) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + match ty { + Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), + Type::ClassLiteral(literal) => Some(if literal.is_known(db, KnownClass::Any) { + Self::Dynamic(DynamicType::Any) + } else { + Self::Class(literal.default_specialization(db)) + }), + Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), + _ => None, + } + } +} + +impl<'db> From> for SubclassOfInner<'db> { + fn from(value: ClassType<'db>) -> Self { + SubclassOfInner::Class(value) + } +} + +impl<'db> From> for Type<'db> { + fn from(value: SubclassOfInner<'db>) -> Self { + match value { + SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic), + SubclassOfInner::Class(class) => class.into(), } } } diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index e3b341fee6ed78..8f6968746521d1 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -3,8 +3,8 @@ use std::cmp::Ordering; use crate::db::Db; use super::{ - class_base::ClassBase, DynamicType, InstanceType, KnownInstanceType, SuperOwnerKind, TodoType, - Type, + class_base::ClassBase, subclass_of::SubclassOfInner, DynamicType, InstanceType, + KnownInstanceType, SuperOwnerKind, TodoType, Type, }; /// Return an [`Ordering`] that describes the canonical order in which two types should appear @@ -109,10 +109,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::SubclassOf(left), Type::SubclassOf(right)) => { match (left.subclass_of(), right.subclass_of()) { - (ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(&right), - (ClassBase::Class(_), _) => Ordering::Less, - (_, ClassBase::Class(_)) => Ordering::Greater, - (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { + (SubclassOfInner::Class(left), SubclassOfInner::Class(right)) => left.cmp(&right), + (SubclassOfInner::Class(_), _) => Ordering::Less, + (_, SubclassOfInner::Class(_)) => Ordering::Greater, + (SubclassOfInner::Dynamic(left), SubclassOfInner::Dynamic(right)) => { dynamic_elements_ordering(left, right) } } @@ -143,6 +143,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(right), (ClassBase::Class(_), _) => Ordering::Less, (_, ClassBase::Class(_)) => Ordering::Greater, + (ClassBase::Protocol, _) => Ordering::Less, + (_, ClassBase::Protocol) => Ordering::Greater, + (ClassBase::Generic, _) => Ordering::Less, + (_, ClassBase::Generic) => Ordering::Greater, (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { dynamic_elements_ordering(*left, *right) } @@ -230,6 +234,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (KnownInstanceType::OrderedDict, _) => Ordering::Less, (_, KnownInstanceType::OrderedDict) => Ordering::Greater, + (KnownInstanceType::Generic, _) => Ordering::Less, + (_, KnownInstanceType::Generic) => Ordering::Greater, + (KnownInstanceType::Protocol, _) => Ordering::Less, (_, KnownInstanceType::Protocol) => Ordering::Greater, @@ -364,7 +371,10 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering #[cfg(not(debug_assertions))] (DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal, - (DynamicType::TodoProtocol, _) => Ordering::Less, - (_, DynamicType::TodoProtocol) => Ordering::Greater, + (DynamicType::SubscriptedGeneric, _) => Ordering::Less, + (_, DynamicType::SubscriptedGeneric) => Ordering::Greater, + + (DynamicType::SubscriptedProtocol, _) => Ordering::Less, + (_, DynamicType::SubscriptedProtocol) => Ordering::Greater, } } From 8fe2dd5e031464630a7aebfdeecb8ecacbb3f3d2 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Apr 2025 14:20:18 -0700 Subject: [PATCH 0022/1161] [red-knot] pull primer projects to run from file (#17473) ## Summary The long line of projects in `mypy_primer.yaml` is hard to work with when adding projects or checking whether they are currently run. Use a one-per-line text file instead. ## Test Plan Ecosystem check on this PR. --- .github/workflows/mypy_primer.yaml | 5 +++- crates/red_knot/docs/mypy_primer.md | 2 +- .../resources/primer/good.txt | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/primer/good.txt diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index 2ca8f2d50ee33f..c2329ae126d398 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -52,6 +52,8 @@ jobs: run: | cd ruff + PRIMER_SELECTOR="$(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt)" + echo "new commit" git rev-list --format=%s --max-count=1 "$GITHUB_SHA" @@ -62,13 +64,14 @@ jobs: cd .. + echo "Project selector: $PRIMER_SELECTOR" # Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs uvx mypy_primer \ --repo ruff \ --type-checker knot \ --old base_commit \ --new "$GITHUB_SHA" \ - --project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument|typeshed-stats|scrapy|werkzeug|bidict|async-utils|python-chess|dacite|python-htmlgen|paroxython|porcupine|psycopg)$' \ + --project-selector "/($PRIMER_SELECTOR)\$" \ --output concise \ --debug > mypy_primer.diff || [ $? -eq 1 ] diff --git a/crates/red_knot/docs/mypy_primer.md b/crates/red_knot/docs/mypy_primer.md index d3898a2a358c84..c4d73add146d38 100644 --- a/crates/red_knot/docs/mypy_primer.md +++ b/crates/red_knot/docs/mypy_primer.md @@ -31,7 +31,7 @@ mypy_primer \ ``` This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the -diff for all projects, you currently need to copy the project-selector regex from the CI pipeline in `.github/workflows/mypy_primer.yaml`. +diff for all projects we currently enable in CI, use `--project-selector "/($(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt))\$"`. You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `knot_paths` configuration option to work correctly. diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/red_knot_python_semantic/resources/primer/good.txt new file mode 100644 index 00000000000000..360e1e72ef199c --- /dev/null +++ b/crates/red_knot_python_semantic/resources/primer/good.txt @@ -0,0 +1,23 @@ +arrow +async-utils +bidict +black +dacite +git-revise +isort +itsdangerous +mypy_primer +packaging +paroxython +porcupine +psycopg +pybind11 +pyinstrument +pyp +python-chess +python-htmlgen +rich +scrapy +typeshed-stats +werkzeug +zipp From 2a478ce1b24e02625497a2f0e5b36b5d1399ef67 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Apr 2025 15:08:57 -0700 Subject: [PATCH 0023/1161] [red-knot] simplify gradually-equivalent types out of unions and intersections (#17467) ## Summary If two types are gradually-equivalent, that means they share the same set of possible materializations. There's no need to keep two such types in the same union or intersection; we should simplify them. Fixes https://github.com/astral-sh/ruff/issues/17465 The one downside here is that now we will simplify e.g. `Unknown | Todo(...)` to just `Unknown`, if `Unknown` was added to the union first. This is correct from a type perspective (they are equivalent types), but it can mean we lose visibility into part of the cause for the type inferring as unknown. I think this is OK, but if we think it's important to avoid this, I can add a special case to try to preserve `Todo` over `Unknown`, if we see them both in the same union or intersection. ## Test Plan Added and updated mdtests. --- .../resources/mdtest/attributes.md | 13 ++++++++++++- .../resources/mdtest/call/union.md | 12 ++++++++++++ .../resources/mdtest/intersection_types.md | 10 +++++----- .../resources/mdtest/subscript/lists.md | 2 +- .../is_gradual_equivalent_to.md | 3 --- crates/red_knot_python_semantic/src/types.rs | 18 ------------------ .../src/types/builder.rs | 8 +++++--- 7 files changed, 35 insertions(+), 31 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index ffec37148dcb63..37f0961461c14f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -302,7 +302,7 @@ class C: c_instance = C() reveal_type(c_instance.a) # revealed: Unknown | Literal[1] -reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking) +reveal_type(c_instance.b) # revealed: Unknown ``` #### Attributes defined in for-loop (unpacking) @@ -1892,6 +1892,17 @@ reveal_type(B().x) # revealed: Unknown | Literal[1] reveal_type(A().x) # revealed: Unknown | Literal[1] ``` +This case additionally tests our union/intersection simplification logic: + +```py +class H: + def __init__(self): + self.x = 1 + + def copy(self, other: "H"): + self.x = other.x or self.x +``` + ### Builtin types attributes This test can probably be removed eventually, but we currently include it because we do not yet diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/union.md b/crates/red_knot_python_semantic/resources/mdtest/call/union.md index b3615496c1fe9d..88d6ad9c378b0b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/union.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/union.md @@ -201,3 +201,15 @@ def _(literals_2: Literal[0, 1], b: bool, flag: bool): # Now union the two: reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int ``` + +## Simplifying gradually-equivalent types + +If two types are gradually equivalent, we can keep just one of them in a union: + +```py +from typing import Any, Union +from knot_extensions import Intersection, Not + +def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]): + reveal_type(x) # revealed: Any & ~int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md b/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md index 6aed38694a92f0..51bf653ef6b3bb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md @@ -842,7 +842,7 @@ def unknown( ### Mixed dynamic types -We currently do not simplify mixed dynamic types, but might consider doing so in the future: +Gradually-equivalent types can be simplified out of intersections: ```py from typing import Any @@ -854,10 +854,10 @@ def mixed( i3: Intersection[Not[Any], Unknown], i4: Intersection[Not[Any], Not[Unknown]], ) -> None: - reveal_type(i1) # revealed: Any & Unknown - reveal_type(i2) # revealed: Any & Unknown - reveal_type(i3) # revealed: Any & Unknown - reveal_type(i4) # revealed: Any & Unknown + reveal_type(i1) # revealed: Any + reveal_type(i2) # revealed: Any + reveal_type(i3) # revealed: Any + reveal_type(i4) # revealed: Any ``` ## Invalid diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md index 9bb53d4e672259..d074d1b82669a8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md @@ -12,7 +12,7 @@ x = [1, 2, 3] reveal_type(x) # revealed: list # TODO reveal int -reveal_type(x[0]) # revealed: Unknown | @Todo(Support for `typing.TypeVar` instances in type expressions) +reveal_type(x[0]) # revealed: Unknown # TODO reveal list reveal_type(x[0:1]) # revealed: @Todo(specialized non-generic class) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md index e3e46a96e682ef..98061b31033767 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md @@ -47,10 +47,7 @@ static_assert(is_gradual_equivalent_to(Intersection[str | int, Not[type[Any]]], static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes)) static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict)) -# TODO: No errors -# error: [static-assert-error] static_assert(is_gradual_equivalent_to(Unknown, Unknown | Any)) -# error: [static-assert-error] static_assert(is_gradual_equivalent_to(Unknown, Intersection[Unknown, Any])) ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index aeb0c67b8e747c..b57443a5e49155 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1441,24 +1441,6 @@ impl<'db> Type<'db> { } } - /// Returns true if both `self` and `other` are the same gradual form - /// (limited to `Any`, `Unknown`, or `Todo`). - pub(crate) fn is_same_gradual_form(self, other: Type<'db>) -> bool { - matches!( - (self, other), - ( - Type::Dynamic(DynamicType::Any), - Type::Dynamic(DynamicType::Any) - ) | ( - Type::Dynamic(DynamicType::Unknown), - Type::Dynamic(DynamicType::Unknown) - ) | ( - Type::Dynamic(DynamicType::Todo(_)), - Type::Dynamic(DynamicType::Todo(_)) - ) - ) - } - /// Returns true if this type and `other` are gradual equivalent. /// /// > Two gradual types `A` and `B` are equivalent diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index f51f59b8718aa3..21171f11fb1c31 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -278,7 +278,7 @@ impl<'db> UnionBuilder<'db> { break; } - if ty.is_same_gradual_form(element_type) + if ty.is_gradual_equivalent_to(self.db, element_type) || ty.is_subtype_of(self.db, element_type) || element_type.is_object(self.db) { @@ -560,7 +560,7 @@ impl<'db> InnerIntersectionBuilder<'db> { for (index, existing_positive) in self.positive.iter().enumerate() { // S & T = S if S <: T if existing_positive.is_subtype_of(db, new_positive) - || existing_positive.is_same_gradual_form(new_positive) + || existing_positive.is_gradual_equivalent_to(db, new_positive) { return; } @@ -656,7 +656,9 @@ impl<'db> InnerIntersectionBuilder<'db> { let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_negative) in self.negative.iter().enumerate() { // ~S & ~T = ~T if S <: T - if existing_negative.is_subtype_of(db, new_negative) { + if existing_negative.is_subtype_of(db, new_negative) + || existing_negative.is_gradual_equivalent_to(db, new_negative) + { to_remove.push(index); } // same rule, reverse order From da6b68cb58824e52efa2a9ae7ebf3c2cbeacad2d Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Sat, 19 Apr 2025 10:12:48 +0900 Subject: [PATCH 0024/1161] [red-knot] infer attribute assignments bound in comprehensions (#17396) ## Summary This PR is a follow-up to #16852. Instance variables bound in comprehensions are recorded, allowing type inference to work correctly. This required adding support for unpacking in comprehension which resolves https://github.com/astral-sh/ruff/issues/15369. ## Test Plan One TODO in `mdtest/attributes.md` is now resolved, and some new test cases are added. --------- Co-authored-by: Dhruv Manilawala --- crates/red_knot_project/tests/check.rs | 12 +- .../resources/mdtest/attributes.md | 18 ++- .../resources/mdtest/unpacking.md | 92 +++++++++++++ .../src/semantic_index.rs | 2 +- .../src/semantic_index/builder.rs | 124 +++++++++++++----- .../src/semantic_index/definition.rs | 30 +++-- .../src/types/class.rs | 34 ++++- .../src/types/infer.rs | 103 ++++++++------- .../src/types/unpacker.rs | 22 +++- crates/red_knot_python_semantic/src/unpack.rs | 20 ++- 10 files changed, 349 insertions(+), 108 deletions(-) diff --git a/crates/red_knot_project/tests/check.rs b/crates/red_knot_project/tests/check.rs index 091908cf0c54d9..276612c3d81f93 100644 --- a/crates/red_knot_project/tests/check.rs +++ b/crates/red_knot_project/tests/check.rs @@ -6,7 +6,9 @@ use ruff_db::parsed::parsed_module; use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem}; use ruff_python_ast::visitor::source_order; use ruff_python_ast::visitor::source_order::SourceOrderVisitor; -use ruff_python_ast::{self as ast, Alias, Expr, Parameter, ParameterWithDefault, Stmt}; +use ruff_python_ast::{ + self as ast, Alias, Comprehension, Expr, Parameter, ParameterWithDefault, Stmt, +}; fn setup_db(project_root: &SystemPath, system: TestSystem) -> anyhow::Result { let project = ProjectMetadata::discover(project_root, &system)?; @@ -258,6 +260,14 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> { source_order::walk_expr(self, expr); } + fn visit_comprehension(&mut self, comprehension: &Comprehension) { + self.visit_expr(&comprehension.iter); + self.visit_target(&comprehension.target); + for if_expr in &comprehension.ifs { + self.visit_expr(if_expr); + } + } + fn visit_parameter(&mut self, parameter: &Parameter) { let _ty = parameter.inferred_type(&self.model); diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 37f0961461c14f..5077b0eb8f4f78 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -397,15 +397,27 @@ class IntIterable: def __iter__(self) -> IntIterator: return IntIterator() +class TupleIterator: + def __next__(self) -> tuple[int, str]: + return (1, "a") + +class TupleIterable: + def __iter__(self) -> TupleIterator: + return TupleIterator() + class C: def __init__(self) -> None: [... for self.a in IntIterable()] + [... for (self.b, self.c) in TupleIterable()] + [... for self.d in IntIterable() for self.e in IntIterable()] c_instance = C() -# TODO: Should be `Unknown | int` -# error: [unresolved-attribute] -reveal_type(c_instance.a) # revealed: Unknown +reveal_type(c_instance.a) # revealed: Unknown | int +reveal_type(c_instance.b) # revealed: Unknown | int +reveal_type(c_instance.c) # revealed: Unknown | str +reveal_type(c_instance.d) # revealed: Unknown | int +reveal_type(c_instance.e) # revealed: Unknown | int ``` #### Conditionally declared / bound attributes diff --git a/crates/red_knot_python_semantic/resources/mdtest/unpacking.md b/crates/red_knot_python_semantic/resources/mdtest/unpacking.md index 50a7a64388d3b1..6a0f3757375dd5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unpacking.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unpacking.md @@ -708,3 +708,95 @@ with ContextManager() as (a, b, c): reveal_type(b) # revealed: Unknown reveal_type(c) # revealed: Unknown ``` + +## Comprehension + +Unpacking in a comprehension. + +### Same types + +```py +def _(arg: tuple[tuple[int, int], tuple[int, int]]): + # revealed: tuple[int, int] + [reveal_type((a, b)) for a, b in arg] +``` + +### Mixed types (1) + +```py +def _(arg: tuple[tuple[int, int], tuple[int, str]]): + # revealed: tuple[int, int | str] + [reveal_type((a, b)) for a, b in arg] +``` + +### Mixed types (2) + +```py +def _(arg: tuple[tuple[int, str], tuple[str, int]]): + # revealed: tuple[int | str, str | int] + [reveal_type((a, b)) for a, b in arg] +``` + +### Mixed types (3) + +```py +def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, str]]): + # revealed: tuple[int, int | str, int | bytes | str] + [reveal_type((a, b, c)) for a, b, c in arg] +``` + +### Same literal values + +```py +# revealed: tuple[Literal[1, 3], Literal[2, 4]] +[reveal_type((a, b)) for a, b in ((1, 2), (3, 4))] +``` + +### Mixed literal values (1) + +```py +# revealed: tuple[Literal[1, "a"], Literal[2, "b"]] +[reveal_type((a, b)) for a, b in ((1, 2), ("a", "b"))] +``` + +### Mixed literals values (2) + +```py +# error: "Object of type `Literal[1]` is not iterable" +# error: "Object of type `Literal[2]` is not iterable" +# error: "Object of type `Literal[4]` is not iterable" +# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" +# revealed: tuple[Unknown | Literal[3, 5], Unknown | Literal["a", "b"]] +[reveal_type((a, b)) for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c")] +``` + +### Custom iterator (1) + +```py +class Iterator: + def __next__(self) -> tuple[int, int]: + return (1, 2) + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +# revealed: tuple[int, int] +[reveal_type((a, b)) for a, b in Iterable()] +``` + +### Custom iterator (2) + +```py +class Iterator: + def __next__(self) -> bytes: + return b"" + +class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + +def _(arg: tuple[tuple[int, str], Iterable]): + # revealed: tuple[int | bytes, str | bytes] + [reveal_type((a, b)) for a, b in arg] +``` diff --git a/crates/red_knot_python_semantic/src/semantic_index.rs b/crates/red_knot_python_semantic/src/semantic_index.rs index 7bcfa969fc26cd..0af319bb63e1df 100644 --- a/crates/red_knot_python_semantic/src/semantic_index.rs +++ b/crates/red_knot_python_semantic/src/semantic_index.rs @@ -940,7 +940,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): panic!("expected generator definition") }; let target = comprehension.target(); - let name = target.id().as_str(); + let name = target.as_name_expr().unwrap().id().as_str(); assert_eq!(name, "x"); assert_eq!(target.range(), TextRange::new(23.into(), 24.into())); diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index e1543428dc71ac..8098c8789ff1d4 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -18,11 +18,12 @@ use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; use crate::semantic_index::ast_ids::AstIdsBuilder; use crate::semantic_index::definition::{ AnnotatedAssignmentDefinitionKind, AnnotatedAssignmentDefinitionNodeRef, - AssignmentDefinitionKind, AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, - Definition, DefinitionCategory, DefinitionKind, DefinitionNodeKey, DefinitionNodeRef, - Definitions, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionKind, ForStmtDefinitionNodeRef, - ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef, - StarImportDefinitionNodeRef, TargetKind, WithItemDefinitionKind, WithItemDefinitionNodeRef, + AssignmentDefinitionKind, AssignmentDefinitionNodeRef, ComprehensionDefinitionKind, + ComprehensionDefinitionNodeRef, Definition, DefinitionCategory, DefinitionKind, + DefinitionNodeKey, DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef, + ForStmtDefinitionKind, ForStmtDefinitionNodeRef, ImportDefinitionNodeRef, + ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef, StarImportDefinitionNodeRef, + TargetKind, WithItemDefinitionKind, WithItemDefinitionNodeRef, }; use crate::semantic_index::expression::{Expression, ExpressionKind}; use crate::semantic_index::predicate::{ @@ -850,31 +851,35 @@ impl<'db> SemanticIndexBuilder<'db> { // The `iter` of the first generator is evaluated in the outer scope, while all subsequent // nodes are evaluated in the inner scope. - self.add_standalone_expression(&generator.iter); + let value = self.add_standalone_expression(&generator.iter); self.visit_expr(&generator.iter); self.push_scope(scope); - self.push_assignment(CurrentAssignment::Comprehension { - node: generator, - first: true, - }); - self.visit_expr(&generator.target); - self.pop_assignment(); + self.add_unpackable_assignment( + &Unpackable::Comprehension { + node: generator, + first: true, + }, + &generator.target, + value, + ); for expr in &generator.ifs { self.visit_expr(expr); } for generator in generators_iter { - self.add_standalone_expression(&generator.iter); + let value = self.add_standalone_expression(&generator.iter); self.visit_expr(&generator.iter); - self.push_assignment(CurrentAssignment::Comprehension { - node: generator, - first: false, - }); - self.visit_expr(&generator.target); - self.pop_assignment(); + self.add_unpackable_assignment( + &Unpackable::Comprehension { + node: generator, + first: false, + }, + &generator.target, + value, + ); for expr in &generator.ifs { self.visit_expr(expr); @@ -933,9 +938,30 @@ impl<'db> SemanticIndexBuilder<'db> { let current_assignment = match target { ast::Expr::List(_) | ast::Expr::Tuple(_) => { + if matches!(unpackable, Unpackable::Comprehension { .. }) { + debug_assert_eq!( + self.scopes[self.current_scope()].node().scope_kind(), + ScopeKind::Comprehension + ); + } + // The first iterator of the comprehension is evaluated in the outer scope, while all subsequent + // nodes are evaluated in the inner scope. + // SAFETY: The current scope is the comprehension, and the comprehension scope must have a parent scope. + let value_file_scope = + if let Unpackable::Comprehension { first: true, .. } = unpackable { + self.scope_stack + .iter() + .rev() + .nth(1) + .expect("The comprehension scope must have a parent scope") + .file_scope_id + } else { + self.current_scope() + }; let unpack = Some(Unpack::new( self.db, self.file, + value_file_scope, self.current_scope(), // SAFETY: `target` belongs to the `self.module` tree #[allow(unsafe_code)] @@ -1804,7 +1830,7 @@ where let node_key = NodeKey::from_node(expr); match expr { - ast::Expr::Name(name_node @ ast::ExprName { id, ctx, .. }) => { + ast::Expr::Name(ast::ExprName { id, ctx, .. }) => { let (is_use, is_definition) = match (ctx, self.current_assignment()) { (ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => { // For augmented assignment, the target expression is also used. @@ -1867,12 +1893,17 @@ where // implemented. self.add_definition(symbol, named); } - Some(CurrentAssignment::Comprehension { node, first }) => { + Some(CurrentAssignment::Comprehension { + unpack, + node, + first, + }) => { self.add_definition( symbol, ComprehensionDefinitionNodeRef { + unpack, iterable: &node.iter, - target: name_node, + target: expr, first, is_async: node.is_async, }, @@ -2143,14 +2174,37 @@ where DefinitionKind::WithItem(assignment), ); } - Some(CurrentAssignment::Comprehension { .. }) => { - // TODO: + Some(CurrentAssignment::Comprehension { + unpack, + node, + first, + }) => { + // SAFETY: `iter` and `expr` belong to the `self.module` tree + #[allow(unsafe_code)] + let assignment = ComprehensionDefinitionKind { + target_kind: TargetKind::from(unpack), + iterable: unsafe { + AstNodeRef::new(self.module.clone(), &node.iter) + }, + target: unsafe { AstNodeRef::new(self.module.clone(), expr) }, + first, + is_async: node.is_async, + }; + // Temporarily move to the scope of the method to which the instance attribute is defined. + // SAFETY: `self.scope_stack` is not empty because the targets in comprehensions should always introduce a new scope. + let scope = self.scope_stack.pop().expect("The popped scope must be a comprehension, which must have a parent scope"); + self.register_attribute_assignment( + object, + attr, + DefinitionKind::Comprehension(assignment), + ); + self.scope_stack.push(scope); } Some(CurrentAssignment::AugAssign(_)) => { // TODO: } Some(CurrentAssignment::Named(_)) => { - // TODO: + // A named expression whose target is an attribute is syntactically prohibited } None => {} } @@ -2244,6 +2298,7 @@ enum CurrentAssignment<'a> { Comprehension { node: &'a ast::Comprehension, first: bool, + unpack: Option<(UnpackPosition, Unpack<'a>)>, }, WithItem { item: &'a ast::WithItem, @@ -2257,11 +2312,9 @@ impl CurrentAssignment<'_> { match self { Self::Assign { unpack, .. } | Self::For { unpack, .. } - | Self::WithItem { unpack, .. } => unpack.as_mut().map(|(position, _)| position), - Self::AnnAssign(_) - | Self::AugAssign(_) - | Self::Named(_) - | Self::Comprehension { .. } => None, + | Self::WithItem { unpack, .. } + | Self::Comprehension { unpack, .. } => unpack.as_mut().map(|(position, _)| position), + Self::AnnAssign(_) | Self::AugAssign(_) | Self::Named(_) => None, } } } @@ -2316,13 +2369,17 @@ enum Unpackable<'a> { item: &'a ast::WithItem, is_async: bool, }, + Comprehension { + first: bool, + node: &'a ast::Comprehension, + }, } impl<'a> Unpackable<'a> { const fn kind(&self) -> UnpackKind { match self { Unpackable::Assign(_) => UnpackKind::Assign, - Unpackable::For(_) => UnpackKind::Iterable, + Unpackable::For(_) | Unpackable::Comprehension { .. } => UnpackKind::Iterable, Unpackable::WithItem { .. } => UnpackKind::ContextManager, } } @@ -2337,6 +2394,11 @@ impl<'a> Unpackable<'a> { is_async: *is_async, unpack, }, + Unpackable::Comprehension { node, first } => CurrentAssignment::Comprehension { + node, + first: *first, + unpack, + }, } } } diff --git a/crates/red_knot_python_semantic/src/semantic_index/definition.rs b/crates/red_knot_python_semantic/src/semantic_index/definition.rs index 9334ac69814b80..145e4b205ad446 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/definition.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/definition.rs @@ -281,8 +281,9 @@ pub(crate) struct ExceptHandlerDefinitionNodeRef<'a> { #[derive(Copy, Clone, Debug)] pub(crate) struct ComprehensionDefinitionNodeRef<'a> { + pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>, pub(crate) iterable: &'a ast::Expr, - pub(crate) target: &'a ast::ExprName, + pub(crate) target: &'a ast::Expr, pub(crate) first: bool, pub(crate) is_async: bool, } @@ -374,11 +375,13 @@ impl<'db> DefinitionNodeRef<'db> { is_async, }), DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef { + unpack, iterable, target, first, is_async, }) => DefinitionKind::Comprehension(ComprehensionDefinitionKind { + target_kind: TargetKind::from(unpack), iterable: AstNodeRef::new(parsed.clone(), iterable), target: AstNodeRef::new(parsed, target), first, @@ -474,7 +477,9 @@ impl<'db> DefinitionNodeRef<'db> { unpack: _, is_async: _, }) => DefinitionNodeKey(NodeKey::from_node(target)), - Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(), + Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => { + DefinitionNodeKey(NodeKey::from_node(target)) + } Self::VariadicPositionalParameter(node) => node.into(), Self::VariadicKeywordParameter(node) => node.into(), Self::Parameter(node) => node.into(), @@ -550,7 +555,7 @@ pub enum DefinitionKind<'db> { AnnotatedAssignment(AnnotatedAssignmentDefinitionKind), AugmentedAssignment(AstNodeRef), For(ForStmtDefinitionKind<'db>), - Comprehension(ComprehensionDefinitionKind), + Comprehension(ComprehensionDefinitionKind<'db>), VariadicPositionalParameter(AstNodeRef), VariadicKeywordParameter(AstNodeRef), Parameter(AstNodeRef), @@ -749,19 +754,24 @@ impl MatchPatternDefinitionKind { } #[derive(Clone, Debug)] -pub struct ComprehensionDefinitionKind { - iterable: AstNodeRef, - target: AstNodeRef, - first: bool, - is_async: bool, +pub struct ComprehensionDefinitionKind<'db> { + pub(super) target_kind: TargetKind<'db>, + pub(super) iterable: AstNodeRef, + pub(super) target: AstNodeRef, + pub(super) first: bool, + pub(super) is_async: bool, } -impl ComprehensionDefinitionKind { +impl<'db> ComprehensionDefinitionKind<'db> { pub(crate) fn iterable(&self) -> &ast::Expr { self.iterable.node() } - pub(crate) fn target(&self) -> &ast::ExprName { + pub(crate) fn target_kind(&self) -> TargetKind<'db> { + self.target_kind + } + + pub(crate) fn target(&self) -> &ast::Expr { self.target.node() } diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 2f57261650e3c6..8865a60950cdab 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -1416,14 +1416,42 @@ impl<'db> ClassLiteralType<'db> { } } } - DefinitionKind::Comprehension(_) => { - // TODO: + DefinitionKind::Comprehension(comprehension) => { + match comprehension.target_kind() { + TargetKind::Sequence(_, unpack) => { + // We found an unpacking assignment like: + // + // [... for .., self.name, .. in ] + + let unpacked = infer_unpack_types(db, unpack); + let target_ast_id = comprehension + .target() + .scoped_expression_id(db, unpack.target_scope(db)); + let inferred_ty = unpacked.expression_type(target_ast_id); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + TargetKind::NameOrAttribute => { + // We found an attribute assignment like: + // + // [... for self.name in ] + + let iterable_ty = infer_expression_type( + db, + index.expression(comprehension.iterable()), + ); + // TODO: Potential diagnostics resulting from the iterable are currently not reported. + let inferred_ty = iterable_ty.iterate(db); + + union_of_inferred_types = union_of_inferred_types.add(inferred_ty); + } + } } DefinitionKind::AugmentedAssignment(_) => { // TODO: } DefinitionKind::NamedExpression(_) => { - // TODO: + // A named expression whose target is an attribute is syntactically prohibited } _ => {} } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index e8f4ac2c6e4bb3..125655fff934d4 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -49,9 +49,9 @@ use crate::module_resolver::resolve_module; use crate::node_key::NodeKey; use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId, ScopedExpressionId}; use crate::semantic_index::definition::{ - AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, Definition, DefinitionKind, - DefinitionNodeKey, ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind, - WithItemDefinitionKind, + AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, ComprehensionDefinitionKind, + Definition, DefinitionKind, DefinitionNodeKey, ExceptHandlerDefinitionKind, + ForStmtDefinitionKind, TargetKind, WithItemDefinitionKind, }; use crate::semantic_index::expression::{Expression, ExpressionKind}; use crate::semantic_index::symbol::{ @@ -306,7 +306,7 @@ pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> U let _span = tracing::trace_span!("infer_unpack_types", range=?unpack.range(db), ?file).entered(); - let mut unpacker = Unpacker::new(db, unpack.scope(db)); + let mut unpacker = Unpacker::new(db, unpack.target_scope(db), unpack.value_scope(db)); unpacker.unpack(unpack.target(db), unpack.value(db)); unpacker.finish() } @@ -946,13 +946,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_named_expression_definition(named_expression.node(), definition); } DefinitionKind::Comprehension(comprehension) => { - self.infer_comprehension_definition( - comprehension.iterable(), - comprehension.target(), - comprehension.is_first(), - comprehension.is_async(), - definition, - ); + self.infer_comprehension_definition(comprehension, definition); } DefinitionKind::VariadicPositionalParameter(parameter) => { self.infer_variadic_positional_parameter_definition(parameter, definition); @@ -1937,11 +1931,13 @@ impl<'db> TypeInferenceBuilder<'db> { for item in items { let target = item.optional_vars.as_deref(); if let Some(target) = target { - self.infer_target(target, &item.context_expr, |db, ctx_manager_ty| { + self.infer_target(target, &item.context_expr, |builder, context_expr| { // TODO: `infer_with_statement_definition` reports a diagnostic if `ctx_manager_ty` isn't a context manager // but only if the target is a name. We should report a diagnostic here if the target isn't a name: // `with not_context_manager as a.x: ... - ctx_manager_ty.enter(db) + builder + .infer_standalone_expression(context_expr) + .enter(builder.db()) }); } else { // Call into the context expression inference to validate that it evaluates @@ -2347,7 +2343,9 @@ impl<'db> TypeInferenceBuilder<'db> { } = assignment; for target in targets { - self.infer_target(target, value, |_, ty| ty); + self.infer_target(target, value, |builder, value_expr| { + builder.infer_standalone_expression(value_expr) + }); } } @@ -2357,23 +2355,16 @@ impl<'db> TypeInferenceBuilder<'db> { /// targets (unpacking). If `target` is an attribute expression, we check that the assignment /// is valid. For 'target's that are definitions, this check happens elsewhere. /// - /// The `to_assigned_ty` function is used to convert the inferred type of the `value` expression - /// to the type that is eventually assigned to the `target`. - /// - /// # Panics - /// - /// If the `value` is not a standalone expression. - fn infer_target(&mut self, target: &ast::Expr, value: &ast::Expr, to_assigned_ty: F) + /// The `infer_value_expr` function is used to infer the type of the `value` expression which + /// are not `Name` expressions. The returned type is the one that is eventually assigned to the + /// `target`. + fn infer_target(&mut self, target: &ast::Expr, value: &ast::Expr, infer_value_expr: F) where - F: Fn(&'db dyn Db, Type<'db>) -> Type<'db>, + F: Fn(&mut TypeInferenceBuilder<'db>, &ast::Expr) -> Type<'db>, { let assigned_ty = match target { ast::Expr::Name(_) => None, - _ => { - let value_ty = self.infer_standalone_expression(value); - - Some(to_assigned_ty(self.db(), value_ty)) - } + _ => Some(infer_value_expr(self, value)), }; self.infer_target_impl(target, assigned_ty); } @@ -3126,11 +3117,13 @@ impl<'db> TypeInferenceBuilder<'db> { is_async: _, } = for_statement; - self.infer_target(target, iter, |db, iter_ty| { + self.infer_target(target, iter, |builder, iter_expr| { // TODO: `infer_for_statement_definition` reports a diagnostic if `iter_ty` isn't iterable // but only if the target is a name. We should report a diagnostic here if the target isn't a name: // `for a.x in not_iterable: ... - iter_ty.iterate(db) + builder + .infer_standalone_expression(iter_expr) + .iterate(builder.db()) }); self.infer_body(body); @@ -3959,15 +3952,17 @@ impl<'db> TypeInferenceBuilder<'db> { is_async: _, } = comprehension; - if !is_first { - self.infer_standalone_expression(iter); - } - // TODO more complex assignment targets - if let ast::Expr::Name(name) = target { - self.infer_definition(name); - } else { - self.infer_expression(target); - } + self.infer_target(target, iter, |builder, iter_expr| { + // TODO: `infer_comprehension_definition` reports a diagnostic if `iter_ty` isn't iterable + // but only if the target is a name. We should report a diagnostic here if the target isn't a name: + // `[... for a.x in not_iterable] + if is_first { + infer_same_file_expression_type(builder.db(), builder.index.expression(iter_expr)) + } else { + builder.infer_standalone_expression(iter_expr) + } + .iterate(builder.db()) + }); for expr in ifs { self.infer_expression(expr); } @@ -3975,12 +3970,12 @@ impl<'db> TypeInferenceBuilder<'db> { fn infer_comprehension_definition( &mut self, - iterable: &ast::Expr, - target: &ast::ExprName, - is_first: bool, - is_async: bool, + comprehension: &ComprehensionDefinitionKind<'db>, definition: Definition<'db>, ) { + let iterable = comprehension.iterable(); + let target = comprehension.target(); + let expression = self.index.expression(iterable); let result = infer_expression_types(self.db(), expression); @@ -3990,7 +3985,7 @@ impl<'db> TypeInferenceBuilder<'db> { // (2) We must *not* call `self.extend()` on the result of the type inference, // because `ScopedExpressionId`s are only meaningful within their own scope, so // we'd add types for random wrong expressions in the current scope - let iterable_type = if is_first { + let iterable_type = if comprehension.is_first() { let lookup_scope = self .index .parent_scope_id(self.scope().file_scope_id(self.db())) @@ -4002,14 +3997,26 @@ impl<'db> TypeInferenceBuilder<'db> { result.expression_type(iterable.scoped_expression_id(self.db(), self.scope())) }; - let target_type = if is_async { + let target_type = if comprehension.is_async() { // TODO: async iterables/iterators! -- Alex todo_type!("async iterables/iterators") } else { - iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { - err.report_diagnostic(&self.context, iterable_type, iterable.into()); - err.fallback_element_type(self.db()) - }) + match comprehension.target_kind() { + TargetKind::Sequence(unpack_position, unpack) => { + let unpacked = infer_unpack_types(self.db(), unpack); + if unpack_position == UnpackPosition::First { + self.context.extend(unpacked.diagnostics()); + } + let target_ast_id = target.scoped_expression_id(self.db(), self.scope()); + unpacked.expression_type(target_ast_id) + } + TargetKind::NameOrAttribute => { + iterable_type.try_iterate(self.db()).unwrap_or_else(|err| { + err.report_diagnostic(&self.context, iterable_type, iterable.into()); + err.fallback_element_type(self.db()) + }) + } + } }; self.types.expressions.insert( diff --git a/crates/red_knot_python_semantic/src/types/unpacker.rs b/crates/red_knot_python_semantic/src/types/unpacker.rs index a711357c879b88..ecaf1bdeaba4c3 100644 --- a/crates/red_knot_python_semantic/src/types/unpacker.rs +++ b/crates/red_knot_python_semantic/src/types/unpacker.rs @@ -18,16 +18,22 @@ use super::{TupleType, UnionType}; /// Unpacks the value expression type to their respective targets. pub(crate) struct Unpacker<'db> { context: InferContext<'db>, - scope: ScopeId<'db>, + target_scope: ScopeId<'db>, + value_scope: ScopeId<'db>, targets: FxHashMap>, } impl<'db> Unpacker<'db> { - pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self { + pub(crate) fn new( + db: &'db dyn Db, + target_scope: ScopeId<'db>, + value_scope: ScopeId<'db>, + ) -> Self { Self { - context: InferContext::new(db, scope), + context: InferContext::new(db, target_scope), targets: FxHashMap::default(), - scope, + target_scope, + value_scope, } } @@ -43,7 +49,7 @@ impl<'db> Unpacker<'db> { ); let value_type = infer_expression_types(self.db(), value.expression()) - .expression_type(value.scoped_expression_id(self.db(), self.scope)); + .expression_type(value.scoped_expression_id(self.db(), self.value_scope)); let value_type = match value.kind() { UnpackKind::Assign => { @@ -79,8 +85,10 @@ impl<'db> Unpacker<'db> { ) { match target { ast::Expr::Name(_) | ast::Expr::Attribute(_) => { - self.targets - .insert(target.scoped_expression_id(self.db(), self.scope), value_ty); + self.targets.insert( + target.scoped_expression_id(self.db(), self.target_scope), + value_ty, + ); } ast::Expr::Starred(ast::ExprStarred { value, .. }) => { self.unpack_inner(value, value_expr, value_ty); diff --git a/crates/red_knot_python_semantic/src/unpack.rs b/crates/red_knot_python_semantic/src/unpack.rs index 4dadab3397a19f..20081fe83fcd90 100644 --- a/crates/red_knot_python_semantic/src/unpack.rs +++ b/crates/red_knot_python_semantic/src/unpack.rs @@ -30,7 +30,9 @@ use crate::Db; pub(crate) struct Unpack<'db> { pub(crate) file: File, - pub(crate) file_scope: FileScopeId, + pub(crate) value_file_scope: FileScopeId, + + pub(crate) target_file_scope: FileScopeId, /// The target expression that is being unpacked. For example, in `(a, b) = (1, 2)`, the target /// expression is `(a, b)`. @@ -47,9 +49,19 @@ pub(crate) struct Unpack<'db> { } impl<'db> Unpack<'db> { - /// Returns the scope where the unpacking is happening. - pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.file(db)) + /// Returns the scope in which the unpack value expression belongs. + /// + /// The scope in which the target and value expression belongs to are usually the same + /// except in generator expressions and comprehensions (list/dict/set), where the value + /// expression of the first generator is evaluated in the outer scope, while the ones in the subsequent + /// generators are evaluated in the comprehension scope. + pub(crate) fn value_scope(self, db: &'db dyn Db) -> ScopeId<'db> { + self.value_file_scope(db).to_scope_id(db, self.file(db)) + } + + /// Returns the scope where the unpack target expression belongs to. + pub(crate) fn target_scope(self, db: &'db dyn Db) -> ScopeId<'db> { + self.target_file_scope(db).to_scope_id(db, self.file(db)) } /// Returns the range of the unpack target expression. From 1445836872340e812ff0b631677e33e1482f0ad3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 20 Apr 2025 21:50:26 -0400 Subject: [PATCH 0025/1161] Update taiki-e/install-action digest to 09dc018 (#17503) --- .github/workflows/ci.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1ca4d12bb455d1..95c4c6b18cfb24 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -239,11 +239,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: tool: cargo-insta - name: Red-knot mdtests (GitHub annotations) @@ -293,11 +293,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: tool: cargo-insta - name: "Run tests" @@ -320,7 +320,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: tool: cargo-nextest - name: "Run tests" @@ -403,11 +403,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: tool: cargo-insta - name: "Run tests" @@ -857,7 +857,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2 + uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: tool: cargo-codspeed From 4cafb44ba7f8dd86764687fac09332bf6aa19f2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 20 Apr 2025 21:50:39 -0400 Subject: [PATCH 0026/1161] Update astral-sh/setup-uv action to v5.4.2 (#17504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) | action | patch | `v5.4.1` -> `v5.4.2` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
astral-sh/setup-uv (astral-sh/setup-uv) ### [`v5.4.2`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v5.4.2): 🌈 Make sure uv installed by setup-uv is first in PATH [Compare Source](https://redirect.github.com/astral-sh/setup-uv/compare/v5.4.1...v5.4.2) ##### Changes This release fixes an issue on self-hosted runners. If you manually installed uv with version 0.5.0 or later this version would overwrite the uv version installed by this action. We now make sure the version installed by this action is the first found in PATH ##### 🐛 Bug fixes - Make sure uv installed by setup-uv is first in PATH [@​eifinger](https://redirect.github.com/eifinger) ([#​373](https://redirect.github.com/astral-sh/setup-uv/issues/373)) ##### 🧰 Maintenance - chore: update known checksums for 0.6.14 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​366](https://redirect.github.com/astral-sh/setup-uv/issues/366)) - chore: update known checksums for 0.6.13 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​365](https://redirect.github.com/astral-sh/setup-uv/issues/365)) - chore: update known checksums for 0.6.12 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​362](https://redirect.github.com/astral-sh/setup-uv/issues/362)) - chore: update known checksums for 0.6.11 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​357](https://redirect.github.com/astral-sh/setup-uv/issues/357)) ##### 📚 Documentation - Fix pep440 identifier instead of specifier [@​eifinger](https://redirect.github.com/eifinger) ([#​358](https://redirect.github.com/astral-sh/setup-uv/issues/358))
--- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 6 +++--- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/mypy_primer.yaml | 2 +- .github/workflows/publish-pypi.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 95c4c6b18cfb24..f4e859e5a94ec0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -455,7 +455,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 name: Download Ruff binary to test id: download-cached-binary @@ -681,7 +681,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - name: "Cache pre-commit" uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: @@ -720,7 +720,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: Install uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - name: "Install Insiders dependencies" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} run: uv pip install -r docs/requirements-insiders.txt --system diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index a32ff088d9a54d..100342f179bbf5 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - name: "Install Rust toolchain" run: rustup show - name: "Install mold" diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index c2329ae126d398..490ae970faa4e5 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -35,7 +35,7 @@ jobs: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 with: diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 822a4086e6ade2..6070d9f29b859b 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -22,7 +22,7 @@ jobs: id-token: write steps: - name: "Install uv" - uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: pattern: wheels-* From ada7d4da0d2d9e188f8237615598f0a990bbbd91 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 20 Apr 2025 21:51:27 -0400 Subject: [PATCH 0027/1161] Update Rust crate clap to v4.5.37 (#17507) --- Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5016ae2e2b4872..33b11854e285c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,9 +334,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.36" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.36" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -918,7 +918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1499,7 +1499,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi 0.5.0", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3428,7 +3428,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3441,7 +3441,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.3", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3827,7 +3827,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.0.2", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] From 14ff67fd46efb7cbdc61277f92e30d7acb08c311 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 20 Apr 2025 21:51:31 -0400 Subject: [PATCH 0028/1161] Update Rust crate jiff to v0.2.9 (#17508) --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33b11854e285c0..61189f1a49e1fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1553,9 +1553,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.4" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" +checksum = "59ec30f7142be6fe14e1b021f50b85db8df2d4324ea6e91ec3e5dcde092021d0" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1563,14 +1563,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "jiff-static" -version = "0.2.4" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" +checksum = "526b834d727fd59d37b076b0c3236d9adde1b1729a4361e20b2026f738cc1dbe" dependencies = [ "proc-macro2", "quote", From a56eef444a627100259704d29d36ef53503409e8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 20 Apr 2025 21:51:51 -0400 Subject: [PATCH 0029/1161] Update Rust crate libc to v0.2.172 (#17509) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 61189f1a49e1fc..de08b4658aaccd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1645,9 +1645,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libcst" From d11e959ad58688e6bb4acdb4bcecd1a8b2518f2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 01:57:27 +0000 Subject: [PATCH 0030/1161] Update Rust crate rand to v0.9.1 (#17511) --- Cargo.lock | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de08b4658aaccd..1819f42b3fd781 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2420,13 +2420,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy", ] [[package]] @@ -3084,7 +3083,7 @@ version = "0.0.0" dependencies = [ "anyhow", "itertools 0.14.0", - "rand 0.9.0", + "rand 0.9.1", "ruff_diagnostics", "ruff_source_file", "ruff_text_size", @@ -4328,7 +4327,7 @@ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ "getrandom 0.3.2", "js-sys", - "rand 0.9.0", + "rand 0.9.1", "uuid-macro-internal", "wasm-bindgen", ] From f888e51a34602a411e645e6cb1569fa69eaead74 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 20 Apr 2025 21:57:44 -0400 Subject: [PATCH 0031/1161] Update Rust crate proc-macro2 to v1.0.95 (#17510) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1819f42b3fd781..2dfd0249618805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2327,9 +2327,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] From 8a2dd01db484f44740d253170a13097055835ea3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 01:59:02 +0000 Subject: [PATCH 0032/1161] Update Rust crate shellexpand to v3.1.1 (#17512) --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2dfd0249618805..aa0234297c0ce3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -487,7 +487,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3674,9 +3674,9 @@ dependencies = [ [[package]] name = "shellexpand" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" dependencies = [ "dirs", ] @@ -4598,7 +4598,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] From c077b109ce51317ba975baa230a2271e5e59b5d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 09:49:22 +0100 Subject: [PATCH 0033/1161] Update dependency ruff to v0.11.6 (#17516) --- docs/requirements-insiders.txt | 2 +- docs/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements-insiders.txt b/docs/requirements-insiders.txt index 3f423e6ee858d9..be4bb310d14b68 100644 --- a/docs/requirements-insiders.txt +++ b/docs/requirements-insiders.txt @@ -1,5 +1,5 @@ PyYAML==6.0.2 -ruff==0.9.10 +ruff==0.11.6 mkdocs==1.6.1 mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@39da7a5e761410349e9a1b8abf593b0cdd5453ff mkdocs-redirects==1.2.2 diff --git a/docs/requirements.txt b/docs/requirements.txt index fadd51dfd7c5af..4c241d1be332ee 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.2 -ruff==0.9.10 +ruff==0.11.6 mkdocs==1.6.1 mkdocs-material==9.5.38 mkdocs-redirects==1.2.2 From 9ff4772a2c60daa54c526a34e7562ff68650a17f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 21 Apr 2025 16:17:06 +0100 Subject: [PATCH 0034/1161] [red-knot] Correctly identify protocol classes (#17487) --- .../resources/mdtest/function/return_type.md | 2 - .../resources/mdtest/protocols.md | 78 ++++++++++++++----- .../src/types/call/bind.rs | 9 +++ .../src/types/class.rs | 11 +++ .../src/types/infer.rs | 26 +++---- 5 files changed, 90 insertions(+), 36 deletions(-) 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 f3ce3f4b200f7c..1365807e95b08e 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 @@ -74,8 +74,6 @@ class Baz(Bar): T = TypeVar("T") class Qux(Protocol[T]): - # TODO: no error - # error: [invalid-return-type] def f(self) -> int: ... class Foo(Protocol): diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index ee375cd0cb3dae..53f4d14b4f3651 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -40,27 +40,63 @@ class Foo(Protocol, Protocol): ... # error: [inconsistent-mro] reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] ``` +Protocols can also be generic, either by including `Generic[]` in the bases list, subscripting +`Protocol` directly in the bases list, using PEP-695 type parameters, or some combination of the +above: + +```py +from typing import TypeVar, Generic + +T = TypeVar("T") + +class Bar0(Protocol[T]): + x: T + +class Bar1(Protocol[T], Generic[T]): + x: T + +class Bar2[T](Protocol): + x: T + +class Bar3[T](Protocol[T]): + x: T +``` + +It's an error to include both bare `Protocol` and subscripted `Protocol[]` in the bases list +simultaneously: + +```py +# TODO: should emit a `[duplicate-bases]` error here: +class DuplicateBases(Protocol, Protocol[T]): + x: T + +# TODO: should not have `Generic` multiple times and `Protocol` multiple times +# revealed: tuple[Literal[DuplicateBases], typing.Protocol, typing.Generic, @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] +reveal_type(DuplicateBases.__mro__) +``` + The introspection helper `typing(_extensions).is_protocol` can be used to verify whether a class is a protocol class or not: ```py from typing_extensions import is_protocol -# TODO: should be `Literal[True]` -reveal_type(is_protocol(MyProtocol)) # revealed: bool +reveal_type(is_protocol(MyProtocol)) # revealed: Literal[True] +reveal_type(is_protocol(Bar0)) # revealed: Literal[True] +reveal_type(is_protocol(Bar1)) # revealed: Literal[True] +reveal_type(is_protocol(Bar2)) # revealed: Literal[True] +reveal_type(is_protocol(Bar3)) # revealed: Literal[True] class NotAProtocol: ... -# TODO: should be `Literal[False]` -reveal_type(is_protocol(NotAProtocol)) # revealed: bool +reveal_type(is_protocol(NotAProtocol)) # revealed: Literal[False] ``` A type checker should follow the typeshed stubs if a non-class is passed in, and typeshed's stubs -indicate that the argument passed in must be an instance of `type`. `Literal[False]` should be -inferred as the return type, however. +indicate that the argument passed in must be an instance of `type`. ```py -# TODO: the diagnostic is correct, but should infer `Literal[False]` +# We could also reasonably infer `Literal[False]` here, but it probably doesn't matter that much: # error: [invalid-argument-type] reveal_type(is_protocol("not a class")) # revealed: bool ``` @@ -74,8 +110,7 @@ class SubclassOfMyProtocol(MyProtocol): ... # revealed: tuple[Literal[SubclassOfMyProtocol], Literal[MyProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(SubclassOfMyProtocol.__mro__) -# TODO: should be `Literal[False]` -reveal_type(is_protocol(SubclassOfMyProtocol)) # revealed: bool +reveal_type(is_protocol(SubclassOfMyProtocol)) # revealed: Literal[False] ``` A protocol class may inherit from other protocols, however, as long as it re-inherits from @@ -84,8 +119,7 @@ A protocol class may inherit from other protocols, however, as long as it re-inh ```py class SubProtocol(MyProtocol, Protocol): ... -# TODO: should be `Literal[True]` -reveal_type(is_protocol(SubProtocol)) # revealed: bool +reveal_type(is_protocol(SubProtocol)) # revealed: Literal[True] class OtherProtocol(Protocol): some_attribute: str @@ -95,8 +129,7 @@ class ComplexInheritance(SubProtocol, OtherProtocol, Protocol): ... # revealed: tuple[Literal[ComplexInheritance], Literal[SubProtocol], Literal[MyProtocol], Literal[OtherProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(ComplexInheritance.__mro__) -# TODO: should be `Literal[True]` -reveal_type(is_protocol(ComplexInheritance)) # revealed: bool +reveal_type(is_protocol(ComplexInheritance)) # revealed: Literal[True] ``` If `Protocol` is present in the bases tuple, all other bases in the tuple must be protocol classes, @@ -134,6 +167,8 @@ reveal_type(Fine.__mro__) # revealed: tuple[Literal[Fine], typing.Protocol, typ class StillFine(Protocol, Generic[T], object): ... class EvenThis[T](Protocol, object): ... +class OrThis(Protocol[T], Generic[T]): ... +class AndThis(Protocol[T], Generic[T], object): ... ``` And multiple inheritance from a mix of protocol and non-protocol classes is fine as long as @@ -150,8 +185,7 @@ But if `Protocol` is not present in the bases list, the resulting class doesn't class anymore: ```py -# TODO: should reveal `Literal[False]` -reveal_type(is_protocol(FineAndDandy)) # revealed: bool +reveal_type(is_protocol(FineAndDandy)) # revealed: Literal[False] ``` A class does not *have* to inherit from a protocol class in order for it to be considered a subtype @@ -230,9 +264,10 @@ class Foo(typing.Protocol): class Bar(typing_extensions.Protocol): x: int -# TODO: these should pass -static_assert(typing_extensions.is_protocol(Foo)) # error: [static-assert-error] -static_assert(typing_extensions.is_protocol(Bar)) # error: [static-assert-error] +static_assert(typing_extensions.is_protocol(Foo)) +static_assert(typing_extensions.is_protocol(Bar)) + +# TODO: should pass static_assert(is_equivalent_to(Foo, Bar)) # error: [static-assert-error] ``` @@ -247,9 +282,10 @@ class RuntimeCheckableFoo(typing.Protocol): class RuntimeCheckableBar(typing_extensions.Protocol): x: int -# TODO: these should pass -static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo)) # error: [static-assert-error] -static_assert(typing_extensions.is_protocol(RuntimeCheckableBar)) # error: [static-assert-error] +static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo)) +static_assert(typing_extensions.is_protocol(RuntimeCheckableBar)) + +# TODO: should pass static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar)) # error: [static-assert-error] # These should not error because the protocols are decorated with `@runtime_checkable` diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 7232a08f8daff8..a577dba34b31c4 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -535,6 +535,15 @@ impl<'db> Bindings<'db> { } } + Some(KnownFunction::IsProtocol) => { + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty.into_class_literal() + .is_some_and(|class| class.is_protocol(db)), + )); + } + } + Some(KnownFunction::Overload) => { // TODO: This can be removed once we understand legacy generics because the // typeshed definition for `typing.overload` is an identity function. diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 8865a60950cdab..23a4a1fde635a0 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -582,6 +582,17 @@ impl<'db> ClassLiteralType<'db> { .collect() } + /// Determine if this class is a protocol. + pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool { + self.explicit_bases(db).iter().any(|base| { + matches!( + base, + Type::KnownInstance(KnownInstanceType::Protocol) + | Type::Dynamic(DynamicType::SubscriptedProtocol) + ) + }) + } + /// Return the types of the decorators on this class #[salsa::tracked(return_ref)] fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 125655fff934d4..1733de42e4b145 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -81,9 +81,9 @@ use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ - todo_type, CallDunderError, CallableSignature, CallableType, Class, ClassLiteralType, - ClassType, DataclassMetadata, DynamicType, FunctionDecorators, FunctionType, GenericAlias, - GenericClass, IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, + binding_type, todo_type, CallDunderError, CallableSignature, CallableType, Class, + ClassLiteralType, ClassType, DataclassMetadata, DynamicType, FunctionDecorators, FunctionType, + GenericAlias, GenericClass, IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, NonGenericClass, Parameter, ParameterForm, Parameters, Signature, Signatures, SliceLiteralType, StringLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, @@ -1224,7 +1224,7 @@ impl<'db> TypeInferenceBuilder<'db> { /// Returns `true` if the current scope is the function body scope of a method of a protocol /// (that is, a class which directly inherits `typing.Protocol`.) - fn in_class_that_inherits_protocol_directly(&self) -> bool { + fn in_protocol_class(&self) -> bool { let current_scope_id = self.scope().file_scope_id(self.db()); let current_scope = self.index.scope(current_scope_id); let Some(parent_scope_id) = current_scope.parent() else { @@ -1252,13 +1252,13 @@ impl<'db> TypeInferenceBuilder<'db> { return false; }; - // TODO move this to `Class` once we add proper `Protocol` support - node_ref.bases().iter().any(|base| { - matches!( - self.file_expression_type(base), - Type::KnownInstance(KnownInstanceType::Protocol) - ) - }) + let class_definition = self.index.expect_single_definition(node_ref.node()); + + let Type::ClassLiteral(class) = binding_type(self.db(), class_definition) else { + return false; + }; + + class.is_protocol(self.db()) } /// Returns `true` if the current scope is the function body scope of a function overload (that @@ -1322,7 +1322,7 @@ impl<'db> TypeInferenceBuilder<'db> { if (self.in_stub() || self.in_function_overload_or_abstractmethod() - || self.in_class_that_inherits_protocol_directly()) + || self.in_protocol_class()) && self.return_types_and_ranges.is_empty() && is_stub_suite(&function.body) { @@ -1625,7 +1625,7 @@ impl<'db> TypeInferenceBuilder<'db> { } } else if (self.in_stub() || self.in_function_overload_or_abstractmethod() - || self.in_class_that_inherits_protocol_directly()) + || self.in_protocol_class()) && default .as_ref() .is_some_and(|d| d.is_ellipsis_literal_expr()) From 45b5dedee2782d8e4eaaeb8239e94f76da6f85ff Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 21 Apr 2025 16:24:19 +0100 Subject: [PATCH 0035/1161] [red-knot] Detect (some) invalid protocols (#17488) --- .../resources/mdtest/protocols.md | 4 +- .../src/types/diagnostic.rs | 29 +++++++++++ .../src/types/infer.rs | 52 +++++++++++++------ knot.schema.json | 10 ++++ 4 files changed, 76 insertions(+), 19 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 53f4d14b4f3651..01b63d95f09d34 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -136,13 +136,13 @@ If `Protocol` is present in the bases tuple, all other bases in the tuple must b or `TypeError` is raised at runtime when the class is created. ```py -# TODO: should emit `[invalid-protocol]` +# error: [invalid-protocol] "Protocol class `Invalid` cannot inherit from non-protocol class `NotAProtocol`" class Invalid(NotAProtocol, Protocol): ... # revealed: tuple[Literal[Invalid], Literal[NotAProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(Invalid.__mro__) -# TODO: should emit an `[invalid-protocol`] error +# error: [invalid-protocol] "Protocol class `AlsoInvalid` cannot inherit from non-protocol class `NotAProtocol`" class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ... # revealed: tuple[Literal[AlsoInvalid], Literal[MyProtocol], Literal[OtherProtocol], Literal[NotAProtocol], typing.Protocol, typing.Generic, Literal[object]] diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index bca2c739a8d4e2..93458cd72d729a 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -36,6 +36,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_EXCEPTION_CAUGHT); registry.register_lint(&INVALID_METACLASS); registry.register_lint(&INVALID_PARAMETER_DEFAULT); + registry.register_lint(&INVALID_PROTOCOL); registry.register_lint(&INVALID_RAISE); registry.register_lint(&INVALID_SUPER_ARGUMENT); registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT); @@ -230,6 +231,34 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for invalidly defined protocol classes. + /// + /// ## Why is this bad? + /// An invalidly defined protocol class may lead to the type checker inferring + /// unexpected things. It may also lead to `TypeError`s at runtime. + /// + /// ## Examples + /// A `Protocol` class cannot inherit from a non-`Protocol` class; + /// this raises a `TypeError` at runtime: + /// + /// ```pycon + /// >>> from typing import Protocol + /// >>> class Foo(int, Protocol): ... + /// ... + /// Traceback (most recent call last): + /// File "", line 1, in + /// class Foo(int, Protocol): ... + /// TypeError: Protocols can only inherit from other protocols, got + /// ``` + pub(crate) static INVALID_PROTOCOL = { + summary: "detects invalid protocol class definitions", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + declare_lint! { /// TODO #14889 pub(crate) static INCONSISTENT_MRO = { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 1733de42e4b145..466fe9be5aece8 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -99,8 +99,8 @@ use super::diagnostic::{ report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_type_checking_constant, report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero, - report_unresolved_reference, INVALID_METACLASS, REDUNDANT_CAST, STATIC_ASSERT_ERROR, - SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, + report_unresolved_reference, INVALID_METACLASS, INVALID_PROTOCOL, REDUNDANT_CAST, + STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, }; use super::slots::check_class_slots; use super::string_annotation::{ @@ -763,17 +763,21 @@ impl<'db> TypeInferenceBuilder<'db> { continue; } - // (2) Check for inheritance from plain `Generic`, - // and from classes that inherit from `@final` classes + let is_protocol = class.is_protocol(self.db()); + + // (2) Iterate through the class's explicit bases to check for various possible errors: + // - Check for inheritance from plain `Generic`, + // - Check for inheritance from a `@final` classes + // - If the class is a protocol class: check for inheritance from a non-protocol class for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() { let base_class = match base_class { Type::KnownInstance(KnownInstanceType::Generic) => { - // `Generic` can appear in the MRO of many classes, + // Unsubscripted `Generic` can appear in the MRO of many classes, // but it is never valid as an explicit base class in user code. self.context.report_lint_old( &INVALID_BASE, &class_node.bases()[i], - format_args!("Cannot inherit from plain `Generic`",), + format_args!("Cannot inherit from plain `Generic`"), ); continue; } @@ -782,18 +786,32 @@ impl<'db> TypeInferenceBuilder<'db> { _ => continue, }; - if !base_class.is_final(self.db()) { - continue; + if is_protocol + && !(base_class.is_protocol(self.db()) + || base_class.is_known(self.db(), KnownClass::Object)) + { + self.context.report_lint_old( + &INVALID_PROTOCOL, + &class_node.bases()[i], + format_args!( + "Protocol class `{}` cannot inherit from non-protocol class `{}`", + class.name(self.db()), + base_class.name(self.db()), + ), + ); + } + + if base_class.is_final(self.db()) { + self.context.report_lint_old( + &SUBCLASS_OF_FINAL_CLASS, + &class_node.bases()[i], + format_args!( + "Class `{}` cannot inherit from final class `{}`", + class.name(self.db()), + base_class.name(self.db()), + ), + ); } - self.context.report_lint_old( - &SUBCLASS_OF_FINAL_CLASS, - &class_node.bases()[i], - format_args!( - "Class `{}` cannot inherit from final class `{}`", - class.name(self.db()), - base_class.name(self.db()), - ), - ); } // (3) Check that the class's MRO is resolvable diff --git a/knot.schema.json b/knot.schema.json index 5911ab01d31089..941eee5af93e72 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -460,6 +460,16 @@ } ] }, + "invalid-protocol": { + "title": "detects invalid protocol class definitions", + "description": "## What it does\nChecks for invalidly defined protocol classes.\n\n## Why is this bad?\nAn invalidly defined protocol class may lead to the type checker inferring\nunexpected things. It may also lead to `TypeError`s at runtime.\n\n## Examples\nA `Protocol` class cannot inherit from a non-`Protocol` class;\nthis raises a `TypeError` at runtime:\n\n```pycon\n>>> from typing import Protocol\n>>> class Foo(int, Protocol): ...\n...\nTraceback (most recent call last):\n File \"\", line 1, in \n class Foo(int, Protocol): ...\nTypeError: Protocols can only inherit from other protocols, got \n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-raise": { "title": "detects `raise` statements that raise invalid exceptions or use invalid causes", "description": "Checks for `raise` statements that raise non-exceptions or use invalid\ncauses for their raised exceptions.\n\n## Why is this bad?\nOnly subclasses or instances of `BaseException` can be raised.\nFor an exception's cause, the same rules apply, except that `None` is also\npermitted. Violating these rules results in a `TypeError` at runtime.\n\n## Examples\n```python\ndef f():\n try:\n something()\n except NameError:\n raise \"oops!\" from f\n\ndef g():\n raise NotImplemented from 42\n```\n\nUse instead:\n```python\ndef f():\n try:\n something()\n except NameError as e:\n raise RuntimeError(\"oops!\") from e\n\ndef g():\n raise NotImplementedError from None\n```\n\n## References\n- [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#raise)\n- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)", From be54b840e98a35069ab53746996684c887cc102c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 21 Apr 2025 16:33:35 +0100 Subject: [PATCH 0036/1161] [red-knot] Simplify visibility constraint handling for `*`-import definitions (#17486) --- .../src/semantic_index/builder.rs | 68 ++++++++----------- .../src/semantic_index/use_def.rs | 34 ++++++---- .../semantic_index/use_def/symbol_state.rs | 2 +- 3 files changed, 49 insertions(+), 55 deletions(-) diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index 8098c8789ff1d4..1e8b26aa9ec6d4 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -355,15 +355,14 @@ impl<'db> SemanticIndexBuilder<'db> { self.current_use_def_map_mut().merge(state); } - /// Return a 2-element tuple, where the first element is the [`ScopedSymbolId`] of the - /// symbol added, and the second element is a boolean indicating whether the symbol was *newly* - /// added or not - fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) { + /// Add a symbol to the symbol table and the use-def map. + /// Return the [`ScopedSymbolId`] that uniquely identifies the symbol in both. + fn add_symbol(&mut self, name: Name) -> ScopedSymbolId { let (symbol_id, added) = self.current_symbol_table().add_symbol(name); if added { self.current_use_def_map_mut().add_symbol(symbol_id); } - (symbol_id, added) + symbol_id } fn add_attribute(&mut self, name: Name) -> ScopedSymbolId { @@ -797,7 +796,7 @@ impl<'db> SemanticIndexBuilder<'db> { .. }) => (name, &None, default), }; - let (symbol, _) = self.add_symbol(name.id.clone()); + let symbol = self.add_symbol(name.id.clone()); // TODO create Definition for PEP 695 typevars // note that the "bound" on the typevar is a totally different thing than whether // or not a name is "bound" by a typevar declaration; the latter is always true. @@ -895,20 +894,20 @@ impl<'db> SemanticIndexBuilder<'db> { self.declare_parameter(parameter); } if let Some(vararg) = parameters.vararg.as_ref() { - let (symbol, _) = self.add_symbol(vararg.name.id().clone()); + let symbol = self.add_symbol(vararg.name.id().clone()); self.add_definition( symbol, DefinitionNodeRef::VariadicPositionalParameter(vararg), ); } if let Some(kwarg) = parameters.kwarg.as_ref() { - let (symbol, _) = self.add_symbol(kwarg.name.id().clone()); + let symbol = self.add_symbol(kwarg.name.id().clone()); self.add_definition(symbol, DefinitionNodeRef::VariadicKeywordParameter(kwarg)); } } fn declare_parameter(&mut self, parameter: &'db ast::ParameterWithDefault) { - let (symbol, _) = self.add_symbol(parameter.name().id().clone()); + let symbol = self.add_symbol(parameter.name().id().clone()); let definition = self.add_definition(symbol, parameter); @@ -1139,7 +1138,7 @@ where // The symbol for the function name itself has to be evaluated // at the end to match the runtime evaluation of parameter defaults // and return-type annotations. - let (symbol, _) = self.add_symbol(name.id.clone()); + let symbol = self.add_symbol(name.id.clone()); // Record a use of the function name in the scope that it is defined in, so that it // can be used to find previously defined functions with the same name. This is @@ -1174,11 +1173,11 @@ where ); // In Python runtime semantics, a class is registered after its scope is evaluated. - let (symbol, _) = self.add_symbol(class.name.id.clone()); + let symbol = self.add_symbol(class.name.id.clone()); self.add_definition(symbol, class); } ast::Stmt::TypeAlias(type_alias) => { - let (symbol, _) = self.add_symbol( + let symbol = self.add_symbol( type_alias .name .as_name_expr() @@ -1215,7 +1214,7 @@ where (Name::new(alias.name.id.split('.').next().unwrap()), false) }; - let (symbol, _) = self.add_symbol(symbol_name); + let symbol = self.add_symbol(symbol_name); self.add_definition( symbol, ImportDefinitionNodeRef { @@ -1286,7 +1285,7 @@ where // // For more details, see the doc-comment on `StarImportPlaceholderPredicate`. for export in exported_names(self.db, referenced_module) { - let (symbol_id, newly_added) = self.add_symbol(export.clone()); + let symbol_id = self.add_symbol(export.clone()); let node_ref = StarImportDefinitionNodeRef { node, symbol_id }; let star_import = StarImportPlaceholderPredicate::new( self.db, @@ -1295,28 +1294,15 @@ where referenced_module, ); - // Fast path for if there were no previous definitions - // of the symbol defined through the `*` import: - // we can apply the visibility constraint to *only* the added definition, - // rather than all definitions - if newly_added { - self.push_additional_definition(symbol_id, node_ref); - self.current_use_def_map_mut() - .record_and_negate_star_import_visibility_constraint( - star_import, - symbol_id, - ); - } else { - let pre_definition = self.flow_snapshot(); - self.push_additional_definition(symbol_id, node_ref); - let constraint_id = - self.record_visibility_constraint(star_import.into()); - let post_definition = self.flow_snapshot(); - self.flow_restore(pre_definition.clone()); - self.record_negated_visibility_constraint(constraint_id); - self.flow_merge(post_definition); - self.simplify_visibility_constraints(pre_definition); - } + let pre_definition = + self.current_use_def_map().single_symbol_snapshot(symbol_id); + self.push_additional_definition(symbol_id, node_ref); + self.current_use_def_map_mut() + .record_and_negate_star_import_visibility_constraint( + star_import, + symbol_id, + pre_definition, + ); } continue; @@ -1336,7 +1322,7 @@ where self.has_future_annotations |= alias.name.id == "annotations" && node.module.as_deref() == Some("__future__"); - let (symbol, _) = self.add_symbol(symbol_name.clone()); + let symbol = self.add_symbol(symbol_name.clone()); self.add_definition( symbol, @@ -1754,7 +1740,7 @@ where // which is invalid syntax. However, it's still pretty obvious here that the user // *wanted* `e` to be bound, so we should still create a definition here nonetheless. if let Some(symbol_name) = symbol_name { - let (symbol, _) = self.add_symbol(symbol_name.id.clone()); + let symbol = self.add_symbol(symbol_name.id.clone()); self.add_definition( symbol, @@ -1841,7 +1827,7 @@ where (ast::ExprContext::Del, _) => (false, true), (ast::ExprContext::Invalid, _) => (false, false), }; - let (symbol, _) = self.add_symbol(id.clone()); + let symbol = self.add_symbol(id.clone()); if is_use { self.mark_symbol_used(symbol); @@ -2245,7 +2231,7 @@ where range: _, }) = pattern { - let (symbol, _) = self.add_symbol(name.id().clone()); + let symbol = self.add_symbol(name.id().clone()); let state = self.current_match_case.as_ref().unwrap(); self.add_definition( symbol, @@ -2266,7 +2252,7 @@ where rest: Some(name), .. }) = pattern { - let (symbol, _) = self.add_symbol(name.id().clone()); + let symbol = self.add_symbol(name.id().clone()); let state = self.current_match_case.as_ref().unwrap(); self.add_definition( symbol, diff --git a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs b/crates/red_knot_python_semantic/src/semantic_index/use_def.rs index 50b6d804d58094..ed540c380a28b5 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/use_def.rs @@ -775,7 +775,16 @@ impl<'db> UseDefMapBuilder<'db> { .add_and_constraint(self.scope_start_visibility, constraint); } - /// This method exists solely as a fast path for handling `*`-import visibility constraints. + /// Snapshot the state of a single symbol at the current point in control flow. + /// + /// This is only used for `*`-import visibility constraints, which are handled differently + /// to most other visibility constraints. See the doc-comment for + /// [`Self::record_and_negate_star_import_visibility_constraint`] for more details. + pub(super) fn single_symbol_snapshot(&self, symbol: ScopedSymbolId) -> SymbolState { + self.symbol_states[symbol].clone() + } + + /// This method exists solely for handling `*`-import visibility constraints. /// /// The reason why we add visibility constraints for [`Definition`]s created by `*` imports /// is laid out in the doc-comment for [`StarImportPlaceholderPredicate`]. But treating these @@ -784,12 +793,11 @@ impl<'db> UseDefMapBuilder<'db> { /// dominates. (Although `*` imports are not common generally, they are used in several /// important places by typeshed.) /// - /// To solve these regressions, it was observed that we could add a fast path for `*`-import - /// definitions which added a new symbol to the global scope (as opposed to `*`-import definitions - /// that provided redefinitions for *pre-existing* global-scope symbols). The fast path does a - /// number of things differently to our normal handling of visibility constraints: + /// To solve these regressions, it was observed that we could do significantly less work for + /// `*`-import definitions. We do a number of things differently here to our normal handling of + /// visibility constraints: /// - /// - It only applies and negates the visibility constraints to a single symbol, rather than to + /// - We only apply and negate the visibility constraints to a single symbol, rather than to /// all symbols. This is possible here because, unlike most definitions, we know in advance that /// exactly one definition occurs inside the "if-true" predicate branch, and we know exactly /// which definition it is. @@ -800,9 +808,9 @@ impl<'db> UseDefMapBuilder<'db> { /// the visibility constraints is only important for symbols that did not have any new /// definitions inside either the "if-predicate-true" branch or the "if-predicate-false" branch. /// - /// - It avoids multiple expensive calls to [`Self::snapshot`]. This is possible because we know - /// the symbol is newly added, so we know the prior state of the symbol was - /// [`SymbolState::undefined`]. + /// - We only snapshot the state for a single symbol prior to the definition, rather than doing + /// expensive calls to [`Self::snapshot`]. Again, this is possible because we know + /// that only a single definition occurs inside the "if-predicate-true" predicate branch. /// /// - Normally we take care to check whether an "if-predicate-true" branch or an /// "if-predicate-false" branch contains a terminal statement: these can affect the visibility @@ -815,6 +823,7 @@ impl<'db> UseDefMapBuilder<'db> { &mut self, star_import: StarImportPlaceholderPredicate<'db>, symbol: ScopedSymbolId, + pre_definition_state: SymbolState, ) { let predicate_id = self.add_predicate(star_import.into()); let visibility_id = self.visibility_constraints.add_atom(predicate_id); @@ -822,10 +831,9 @@ impl<'db> UseDefMapBuilder<'db> { .visibility_constraints .add_not_constraint(visibility_id); - let mut post_definition_state = std::mem::replace( - &mut self.symbol_states[symbol], - SymbolState::undefined(self.scope_start_visibility), - ); + let mut post_definition_state = + std::mem::replace(&mut self.symbol_states[symbol], pre_definition_state); + post_definition_state .record_visibility_constraint(&mut self.visibility_constraints, visibility_id); diff --git a/crates/red_knot_python_semantic/src/semantic_index/use_def/symbol_state.rs b/crates/red_knot_python_semantic/src/semantic_index/use_def/symbol_state.rs index 09c1b42747d688..6807baf8e0a057 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/use_def/symbol_state.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/use_def/symbol_state.rs @@ -314,7 +314,7 @@ impl SymbolBindings { } #[derive(Clone, Debug, PartialEq, Eq)] -pub(super) struct SymbolState { +pub(in crate::semantic_index) struct SymbolState { declarations: SymbolDeclarations, bindings: SymbolBindings, } From a4531bf8655b16e1c018e843c1ffd7fde31c94df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:09:54 +0100 Subject: [PATCH 0037/1161] Update pre-commit dependencies (#17506) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Waygood --- .github/workflows/build-binaries.yml | 2 +- .github/workflows/ci.yaml | 12 ++++++------ .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/daily_property_tests.yaml | 2 +- .pre-commit-config.yaml | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 02c09a6ad5101f..52f930a31a55e1 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -377,7 +377,7 @@ jobs: args: --release --locked --out dist - name: "Test wheel" if: matrix.target == 'x86_64-unknown-linux-musl' - uses: addnab/docker-run-action@v3 + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 with: image: alpine:latest options: -v ${{ github.workspace }}:/io -w /io diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f4e859e5a94ec0..4f9fad22d36721 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -237,7 +237,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install mold" - uses: rui314/setup-mold@v1 + uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - name: "Install cargo nextest" uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: @@ -291,7 +291,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install mold" - uses: rui314/setup-mold@v1 + uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - name: "Install cargo nextest" uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: @@ -376,7 +376,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install mold" - uses: rui314/setup-mold@v1 + uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - name: "Build" run: cargo build --release --locked @@ -401,7 +401,7 @@ jobs: MSRV: ${{ steps.msrv.outputs.value }} run: rustup default "${MSRV}" - name: "Install mold" - uses: rui314/setup-mold@v1 + uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - name: "Install cargo nextest" uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 with: @@ -433,7 +433,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo-binstall" - uses: cargo-bins/cargo-binstall@main + uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3 with: tool: cargo-fuzz@0.11.2 - name: "Install cargo-fuzz" @@ -641,7 +641,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: cargo-bins/cargo-binstall@main + - uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3 - run: cargo binstall --no-confirm cargo-shear - run: cargo shear diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index 100342f179bbf5..68619bf00d7733 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -38,7 +38,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install mold" - uses: rui314/setup-mold@v1 + uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Build ruff # A debug build means the script runs slower once it gets started, diff --git a/.github/workflows/daily_property_tests.yaml b/.github/workflows/daily_property_tests.yaml index a4afc6d80b63d1..6ca84050715e60 100644 --- a/.github/workflows/daily_property_tests.yaml +++ b/.github/workflows/daily_property_tests.yaml @@ -36,7 +36,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install mold" - uses: rui314/setup-mold@v1 + uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: Build Red Knot # A release build takes longer (2 min vs 1 min), but the property tests run much faster in release diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6192e40e56f5e1..b4572a5d891495 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -79,7 +79,7 @@ repos: pass_filenames: false # This makes it a lot faster - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.5 + rev: v0.11.6 hooks: - id: ruff-format - id: ruff @@ -97,7 +97,7 @@ repos: # zizmor detects security vulnerabilities in GitHub Actions workflows. # Additional configuration for the tool is found in `.github/zizmor.yml` - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.5.2 + rev: v1.6.0 hooks: - id: zizmor From 9c0772d8f0ce669048d352e98c56d7a6a5bc4a51 Mon Sep 17 00:00:00 2001 From: w0nder1ng <94079074+w0nder1ng@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:29:24 -0400 Subject: [PATCH 0038/1161] [`perflint`] Allow list function calls to be replaced with a comprehension (`PERF401`) (#17519) This is an implementation of the discussion from #16719. This change will allow list function calls to be replaced with comprehensions: ```python result = list() for i in range(3): result.append(i + 1) # becomes result = [i + 1 for i in range(3)] ``` I added a new test to `PERF401.py` to verify that this fix will now work for `list()`. --- .../test/fixtures/perflint/PERF401.py | 6 +++++ .../rules/manual_list_comprehension.rs | 9 +++++++ ...__perflint__tests__PERF401_PERF401.py.snap | 11 +++++++++ ...t__tests__preview__PERF401_PERF401.py.snap | 24 +++++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py b/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py index 24ff17e6c7e410..b0e6ff0759ef69 100644 --- a/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py +++ b/crates/ruff_linter/resources/test/fixtures/perflint/PERF401.py @@ -260,3 +260,9 @@ def f(): for i in range(5): if j := i: items.append(j) + +def f(): + values = [1, 2, 3] + result = list() # this should be replaced with a comprehension + for i in values: + result.append(i + 1) # PERF401 diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs index 35a0cd2d3e3394..9d3a5ac9160b27 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs @@ -270,6 +270,15 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF list_binding_value.is_some_and(|binding_value| match binding_value { // `value = []` Expr::List(list_expr) => list_expr.is_empty(), + // `value = list()` + // This might be linted against, but turning it into a list comprehension will also remove it + Expr::Call(call) => { + checker + .semantic() + .resolve_builtin_symbol(&call.func) + .is_some_and(|name| name == "list") + && call.arguments.is_empty() + } _ => false, }); diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap index d21c46905a8f29..c2de08bb3fd309 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF401_PERF401.py.snap @@ -208,5 +208,16 @@ PERF401.py:262:13: PERF401 Use a list comprehension to create a transformed list 261 | if j := i: 262 | items.append(j) | ^^^^^^^^^^^^^^^ PERF401 +263 | +264 | def f(): + | + = help: Replace for loop with list comprehension + +PERF401.py:268:9: PERF401 Use a list comprehension to create a transformed list + | +266 | result = list() # this should be replaced with a comprehension +267 | for i in values: +268 | result.append(i + 1) # PERF401 + | ^^^^^^^^^^^^^^^^^^^^ PERF401 | = help: Replace for loop with list comprehension diff --git a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap index 8bcbb47d03a138..47100714a67ee4 100644 --- a/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap +++ b/crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__preview__PERF401_PERF401.py.snap @@ -492,6 +492,8 @@ PERF401.py:262:13: PERF401 [*] Use a list comprehension to create a transformed 261 | if j := i: 262 | items.append(j) | ^^^^^^^^^^^^^^^ PERF401 +263 | +264 | def f(): | = help: Replace for loop with list comprehension @@ -505,3 +507,25 @@ PERF401.py:262:13: PERF401 [*] Use a list comprehension to create a transformed 261 |- if j := i: 262 |- items.append(j) 259 |+ items = [j for i in range(5) if (j := i)] +263 260 | +264 261 | def f(): +265 262 | values = [1, 2, 3] + +PERF401.py:268:9: PERF401 [*] Use a list comprehension to create a transformed list + | +266 | result = list() # this should be replaced with a comprehension +267 | for i in values: +268 | result.append(i + 1) # PERF401 + | ^^^^^^^^^^^^^^^^^^^^ PERF401 + | + = help: Replace for loop with list comprehension + +ℹ Unsafe fix +263 263 | +264 264 | def f(): +265 265 | values = [1, 2, 3] +266 |- result = list() # this should be replaced with a comprehension +267 |- for i in values: +268 |- result.append(i + 1) # PERF401 + 266 |+ # this should be replaced with a comprehension + 267 |+ result = [i + 1 for i in values] # PERF401 From 21561000b12843e03740d0566afdca05db2713a3 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 22 Apr 2025 02:14:58 +0800 Subject: [PATCH 0039/1161] [`pyupgrade`] Add fix safety section to docs (`UP030`) (#17443) ## Summary add fix safety section to format_literals, for #15584 --- .../ruff_linter/src/rules/pyupgrade/rules/format_literals.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs index b14f9ad181ceb5..bfa426788997ba 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/format_literals.rs @@ -39,6 +39,10 @@ use crate::Locator; /// "{}, {}".format("Hello", "World") # "Hello, World" /// ``` /// +/// This fix is marked as unsafe because: +/// - Comments attached to arguments are not moved, which can cause comments to mismatch the actual arguments. +/// - If arguments have side effects (e.g., print), reordering may change program behavior. +/// /// ## References /// - [Python documentation: Format String Syntax](https://docs.python.org/3/library/string.html#format-string-syntax) /// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) From 53ffe7143f4f09a09385f81456376829398d4bb8 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Mon, 21 Apr 2025 23:29:36 +0100 Subject: [PATCH 0040/1161] [red-knot] Add basic subtyping between class literal and callable (#17469) ## Summary This covers step 1 from https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable Part of #17343 ## Test Plan Update is_subtype_of.md and is_assignable_to.md --------- Co-authored-by: Carl Meyer --- .../mdtest/type_properties/is_subtype_of.md | 41 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 21 ++++++++++ 2 files changed, 62 insertions(+) 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 c61f74859832c8..6eb506e602ea05 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 @@ -1125,6 +1125,47 @@ def f(fn: Callable[[int], int]) -> None: ... f(a) ``` +### Class literals + +#### Classes with metaclasses + +```py +from typing import Callable, overload +from typing_extensions import Self +from knot_extensions import TypeOf, static_assert, is_subtype_of + +class MetaWithReturn(type): + def __call__(cls) -> "A": + return super().__call__() + +class A(metaclass=MetaWithReturn): ... + +static_assert(is_subtype_of(TypeOf[A], Callable[[], A])) +static_assert(not is_subtype_of(TypeOf[A], Callable[[object], A])) + +class MetaWithDifferentReturn(type): + def __call__(cls) -> int: + return super().__call__() + +class B(metaclass=MetaWithDifferentReturn): ... + +static_assert(is_subtype_of(TypeOf[B], Callable[[], int])) +static_assert(not is_subtype_of(TypeOf[B], Callable[[], B])) + +class MetaWithOverloadReturn(type): + @overload + def __call__(cls, x: int) -> int: ... + @overload + def __call__(cls) -> str: ... + def __call__(cls, x: int | None = None) -> str | int: + return super().__call__() + +class C(metaclass=MetaWithOverloadReturn): ... + +static_assert(is_subtype_of(TypeOf[C], Callable[[int], int])) +static_assert(is_subtype_of(TypeOf[C], Callable[[], str])) +``` + ### Bound methods ```py diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b57443a5e49155..c7464b8e9c2929 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1134,6 +1134,27 @@ impl<'db> Type<'db> { self_subclass_ty.is_subtype_of(db, target_subclass_ty) } + (Type::ClassLiteral(_), Type::Callable(_)) => { + let metaclass_call_symbol = self + .member_lookup_with_policy( + db, + "__call__".into(), + MemberLookupPolicy::NO_INSTANCE_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ) + .symbol; + + if let Symbol::Type(Type::BoundMethod(new_function), _) = metaclass_call_symbol { + // TODO: this intentionally diverges from step 1 in + // https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable + // by always respecting the signature of the metaclass `__call__`, rather than + // using a heuristic which makes unwarranted assumptions to sometimes ignore it. + let new_function = new_function.into_callable_type(db); + return new_function.is_subtype_of(db, target); + } + false + } + // `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`. // `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object // is an instance of its metaclass `abc.ABCMeta`. From 9b5fe51b32bfdb85c43c0a185f33d46ab7ab68ef Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Tue, 22 Apr 2025 01:20:04 +0100 Subject: [PATCH 0041/1161] [red-knot] Fix variable name (#17532) --- crates/red_knot_python_semantic/src/types.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index c7464b8e9c2929..c0ccc6e7eea9d8 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1135,7 +1135,7 @@ impl<'db> Type<'db> { } (Type::ClassLiteral(_), Type::Callable(_)) => { - let metaclass_call_symbol = self + let metaclass_call_function_symbol = self .member_lookup_with_policy( db, "__call__".into(), @@ -1144,13 +1144,15 @@ impl<'db> Type<'db> { ) .symbol; - if let Symbol::Type(Type::BoundMethod(new_function), _) = metaclass_call_symbol { + if let Symbol::Type(Type::BoundMethod(metaclass_call_function), _) = + metaclass_call_function_symbol + { // TODO: this intentionally diverges from step 1 in // https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable // by always respecting the signature of the metaclass `__call__`, rather than // using a heuristic which makes unwarranted assumptions to sometimes ignore it. - let new_function = new_function.into_callable_type(db); - return new_function.is_subtype_of(db, target); + let metaclass_call_function = metaclass_call_function.into_callable_type(db); + return metaclass_call_function.is_subtype_of(db, target); } false } From ed4866a00bcbec022731b7192e6d823d7ec75b84 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:18:13 +0200 Subject: [PATCH 0042/1161] Update actions/setup-node action to v4.4.0 (#17514) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/publish-knot-playground.yml | 2 +- .github/workflows/publish-playground.yml | 2 +- .github/workflows/publish-wasm.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4f9fad22d36721..7d05ad336b9a75 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -346,7 +346,7 @@ jobs: - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 cache: "npm" @@ -821,7 +821,7 @@ jobs: - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "npm" diff --git a/.github/workflows/publish-knot-playground.yml b/.github/workflows/publish-knot-playground.yml index 071c81f907d167..80b96525c4f480 100644 --- a/.github/workflows/publish-knot-playground.yml +++ b/.github/workflows/publish-knot-playground.yml @@ -35,7 +35,7 @@ jobs: persist-credentials: false - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 - uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0 diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml index 63d85b7fa083b7..e7bf15dde27de7 100644 --- a/.github/workflows/publish-playground.yml +++ b/.github/workflows/publish-playground.yml @@ -29,7 +29,7 @@ jobs: persist-credentials: false - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22 cache: "npm" diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index 309e2490dc2db9..81193b9352d272 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -45,7 +45,7 @@ jobs: jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json mv /tmp/package.json crates/ruff_wasm/pkg - run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 registry-url: "https://registry.npmjs.org" From 2894aaa94365332b3918acb37b3d63cdc73e42c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:18:54 +0200 Subject: [PATCH 0043/1161] Update dependency uuid to v11.1.0 (#17517) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- playground/api/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playground/api/package-lock.json b/playground/api/package-lock.json index 5d4a5076ca18ce..c97fd35a5b5c5b 100644 --- a/playground/api/package-lock.json +++ b/playground/api/package-lock.json @@ -1708,9 +1708,9 @@ } }, "node_modules/uuid": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", - "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" From c4581788b27a285441724ff1a9895fe08aee9fa9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:19:55 +0200 Subject: [PATCH 0044/1161] Update dependency smol-toml to v1.3.3 (#17505) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- playground/package-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index c3a4847fff532e..b536c49a0ec4ae 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -44,7 +44,7 @@ "react-resizable-panels": "^2.1.7", "red_knot_wasm": "file:red_knot_wasm", "shared": "0.0.0", - "smol-toml": "^1.3.1" + "smol-toml": "^1.3.3" }, "devDependencies": { "vite-plugin-static-copy": "^2.3.0" @@ -5393,9 +5393,9 @@ } }, "node_modules/smol-toml": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.1.tgz", - "integrity": "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.3.tgz", + "integrity": "sha512-KMVLNWu490KlNfD0lbfDBUktJIEaZRBj1eeK0SMfdpO/rfyARIzlnPVI1Ge4l0vtSJmQUAiGKxMyLGrCT38iyA==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -6090,7 +6090,7 @@ "react-resizable-panels": "^2.0.0", "ruff_wasm": "file:ruff_wasm", "shared": "0.0.0", - "smol-toml": "^1.3.0" + "smol-toml": "^1.3.3" } }, "ruff/ruff_wasm": { From f83295fe51a505ef43eaa59397b82c767c5a3046 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:30:07 +0200 Subject: [PATCH 0045/1161] Update dependency react-resizable-panels to v2.1.8 (#17513) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- playground/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index b536c49a0ec4ae..f25650f3f0b446 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -41,7 +41,7 @@ "pyodide": "^0.27.4", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable-panels": "^2.1.7", + "react-resizable-panels": "^2.1.8", "red_knot_wasm": "file:red_knot_wasm", "shared": "0.0.0", "smol-toml": "^1.3.3" @@ -4969,9 +4969,9 @@ "license": "MIT" }, "node_modules/react-resizable-panels": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", - "integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.8.tgz", + "integrity": "sha512-oDvD0sw34Ecx00cQFLiRJpAE2fCgNLBr8DMrBzkrsaUiLpAycIQoY3eAWfMblDql3pTIMZ60wJ/P89RO1htM2w==", "license": "MIT", "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", @@ -6087,7 +6087,7 @@ "monaco-editor": "^0.52.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable-panels": "^2.0.0", + "react-resizable-panels": "^2.1.8", "ruff_wasm": "file:ruff_wasm", "shared": "0.0.0", "smol-toml": "^1.3.3" @@ -6103,7 +6103,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.3.2", "react": "^19.0.0", - "react-resizable-panels": "^2.1.7" + "react-resizable-panels": "^2.1.8" } } } From 37a0836bd2bb9bb456a4e4bb37ee4cb55d1f0509 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 22 Apr 2025 10:33:02 +0200 Subject: [PATCH 0046/1161] [red-knot] `typing.dataclass_transform` (#17445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * Add initial support for `typing.dataclass_transform` * Support decorating a function decorator with `@dataclass_transform(…)` (used by `attrs`, `strawberry`) * Support decorating a metaclass with `@dataclass_transform(…)` (used by `pydantic`, but doesn't work yet, because we don't seem to model `__new__` calls correctly?) * *No* support yet for decorating base classes with `@dataclass_transform(…)`. I haven't figured out how this even supposed to work. And haven't seen it being used. * Add `strawberry` as an ecosystem project, as it makes heavy use of `@dataclass_transform` ## Test Plan New Markdown tests --- .github/workflows/mypy_primer.yaml | 2 +- Cargo.toml | 4 + .../resources/mdtest/dataclass_transform.md | 293 ++++++++++++++++++ .../resources/mdtest/dataclasses.md | 2 +- .../resources/primer/good.txt | 1 + crates/red_knot_python_semantic/src/types.rs | 120 ++++++- .../src/types/call/bind.rs | 112 +++++-- .../src/types/class.rs | 175 ++++++----- .../src/types/class_base.rs | 1 + .../src/types/display.rs | 5 +- .../src/types/infer.rs | 34 +- .../src/types/type_ordering.rs | 6 + 12 files changed, 636 insertions(+), 119 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/dataclass_transform.md diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index 490ae970faa4e5..ac0dc7a2a16932 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -45,7 +45,7 @@ jobs: - name: Install mypy_primer run: | - uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v5" + uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v6" - name: Run mypy_primer shell: bash diff --git a/Cargo.toml b/Cargo.toml index 933ae24c5faad2..7a12fd6c3d6720 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -231,6 +231,10 @@ unused_peekable = "warn" # Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved. large_stack_arrays = "allow" +# Salsa generates functions with parameters for each field of a `salsa::interned` struct. +# If we don't allow this, we get warnings for structs with too many fields. +too_many_arguments = "allow" + [profile.release] # Note that we set these explicitly, and these values # were chosen based on a trade-off between compile times diff --git a/crates/red_knot_python_semantic/resources/mdtest/dataclass_transform.md b/crates/red_knot_python_semantic/resources/mdtest/dataclass_transform.md new file mode 100644 index 00000000000000..a57fcd612ee409 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/dataclass_transform.md @@ -0,0 +1,293 @@ +# `typing.dataclass_transform` + +```toml +[environment] +python-version = "3.12" +``` + +`dataclass_transform` is a decorator that can be used to let type checkers know that a function, +class, or metaclass is a `dataclass`-like construct. + +## Basic example + +```py +from typing_extensions import dataclass_transform + +@dataclass_transform() +def my_dataclass[T](cls: type[T]) -> type[T]: + # modify cls + return cls + +@my_dataclass +class Person: + name: str + age: int | None = None + +Person("Alice", 20) +Person("Bob", None) +Person("Bob") + +# error: [missing-argument] +Person() +``` + +## Decorating decorators that take parameters themselves + +If we want our `dataclass`-like decorator to also take parameters, that is also possible: + +```py +from typing_extensions import dataclass_transform, Callable + +@dataclass_transform() +def versioned_class[T](*, version: int = 1): + def decorator(cls): + # modify cls + return cls + return decorator + +@versioned_class(version=2) +class Person: + name: str + age: int | None = None + +Person("Alice", 20) + +# error: [missing-argument] +Person() +``` + +We properly type-check the arguments to the decorator: + +```py +from typing_extensions import dataclass_transform, Callable + +# error: [invalid-argument-type] +@versioned_class(version="a string") +class C: + name: str +``` + +## Types of decorators + +The examples from this section are straight from the Python documentation on +[`typing.dataclass_transform`]. + +### Decorating a decorator function + +```py +from typing_extensions import dataclass_transform + +@dataclass_transform() +def create_model[T](cls: type[T]) -> type[T]: + ... + return cls + +@create_model +class CustomerModel: + id: int + name: str + +CustomerModel(id=1, name="Test") +``` + +### Decorating a metaclass + +```py +from typing_extensions import dataclass_transform + +@dataclass_transform() +class ModelMeta(type): ... + +class ModelBase(metaclass=ModelMeta): ... + +class CustomerModel(ModelBase): + id: int + name: str + +CustomerModel(id=1, name="Test") + +# error: [missing-argument] +CustomerModel() +``` + +### Decorating a base class + +```py +from typing_extensions import dataclass_transform + +@dataclass_transform() +class ModelBase: ... + +class CustomerModel(ModelBase): + id: int + name: str + +# TODO: this is not supported yet +# error: [unknown-argument] +# error: [unknown-argument] +CustomerModel(id=1, name="Test") +``` + +## Arguments to `dataclass_transform` + +### `eq_default` + +`eq=True/False` does not have a observable effect (apart from a minor change regarding whether +`other` is positional-only or not, which is not modelled at the moment). + +### `order_default` + +The `order_default` argument controls whether methods such as `__lt__` are generated by default. +This can be overwritten using the `order` argument to the custom decorator: + +```py +from typing_extensions import dataclass_transform + +@dataclass_transform() +def normal(*, order: bool = False): + raise NotImplementedError + +@dataclass_transform(order_default=False) +def order_default_false(*, order: bool = False): + raise NotImplementedError + +@dataclass_transform(order_default=True) +def order_default_true(*, order: bool = True): + raise NotImplementedError + +@normal +class Normal: + inner: int + +Normal(1) < Normal(2) # error: [unsupported-operator] + +@normal(order=True) +class NormalOverwritten: + inner: int + +NormalOverwritten(1) < NormalOverwritten(2) + +@order_default_false +class OrderFalse: + inner: int + +OrderFalse(1) < OrderFalse(2) # error: [unsupported-operator] + +@order_default_false(order=True) +class OrderFalseOverwritten: + inner: int + +OrderFalseOverwritten(1) < OrderFalseOverwritten(2) + +@order_default_true +class OrderTrue: + inner: int + +OrderTrue(1) < OrderTrue(2) + +@order_default_true(order=False) +class OrderTrueOverwritten: + inner: int + +# error: [unsupported-operator] +OrderTrueOverwritten(1) < OrderTrueOverwritten(2) +``` + +### `kw_only_default` + +To do + +### `field_specifiers` + +To do + +## Overloaded dataclass-like decorators + +In the case of an overloaded decorator, the `dataclass_transform` decorator can be applied to the +implementation, or to *one* of the overloads. + +### Applying `dataclass_transform` to the implementation + +```py +from typing_extensions import dataclass_transform, TypeVar, Callable, overload + +T = TypeVar("T", bound=type) + +@overload +def versioned_class( + cls: T, + *, + version: int = 1, +) -> T: ... +@overload +def versioned_class( + *, + version: int = 1, +) -> Callable[[T], T]: ... +@dataclass_transform() +def versioned_class( + cls: T | None = None, + *, + version: int = 1, +) -> T | Callable[[T], T]: + raise NotImplementedError + +@versioned_class +class D1: + x: str + +@versioned_class(version=2) +class D2: + x: str + +D1("a") +D2("a") + +D1(1.2) # error: [invalid-argument-type] +D2(1.2) # error: [invalid-argument-type] +``` + +### Applying `dataclass_transform` to an overload + +```py +from typing_extensions import dataclass_transform, TypeVar, Callable, overload + +T = TypeVar("T", bound=type) + +@overload +@dataclass_transform() +def versioned_class( + cls: T, + *, + version: int = 1, +) -> T: ... +@overload +def versioned_class( + *, + version: int = 1, +) -> Callable[[T], T]: ... +def versioned_class( + cls: T | None = None, + *, + version: int = 1, +) -> T | Callable[[T], T]: + raise NotImplementedError + +@versioned_class +class D1: + x: str + +@versioned_class(version=2) +class D2: + x: str + +# TODO: these should not be errors +D1("a") # error: [too-many-positional-arguments] +D2("a") # error: [too-many-positional-arguments] + +# TODO: these should be invalid-argument-type errors +D1(1.2) # error: [too-many-positional-arguments] +D2(1.2) # error: [too-many-positional-arguments] +``` + +[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform diff --git a/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md b/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md index 94958c883b730e..0f23feea9f1c76 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md @@ -689,7 +689,7 @@ from dataclasses import dataclass dataclass_with_order = dataclass(order=True) -reveal_type(dataclass_with_order) # revealed: +reveal_type(dataclass_with_order) # revealed: @dataclass_with_order class C: diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/red_knot_python_semantic/resources/primer/good.txt index 360e1e72ef199c..3dca5c49f9c638 100644 --- a/crates/red_knot_python_semantic/resources/primer/good.txt +++ b/crates/red_knot_python_semantic/resources/primer/good.txt @@ -18,6 +18,7 @@ python-chess python-htmlgen rich scrapy +strawberry typeshed-stats werkzeug zipp diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index c0ccc6e7eea9d8..ca20e3c55e17be 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -339,12 +339,12 @@ impl<'db> PropertyInstanceType<'db> { } bitflags! { - /// Used as the return type of `dataclass(…)` calls. Keeps track of the arguments + /// Used for the return type of `dataclass(…)` calls. Keeps track of the arguments /// that were passed in. For the precise meaning of the fields, see [1]. /// /// [1]: https://docs.python.org/3/library/dataclasses.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct DataclassMetadata: u16 { + pub struct DataclassParams: u16 { const INIT = 0b0000_0000_0001; const REPR = 0b0000_0000_0010; const EQ = 0b0000_0000_0100; @@ -358,12 +358,57 @@ bitflags! { } } -impl Default for DataclassMetadata { +impl Default for DataclassParams { fn default() -> Self { Self::INIT | Self::REPR | Self::EQ | Self::MATCH_ARGS } } +impl From for DataclassParams { + fn from(params: DataclassTransformerParams) -> Self { + let mut result = Self::default(); + + result.set( + Self::EQ, + params.contains(DataclassTransformerParams::EQ_DEFAULT), + ); + result.set( + Self::ORDER, + params.contains(DataclassTransformerParams::ORDER_DEFAULT), + ); + result.set( + Self::KW_ONLY, + params.contains(DataclassTransformerParams::KW_ONLY_DEFAULT), + ); + result.set( + Self::FROZEN, + params.contains(DataclassTransformerParams::FROZEN_DEFAULT), + ); + + result + } +} + +bitflags! { + /// Used for the return type of `dataclass_transform(…)` calls. Keeps track of the + /// arguments that were passed in. For the precise meaning of the fields, see [1]. + /// + /// [1]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] + pub struct DataclassTransformerParams: u8 { + const EQ_DEFAULT = 0b0000_0001; + const ORDER_DEFAULT = 0b0000_0010; + const KW_ONLY_DEFAULT = 0b0000_0100; + const FROZEN_DEFAULT = 0b0000_1000; + } +} + +impl Default for DataclassTransformerParams { + fn default() -> Self { + Self::EQ_DEFAULT + } +} + /// Representation of a type: a set of possible values at runtime. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub enum Type<'db> { @@ -404,7 +449,9 @@ pub enum Type<'db> { /// A special callable that is returned by a `dataclass(…)` call. It is usually /// used as a decorator. Note that this is only used as a return type for actual /// `dataclass` calls, not for the argumentless `@dataclass` decorator. - DataclassDecorator(DataclassMetadata), + DataclassDecorator(DataclassParams), + /// A special callable that is returned by a `dataclass_transform(…)` call. + DataclassTransformer(DataclassTransformerParams), /// The type of an arbitrary callable object with a certain specified signature. Callable(CallableType<'db>), /// A specific module object @@ -524,7 +571,8 @@ impl<'db> Type<'db> { | Self::BoundMethod(_) | Self::WrapperDescriptor(_) | Self::MethodWrapper(_) - | Self::DataclassDecorator(_) => false, + | Self::DataclassDecorator(_) + | Self::DataclassTransformer(_) => false, Self::GenericAlias(generic) => generic .specialization(db) @@ -837,7 +885,8 @@ impl<'db> Type<'db> { | Type::MethodWrapper(_) | Type::BoundMethod(_) | Type::WrapperDescriptor(_) - | Self::DataclassDecorator(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::KnownInstance(_) @@ -1073,7 +1122,7 @@ impl<'db> Type<'db> { self_callable.is_subtype_of(db, other_callable) } - (Type::DataclassDecorator(_), _) => { + (Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => { // TODO: Implement subtyping using an equivalent `Callable` type. false } @@ -1628,6 +1677,7 @@ impl<'db> Type<'db> { | Type::MethodWrapper(..) | Type::WrapperDescriptor(..) | Type::DataclassDecorator(..) + | Type::DataclassTransformer(..) | Type::IntLiteral(..) | Type::SliceLiteral(..) | Type::StringLiteral(..) @@ -1644,6 +1694,7 @@ impl<'db> Type<'db> { | Type::MethodWrapper(..) | Type::WrapperDescriptor(..) | Type::DataclassDecorator(..) + | Type::DataclassTransformer(..) | Type::IntLiteral(..) | Type::SliceLiteral(..) | Type::StringLiteral(..) @@ -1838,8 +1889,14 @@ impl<'db> Type<'db> { true } - (Type::Callable(_) | Type::DataclassDecorator(_), _) - | (_, Type::Callable(_) | Type::DataclassDecorator(_)) => { + ( + Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), + _, + ) + | ( + _, + Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), + ) => { // TODO: Implement disjointness for general callable type with other types false } @@ -1902,6 +1959,7 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::ModuleLiteral(..) | Type::IntLiteral(_) | Type::BooleanLiteral(_) @@ -2033,7 +2091,7 @@ impl<'db> Type<'db> { // (this variant represents `f.__get__`, where `f` is any function) false } - Type::DataclassDecorator(_) => false, + Type::DataclassDecorator(_) | Type::DataclassTransformer(_) => false, Type::Instance(InstanceType { class }) => { class.known(db).is_some_and(KnownClass::is_singleton) } @@ -2126,7 +2184,8 @@ impl<'db> Type<'db> { | Type::AlwaysFalsy | Type::Callable(_) | Type::PropertyInstance(_) - | Type::DataclassDecorator(_) => false, + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) => false, } } @@ -2262,6 +2321,7 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::ModuleLiteral(_) | Type::KnownInstance(_) | Type::AlwaysTruthy @@ -2357,7 +2417,9 @@ impl<'db> Type<'db> { Type::DataclassDecorator(_) => KnownClass::FunctionType .to_instance(db) .instance_member(db, name), - Type::Callable(_) => KnownClass::Object.to_instance(db).instance_member(db, name), + Type::Callable(_) | Type::DataclassTransformer(_) => { + KnownClass::Object.to_instance(db).instance_member(db, name) + } Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { None => KnownClass::Object.to_instance(db).instance_member(db, name), @@ -2774,7 +2836,7 @@ impl<'db> Type<'db> { Type::DataclassDecorator(_) => KnownClass::FunctionType .to_instance(db) .member_lookup_with_policy(db, name, policy), - Type::Callable(_) => KnownClass::Object + Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Object .to_instance(db) .member_lookup_with_policy(db, name, policy), @@ -3080,6 +3142,7 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::ModuleLiteral(_) | Type::SliceLiteral(_) | Type::AlwaysTruthy => Truthiness::AlwaysTrue, @@ -3387,6 +3450,18 @@ impl<'db> Type<'db> { )) } + // TODO: We should probably also check the original return type of the function + // that was decorated with `@dataclass_transform`, to see if it is consistent with + // with what we configure here. + Type::DataclassTransformer(_) => Signatures::single(CallableSignature::single( + self, + Signature::new( + Parameters::new([Parameter::positional_only(Some(Name::new_static("func"))) + .with_annotated_type(Type::object(db))]), + None, + ), + )), + Type::FunctionLiteral(function_type) => match function_type.known(db) { Some( KnownFunction::IsEquivalentTo @@ -3500,8 +3575,7 @@ impl<'db> Type<'db> { Parameters::new([Parameter::positional_only(Some( Name::new_static("cls"), )) - // TODO: type[_T] - .with_annotated_type(Type::any())]), + .with_annotated_type(KnownClass::Type.to_instance(db))]), None, ), // TODO: make this overload Python-version-dependent @@ -4289,6 +4363,7 @@ impl<'db> Type<'db> { | Type::BoundMethod(_) | Type::WrapperDescriptor(_) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::Instance(_) | Type::KnownInstance(_) | Type::PropertyInstance(_) @@ -4359,6 +4434,7 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::Never | Type::FunctionLiteral(_) | Type::BoundSuper(_) @@ -4574,7 +4650,7 @@ impl<'db> Type<'db> { Type::MethodWrapper(_) => KnownClass::MethodWrapperType.to_class_literal(db), Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType.to_class_literal(db), Type::DataclassDecorator(_) => KnownClass::FunctionType.to_class_literal(db), - Type::Callable(_) => KnownClass::Type.to_instance(db), + Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db), Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db), @@ -4714,6 +4790,7 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::ModuleLiteral(_) // A non-generic class never needs to be specialized. A generic class is specialized // explicitly (via a subscript expression) or implicitly (via a call), and not because @@ -4820,6 +4897,7 @@ impl<'db> Type<'db> { | Self::MethodWrapper(_) | Self::WrapperDescriptor(_) | Self::DataclassDecorator(_) + | Self::DataclassTransformer(_) | Self::PropertyInstance(_) | Self::BoundSuper(_) | Self::Tuple(_) => self.to_meta_type(db).definition(db), @@ -5883,6 +5961,10 @@ pub struct FunctionType<'db> { /// A set of special decorators that were applied to this function decorators: FunctionDecorators, + /// The arguments to `dataclass_transformer`, if this function was annotated + /// with `@dataclass_transformer(...)`. + dataclass_transformer_params: Option, + /// The generic context of a generic function. generic_context: Option>, @@ -6019,6 +6101,7 @@ impl<'db> FunctionType<'db> { self.known(db), self.body_scope(db), self.decorators(db), + self.dataclass_transformer_params(db), Some(generic_context), self.specialization(db), ) @@ -6035,6 +6118,7 @@ impl<'db> FunctionType<'db> { self.known(db), self.body_scope(db), self.decorators(db), + self.dataclass_transformer_params(db), self.generic_context(db), Some(specialization), ) @@ -6079,6 +6163,8 @@ pub enum KnownFunction { GetProtocolMembers, /// `typing(_extensions).runtime_checkable` RuntimeCheckable, + /// `typing(_extensions).dataclass_transform` + DataclassTransform, /// `abc.abstractmethod` #[strum(serialize = "abstractmethod")] @@ -6143,6 +6229,7 @@ impl KnownFunction { | Self::IsProtocol | Self::GetProtocolMembers | Self::RuntimeCheckable + | Self::DataclassTransform | Self::NoTypeCheck => { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } @@ -7516,6 +7603,7 @@ pub(crate) mod tests { | KnownFunction::IsProtocol | KnownFunction::GetProtocolMembers | KnownFunction::RuntimeCheckable + | KnownFunction::DataclassTransform | KnownFunction::NoTypeCheck => KnownModule::TypingExtensions, KnownFunction::IsSingleton diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index a577dba34b31c4..6687bd28466053 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -19,8 +19,9 @@ use crate::types::diagnostic::{ use crate::types::generics::{Specialization, SpecializationBuilder}; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ - BoundMethodType, DataclassMetadata, FunctionDecorators, KnownClass, KnownFunction, - KnownInstanceType, MethodWrapperKind, PropertyInstanceType, UnionType, WrapperDescriptorKind, + BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, FunctionType, + KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, + UnionType, WrapperDescriptorKind, }; use ruff_db::diagnostic::{Annotation, Severity, Span, SubDiagnostic}; use ruff_python_ast as ast; @@ -210,8 +211,17 @@ impl<'db> Bindings<'db> { /// Evaluates the return type of certain known callables, where we have special-case logic to /// determine the return type in a way that isn't directly expressible in the type system. fn evaluate_known_cases(&mut self, db: &'db dyn Db) { + let to_bool = |ty: &Option>, default: bool| -> bool { + if let Some(Type::BooleanLiteral(value)) = ty { + *value + } else { + // TODO: emit a diagnostic if we receive `bool` + default + } + }; + // Each special case listed here should have a corresponding clause in `Type::signatures`. - for binding in &mut self.elements { + for (binding, callable_signature) in self.elements.iter_mut().zip(self.signatures.iter()) { let binding_type = binding.callable_type; let Some((overload_index, overload)) = binding.matching_overload_mut() else { continue; @@ -413,6 +423,21 @@ impl<'db> Bindings<'db> { } } + Type::DataclassTransformer(params) => { + if let [Some(Type::FunctionLiteral(function))] = overload.parameter_types() { + overload.set_return_type(Type::FunctionLiteral(FunctionType::new( + db, + function.name(db), + function.known(db), + function.body_scope(db), + function.decorators(db), + Some(params), + function.generic_context(db), + function.specialization(db), + ))); + } + } + Type::BoundMethod(bound_method) if bound_method.self_instance(db).is_property_instance() => { @@ -598,53 +623,90 @@ impl<'db> Bindings<'db> { if let [init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot] = overload.parameter_types() { - let to_bool = |ty: &Option>, default: bool| -> bool { - if let Some(Type::BooleanLiteral(value)) = ty { - *value - } else { - // TODO: emit a diagnostic if we receive `bool` - default - } - }; - - let mut metadata = DataclassMetadata::empty(); + let mut params = DataclassParams::empty(); if to_bool(init, true) { - metadata |= DataclassMetadata::INIT; + params |= DataclassParams::INIT; } if to_bool(repr, true) { - metadata |= DataclassMetadata::REPR; + params |= DataclassParams::REPR; } if to_bool(eq, true) { - metadata |= DataclassMetadata::EQ; + params |= DataclassParams::EQ; } if to_bool(order, false) { - metadata |= DataclassMetadata::ORDER; + params |= DataclassParams::ORDER; } if to_bool(unsafe_hash, false) { - metadata |= DataclassMetadata::UNSAFE_HASH; + params |= DataclassParams::UNSAFE_HASH; } if to_bool(frozen, false) { - metadata |= DataclassMetadata::FROZEN; + params |= DataclassParams::FROZEN; } if to_bool(match_args, true) { - metadata |= DataclassMetadata::MATCH_ARGS; + params |= DataclassParams::MATCH_ARGS; } if to_bool(kw_only, false) { - metadata |= DataclassMetadata::KW_ONLY; + params |= DataclassParams::KW_ONLY; } if to_bool(slots, false) { - metadata |= DataclassMetadata::SLOTS; + params |= DataclassParams::SLOTS; } if to_bool(weakref_slot, false) { - metadata |= DataclassMetadata::WEAKREF_SLOT; + params |= DataclassParams::WEAKREF_SLOT; } - overload.set_return_type(Type::DataclassDecorator(metadata)); + overload.set_return_type(Type::DataclassDecorator(params)); } } - _ => {} + Some(KnownFunction::DataclassTransform) => { + if let [eq_default, order_default, kw_only_default, frozen_default, _field_specifiers, _kwargs] = + overload.parameter_types() + { + let mut params = DataclassTransformerParams::empty(); + + if to_bool(eq_default, true) { + params |= DataclassTransformerParams::EQ_DEFAULT; + } + if to_bool(order_default, false) { + params |= DataclassTransformerParams::ORDER_DEFAULT; + } + if to_bool(kw_only_default, false) { + params |= DataclassTransformerParams::KW_ONLY_DEFAULT; + } + if to_bool(frozen_default, false) { + params |= DataclassTransformerParams::FROZEN_DEFAULT; + } + + overload.set_return_type(Type::DataclassTransformer(params)); + } + } + + _ => { + if let Some(params) = function_type.dataclass_transformer_params(db) { + // This is a call to a custom function that was decorated with `@dataclass_transformer`. + // If this function was called with a keyword argument like `order=False`, we extract + // the argument type and overwrite the corresponding flag in `dataclass_params` after + // constructing them from the `dataclass_transformer`-parameter defaults. + + let mut dataclass_params = DataclassParams::from(params); + + if let Some(Some(Type::BooleanLiteral(order))) = callable_signature + .iter() + .nth(overload_index) + .and_then(|signature| { + let (idx, _) = + signature.parameters().keyword_by_name("order")?; + overload.parameter_types().get(idx) + }) + { + dataclass_params.set(DataclassParams::ORDER, *order); + } + + overload.set_return_type(Type::DataclassDecorator(dataclass_params)); + } + } }, Type::ClassLiteral(class) => match class.known(db) { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 23a4a1fde635a0..e8b92667ac03eb 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -10,7 +10,7 @@ use crate::semantic_index::definition::Definition; use crate::semantic_index::DeclarationWithConstraint; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{Parameter, Parameters}; -use crate::types::{CallableType, DataclassMetadata, Signature}; +use crate::types::{CallableType, DataclassParams, DataclassTransformerParams, Signature}; use crate::{ module_resolver::file_to_module, semantic_index::{ @@ -106,7 +106,8 @@ pub struct Class<'db> { pub(crate) known: Option, - pub(crate) dataclass_metadata: Option, + pub(crate) dataclass_params: Option, + pub(crate) dataclass_transformer_params: Option, } impl<'db> Class<'db> { @@ -469,8 +470,8 @@ impl<'db> ClassLiteralType<'db> { self.class(db).known } - pub(crate) fn dataclass_metadata(self, db: &'db dyn Db) -> Option { - self.class(db).dataclass_metadata + pub(crate) fn dataclass_params(self, db: &'db dyn Db) -> Option { + self.class(db).dataclass_params } /// Return `true` if this class represents `known_class` @@ -699,6 +700,7 @@ impl<'db> ClassLiteralType<'db> { /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { self.try_metaclass(db) + .map(|(ty, _)| ty) .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown()) } @@ -712,7 +714,10 @@ impl<'db> ClassLiteralType<'db> { /// Return the metaclass of this class, or an error if the metaclass cannot be inferred. #[salsa::tracked] - pub(super) fn try_metaclass(self, db: &'db dyn Db) -> Result, MetaclassError<'db>> { + pub(super) fn try_metaclass( + self, + db: &'db dyn Db, + ) -> Result<(Type<'db>, Option), MetaclassError<'db>> { let class = self.class(db); tracing::trace!("ClassLiteralType::try_metaclass: {}", class.name); @@ -723,7 +728,7 @@ impl<'db> ClassLiteralType<'db> { // We emit diagnostics for cyclic class definitions elsewhere. // Avoid attempting to infer the metaclass if the class is cyclically defined: // it would be easy to enter an infinite loop. - return Ok(SubclassOfType::subclass_of_unknown()); + return Ok((SubclassOfType::subclass_of_unknown(), None)); } let explicit_metaclass = self.explicit_metaclass(db); @@ -768,7 +773,7 @@ impl<'db> ClassLiteralType<'db> { }), }; - return return_ty_result.map(|ty| ty.to_meta_type(db)); + return return_ty_result.map(|ty| (ty.to_meta_type(db), None)); }; // Reconcile all base classes' metaclasses with the candidate metaclass. @@ -805,7 +810,10 @@ impl<'db> ClassLiteralType<'db> { }); } - Ok(candidate.metaclass.into()) + Ok(( + candidate.metaclass.into(), + candidate.metaclass.class(db).dataclass_transformer_params, + )) } /// Returns the class member of this class named `name`. @@ -969,12 +977,8 @@ impl<'db> ClassLiteralType<'db> { }); if symbol.symbol.is_unbound() { - if let Some(metadata) = self.dataclass_metadata(db) { - if let Some(dataclass_member) = - self.own_dataclass_member(db, specialization, metadata, name) - { - return Symbol::bound(dataclass_member).into(); - } + if let Some(dataclass_member) = self.own_dataclass_member(db, specialization, name) { + return Symbol::bound(dataclass_member).into(); } } @@ -986,70 +990,97 @@ impl<'db> ClassLiteralType<'db> { self, db: &'db dyn Db, specialization: Option>, - metadata: DataclassMetadata, name: &str, ) -> Option> { - if name == "__init__" && metadata.contains(DataclassMetadata::INIT) { - let mut parameters = vec![]; - - for (name, (mut attr_ty, mut default_ty)) in self.dataclass_fields(db, specialization) { - // The descriptor handling below is guarded by this fully-static check, because dynamic - // types like `Any` are valid (data) descriptors: since they have all possible attributes, - // they also have a (callable) `__set__` method. The problem is that we can't determine - // the type of the value parameter this way. Instead, we want to use the dynamic type - // itself in this case, so we skip the special descriptor handling. - if attr_ty.is_fully_static(db) { - let dunder_set = attr_ty.class_member(db, "__set__".into()); - if let Some(dunder_set) = dunder_set.symbol.ignore_possibly_unbound() { - // This type of this attribute is a data descriptor. Instead of overwriting the - // descriptor attribute, data-classes will (implicitly) call the `__set__` method - // of the descriptor. This means that the synthesized `__init__` parameter for - // this attribute is determined by possible `value` parameter types with which - // the `__set__` method can be called. We build a union of all possible options - // to account for possible overloads. - let mut value_types = UnionBuilder::new(db); - for signature in &dunder_set.signatures(db) { - for overload in signature { - if let Some(value_param) = overload.parameters().get_positional(2) { - value_types = value_types.add( - value_param.annotated_type().unwrap_or_else(Type::unknown), - ); - } else if overload.parameters().is_gradual() { - value_types = value_types.add(Type::unknown()); + let params = self.dataclass_params(db); + let has_dataclass_param = |param| params.is_some_and(|params| params.contains(param)); + + match name { + "__init__" => { + let has_synthesized_dunder_init = has_dataclass_param(DataclassParams::INIT) + || self + .try_metaclass(db) + .is_ok_and(|(_, transformer_params)| transformer_params.is_some()); + + if !has_synthesized_dunder_init { + return None; + } + + let mut parameters = vec![]; + + for (name, (mut attr_ty, mut default_ty)) in + self.dataclass_fields(db, specialization) + { + // The descriptor handling below is guarded by this fully-static check, because dynamic + // types like `Any` are valid (data) descriptors: since they have all possible attributes, + // they also have a (callable) `__set__` method. The problem is that we can't determine + // the type of the value parameter this way. Instead, we want to use the dynamic type + // itself in this case, so we skip the special descriptor handling. + if attr_ty.is_fully_static(db) { + let dunder_set = attr_ty.class_member(db, "__set__".into()); + if let Some(dunder_set) = dunder_set.symbol.ignore_possibly_unbound() { + // This type of this attribute is a data descriptor. Instead of overwriting the + // descriptor attribute, data-classes will (implicitly) call the `__set__` method + // of the descriptor. This means that the synthesized `__init__` parameter for + // this attribute is determined by possible `value` parameter types with which + // the `__set__` method can be called. We build a union of all possible options + // to account for possible overloads. + let mut value_types = UnionBuilder::new(db); + for signature in &dunder_set.signatures(db) { + for overload in signature { + if let Some(value_param) = + overload.parameters().get_positional(2) + { + value_types = value_types.add( + value_param + .annotated_type() + .unwrap_or_else(Type::unknown), + ); + } else if overload.parameters().is_gradual() { + value_types = value_types.add(Type::unknown()); + } } } - } - attr_ty = value_types.build(); - - // The default value of the attribute is *not* determined by the right hand side - // of the class-body assignment. Instead, the runtime invokes `__get__` on the - // descriptor, as if it had been called on the class itself, i.e. it passes `None` - // for the `instance` argument. - - if let Some(ref mut default_ty) = default_ty { - *default_ty = default_ty - .try_call_dunder_get(db, Type::none(db), Type::ClassLiteral(self)) - .map(|(return_ty, _)| return_ty) - .unwrap_or_else(Type::unknown); + attr_ty = value_types.build(); + + // The default value of the attribute is *not* determined by the right hand side + // of the class-body assignment. Instead, the runtime invokes `__get__` on the + // descriptor, as if it had been called on the class itself, i.e. it passes `None` + // for the `instance` argument. + + if let Some(ref mut default_ty) = default_ty { + *default_ty = default_ty + .try_call_dunder_get( + db, + Type::none(db), + Type::ClassLiteral(self), + ) + .map(|(return_ty, _)| return_ty) + .unwrap_or_else(Type::unknown); + } } } - } - let mut parameter = - Parameter::positional_or_keyword(name).with_annotated_type(attr_ty); + let mut parameter = + Parameter::positional_or_keyword(name).with_annotated_type(attr_ty); - if let Some(default_ty) = default_ty { - parameter = parameter.with_default_type(default_ty); + if let Some(default_ty) = default_ty { + parameter = parameter.with_default_type(default_ty); + } + + parameters.push(parameter); } - parameters.push(parameter); - } + let init_signature = + Signature::new(Parameters::new(parameters), Some(Type::none(db))); - let init_signature = Signature::new(Parameters::new(parameters), Some(Type::none(db))); + Some(Type::Callable(CallableType::single(db, init_signature))) + } + "__lt__" | "__le__" | "__gt__" | "__ge__" => { + if !has_dataclass_param(DataclassParams::ORDER) { + return None; + } - return Some(Type::Callable(CallableType::single(db, init_signature))); - } else if matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") { - if metadata.contains(DataclassMetadata::ORDER) { let signature = Signature::new( Parameters::new([Parameter::positional_or_keyword(Name::new_static("other")) // TODO: could be `Self`. @@ -1059,11 +1090,17 @@ impl<'db> ClassLiteralType<'db> { Some(KnownClass::Bool.to_instance(db)), ); - return Some(Type::Callable(CallableType::single(db, signature))); + Some(Type::Callable(CallableType::single(db, signature))) } + _ => None, } + } - None + fn is_dataclass(self, db: &'db dyn Db) -> bool { + self.dataclass_params(db).is_some() + || self + .try_metaclass(db) + .is_ok_and(|(_, transformer_params)| transformer_params.is_some()) } /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. @@ -1079,7 +1116,7 @@ impl<'db> ClassLiteralType<'db> { .filter_map(|superclass| { if let Some(class) = superclass.into_class() { let class_literal = class.class_literal(db).0; - if class_literal.dataclass_metadata(db).is_some() { + if class_literal.is_dataclass(db) { Some(class_literal) } else { None diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 5f342a3f3a979f..6142cd322fb0da 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -90,6 +90,7 @@ impl<'db> ClassBase<'db> { | Type::MethodWrapper(_) | Type::WrapperDescriptor(_) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::BytesLiteral(_) | Type::IntLiteral(_) | Type::StringLiteral(_) diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index b420d3ef72c58d..20ca22bd981a60 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -195,7 +195,10 @@ impl Display for DisplayRepresentation<'_> { write!(f, "") } Type::DataclassDecorator(_) => { - f.write_str("") + f.write_str("") + } + Type::DataclassTransformer(_) => { + f.write_str("") } Type::Union(union) => union.display(self.db).fmt(f), Type::Intersection(intersection) => intersection.display(self.db).fmt(f), diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 466fe9be5aece8..13e20cffd467d5 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -82,7 +82,7 @@ use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ binding_type, todo_type, CallDunderError, CallableSignature, CallableType, Class, - ClassLiteralType, ClassType, DataclassMetadata, DynamicType, FunctionDecorators, FunctionType, + ClassLiteralType, ClassType, DataclassParams, DynamicType, FunctionDecorators, FunctionType, GenericAlias, GenericClass, IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, NonGenericClass, Parameter, ParameterForm, Parameters, Signature, Signatures, SliceLiteralType, StringLiteralType, @@ -1457,6 +1457,7 @@ impl<'db> TypeInferenceBuilder<'db> { let mut decorator_types_and_nodes = Vec::with_capacity(decorator_list.len()); let mut function_decorators = FunctionDecorators::empty(); + let mut dataclass_transformer_params = None; for decorator in decorator_list { let decorator_ty = self.infer_decorator(decorator); @@ -1477,6 +1478,8 @@ impl<'db> TypeInferenceBuilder<'db> { function_decorators |= FunctionDecorators::CLASSMETHOD; continue; } + } else if let Type::DataclassTransformer(params) = decorator_ty { + dataclass_transformer_params = Some(params); } decorator_types_and_nodes.push((decorator_ty, decorator)); @@ -1523,6 +1526,7 @@ impl<'db> TypeInferenceBuilder<'db> { function_kind, body_scope, function_decorators, + dataclass_transformer_params, generic_context, specialization, )); @@ -1757,19 +1761,32 @@ impl<'db> TypeInferenceBuilder<'db> { body: _, } = class_node; - let mut dataclass_metadata = None; + let mut dataclass_params = None; + let mut dataclass_transformer_params = None; for decorator in decorator_list { let decorator_ty = self.infer_decorator(decorator); if decorator_ty .into_function_literal() .is_some_and(|function| function.is_known(self.db(), KnownFunction::Dataclass)) { - dataclass_metadata = Some(DataclassMetadata::default()); + dataclass_params = Some(DataclassParams::default()); continue; } - if let Type::DataclassDecorator(metadata) = decorator_ty { - dataclass_metadata = Some(metadata); + if let Type::DataclassDecorator(params) = decorator_ty { + dataclass_params = Some(params); + continue; + } + + if let Type::FunctionLiteral(f) = decorator_ty { + if let Some(params) = f.dataclass_transformer_params(self.db()) { + dataclass_params = Some(params.into()); + continue; + } + } + + if let Type::DataclassTransformer(params) = decorator_ty { + dataclass_transformer_params = Some(params); continue; } } @@ -1789,7 +1806,8 @@ impl<'db> TypeInferenceBuilder<'db> { name: name.id.clone(), body_scope, known: maybe_known_class, - dataclass_metadata, + dataclass_params, + dataclass_transformer_params, }; let class_literal = match generic_context { Some(generic_context) => { @@ -2502,6 +2520,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::MethodWrapper(_) | Type::WrapperDescriptor(_) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::TypeVar(..) | Type::AlwaysTruthy | Type::AlwaysFalsy => { @@ -4882,6 +4901,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::BoundMethod(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) @@ -5164,6 +5184,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::GenericAlias(_) @@ -5188,6 +5209,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::WrapperDescriptor(_) | Type::MethodWrapper(_) | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::GenericAlias(_) diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 8f6968746521d1..111b758bc7bb5f 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -79,6 +79,12 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::DataclassDecorator(_), _) => Ordering::Less, (_, Type::DataclassDecorator(_)) => Ordering::Greater, + (Type::DataclassTransformer(left), Type::DataclassTransformer(right)) => { + left.bits().cmp(&right.bits()) + } + (Type::DataclassTransformer(_), _) => Ordering::Less, + (_, Type::DataclassTransformer(_)) => Ordering::Greater, + (Type::Callable(left), Type::Callable(right)) => { debug_assert_eq!(*left, left.normalized(db)); debug_assert_eq!(*right, right.normalized(db)); From 38a3b056e39a89af9c569c10a6382c5331f1daf2 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 22 Apr 2025 11:55:16 +0200 Subject: [PATCH 0047/1161] [red-knot] mypy_primer: Use upstream repo (#17500) ## Summary Switch to the official version of [`mypy_primer`](https://github.com/hauntsaninja/mypy_primer), now that Red Knot support has been upstreamed (see https://github.com/hauntsaninja/mypy_primer/pull/138, https://github.com/hauntsaninja/mypy_primer/pull/135, https://github.com/hauntsaninja/mypy_primer/pull/151, https://github.com/hauntsaninja/mypy_primer/pull/155). ## Test Plan Locally and in CI --- .github/workflows/mypy_primer.yaml | 2 +- crates/red_knot/docs/mypy_primer.md | 9 ++++----- .../red_knot_python_semantic/resources/primer/good.txt | 2 -- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index ac0dc7a2a16932..7ceb1d9f2dc422 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -45,7 +45,7 @@ jobs: - name: Install mypy_primer run: | - uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v6" + uv tool install "git+https://github.com/hauntsaninja/mypy_primer@ebaa9fd27b51a278873b63676fd25490cec6823b" - name: Run mypy_primer shell: bash diff --git a/crates/red_knot/docs/mypy_primer.md b/crates/red_knot/docs/mypy_primer.md index c4d73add146d38..bcf288265effd1 100644 --- a/crates/red_knot/docs/mypy_primer.md +++ b/crates/red_knot/docs/mypy_primer.md @@ -2,16 +2,16 @@ ## Basics -For now, we use our own [fork of mypy primer]. It can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run: +`mypy_primer` can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run: ```sh -uvx --from "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support" mypy_primer -h +uvx --from "git+https://github.com/hauntsaninja/mypy_primer" mypy_primer -h ``` Alternatively, you can install the forked version of `mypy_primer` using: ```sh -uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support" +uv tool install "git+https://github.com/hauntsaninja/mypy_primer" ``` and then run it using `uvx mypy_primer` or just `mypy_primer`, if your `PATH` is set up accordingly (see: [Tool executables]). @@ -56,6 +56,5 @@ mypy_primer --repo /path/to/ruff --old origin/main --new my/local-branch … Note that you might need to clean up `/tmp/mypy_primer` in order for this to work correctly. -[fork of mypy primer]: https://github.com/astral-sh/mypy_primer/tree/add-red-knot-support -[full list of ecosystem projects]: https://github.com/astral-sh/mypy_primer/blob/add-red-knot-support/mypy_primer/projects.py +[full list of ecosystem projects]: https://github.com/hauntsaninja/mypy_primer/blob/master/mypy_primer/projects.py [tool executables]: https://docs.astral.sh/uv/concepts/tools/#tool-executables diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/red_knot_python_semantic/resources/primer/good.txt index 3dca5c49f9c638..d813eccdd0c13d 100644 --- a/crates/red_knot_python_semantic/resources/primer/good.txt +++ b/crates/red_knot_python_semantic/resources/primer/good.txt @@ -1,4 +1,3 @@ -arrow async-utils bidict black @@ -18,7 +17,6 @@ python-chess python-htmlgen rich scrapy -strawberry typeshed-stats werkzeug zipp From d2b20f736789ab954445604546e25562e3906a19 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 22 Apr 2025 12:05:57 +0200 Subject: [PATCH 0048/1161] [red-knot] mypy_primer: capture backtraces (#17543) ## Summary `mypy_primer` is not deterministic (we pin `mypy_primer` itself, but projects change over time and we just pull in the latest version). We've also seen occasional panics being caught in `mypy_primer` runs, so this is trying to make these CI failures more helpful. --- .github/workflows/mypy_primer.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index 7ceb1d9f2dc422..7382671f114fa7 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -21,6 +21,7 @@ env: CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 + RUST_BACKTRACE: 1 jobs: mypy_primer: From ae6fde152cbb90e804234623c3842d5f78d9c4da Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 22 Apr 2025 12:34:46 +0100 Subject: [PATCH 0049/1161] [red-knot] Move `InstanceType` to its own submodule (#17525) --- crates/red_knot_python_semantic/src/types.rs | 208 +++++----- .../src/types/builder.rs | 8 +- .../src/types/class.rs | 391 +----------------- .../src/types/display.rs | 8 +- .../src/types/infer.rs | 29 +- .../src/types/instance.rs | 73 ++++ .../src/types/known_instance.rs | 372 +++++++++++++++++ .../src/types/type_ordering.rs | 16 +- 8 files changed, 577 insertions(+), 528 deletions(-) create mode 100644 crates/red_knot_python_semantic/src/types/instance.rs create mode 100644 crates/red_knot_python_semantic/src/types/known_instance.rs diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ca20e3c55e17be..06b189a2d4ebbb 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -47,9 +47,10 @@ pub(crate) use crate::types::narrow::infer_narrowing_constraint; use crate::types::signatures::{Parameter, ParameterForm, Parameters}; use crate::{Db, FxOrderSet, Module, Program}; pub(crate) use class::{ - Class, ClassLiteralType, ClassType, GenericAlias, GenericClass, InstanceType, KnownClass, - KnownInstanceType, NonGenericClass, + Class, ClassLiteralType, ClassType, GenericAlias, GenericClass, KnownClass, NonGenericClass, }; +pub(crate) use instance::InstanceType; +pub(crate) use known_instance::KnownInstanceType; mod builder; mod call; @@ -60,6 +61,8 @@ mod diagnostic; mod display; mod generics; mod infer; +mod instance; +mod known_instance; mod mro; mod narrow; mod signatures; @@ -462,7 +465,8 @@ pub enum Type<'db> { GenericAlias(GenericAlias<'db>), /// The set of all class objects that are subclasses of the given class (C), spelled `type[C]`. SubclassOf(SubclassOfType<'db>), - /// The set of Python objects with the given class in their __class__'s method resolution order + /// The set of Python objects with the given class in their __class__'s method resolution order. + /// Construct this variant using the `Type::instance` constructor function. Instance(InstanceType<'db>), /// A single Python object that requires special treatment in the type system KnownInstance(KnownInstanceType<'db>), @@ -527,17 +531,20 @@ impl<'db> Type<'db> { fn is_none(&self, db: &'db dyn Db) -> bool { self.into_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType)) + .is_some_and(|instance| instance.class().is_known(db, KnownClass::NoneType)) } pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { - self.into_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::NotImplementedType)) + self.into_instance().is_some_and(|instance| { + instance + .class() + .is_known(db, KnownClass::NotImplementedType) + }) } pub fn is_object(&self, db: &'db dyn Db) -> bool { self.into_instance() - .is_some_and(|instance| instance.class.is_object(db)) + .is_some_and(|instance| instance.class().is_object(db)) } pub const fn is_todo(&self) -> bool { @@ -689,10 +696,6 @@ impl<'db> Type<'db> { ) } - pub const fn is_instance(&self) -> bool { - matches!(self, Type::Instance(..)) - } - pub const fn is_property_instance(&self) -> bool { matches!(self, Type::PropertyInstance(..)) } @@ -793,13 +796,6 @@ impl<'db> Type<'db> { .expect("Expected a Type::IntLiteral variant") } - pub const fn into_instance(self) -> Option> { - match self { - Type::Instance(instance_type) => Some(instance_type), - _ => None, - } - } - pub const fn into_known_instance(self) -> Option> { match self { Type::KnownInstance(known_instance) => Some(known_instance), @@ -828,10 +824,6 @@ impl<'db> Type<'db> { matches!(self, Type::LiteralString) } - pub const fn instance(class: ClassType<'db>) -> Self { - Self::Instance(InstanceType { class }) - } - pub fn string_literal(db: &'db dyn Db, string: &str) -> Self { Self::StringLiteral(StringLiteralType::new(db, string)) } @@ -963,7 +955,7 @@ impl<'db> Type<'db> { (_, Type::Never) => false, // Everything is a subtype of `object`. - (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, + (_, Type::Instance(instance)) if instance.class().is_object(db) => true, // A fully static typevar is always a subtype of itself, and is never a subtype of any // other typevar, since there is no guarantee that they will be specialized to the same @@ -1285,7 +1277,7 @@ impl<'db> Type<'db> { // All types are assignable to `object`. // TODO this special case might be removable once the below cases are comprehensive - (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, + (_, Type::Instance(instance)) if instance.class().is_object(db) => true, // A typevar is always assignable to itself, and is never assignable to any other // typevar, since there is no guarantee that they will be specialized to the same @@ -1440,13 +1432,13 @@ impl<'db> Type<'db> { // TODO: This is a workaround to avoid false positives (e.g. when checking function calls // with `SupportsIndex` parameters), which should be removed when we understand protocols. - (lhs, Type::Instance(InstanceType { class })) - if class.is_known(db, KnownClass::SupportsIndex) => + (lhs, Type::Instance(instance)) + if instance.class().is_known(db, KnownClass::SupportsIndex) => { match lhs { - Type::Instance(InstanceType { class }) + Type::Instance(instance) if matches!( - class.known(db), + instance.class().known(db), Some(KnownClass::Int | KnownClass::SupportsIndex) ) => { @@ -1458,9 +1450,7 @@ impl<'db> Type<'db> { } // TODO: ditto for avoiding false positives when checking function calls with `Sized` parameters. - (lhs, Type::Instance(InstanceType { class })) - if class.is_known(db, KnownClass::Sized) => - { + (lhs, Type::Instance(instance)) if instance.class().is_known(db, KnownClass::Sized) => { matches!( lhs.to_meta_type(db).member(db, "__len__"), SymbolAndQualifiers { @@ -1771,9 +1761,9 @@ impl<'db> Type<'db> { .is_disjoint_from(db, other), }, - (Type::KnownInstance(known_instance), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::KnownInstance(known_instance)) => { - !known_instance.is_instance_of(db, class) + (Type::KnownInstance(known_instance), Type::Instance(instance)) + | (Type::Instance(instance), Type::KnownInstance(known_instance)) => { + !known_instance.is_instance_of(db, instance.class()) } (known_instance_ty @ Type::KnownInstance(_), Type::Tuple(_)) @@ -1781,20 +1771,20 @@ impl<'db> Type<'db> { known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db)) } - (Type::BooleanLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::BooleanLiteral(..)) => { + (Type::BooleanLiteral(..), Type::Instance(instance)) + | (Type::Instance(instance), Type::BooleanLiteral(..)) => { // A `Type::BooleanLiteral()` must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) - !KnownClass::Bool.is_subclass_of(db, class) + !KnownClass::Bool.is_subclass_of(db, instance.class()) } (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true, - (Type::IntLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::IntLiteral(..)) => { + (Type::IntLiteral(..), Type::Instance(instance)) + | (Type::Instance(instance), Type::IntLiteral(..)) => { // A `Type::IntLiteral()` must be an instance of exactly `int` // (it cannot be an instance of an `int` subclass) - !KnownClass::Int.is_subclass_of(db, class) + !KnownClass::Int.is_subclass_of(db, instance.class()) } (Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true, @@ -1802,34 +1792,28 @@ impl<'db> Type<'db> { (Type::StringLiteral(..), Type::LiteralString) | (Type::LiteralString, Type::StringLiteral(..)) => false, - ( - Type::StringLiteral(..) | Type::LiteralString, - Type::Instance(InstanceType { class }), - ) - | ( - Type::Instance(InstanceType { class }), - Type::StringLiteral(..) | Type::LiteralString, - ) => { + (Type::StringLiteral(..) | Type::LiteralString, Type::Instance(instance)) + | (Type::Instance(instance), Type::StringLiteral(..) | Type::LiteralString) => { // A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str` // (it cannot be an instance of a `str` subclass) - !KnownClass::Str.is_subclass_of(db, class) + !KnownClass::Str.is_subclass_of(db, instance.class()) } (Type::LiteralString, Type::LiteralString) => false, (Type::LiteralString, _) | (_, Type::LiteralString) => true, - (Type::BytesLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::BytesLiteral(..)) => { + (Type::BytesLiteral(..), Type::Instance(instance)) + | (Type::Instance(instance), Type::BytesLiteral(..)) => { // A `Type::BytesLiteral()` must be an instance of exactly `bytes` // (it cannot be an instance of a `bytes` subclass) - !KnownClass::Bytes.is_subclass_of(db, class) + !KnownClass::Bytes.is_subclass_of(db, instance.class()) } - (Type::SliceLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::SliceLiteral(..)) => { + (Type::SliceLiteral(..), Type::Instance(instance)) + | (Type::Instance(instance), Type::SliceLiteral(..)) => { // A `Type::SliceLiteral` must be an instance of exactly `slice` // (it cannot be an instance of a `slice` subclass) - !KnownClass::Slice.is_subclass_of(db, class) + !KnownClass::Slice.is_subclass_of(db, instance.class()) } // A class-literal type `X` is always disjoint from an instance type `Y`, @@ -1844,11 +1828,11 @@ impl<'db> Type<'db> { .metaclass_instance_type(db) .is_subtype_of(db, instance), - (Type::FunctionLiteral(..), Type::Instance(InstanceType { class })) - | (Type::Instance(InstanceType { class }), Type::FunctionLiteral(..)) => { + (Type::FunctionLiteral(..), Type::Instance(instance)) + | (Type::Instance(instance), Type::FunctionLiteral(..)) => { // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` // (it cannot be an instance of a `types.FunctionType` subclass) - !KnownClass::FunctionType.is_subclass_of(db, class) + !KnownClass::FunctionType.is_subclass_of(db, instance.class()) } (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType @@ -1907,13 +1891,7 @@ impl<'db> Type<'db> { other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db)) } - ( - Type::Instance(InstanceType { class: left_class }), - Type::Instance(InstanceType { class: right_class }), - ) => { - (left_class.is_final(db) && !left_class.is_subclass_of(db, right_class)) - || (right_class.is_final(db) && !right_class.is_subclass_of(db, left_class)) - } + (Type::Instance(left), Type::Instance(right)) => left.is_disjoint_from(db, right), (Type::Tuple(tuple), Type::Tuple(other_tuple)) => { let self_elements = tuple.elements(db); @@ -2092,9 +2070,7 @@ impl<'db> Type<'db> { false } Type::DataclassDecorator(_) | Type::DataclassTransformer(_) => false, - Type::Instance(InstanceType { class }) => { - class.known(db).is_some_and(KnownClass::is_singleton) - } + Type::Instance(instance) => instance.is_singleton(db), Type::PropertyInstance(_) => false, Type::Tuple(..) => { // The empty tuple is a singleton on CPython and PyPy, but not on other Python @@ -2166,9 +2142,7 @@ impl<'db> Type<'db> { .iter() .all(|elem| elem.is_single_valued(db)), - Type::Instance(InstanceType { class }) => { - class.known(db).is_some_and(KnownClass::is_single_valued) - } + Type::Instance(instance) => instance.is_single_valued(db), Type::BoundSuper(_) => { // At runtime two super instances never compare equal, even if their arguments are identical. @@ -2309,7 +2283,7 @@ impl<'db> Type<'db> { // We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type). // So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the // MRO of the class `object`. - Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Type) => { + Type::Instance(instance) if instance.class().is_known(db, KnownClass::Type) => { KnownClass::Object .to_class_literal(db) .find_name_in_mro_with_policy(db, name, policy) @@ -2399,7 +2373,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Never => Symbol::bound(self).into(), - Type::Instance(InstanceType { class }) => class.instance_member(db, name), + Type::Instance(instance) => instance.class().instance_member(db, name), Type::FunctionLiteral(_) => KnownClass::FunctionType .to_instance(db) @@ -2840,9 +2814,9 @@ impl<'db> Type<'db> { .to_instance(db) .member_lookup_with_policy(db, name, policy), - Type::Instance(InstanceType { class }) + Type::Instance(instance) if matches!(name.as_str(), "major" | "minor") - && class.is_known(db, KnownClass::VersionInfo) => + && instance.class().is_known(db, KnownClass::VersionInfo) => { let python_version = Program::get(db).python_version(db); let segment = if name == "major" { @@ -2912,10 +2886,11 @@ impl<'db> Type<'db> { // attributes on the original type. But in typeshed its return type is `Any`. // It will need a special handling, so it remember the origin type to properly // resolve the attribute. - if self.into_instance().is_some_and(|instance| { - instance.class.is_known(db, KnownClass::ModuleType) - || instance.class.is_known(db, KnownClass::GenericAlias) - }) { + if matches!( + self.into_instance() + .and_then(|instance| instance.class().known(db)), + Some(KnownClass::ModuleType | KnownClass::GenericAlias) + ) { return Symbol::Unbound.into(); } @@ -3173,7 +3148,7 @@ impl<'db> Type<'db> { } }, - Type::Instance(InstanceType { class }) => match class.known(db) { + Type::Instance(instance) => match instance.class().known(db) { Some(known_class) => known_class.bool(), None => try_dunder_bool()?, }, @@ -4561,7 +4536,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) => Ok(*self), - Type::Instance(InstanceType { class }) => match class.known(db) { + Type::Instance(instance) => match instance.class().known(db) { Some(KnownClass::TypeVar) => Ok(todo_type!( "Support for `typing.TypeVar` instances in type expressions" )), @@ -4637,8 +4612,8 @@ impl<'db> Type<'db> { pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> { match self { Type::Never => Type::Never, - Type::Instance(InstanceType { class }) => SubclassOfType::from(db, *class), - Type::KnownInstance(known_instance) => known_instance.class().to_class_literal(db), + Type::Instance(instance) => instance.to_meta_type(db), + Type::KnownInstance(known_instance) => known_instance.to_meta_type(db), Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db), Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), Type::BooleanLiteral(_) => KnownClass::Bool.to_class_literal(db), @@ -4872,7 +4847,9 @@ impl<'db> Type<'db> { Some(TypeDefinition::Class(class_literal.definition(db))) } Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))), - Self::Instance(instance) => Some(TypeDefinition::Class(instance.class.definition(db))), + Self::Instance(instance) => { + Some(TypeDefinition::Class(instance.class().definition(db))) + } Self::KnownInstance(instance) => match instance { KnownInstanceType::TypeVar(var) => { Some(TypeDefinition::TypeVar(var.definition(db))) @@ -7198,7 +7175,7 @@ impl<'db> SuperOwnerKind<'db> { match self { SuperOwnerKind::Dynamic(dynamic) => Either::Left(ClassBase::Dynamic(dynamic).mro(db)), SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)), - SuperOwnerKind::Instance(instance) => Either::Right(instance.class.iter_mro(db)), + SuperOwnerKind::Instance(instance) => Either::Right(instance.class().iter_mro(db)), } } @@ -7214,7 +7191,7 @@ impl<'db> SuperOwnerKind<'db> { match self { SuperOwnerKind::Dynamic(_) => None, SuperOwnerKind::Class(class) => Some(class), - SuperOwnerKind::Instance(instance) => Some(instance.class), + SuperOwnerKind::Instance(instance) => Some(instance.class()), } } @@ -7385,35 +7362,38 @@ impl<'db> BoundSuperType<'db> { policy: MemberLookupPolicy, ) -> SymbolAndQualifiers<'db> { let owner = self.owner(db); - match owner { - SuperOwnerKind::Dynamic(_) => owner - .into_type() - .find_name_in_mro_with_policy(db, name, policy) - .expect("Calling `find_name_in_mro` on dynamic type should return `Some`"), - SuperOwnerKind::Class(class) | SuperOwnerKind::Instance(InstanceType { class }) => { - let (class_literal, _) = class.class_literal(db); - // TODO properly support super() with generic types - // * requires a fix for https://github.com/astral-sh/ruff/issues/17432 - // * also requires understanding how we should handle cases like this: - // ```python - // b_int: B[int] - // b_unknown: B - // - // super(B, b_int) - // super(B[int], b_unknown) - // ``` - match class_literal { - ClassLiteralType::Generic(_) => { - Symbol::bound(todo_type!("super in generic class")).into() - } - ClassLiteralType::NonGeneric(_) => class_literal.class_member_from_mro( - db, - name, - policy, - self.skip_until_after_pivot(db, owner.iter_mro(db)), - ), - } + let class = match owner { + SuperOwnerKind::Dynamic(_) => { + return owner + .into_type() + .find_name_in_mro_with_policy(db, name, policy) + .expect("Calling `find_name_in_mro` on dynamic type should return `Some`") } + SuperOwnerKind::Class(class) => *class, + SuperOwnerKind::Instance(instance) => instance.class(), + }; + + let (class_literal, _) = class.class_literal(db); + // TODO properly support super() with generic types + // * requires a fix for https://github.com/astral-sh/ruff/issues/17432 + // * also requires understanding how we should handle cases like this: + // ```python + // b_int: B[int] + // b_unknown: B + // + // super(B, b_int) + // super(B[int], b_unknown) + // ``` + match class_literal { + ClassLiteralType::Generic(_) => { + Symbol::bound(todo_type!("super in generic class")).into() + } + ClassLiteralType::NonGeneric(_) => class_literal.class_member_from_mro( + db, + name, + policy, + self.skip_until_after_pivot(db, owner.iter_mro(db)), + ), } } } diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 21171f11fb1c31..41d194652e3d80 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -493,7 +493,7 @@ impl<'db> InnerIntersectionBuilder<'db> { _ => { let known_instance = new_positive .into_instance() - .and_then(|instance| instance.class.known(db)); + .and_then(|instance| instance.class().known(db)); if known_instance == Some(KnownClass::Object) { // `object & T` -> `T`; it is always redundant to add `object` to an intersection @@ -513,7 +513,7 @@ impl<'db> InnerIntersectionBuilder<'db> { new_positive = Type::BooleanLiteral(false); } Type::Instance(instance) - if instance.class.is_known(db, KnownClass::Bool) => + if instance.class().is_known(db, KnownClass::Bool) => { match new_positive { // `bool & AlwaysTruthy` -> `Literal[True]` @@ -607,7 +607,7 @@ impl<'db> InnerIntersectionBuilder<'db> { self.positive .iter() .filter_map(|ty| ty.into_instance()) - .filter_map(|instance| instance.class.known(db)) + .filter_map(|instance| instance.class().known(db)) .any(KnownClass::is_bool) }; @@ -623,7 +623,7 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::Never => { // Adding ~Never to an intersection is a no-op. } - Type::Instance(instance) if instance.class.is_object(db) => { + Type::Instance(instance) if instance.class().is_object(db) => { // Adding ~object to an intersection results in Never. *self = Self::default(); self.positive.insert(Type::Never); diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index e8b92667ac03eb..e90ef080d49bc1 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -4,13 +4,15 @@ use std::sync::{LazyLock, Mutex}; use super::{ class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder, KnownFunction, MemberLookupPolicy, Mro, MroError, MroIterator, SubclassOfType, Truthiness, - Type, TypeAliasType, TypeQualifiers, TypeVarInstance, + Type, TypeQualifiers, }; use crate::semantic_index::definition::Definition; use crate::semantic_index::DeclarationWithConstraint; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{Parameter, Parameters}; -use crate::types::{CallableType, DataclassParams, DataclassTransformerParams, Signature}; +use crate::types::{ + CallableType, DataclassParams, DataclassTransformerParams, KnownInstanceType, Signature, +}; use crate::{ module_resolver::file_to_module, semantic_index::{ @@ -1690,40 +1692,6 @@ impl InheritanceCycle { } } -/// A type representing the set of runtime objects which are instances of a certain class. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] -pub struct InstanceType<'db> { - pub class: ClassType<'db>, -} - -impl<'db> InstanceType<'db> { - pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { - // N.B. The subclass relation is fully static - self.class.is_subclass_of(db, other.class) - } - - pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { - self.class.is_equivalent_to(db, other.class) - } - - pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool { - self.class.is_assignable_to(db, other.class) - } - - pub(super) fn is_gradual_equivalent_to( - self, - db: &'db dyn Db, - other: InstanceType<'db>, - ) -> bool { - self.class.is_gradual_equivalent_to(db, other.class) - } -} - -impl<'db> From> for Type<'db> { - fn from(value: InstanceType<'db>) -> Self { - Self::Instance(value) - } -} /// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow /// for easier syntax when interacting with very common classes. /// @@ -2447,357 +2415,6 @@ impl<'db> KnownClassLookupError<'db> { } } -/// Enumeration of specific runtime that are special enough to be considered their own type. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] -pub enum KnownInstanceType<'db> { - /// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`) - Annotated, - /// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`) - Literal, - /// The symbol `typing.LiteralString` (which can also be found as `typing_extensions.LiteralString`) - LiteralString, - /// The symbol `typing.Optional` (which can also be found as `typing_extensions.Optional`) - Optional, - /// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`) - Union, - /// The symbol `typing.NoReturn` (which can also be found as `typing_extensions.NoReturn`) - NoReturn, - /// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`) - Never, - /// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`) - /// This is not used since typeshed switched to representing `Any` as a class; now we use - /// `KnownClass::Any` instead. But we still support the old `Any = object()` representation, at - /// least for now. TODO maybe remove? - Any, - /// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`) - Tuple, - /// The symbol `typing.List` (which can also be found as `typing_extensions.List`) - List, - /// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`) - Dict, - /// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`) - Set, - /// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`) - FrozenSet, - /// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`) - ChainMap, - /// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`) - Counter, - /// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`) - DefaultDict, - /// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`) - Deque, - /// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`) - OrderedDict, - /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) - Protocol, - /// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`) - Generic, - /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) - Type, - /// A single instance of `typing.TypeVar` - TypeVar(TypeVarInstance<'db>), - /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) - TypeAliasType(TypeAliasType<'db>), - /// The symbol `knot_extensions.Unknown` - Unknown, - /// The symbol `knot_extensions.AlwaysTruthy` - AlwaysTruthy, - /// The symbol `knot_extensions.AlwaysFalsy` - AlwaysFalsy, - /// The symbol `knot_extensions.Not` - Not, - /// The symbol `knot_extensions.Intersection` - Intersection, - /// The symbol `knot_extensions.TypeOf` - TypeOf, - /// The symbol `knot_extensions.CallableTypeOf` - CallableTypeOf, - - // Various special forms, special aliases and type qualifiers that we don't yet understand - // (all currently inferred as TODO in most contexts): - TypingSelf, - Final, - ClassVar, - Callable, - Concatenate, - Unpack, - Required, - NotRequired, - TypeAlias, - TypeGuard, - TypeIs, - ReadOnly, - // TODO: fill this enum out with more special forms, etc. -} - -impl<'db> KnownInstanceType<'db> { - /// Evaluate the known instance in boolean context - pub(crate) const fn bool(self) -> Truthiness { - match self { - Self::Annotated - | Self::Literal - | Self::LiteralString - | Self::Optional - | Self::TypeVar(_) - | Self::Union - | Self::NoReturn - | Self::Never - | Self::Any - | Self::Tuple - | Self::Type - | Self::TypingSelf - | Self::Final - | Self::ClassVar - | Self::Callable - | Self::Concatenate - | Self::Unpack - | Self::Required - | Self::NotRequired - | Self::TypeAlias - | Self::TypeGuard - | Self::TypeIs - | Self::List - | Self::Dict - | Self::DefaultDict - | Self::Set - | Self::FrozenSet - | Self::Counter - | Self::Deque - | Self::ChainMap - | Self::OrderedDict - | Self::Protocol - | Self::Generic - | Self::ReadOnly - | Self::TypeAliasType(_) - | Self::Unknown - | Self::AlwaysTruthy - | Self::AlwaysFalsy - | Self::Not - | Self::Intersection - | Self::TypeOf - | Self::CallableTypeOf => Truthiness::AlwaysTrue, - } - } - - /// Return the repr of the symbol at runtime - pub(crate) fn repr(self, db: &'db dyn Db) -> &'db str { - match self { - Self::Annotated => "typing.Annotated", - Self::Literal => "typing.Literal", - Self::LiteralString => "typing.LiteralString", - Self::Optional => "typing.Optional", - Self::Union => "typing.Union", - Self::NoReturn => "typing.NoReturn", - Self::Never => "typing.Never", - Self::Any => "typing.Any", - Self::Tuple => "typing.Tuple", - Self::Type => "typing.Type", - Self::TypingSelf => "typing.Self", - Self::Final => "typing.Final", - Self::ClassVar => "typing.ClassVar", - Self::Callable => "typing.Callable", - Self::Concatenate => "typing.Concatenate", - Self::Unpack => "typing.Unpack", - Self::Required => "typing.Required", - Self::NotRequired => "typing.NotRequired", - Self::TypeAlias => "typing.TypeAlias", - Self::TypeGuard => "typing.TypeGuard", - Self::TypeIs => "typing.TypeIs", - Self::List => "typing.List", - Self::Dict => "typing.Dict", - Self::DefaultDict => "typing.DefaultDict", - Self::Set => "typing.Set", - Self::FrozenSet => "typing.FrozenSet", - Self::Counter => "typing.Counter", - Self::Deque => "typing.Deque", - Self::ChainMap => "typing.ChainMap", - Self::OrderedDict => "typing.OrderedDict", - Self::Protocol => "typing.Protocol", - Self::Generic => "typing.Generic", - Self::ReadOnly => "typing.ReadOnly", - Self::TypeVar(typevar) => typevar.name(db), - Self::TypeAliasType(_) => "typing.TypeAliasType", - Self::Unknown => "knot_extensions.Unknown", - Self::AlwaysTruthy => "knot_extensions.AlwaysTruthy", - Self::AlwaysFalsy => "knot_extensions.AlwaysFalsy", - Self::Not => "knot_extensions.Not", - Self::Intersection => "knot_extensions.Intersection", - Self::TypeOf => "knot_extensions.TypeOf", - Self::CallableTypeOf => "knot_extensions.CallableTypeOf", - } - } - - /// Return the [`KnownClass`] which this symbol is an instance of - pub(crate) const fn class(self) -> KnownClass { - match self { - Self::Annotated => KnownClass::SpecialForm, - Self::Literal => KnownClass::SpecialForm, - Self::LiteralString => KnownClass::SpecialForm, - Self::Optional => KnownClass::SpecialForm, - Self::Union => KnownClass::SpecialForm, - Self::NoReturn => KnownClass::SpecialForm, - Self::Never => KnownClass::SpecialForm, - Self::Any => KnownClass::Object, - Self::Tuple => KnownClass::SpecialForm, - Self::Type => KnownClass::SpecialForm, - Self::TypingSelf => KnownClass::SpecialForm, - Self::Final => KnownClass::SpecialForm, - Self::ClassVar => KnownClass::SpecialForm, - Self::Callable => KnownClass::SpecialForm, - Self::Concatenate => KnownClass::SpecialForm, - Self::Unpack => KnownClass::SpecialForm, - Self::Required => KnownClass::SpecialForm, - Self::NotRequired => KnownClass::SpecialForm, - Self::TypeAlias => KnownClass::SpecialForm, - Self::TypeGuard => KnownClass::SpecialForm, - Self::TypeIs => KnownClass::SpecialForm, - Self::ReadOnly => KnownClass::SpecialForm, - Self::List => KnownClass::StdlibAlias, - Self::Dict => KnownClass::StdlibAlias, - Self::DefaultDict => KnownClass::StdlibAlias, - Self::Set => KnownClass::StdlibAlias, - Self::FrozenSet => KnownClass::StdlibAlias, - Self::Counter => KnownClass::StdlibAlias, - Self::Deque => KnownClass::StdlibAlias, - Self::ChainMap => KnownClass::StdlibAlias, - Self::OrderedDict => KnownClass::StdlibAlias, - Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says - Self::Generic => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says - Self::TypeVar(_) => KnownClass::TypeVar, - Self::TypeAliasType(_) => KnownClass::TypeAliasType, - Self::TypeOf => KnownClass::SpecialForm, - Self::Not => KnownClass::SpecialForm, - Self::Intersection => KnownClass::SpecialForm, - Self::CallableTypeOf => KnownClass::SpecialForm, - Self::Unknown => KnownClass::Object, - Self::AlwaysTruthy => KnownClass::Object, - Self::AlwaysFalsy => KnownClass::Object, - } - } - - /// Return the instance type which this type is a subtype of. - /// - /// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`, - /// so `KnownInstanceType::Literal.instance_fallback(db)` - /// returns `Type::Instance(InstanceType { class: })`. - pub(super) fn instance_fallback(self, db: &dyn Db) -> Type { - self.class().to_instance(db) - } - - /// Return `true` if this symbol is an instance of `class`. - pub(super) fn is_instance_of(self, db: &'db dyn Db, class: ClassType<'db>) -> bool { - self.class().is_subclass_of(db, class) - } - - pub(super) fn try_from_file_and_name( - db: &'db dyn Db, - file: File, - symbol_name: &str, - ) -> Option { - let candidate = match symbol_name { - "Any" => Self::Any, - "ClassVar" => Self::ClassVar, - "Deque" => Self::Deque, - "List" => Self::List, - "Dict" => Self::Dict, - "DefaultDict" => Self::DefaultDict, - "Set" => Self::Set, - "FrozenSet" => Self::FrozenSet, - "Counter" => Self::Counter, - "ChainMap" => Self::ChainMap, - "OrderedDict" => Self::OrderedDict, - "Generic" => Self::Generic, - "Protocol" => Self::Protocol, - "Optional" => Self::Optional, - "Union" => Self::Union, - "NoReturn" => Self::NoReturn, - "Tuple" => Self::Tuple, - "Type" => Self::Type, - "Callable" => Self::Callable, - "Annotated" => Self::Annotated, - "Literal" => Self::Literal, - "Never" => Self::Never, - "Self" => Self::TypingSelf, - "Final" => Self::Final, - "Unpack" => Self::Unpack, - "Required" => Self::Required, - "TypeAlias" => Self::TypeAlias, - "TypeGuard" => Self::TypeGuard, - "TypeIs" => Self::TypeIs, - "ReadOnly" => Self::ReadOnly, - "Concatenate" => Self::Concatenate, - "NotRequired" => Self::NotRequired, - "LiteralString" => Self::LiteralString, - "Unknown" => Self::Unknown, - "AlwaysTruthy" => Self::AlwaysTruthy, - "AlwaysFalsy" => Self::AlwaysFalsy, - "Not" => Self::Not, - "Intersection" => Self::Intersection, - "TypeOf" => Self::TypeOf, - "CallableTypeOf" => Self::CallableTypeOf, - _ => return None, - }; - - candidate - .check_module(file_to_module(db, file)?.known()?) - .then_some(candidate) - } - - /// Return `true` if `module` is a module from which this `KnownInstance` variant can validly originate. - /// - /// Most variants can only exist in one module, which is the same as `self.class().canonical_module()`. - /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. - pub(super) fn check_module(self, module: KnownModule) -> bool { - match self { - Self::Any - | Self::ClassVar - | Self::Deque - | Self::List - | Self::Dict - | Self::DefaultDict - | Self::Set - | Self::FrozenSet - | Self::Counter - | Self::ChainMap - | Self::OrderedDict - | Self::Optional - | Self::Union - | Self::NoReturn - | Self::Tuple - | Self::Type - | Self::Generic - | Self::Callable => module.is_typing(), - Self::Annotated - | Self::Protocol - | Self::Literal - | Self::LiteralString - | Self::Never - | Self::TypingSelf - | Self::Final - | Self::Concatenate - | Self::Unpack - | Self::Required - | Self::NotRequired - | Self::TypeAlias - | Self::TypeGuard - | Self::TypeIs - | Self::ReadOnly - | Self::TypeAliasType(_) - | Self::TypeVar(_) => { - matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) - } - Self::Unknown - | Self::AlwaysTruthy - | Self::AlwaysFalsy - | Self::Not - | Self::Intersection - | Self::TypeOf - | Self::CallableTypeOf => module.is_knot_extensions(), - } - } -} - #[derive(Debug, Clone, PartialEq, Eq, salsa::Update)] pub(super) struct MetaclassError<'db> { kind: MetaclassErrorKind<'db>, diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 20ca22bd981a60..5687b6c35c0081 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -10,9 +10,9 @@ use crate::types::class::{ClassType, GenericAlias, GenericClass}; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ - FunctionSignature, InstanceType, IntersectionType, KnownClass, MethodWrapperKind, - StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, - UnionType, WrapperDescriptorKind, + FunctionSignature, IntersectionType, KnownClass, MethodWrapperKind, StringLiteralType, + SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType, + WrapperDescriptorKind, }; use crate::Db; use rustc_hash::FxHashMap; @@ -73,7 +73,7 @@ impl Display for DisplayRepresentation<'_> { match self.ty { Type::Dynamic(dynamic) => dynamic.fmt(f), Type::Never => f.write_str("Never"), - Type::Instance(InstanceType { class }) => match (class, class.known(self.db)) { + Type::Instance(instance) => match (instance.class(), instance.class().known(self.db)) { (_, Some(KnownClass::NoneType)) => f.write_str("None"), (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), (ClassType::NonGeneric(class), _) => f.write_str(&class.class(self.db).name), diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 13e20cffd467d5..d2649620dc0e36 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1041,7 +1041,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} Type::Instance(instance) if matches!( - instance.class.known(self.db()), + instance.class().known(self.db()), Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) ) => {} _ => return false, @@ -2475,7 +2475,7 @@ impl<'db> TypeInferenceBuilder<'db> { } // Super instances do not allow attribute assignment - Type::Instance(instance) if instance.class.is_known(db, KnownClass::Super) => { + Type::Instance(instance) if instance.class().is_known(db, KnownClass::Super) => { if emit_diagnostics { self.context.report_lint_old( &INVALID_ASSIGNMENT, @@ -2991,7 +2991,10 @@ impl<'db> TypeInferenceBuilder<'db> { // Handle various singletons. if let Type::Instance(instance) = declared_ty.inner_type() { - if instance.class.is_known(self.db(), KnownClass::SpecialForm) { + if instance + .class() + .is_known(self.db(), KnownClass::SpecialForm) + { if let Some(name_expr) = target.as_name_expr() { if let Some(known_instance) = KnownInstanceType::try_from_file_and_name( self.db(), @@ -5780,7 +5783,9 @@ impl<'db> TypeInferenceBuilder<'db> { range, ), (Type::Tuple(_), Type::Instance(instance)) - if instance.class.is_known(self.db(), KnownClass::VersionInfo) => + if instance + .class() + .is_known(self.db(), KnownClass::VersionInfo) => { self.infer_binary_type_comparison( left, @@ -5790,7 +5795,9 @@ impl<'db> TypeInferenceBuilder<'db> { ) } (Type::Instance(instance), Type::Tuple(_)) - if instance.class.is_known(self.db(), KnownClass::VersionInfo) => + if instance + .class() + .is_known(self.db(), KnownClass::VersionInfo) => { self.infer_binary_type_comparison( Type::version_info_tuple(self.db()), @@ -6168,12 +6175,16 @@ impl<'db> TypeInferenceBuilder<'db> { ( Type::Instance(instance), Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::SliceLiteral(_), - ) if instance.class.is_known(self.db(), KnownClass::VersionInfo) => self - .infer_subscript_expression_types( + ) if instance + .class() + .is_known(self.db(), KnownClass::VersionInfo) => + { + self.infer_subscript_expression_types( value_node, Type::version_info_tuple(self.db()), slice_ty, - ), + ) + } // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` (Type::Tuple(tuple_ty), Type::IntLiteral(int)) if i32::try_from(int).is_ok() => { @@ -6459,7 +6470,7 @@ impl<'db> TypeInferenceBuilder<'db> { }, Some(Type::BooleanLiteral(b)) => SliceArg::Arg(Some(i32::from(b))), Some(Type::Instance(instance)) - if instance.class.is_known(self.db(), KnownClass::NoneType) => + if instance.class().is_known(self.db(), KnownClass::NoneType) => { SliceArg::Arg(None) } diff --git a/crates/red_knot_python_semantic/src/types/instance.rs b/crates/red_knot_python_semantic/src/types/instance.rs new file mode 100644 index 00000000000000..1b0ec2f4fde9e5 --- /dev/null +++ b/crates/red_knot_python_semantic/src/types/instance.rs @@ -0,0 +1,73 @@ +//! Instance types: both nominal and structural. + +use super::{ClassType, KnownClass, SubclassOfType, Type}; +use crate::Db; + +impl<'db> Type<'db> { + pub(crate) const fn instance(class: ClassType<'db>) -> Self { + Self::Instance(InstanceType { class }) + } + + pub(crate) const fn into_instance(self) -> Option> { + match self { + Type::Instance(instance_type) => Some(instance_type), + _ => None, + } + } +} + +/// A type representing the set of runtime objects which are instances of a certain nominal class. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] +pub struct InstanceType<'db> { + // Keep this field private, so that the only way of constructing `InstanceType` instances + // is through the `Type::instance` constructor function. + class: ClassType<'db>, +} + +impl<'db> InstanceType<'db> { + pub(super) fn class(self) -> ClassType<'db> { + self.class + } + + pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { + // N.B. The subclass relation is fully static + self.class.is_subclass_of(db, other.class) + } + + pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.class.is_equivalent_to(db, other.class) + } + + pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { + self.class.is_assignable_to(db, other.class) + } + + pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool { + (self.class.is_final(db) && !self.class.is_subclass_of(db, other.class)) + || (other.class.is_final(db) && !other.class.is_subclass_of(db, self.class)) + } + + pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.class.is_gradual_equivalent_to(db, other.class) + } + + pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool { + self.class.known(db).is_some_and(KnownClass::is_singleton) + } + + pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool { + self.class + .known(db) + .is_some_and(KnownClass::is_single_valued) + } + + pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + SubclassOfType::from(db, self.class) + } +} + +impl<'db> From> for Type<'db> { + fn from(value: InstanceType<'db>) -> Self { + Self::Instance(value) + } +} diff --git a/crates/red_knot_python_semantic/src/types/known_instance.rs b/crates/red_knot_python_semantic/src/types/known_instance.rs new file mode 100644 index 00000000000000..27ed7a533a1438 --- /dev/null +++ b/crates/red_knot_python_semantic/src/types/known_instance.rs @@ -0,0 +1,372 @@ +//! The `KnownInstance` type. +//! +//! Despite its name, this is quite a different type from [`super::InstanceType`]. +//! For the vast majority of instance-types in Python, we cannot say how many possible +//! inhabitants there are or could be of that type at runtime. Each variant of the +//! [`KnownInstanceType`] enum, however, represents a specific runtime symbol +//! that requires heavy special-casing in the type system. Thus any one `KnownInstance` +//! variant can only be inhabited by one or two specific objects at runtime with +//! locations that are known in advance. + +use super::{class::KnownClass, ClassType, Truthiness, Type, TypeAliasType, TypeVarInstance}; +use crate::db::Db; +use crate::module_resolver::{file_to_module, KnownModule}; +use ruff_db::files::File; + +/// Enumeration of specific runtime symbols that are special enough +/// that they can each be considered to inhabit a unique type. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum KnownInstanceType<'db> { + /// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`) + Annotated, + /// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`) + Literal, + /// The symbol `typing.LiteralString` (which can also be found as `typing_extensions.LiteralString`) + LiteralString, + /// The symbol `typing.Optional` (which can also be found as `typing_extensions.Optional`) + Optional, + /// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`) + Union, + /// The symbol `typing.NoReturn` (which can also be found as `typing_extensions.NoReturn`) + NoReturn, + /// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`) + Never, + /// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`) + /// This is not used since typeshed switched to representing `Any` as a class; now we use + /// `KnownClass::Any` instead. But we still support the old `Any = object()` representation, at + /// least for now. TODO maybe remove? + Any, + /// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`) + Tuple, + /// The symbol `typing.List` (which can also be found as `typing_extensions.List`) + List, + /// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`) + Dict, + /// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`) + Set, + /// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`) + FrozenSet, + /// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`) + ChainMap, + /// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`) + Counter, + /// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`) + DefaultDict, + /// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`) + Deque, + /// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`) + OrderedDict, + /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) + Protocol, + /// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`) + Generic, + /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) + Type, + /// A single instance of `typing.TypeVar` + TypeVar(TypeVarInstance<'db>), + /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) + TypeAliasType(TypeAliasType<'db>), + /// The symbol `knot_extensions.Unknown` + Unknown, + /// The symbol `knot_extensions.AlwaysTruthy` + AlwaysTruthy, + /// The symbol `knot_extensions.AlwaysFalsy` + AlwaysFalsy, + /// The symbol `knot_extensions.Not` + Not, + /// The symbol `knot_extensions.Intersection` + Intersection, + /// The symbol `knot_extensions.TypeOf` + TypeOf, + /// The symbol `knot_extensions.CallableTypeOf` + CallableTypeOf, + /// The symbol `typing.Callable` + /// (which can also be found as `typing_extensions.Callable` or as `collections.abc.Callable`) + Callable, + + // Various special forms, special aliases and type qualifiers that we don't yet understand + // (all currently inferred as TODO in most contexts): + TypingSelf, + Final, + ClassVar, + Concatenate, + Unpack, + Required, + NotRequired, + TypeAlias, + TypeGuard, + TypeIs, + ReadOnly, + // TODO: fill this enum out with more special forms, etc. +} + +impl<'db> KnownInstanceType<'db> { + /// Evaluate the known instance in boolean context + pub(crate) const fn bool(self) -> Truthiness { + match self { + Self::Annotated + | Self::Literal + | Self::LiteralString + | Self::Optional + | Self::TypeVar(_) + | Self::Union + | Self::NoReturn + | Self::Never + | Self::Any + | Self::Tuple + | Self::Type + | Self::TypingSelf + | Self::Final + | Self::ClassVar + | Self::Callable + | Self::Concatenate + | Self::Unpack + | Self::Required + | Self::NotRequired + | Self::TypeAlias + | Self::TypeGuard + | Self::TypeIs + | Self::List + | Self::Dict + | Self::DefaultDict + | Self::Set + | Self::FrozenSet + | Self::Counter + | Self::Deque + | Self::ChainMap + | Self::OrderedDict + | Self::Protocol + | Self::Generic + | Self::ReadOnly + | Self::TypeAliasType(_) + | Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::Not + | Self::Intersection + | Self::TypeOf + | Self::CallableTypeOf => Truthiness::AlwaysTrue, + } + } + + /// Return the repr of the symbol at runtime + pub(crate) fn repr(self, db: &'db dyn Db) -> &'db str { + match self { + Self::Annotated => "typing.Annotated", + Self::Literal => "typing.Literal", + Self::LiteralString => "typing.LiteralString", + Self::Optional => "typing.Optional", + Self::Union => "typing.Union", + Self::NoReturn => "typing.NoReturn", + Self::Never => "typing.Never", + Self::Any => "typing.Any", + Self::Tuple => "typing.Tuple", + Self::Type => "typing.Type", + Self::TypingSelf => "typing.Self", + Self::Final => "typing.Final", + Self::ClassVar => "typing.ClassVar", + Self::Callable => "typing.Callable", + Self::Concatenate => "typing.Concatenate", + Self::Unpack => "typing.Unpack", + Self::Required => "typing.Required", + Self::NotRequired => "typing.NotRequired", + Self::TypeAlias => "typing.TypeAlias", + Self::TypeGuard => "typing.TypeGuard", + Self::TypeIs => "typing.TypeIs", + Self::List => "typing.List", + Self::Dict => "typing.Dict", + Self::DefaultDict => "typing.DefaultDict", + Self::Set => "typing.Set", + Self::FrozenSet => "typing.FrozenSet", + Self::Counter => "typing.Counter", + Self::Deque => "typing.Deque", + Self::ChainMap => "typing.ChainMap", + Self::OrderedDict => "typing.OrderedDict", + Self::Protocol => "typing.Protocol", + Self::Generic => "typing.Generic", + Self::ReadOnly => "typing.ReadOnly", + Self::TypeVar(typevar) => typevar.name(db), + Self::TypeAliasType(_) => "typing.TypeAliasType", + Self::Unknown => "knot_extensions.Unknown", + Self::AlwaysTruthy => "knot_extensions.AlwaysTruthy", + Self::AlwaysFalsy => "knot_extensions.AlwaysFalsy", + Self::Not => "knot_extensions.Not", + Self::Intersection => "knot_extensions.Intersection", + Self::TypeOf => "knot_extensions.TypeOf", + Self::CallableTypeOf => "knot_extensions.CallableTypeOf", + } + } + + /// Return the [`KnownClass`] which this symbol is an instance of + pub(crate) const fn class(self) -> KnownClass { + match self { + Self::Annotated => KnownClass::SpecialForm, + Self::Literal => KnownClass::SpecialForm, + Self::LiteralString => KnownClass::SpecialForm, + Self::Optional => KnownClass::SpecialForm, + Self::Union => KnownClass::SpecialForm, + Self::NoReturn => KnownClass::SpecialForm, + Self::Never => KnownClass::SpecialForm, + Self::Any => KnownClass::Object, + Self::Tuple => KnownClass::SpecialForm, + Self::Type => KnownClass::SpecialForm, + Self::TypingSelf => KnownClass::SpecialForm, + Self::Final => KnownClass::SpecialForm, + Self::ClassVar => KnownClass::SpecialForm, + Self::Callable => KnownClass::SpecialForm, + Self::Concatenate => KnownClass::SpecialForm, + Self::Unpack => KnownClass::SpecialForm, + Self::Required => KnownClass::SpecialForm, + Self::NotRequired => KnownClass::SpecialForm, + Self::TypeAlias => KnownClass::SpecialForm, + Self::TypeGuard => KnownClass::SpecialForm, + Self::TypeIs => KnownClass::SpecialForm, + Self::ReadOnly => KnownClass::SpecialForm, + Self::List => KnownClass::StdlibAlias, + Self::Dict => KnownClass::StdlibAlias, + Self::DefaultDict => KnownClass::StdlibAlias, + Self::Set => KnownClass::StdlibAlias, + Self::FrozenSet => KnownClass::StdlibAlias, + Self::Counter => KnownClass::StdlibAlias, + Self::Deque => KnownClass::StdlibAlias, + Self::ChainMap => KnownClass::StdlibAlias, + Self::OrderedDict => KnownClass::StdlibAlias, + Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says + Self::Generic => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says + Self::TypeVar(_) => KnownClass::TypeVar, + Self::TypeAliasType(_) => KnownClass::TypeAliasType, + Self::TypeOf => KnownClass::SpecialForm, + Self::Not => KnownClass::SpecialForm, + Self::Intersection => KnownClass::SpecialForm, + Self::CallableTypeOf => KnownClass::SpecialForm, + Self::Unknown => KnownClass::Object, + Self::AlwaysTruthy => KnownClass::Object, + Self::AlwaysFalsy => KnownClass::Object, + } + } + + /// Return the instance type which this type is a subtype of. + /// + /// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`, + /// so `KnownInstanceType::Literal.instance_fallback(db)` + /// returns `Type::Instance(InstanceType { class: })`. + pub(super) fn instance_fallback(self, db: &dyn Db) -> Type { + self.class().to_instance(db) + } + + /// Return `true` if this symbol is an instance of `class`. + pub(super) fn is_instance_of(self, db: &'db dyn Db, class: ClassType<'db>) -> bool { + self.class().is_subclass_of(db, class) + } + + pub(super) fn try_from_file_and_name( + db: &'db dyn Db, + file: File, + symbol_name: &str, + ) -> Option { + let candidate = match symbol_name { + "Any" => Self::Any, + "ClassVar" => Self::ClassVar, + "Deque" => Self::Deque, + "List" => Self::List, + "Dict" => Self::Dict, + "DefaultDict" => Self::DefaultDict, + "Set" => Self::Set, + "FrozenSet" => Self::FrozenSet, + "Counter" => Self::Counter, + "ChainMap" => Self::ChainMap, + "OrderedDict" => Self::OrderedDict, + "Generic" => Self::Generic, + "Protocol" => Self::Protocol, + "Optional" => Self::Optional, + "Union" => Self::Union, + "NoReturn" => Self::NoReturn, + "Tuple" => Self::Tuple, + "Type" => Self::Type, + "Callable" => Self::Callable, + "Annotated" => Self::Annotated, + "Literal" => Self::Literal, + "Never" => Self::Never, + "Self" => Self::TypingSelf, + "Final" => Self::Final, + "Unpack" => Self::Unpack, + "Required" => Self::Required, + "TypeAlias" => Self::TypeAlias, + "TypeGuard" => Self::TypeGuard, + "TypeIs" => Self::TypeIs, + "ReadOnly" => Self::ReadOnly, + "Concatenate" => Self::Concatenate, + "NotRequired" => Self::NotRequired, + "LiteralString" => Self::LiteralString, + "Unknown" => Self::Unknown, + "AlwaysTruthy" => Self::AlwaysTruthy, + "AlwaysFalsy" => Self::AlwaysFalsy, + "Not" => Self::Not, + "Intersection" => Self::Intersection, + "TypeOf" => Self::TypeOf, + "CallableTypeOf" => Self::CallableTypeOf, + _ => return None, + }; + + candidate + .check_module(file_to_module(db, file)?.known()?) + .then_some(candidate) + } + + /// Return `true` if `module` is a module from which this `KnownInstance` variant can validly originate. + /// + /// Most variants can only exist in one module, which is the same as `self.class().canonical_module()`. + /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. + pub(super) fn check_module(self, module: KnownModule) -> bool { + match self { + Self::Any + | Self::ClassVar + | Self::Deque + | Self::List + | Self::Dict + | Self::DefaultDict + | Self::Set + | Self::FrozenSet + | Self::Counter + | Self::ChainMap + | Self::OrderedDict + | Self::Optional + | Self::Union + | Self::NoReturn + | Self::Tuple + | Self::Type + | Self::Generic + | Self::Callable => module.is_typing(), + Self::Annotated + | Self::Protocol + | Self::Literal + | Self::LiteralString + | Self::Never + | Self::TypingSelf + | Self::Final + | Self::Concatenate + | Self::Unpack + | Self::Required + | Self::NotRequired + | Self::TypeAlias + | Self::TypeGuard + | Self::TypeIs + | Self::ReadOnly + | Self::TypeAliasType(_) + | Self::TypeVar(_) => { + matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) + } + Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::Not + | Self::Intersection + | Self::TypeOf + | Self::CallableTypeOf => module.is_knot_extensions(), + } + } + + pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + self.class().to_class_literal(db) + } +} diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 111b758bc7bb5f..8037611f5109fc 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -3,8 +3,8 @@ use std::cmp::Ordering; use crate::db::Db; use super::{ - class_base::ClassBase, subclass_of::SubclassOfInner, DynamicType, InstanceType, - KnownInstanceType, SuperOwnerKind, TodoType, Type, + class_base::ClassBase, subclass_of::SubclassOfInner, DynamicType, KnownInstanceType, + SuperOwnerKind, TodoType, Type, }; /// Return an [`Ordering`] that describes the canonical order in which two types should appear @@ -126,10 +126,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::SubclassOf(_), _) => Ordering::Less, (_, Type::SubclassOf(_)) => Ordering::Greater, - ( - Type::Instance(InstanceType { class: left }), - Type::Instance(InstanceType { class: right }), - ) => left.cmp(right), + (Type::Instance(left), Type::Instance(right)) => left.class().cmp(&right.class()), (Type::Instance(_), _) => Ordering::Less, (_, Type::Instance(_)) => Ordering::Greater, @@ -161,10 +158,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => left.cmp(right), (SuperOwnerKind::Class(_), _) => Ordering::Less, (_, SuperOwnerKind::Class(_)) => Ordering::Greater, - ( - SuperOwnerKind::Instance(InstanceType { class: left }), - SuperOwnerKind::Instance(InstanceType { class: right }), - ) => left.cmp(right), + (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => { + left.class().cmp(&right.class()) + } (SuperOwnerKind::Instance(_), _) => Ordering::Less, (_, SuperOwnerKind::Instance(_)) => Ordering::Greater, (SuperOwnerKind::Dynamic(left), SuperOwnerKind::Dynamic(right)) => { From 83d5ad8983466786aca7f66a1c25dfc1891ff1c3 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 22 Apr 2025 13:39:42 +0200 Subject: [PATCH 0050/1161] [red-knot] mypy_primer: extend ecosystem checks (#17544) ## Summary Takes the `good.txt` changes from #17474, and removes the following projects: - arrow (not part of mypy_primer upstream) - freqtrade, hydpy, ibis, pandera, xarray (saw panics locally, all related to try_metaclass cycles) Increases the mypy_primer CI run time to ~4 min. ## Test Plan Three successful CI runs. --- .../resources/primer/good.txt | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/red_knot_python_semantic/resources/primer/good.txt index d813eccdd0c13d..f1501dc9d5bfa1 100644 --- a/crates/red_knot_python_semantic/resources/primer/good.txt +++ b/crates/red_knot_python_semantic/resources/primer/good.txt @@ -1,22 +1,105 @@ +AutoSplit +Expression +PyGithub +PyWinCtl +SinbadCogs +aiohttp-devtools +aioredis +aiortc +alectryon +anyio +apprise +arviz async-utils +asynq +attrs +bandersnatch +beartype bidict black +bokeh +boostedblob +check-jsonschema +cki-lib +cloud-init +com2ann +comtypes +cwltool dacite +dd-trace-py +dedupe +django-stubs +downforeveryone +dulwich +flake8 +flake8-pyi git-revise +graphql-core +httpx-caching +hydra-zen +ignite +imagehash isort itsdangerous +janus +jax +jinja +koda-validate +kopf +kornia +mitmproxy +mkdocs +mkosi +mongo-python-driver +more-itertools +mypy-protobuf mypy_primer +nionutils +nox +openlibrary +operator +optuna +paasta packaging paroxython +parso +pegen porcupine +ppb-vector psycopg +pwndbg pybind11 +pycryptodome +pydantic pyinstrument +pyjwt +pylint +pylox +pyodide pyp +pyppeteer +pytest-robotframework python-chess python-htmlgen +python-sop +rclip rich +rotki +schema_salad scrapy +sockeye +speedrun.com_global_scoreboard_webapp +starlette +static-frame +stone +tornado +twine typeshed-stats +urllib3 +vision +websockets werkzeug +xarray-dataclasses +yarl zipp +zulip From 0299a52fb125eb027470b55e8fe92ec1ae8240a4 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 22 Apr 2025 05:15:36 -0700 Subject: [PATCH 0051/1161] [red-knot] Add list of failing/slow ecosystem projects (#17474) ## Summary I ran red-knot on every project in mypy-primer. I moved every project where red-knot ran to completion (fast enough, and mypy-primer could handle its output) into `good.txt`, so it will run in our CI. The remaining projects I left listed in `bad.txt`, with a comment summarizing the failure mode (a few don't fail, they are just slow -- on a debug build, at least -- or output too many diagnostics for mypy-primer to handle.) We will now run CI on 109 projects; 34 are left in `bad.txt`. ## Test Plan CI on this PR! --------- Co-authored-by: David Peter --- .../resources/primer/bad.txt | 34 +++++++++++++++++++ .../resources/primer/good.txt | 4 +++ 2 files changed, 38 insertions(+) create mode 100644 crates/red_knot_python_semantic/resources/primer/bad.txt diff --git a/crates/red_knot_python_semantic/resources/primer/bad.txt b/crates/red_knot_python_semantic/resources/primer/bad.txt new file mode 100644 index 00000000000000..d263c922c91865 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/primer/bad.txt @@ -0,0 +1,34 @@ +Tanjun # cycle panic (signature_) +aiohttp # missing expression ID +alerta # missing expression ID +altair # cycle panics (try_metaclass_) +antidote # hangs / slow +artigraph # cycle panics (value_type_) +colour # cycle panics (try_metaclass_) +core # cycle panics (value_type_) +cpython # missing expression ID, access to field whilst being initialized, too many cycle iterations +discord.py # some kind of hang, only when multi-threaded? +freqtrade # cycle panics (try_metaclass_) +hydpy # cycle panics (try_metaclass_) +ibis # cycle panics (try_metaclass_) +manticore # stack overflow +materialize # stack overflow +meson # missing expression ID +mypy # cycle panic (signature_) +pandas # slow +pandas-stubs # cycle panics (try_metaclass_) +pandera # cycle panics (try_metaclass_) +prefect # slow +pytest # cycle panics (signature_), missing expression ID +pywin32 # bad use-def map (binding with definitely-visible unbound) +schemathesis # cycle panics (signature_) +scikit-learn # success, but mypy-primer hangs processing the output +scipy # missing expression ID +spack # success, but mypy-primer hangs processing the output +spark # cycle panics (try_metaclass_) +sphinx # missing expression ID +steam.py # missing expression ID +streamlit # cycle panic (signature_) +sympy # stack overflow +trio # missing expression ID +xarray # cycle panics (try_metaclass_) diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/red_knot_python_semantic/resources/primer/good.txt index f1501dc9d5bfa1..2c7a36665a4043 100644 --- a/crates/red_knot_python_semantic/resources/primer/good.txt +++ b/crates/red_knot_python_semantic/resources/primer/good.txt @@ -30,6 +30,7 @@ dd-trace-py dedupe django-stubs downforeveryone +dragonchain dulwich flake8 flake8-pyi @@ -64,6 +65,8 @@ packaging paroxython parso pegen +pip +poetry porcupine ppb-vector psycopg @@ -87,6 +90,7 @@ rich rotki schema_salad scrapy +setuptools sockeye speedrun.com_global_scoreboard_webapp starlette From 775815ef2201fe546049aabefce4783630e6d94e Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Tue, 22 Apr 2025 10:05:15 -0400 Subject: [PATCH 0052/1161] Update cargo-dist and apply config improvements (#17453) --- .github/workflows/release.yml | 19 ++++++++++--------- Cargo.toml | 10 ++++++---- crates/ruff/Cargo.toml | 3 +++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c12aa00fc11617..79bc54e7b9d76c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,7 @@ permissions: # If there's a prerelease-style suffix to the version, then the release(s) # will be marked as a prerelease. on: + pull_request: workflow_dispatch: inputs: tag: @@ -60,7 +61,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 with: persist-credentials: false submodules: recursive @@ -68,9 +69,9 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4-prerelease.1/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4/cargo-dist-installer.sh | sh" - name: Cache dist - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: name: cargo-dist-cache path: ~/.cargo/bin/dist @@ -86,7 +87,7 @@ jobs: cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json @@ -123,7 +124,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 with: persist-credentials: false submodules: recursive @@ -153,7 +154,7 @@ jobs: cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: name: artifacts-build-global path: | @@ -174,7 +175,7 @@ jobs: outputs: val: ${{ steps.host.outputs.manifest }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 with: persist-credentials: false submodules: recursive @@ -200,7 +201,7 @@ jobs: cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: # Overwrite the previous copy name: artifacts-dist-manifest @@ -250,7 +251,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 with: persist-credentials: false submodules: recursive diff --git a/Cargo.toml b/Cargo.toml index 7a12fd6c3d6720..6f89d108158d28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -276,7 +276,9 @@ inherits = "release" # Config for 'dist' [workspace.metadata.dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.28.4-prerelease.1" +cargo-dist-version = "0.28.4" +# Make distability of apps opt-in instead of opt-out +dist = false # CI backends to support ci = "github" # The installers to generate for each app @@ -310,7 +312,7 @@ auto-includes = false # Whether dist should create a Github Release or use an existing draft create-release = true # Which actions to run on pull requests -pr-run-mode = "skip" +pr-run-mode = "plan" # Whether CI should trigger releases with dispatches instead of tag pushes dispatch-releases = true # Which phase dist should use to create the GitHub release @@ -338,7 +340,7 @@ install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"] global = "depot-ubuntu-latest-4" [workspace.metadata.dist.github-action-commits] -"actions/checkout" = "11bd71901bbe5b1630ceea73d27597364c9af683" # v4 -"actions/upload-artifact" = "ea165f8d65b6e75b540449e92b4886f43607fa02" # v4.6.2 +"actions/checkout" = "85e6279cec87321a52edac9c87bce653a07cf6c2" # v4 +"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2 "actions/download-artifact" = "95815c38cf2ff2164869cbab79da8d1f422bc89e" # v4.2.1 "actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3 diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 16a60d1d4c4b1b..96db8ece8f2e1c 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -77,6 +77,9 @@ test-case = { workspace = true } # Used via macro expansion. ignored = ["jiff"] +[package.metadata.dist] +dist = true + [target.'cfg(target_os = "windows")'.dependencies] mimalloc = { workspace = true } From 6bdffc3cbff2194afdff0e46fccb03cf6147d367 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 22 Apr 2025 15:14:10 +0100 Subject: [PATCH 0053/1161] [red-knot] Consider two instance types disjoint if the underlying classes have disjoint metaclasses (#17545) --- .../type_properties/is_disjoint_from.md | 20 +++++++++++++-- .../src/types/instance.rs | 25 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) 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 173e643f3a1ac8..981851a6c751bf 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 @@ -56,6 +56,19 @@ static_assert(not is_disjoint_from(FinalSubclass, A)) # ... which makes it disjoint from B1, B2: static_assert(is_disjoint_from(B1, FinalSubclass)) static_assert(is_disjoint_from(B2, FinalSubclass)) + +# Instance types can also be disjoint if they have disjoint metaclasses. +# No possible subclass of `Meta1` and `Meta2` could exist, therefore +# no possible subclass of `UsesMeta1` and `UsesMeta2` can exist: +class Meta1(type): ... +class UsesMeta1(metaclass=Meta1): ... + +@final +class Meta2(type): ... + +class UsesMeta2(metaclass=Meta2): ... + +static_assert(is_disjoint_from(UsesMeta1, UsesMeta2)) ``` ## Tuple types @@ -342,8 +355,8 @@ static_assert(is_disjoint_from(Meta1, type[UsesMeta2])) ### `type[T]` versus `type[S]` -By the same token, `type[T]` is disjoint from `type[S]` if the metaclass of `T` is disjoint from the -metaclass of `S`. +By the same token, `type[T]` is disjoint from `type[S]` if `T` is `@final`, `S` is `@final`, or the +metaclass of `T` is disjoint from the metaclass of `S`. ```py from typing import final @@ -353,6 +366,9 @@ from knot_extensions import static_assert, is_disjoint_from class Meta1(type): ... class Meta2(type): ... + +static_assert(is_disjoint_from(type[Meta1], type[Meta2])) + class UsesMeta1(metaclass=Meta1): ... class UsesMeta2(metaclass=Meta2): ... diff --git a/crates/red_knot_python_semantic/src/types/instance.rs b/crates/red_knot_python_semantic/src/types/instance.rs index 1b0ec2f4fde9e5..fb1b1deb764a4f 100644 --- a/crates/red_knot_python_semantic/src/types/instance.rs +++ b/crates/red_knot_python_semantic/src/types/instance.rs @@ -43,8 +43,29 @@ impl<'db> InstanceType<'db> { } pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool { - (self.class.is_final(db) && !self.class.is_subclass_of(db, other.class)) - || (other.class.is_final(db) && !other.class.is_subclass_of(db, self.class)) + if self.class.is_final(db) && !self.class.is_subclass_of(db, other.class) { + return true; + } + + if other.class.is_final(db) && !other.class.is_subclass_of(db, self.class) { + return true; + } + + // Check to see whether the metaclasses of `self` and `other` are disjoint. + // Avoid this check if the metaclass of either `self` or `other` is `type`, + // however, since we end up with infinite recursion in that case due to the fact + // that `type` is its own metaclass (and we know that `type` cannot be disjoint + // from any metaclass, anyway). + let type_type = KnownClass::Type.to_instance(db); + let self_metaclass = self.class.metaclass_instance_type(db); + if self_metaclass == type_type { + return false; + } + let other_metaclass = other.class.metaclass_instance_type(db); + if other_metaclass == type_type { + return false; + } + self_metaclass.is_disjoint_from(db, other_metaclass) } pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { From 4775719abf147e80455e2e59c225b07dd5f6efa7 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 22 Apr 2025 17:36:13 +0200 Subject: [PATCH 0054/1161] [red-knot] mypy_primer: larger depot runner (#17547) ## Summary A switch from 16 to 32 cores reduces the `mypy_primer` CI time from 3.5-4 min to 2.5-3 min. There's also a 64-core runner, but the 4 min -> 3 min change when doubling the cores once does suggest that it doesn't parallelize *this* well. --- .github/actionlint.yaml | 1 + .github/workflows/mypy_primer.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index f86b551af83eb1..c3464e3992f21e 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -6,5 +6,6 @@ self-hosted-runner: labels: - depot-ubuntu-latest-8 - depot-ubuntu-22.04-16 + - depot-ubuntu-22.04-32 - github-windows-2025-x86_64-8 - github-windows-2025-x86_64-16 diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index 7382671f114fa7..dab2bd4c845396 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -26,7 +26,7 @@ env: jobs: mypy_primer: name: Run mypy_primer - runs-on: depot-ubuntu-22.04-16 + runs-on: depot-ubuntu-22.04-32 timeout-minutes: 20 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From 14f71ceb83f5e3ef392d1ea49817315778f68337 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 15 Apr 2025 12:20:16 -0400 Subject: [PATCH 0055/1161] red_knot_python_semantic: add helper method for creating a secondary annotation I suspect this will be used pretty frequently (I wanted it immediately). And more practically, this avoids needing to import `Annotation` to create it. --- crates/red_knot_python_semantic/src/types/context.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types/context.rs b/crates/red_knot_python_semantic/src/types/context.rs index 5f92be04ab26fe..40cd9b713784e5 100644 --- a/crates/red_knot_python_semantic/src/types/context.rs +++ b/crates/red_knot_python_semantic/src/types/context.rs @@ -65,6 +65,14 @@ impl<'db> InferContext<'db> { Span::from(self.file()).with_range(ranged.range()) } + /// Create a secondary annotation attached to the range of the given value in + /// the file currently being type checked. + /// + /// The annotation returned has no message attached to it. + pub(crate) fn secondary(&self, ranged: T) -> Annotation { + Annotation::secondary(self.span(ranged)) + } + pub(crate) fn db(&self) -> &'db dyn Db { self.db } From 3b300559ab5ddee40ca353da6303b02f8f10f1a4 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 16 Apr 2025 10:26:29 -0400 Subject: [PATCH 0056/1161] red_knot_python_semantic: remove `#[must_use]` on diagnostic guard constructor I believe this was an artifact of an older iteration of the diagnostic reporting API. But this is strictly not necessary now, and indeed, might even be annoying. It is okay, but perhaps looks a little odd, to do `builder.into_diagnostic("...")` if you don't want to add anything else to the diagnostic. --- crates/red_knot_python_semantic/src/types/context.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/context.rs b/crates/red_knot_python_semantic/src/types/context.rs index 40cd9b713784e5..4a6c24186c53c0 100644 --- a/crates/red_knot_python_semantic/src/types/context.rs +++ b/crates/red_knot_python_semantic/src/types/context.rs @@ -404,7 +404,6 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. - #[must_use] pub(super) fn into_diagnostic( self, message: impl std::fmt::Display, @@ -541,7 +540,6 @@ impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> { /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. - #[must_use] pub(super) fn into_diagnostic( self, message: impl std::fmt::Display, From 298f43f34eacae9dbdf319199372a76660775b59 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 16 Apr 2025 09:27:25 -0400 Subject: [PATCH 0057/1161] red_knot_python_semantic: add invalid assignment diagnostic snapshot This tests the diagnostic rendering of a case that wasn't previously covered by snapshots: when unpacking fails because there are too few values, but where the left hand side can tolerate "N or more." In the code, this is a distinct diagnostic, so we capture it here. (Sorry about the diff here, but it made sense to rename the other sections and that changes the name of the snapshot file.) --- .../resources/mdtest/diagnostics/unpacking.md | 10 +++++-- ...ng_-_Exactly_too_few_values_to_unpack.snap | 28 +++++++++++++++++++ ..._-_Exactly_too_many_values_to_unpack.snap} | 2 +- ..._Unpacking_-_Too_few_values_to_unpack.snap | 6 ++-- 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap rename crates/red_knot_python_semantic/resources/mdtest/snapshots/{unpacking.md_-_Unpacking_-_Too_many_values_to_unpack.snap => unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap} (86%) diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md index 481d55a1515392..3c731106f332cd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md @@ -8,14 +8,20 @@ a, b = 1 # error: [not-iterable] ``` -## Too many values to unpack +## Exactly too many values to unpack ```py a, b = (1, 2, 3) # error: [invalid-assignment] ``` -## Too few values to unpack +## Exactly too few values to unpack ```py a, b = (1,) # error: [invalid-assignment] ``` + +## Too few values to unpack + +```py +[a, *b, c, d] = (1, 2) # error: [invalid-assignment] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap new file mode 100644 index 00000000000000..dafd1820cb6fa2 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap @@ -0,0 +1,28 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unpacking.md - Unpacking - Exactly too few values to unpack +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | a, b = (1,) # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error: lint:invalid-assignment + --> /src/mdtest_snippet.py:1:1 + | +1 | a, b = (1,) # error: [invalid-assignment] + | ^^^^ Not enough values to unpack (expected 2, got 1) + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_many_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_many_values_to_unpack.snap rename to crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap index f1f5bb30d13d98..23658be3993dca 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_many_values_to_unpack.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap @@ -3,7 +3,7 @@ source: crates/red_knot_test/src/lib.rs expression: snapshot --- --- -mdtest name: unpacking.md - Unpacking - Too many values to unpack +mdtest name: unpacking.md - Unpacking - Exactly too many values to unpack mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md --- diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap index 52ded770984101..6bd0e5760200ca 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap @@ -12,7 +12,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack ## mdtest_snippet.py ``` -1 | a, b = (1,) # error: [invalid-assignment] +1 | [a, *b, c, d] = (1, 2) ``` # Diagnostics @@ -21,8 +21,8 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack error: lint:invalid-assignment --> /src/mdtest_snippet.py:1:1 | -1 | a, b = (1,) # error: [invalid-assignment] - | ^^^^ Not enough values to unpack (expected 2, got 1) +1 | [a, *b, c, d] = (1, 2) + | ^^^^^^^^^^^^^ Not enough values to unpack (expected 3 or more, got 2) | ``` From 890ba725d901030b2a6b1dd4cbe6e3c848813570 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 16 Apr 2025 09:25:50 -0400 Subject: [PATCH 0058/1161] red_knot_python_semantic: migrate INVALID_ASSIGNMENT for unpacking This moves all INVALID_ASSIGNMENT lints related to unpacking over to the new diagnostic model. While we're here, we improve the diagnostic a bit by adding a secondary annotation covering where the value is. We also split apart the original singular message into one message for the diagnostic and the "expected versus got" into annotation messages. --- ...ng_-_Exactly_too_few_values_to_unpack.snap | 6 +- ...g_-_Exactly_too_many_values_to_unpack.snap | 6 +- ..._Unpacking_-_Too_few_values_to_unpack.snap | 10 ++- .../resources/mdtest/unpacking.md | 40 ++++----- .../src/types/unpacker.rs | 88 +++++++++++-------- 5 files changed, 85 insertions(+), 65 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap index dafd1820cb6fa2..c3c5ebe6bec434 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap @@ -18,11 +18,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack # Diagnostics ``` -error: lint:invalid-assignment +error: lint:invalid-assignment: Not enough values to unpack --> /src/mdtest_snippet.py:1:1 | 1 | a, b = (1,) # error: [invalid-assignment] - | ^^^^ Not enough values to unpack (expected 2, got 1) + | ^^^^ ---- Got 1 + | | + | Expected 2 | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap index 23658be3993dca..daf1af653e73a6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap @@ -18,11 +18,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack # Diagnostics ``` -error: lint:invalid-assignment +error: lint:invalid-assignment: Too many values to unpack --> /src/mdtest_snippet.py:1:1 | 1 | a, b = (1, 2, 3) # error: [invalid-assignment] - | ^^^^ Too many values to unpack (expected 2, got 3) + | ^^^^ --------- Got 3 + | | + | Expected 2 | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap index 6bd0e5760200ca..dc312682555609 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap @@ -12,17 +12,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack ## mdtest_snippet.py ``` -1 | [a, *b, c, d] = (1, 2) +1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment] ``` # Diagnostics ``` -error: lint:invalid-assignment +error: lint:invalid-assignment: Not enough values to unpack --> /src/mdtest_snippet.py:1:1 | -1 | [a, *b, c, d] = (1, 2) - | ^^^^^^^^^^^^^ Not enough values to unpack (expected 3 or more, got 2) +1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment] + | ^^^^^^^^^^^^^ ------ Got 2 + | | + | Expected 3 or more | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unpacking.md b/crates/red_knot_python_semantic/resources/mdtest/unpacking.md index 6a0f3757375dd5..dac296b838849e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unpacking.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unpacking.md @@ -65,7 +65,7 @@ reveal_type(c) # revealed: Literal[4] ### Uneven unpacking (1) ```py -# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 3" (a, b, c) = (1, 2) reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -75,7 +75,7 @@ reveal_type(c) # revealed: Unknown ### Uneven unpacking (2) ```py -# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)" +# error: [invalid-assignment] "Too many values to unpack: Expected 2" (a, b) = (1, 2, 3) reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -84,7 +84,7 @@ reveal_type(b) # revealed: Unknown ### Nested uneven unpacking (1) ```py -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" (a, (b, c), d) = (1, (2,), 3) reveal_type(a) # revealed: Literal[1] reveal_type(b) # revealed: Unknown @@ -95,7 +95,7 @@ reveal_type(d) # revealed: Literal[3] ### Nested uneven unpacking (2) ```py -# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)" +# error: [invalid-assignment] "Too many values to unpack: Expected 2" (a, (b, c), d) = (1, (2, 3, 4), 5) reveal_type(a) # revealed: Literal[1] reveal_type(b) # revealed: Unknown @@ -106,7 +106,7 @@ reveal_type(d) # revealed: Literal[5] ### Starred expression (1) ```py -# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more" [a, *b, c, d] = (1, 2) reveal_type(a) # revealed: Unknown # TODO: Should be list[Any] once support for assigning to starred expression is added @@ -159,7 +159,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking) ### Starred expression (6) ```py -# error: [invalid-assignment] "Not enough values to unpack (expected 5 or more, got 1)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 5 or more" (a, b, c, *d, e, f) = (1,) reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -225,7 +225,7 @@ reveal_type(b) # revealed: LiteralString ### Uneven unpacking (1) ```py -# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 3" a, b, c = "ab" reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -235,7 +235,7 @@ reveal_type(c) # revealed: Unknown ### Uneven unpacking (2) ```py -# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)" +# error: [invalid-assignment] "Too many values to unpack: Expected 2" a, b = "abc" reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -244,7 +244,7 @@ reveal_type(b) # revealed: Unknown ### Starred expression (1) ```py -# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more" (a, *b, c, d) = "ab" reveal_type(a) # revealed: Unknown # TODO: Should be list[LiteralString] once support for assigning to starred expression is added @@ -254,7 +254,7 @@ reveal_type(d) # revealed: Unknown ``` ```py -# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 1)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more" (a, b, *c, d) = "a" reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -306,7 +306,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking) ### Unicode ```py -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" (a, b) = "é" reveal_type(a) # revealed: Unknown @@ -316,7 +316,7 @@ reveal_type(b) # revealed: Unknown ### Unicode escape (1) ```py -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" (a, b) = "\u9e6c" reveal_type(a) # revealed: Unknown @@ -326,7 +326,7 @@ reveal_type(b) # revealed: Unknown ### Unicode escape (2) ```py -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" (a, b) = "\U0010ffff" reveal_type(a) # revealed: Unknown @@ -420,8 +420,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]): ```py def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]): - # error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)" - # error: [invalid-assignment] "Too many values to unpack (expected 2, got 5)" + # error: [invalid-assignment] "Too many values to unpack: Expected 2" + # error: [invalid-assignment] "Too many values to unpack: Expected 2" a, b = arg reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -431,8 +431,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]): ```py def _(arg: tuple[int, bytes] | tuple[int, str]): - # error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)" - # error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)" + # error: [invalid-assignment] "Not enough values to unpack: Expected 3" + # error: [invalid-assignment] "Not enough values to unpack: Expected 3" a, b, c = arg reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -575,7 +575,7 @@ for a, b in ((1, 2), ("a", "b")): # error: "Object of type `Literal[1]` is not iterable" # error: "Object of type `Literal[2]` is not iterable" # error: "Object of type `Literal[4]` is not iterable" -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"): reveal_type(a) # revealed: Unknown | Literal[3, 5] reveal_type(b) # revealed: Unknown | Literal["a", "b"] @@ -702,7 +702,7 @@ class ContextManager: def __exit__(self, *args) -> None: pass -# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 3" with ContextManager() as (a, b, c): reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -765,7 +765,7 @@ def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, s # error: "Object of type `Literal[1]` is not iterable" # error: "Object of type `Literal[2]` is not iterable" # error: "Object of type `Literal[4]` is not iterable" -# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)" +# error: [invalid-assignment] "Not enough values to unpack: Expected 2" # revealed: tuple[Unknown | Literal[3, 5], Unknown | Literal["a", "b"]] [reveal_type((a, b)) for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c")] ``` diff --git a/crates/red_knot_python_semantic/src/types/unpacker.rs b/crates/red_knot_python_semantic/src/types/unpacker.rs index ecaf1bdeaba4c3..d74381553cb411 100644 --- a/crates/red_knot_python_semantic/src/types/unpacker.rs +++ b/crates/red_knot_python_semantic/src/types/unpacker.rs @@ -134,35 +134,45 @@ impl<'db> Unpacker<'db> { }; if let Some(tuple_ty) = ty.into_tuple() { - let tuple_ty_elements = self.tuple_ty_elements(target, elts, tuple_ty); + let tuple_ty_elements = + self.tuple_ty_elements(target, elts, tuple_ty, value_expr); - let length_mismatch = match elts.len().cmp(&tuple_ty_elements.len()) { - Ordering::Less => { - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( - "Too many values to unpack (expected {}, got {})", - elts.len(), - tuple_ty_elements.len() - ), - ); - true - } - Ordering::Greater => { - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( - "Not enough values to unpack (expected {}, got {})", - elts.len(), - tuple_ty_elements.len() - ), - ); - true - } - Ordering::Equal => false, - }; + let length_mismatch = + match elts.len().cmp(&tuple_ty_elements.len()) { + Ordering::Less => { + if let Some(builder) = + self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + let mut diag = + builder.into_diagnostic("Too many values to unpack"); + diag.set_primary_message(format_args!( + "Expected {}", + elts.len(), + )); + diag.annotate(self.context.secondary(value_expr).message( + format_args!("Got {}", tuple_ty_elements.len()), + )); + } + true + } + Ordering::Greater => { + if let Some(builder) = + self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + let mut diag = + builder.into_diagnostic("Not enough values to unpack"); + diag.set_primary_message(format_args!( + "Expected {}", + elts.len(), + )); + diag.annotate(self.context.secondary(value_expr).message( + format_args!("Got {}", tuple_ty_elements.len()), + )); + } + true + } + Ordering::Equal => false, + }; for (index, ty) in tuple_ty_elements.iter().enumerate() { if let Some(element_types) = target_types.get_mut(index) { @@ -203,11 +213,15 @@ impl<'db> Unpacker<'db> { /// Returns the [`Type`] elements inside the given [`TupleType`] taking into account that there /// can be a starred expression in the `elements`. + /// + /// `value_expr` is an AST reference to the value being unpacked. It is + /// only used for diagnostics. fn tuple_ty_elements( &self, expr: &ast::Expr, targets: &[ast::Expr], tuple_ty: TupleType<'db>, + value_expr: AnyNodeRef<'_>, ) -> Cow<'_, [Type<'db>]> { // If there is a starred expression, it will consume all of the types at that location. let Some(starred_index) = targets.iter().position(ast::Expr::is_starred_expr) else { @@ -254,15 +268,15 @@ impl<'db> Unpacker<'db> { Cow::Owned(element_types) } else { - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - expr, - format_args!( - "Not enough values to unpack (expected {} or more, got {})", - targets.len() - 1, - tuple_ty.len(self.db()) - ), - ); + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, expr) { + let mut diag = builder.into_diagnostic("Not enough values to unpack"); + diag.set_primary_message(format_args!("Expected {} or more", targets.len() - 1)); + diag.annotate( + self.context + .secondary(value_expr) + .message(format_args!("Got {}", tuple_ty.len(self.db()))), + ); + } Cow::Owned(vec![Type::unknown(); targets.len()]) } From 6dc2d29966dd0e691eb3dc037e94ae33e21552f8 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 16 Apr 2025 10:29:41 -0400 Subject: [PATCH 0059/1161] red_knot_python_semantic: migrate INVALID_ASSIGNMENT for shadowing We mostly keep things the same here, but the message has been moved from the annotation to the diagnostic's top-line message. I think this is perhaps a little worse, but some bigger improvements could be made here. Indeed, we could perhaps even add a "fix" here. --- .../resources/mdtest/diagnostics/shadowing.md | 19 +++++++++++ .../resources/mdtest/shadowing/class.md | 2 +- .../resources/mdtest/shadowing/function.md | 2 +- ..._attributes_with_class-level_defaults.snap | 8 ++--- ...assignment_-_Pure_instance_attributes.snap | 4 +-- ..._-_Attribute_assignment_-_`ClassVar`s.snap | 4 +-- ..._Shadowing_-_Implicit_class_shadowing.snap | 33 +++++++++++++++++++ ...adowing_-_Implicit_function_shadowing.snap | 33 +++++++++++++++++++ .../src/types/diagnostic.rs | 21 ++++++++---- 9 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md new file mode 100644 index 00000000000000..c63631c1e43a70 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md @@ -0,0 +1,19 @@ +# Shadowing + + + +## Implicit class shadowing + +```py +class C: ... + +C = 1 # error: [invalid-assignment] +``` + +## Implicit function shadowing + +```py +def f(): ... + +f = 1 # error: [invalid-assignment] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md b/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md index 212c97baf99783..97550ec57ca848 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md +++ b/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md @@ -5,7 +5,7 @@ ```py class C: ... -C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional" +C = 1 # error: "Implicit shadowing of class `C`" ``` ## Explicit diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md b/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md index ea976c3593b3b1..dda365e13a644d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md +++ b/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md @@ -15,7 +15,7 @@ def f(x: str): ```py def f(): ... -f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional" +f = 1 # error: "Implicit shadowing of function `f`" ``` ## Explicit shadowing diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap index eb06b093089442..8ea6c938749b4d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap @@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib # Diagnostics ``` -error: lint:invalid-assignment +error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` --> /src/mdtest_snippet.py:6:1 | 4 | instance = C() 5 | instance.attr = 1 # fine 6 | instance.attr = "wrong" # error: [invalid-assignment] - | ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` + | ^^^^^^^^^^^^^ 7 | 8 | C.attr = 1 # fine | @@ -40,12 +40,12 @@ error: lint:invalid-assignment ``` ``` -error: lint:invalid-assignment +error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` --> /src/mdtest_snippet.py:9:1 | 8 | C.attr = 1 # fine 9 | C.attr = "wrong" # error: [invalid-assignment] - | ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` + | ^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap index 78fe6d1da50b88..a1e4f9274abcb1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap @@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib # Diagnostics ``` -error: lint:invalid-assignment +error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` --> /src/mdtest_snippet.py:7:1 | 5 | instance = C() 6 | instance.attr = 1 # fine 7 | instance.attr = "wrong" # error: [invalid-assignment] - | ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` + | ^^^^^^^^^^^^^ 8 | 9 | C.attr = 1 # error: [invalid-attribute-access] | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap index 59389d203668a8..573d8937adf928 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap @@ -27,12 +27,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib # Diagnostics ``` -error: lint:invalid-assignment +error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` --> /src/mdtest_snippet.py:7:1 | 6 | C.attr = 1 # fine 7 | C.attr = "wrong" # error: [invalid-assignment] - | ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` + | ^^^^^^ 8 | 9 | instance = C() | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap new file mode 100644 index 00000000000000..74d8fb95bb5673 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap @@ -0,0 +1,33 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: shadowing.md - Shadowing - Implicit class shadowing +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: ... +2 | +3 | C = 1 # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error: lint:invalid-assignment: Implicit shadowing of class `C` + --> /src/mdtest_snippet.py:3:1 + | +1 | class C: ... +2 | +3 | C = 1 # error: [invalid-assignment] + | ^ + | +info: Annotate to make it explicit if this is intentional + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap new file mode 100644 index 00000000000000..04de7c982bc49e --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap @@ -0,0 +1,33 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: shadowing.md - Shadowing - Implicit function shadowing +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | def f(): ... +2 | +3 | f = 1 # error: [invalid-assignment] +``` + +# Diagnostics + +``` +error: lint:invalid-assignment: Implicit shadowing of function `f` + --> /src/mdtest_snippet.py:3:1 + | +1 | def f(): ... +2 | +3 | f = 1 # error: [invalid-assignment] + | ^ + | +info: Annotate to make it explicit if this is intentional + +``` diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 93458cd72d729a..f82eae4d0b32f5 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -1116,19 +1116,26 @@ fn report_invalid_assignment_with_message( target_ty: Type, message: std::fmt::Arguments, ) { + let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, node) else { + return; + }; match target_ty { Type::ClassLiteral(class) => { - context.report_lint_old(&INVALID_ASSIGNMENT, node, format_args!( - "Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional", - class.name(context.db()))); + let mut diag = builder.into_diagnostic(format_args!( + "Implicit shadowing of class `{}`", + class.name(context.db()), + )); + diag.info("Annotate to make it explicit if this is intentional"); } Type::FunctionLiteral(function) => { - context.report_lint_old(&INVALID_ASSIGNMENT, node, format_args!( - "Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional", - function.name(context.db()))); + let mut diag = builder.into_diagnostic(format_args!( + "Implicit shadowing of function `{}`", + function.name(context.db()), + )); + diag.info("Annotate to make it explicit if this is intentional"); } _ => { - context.report_lint_old(&INVALID_ASSIGNMENT, node, message); + builder.into_diagnostic(message); } } } From b8b624d8904f095df6ee92aa4a061313d7ff50c2 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 16 Apr 2025 10:42:47 -0400 Subject: [PATCH 0060/1161] red_knot_python_semantic: migrate INVALID_ASSIGNMENT for inference This finishes the migration for the `INVALID_ASSIGNMENT` lint. Notice how I'm steadily losing steam in terms of actually improving the diagnostics. This change is more mechanical, because taking the time to revamp every diagnostic is a ton of effort. Probably future migrations will be similar unless there are easy pickings. --- ..._-_Invalid_`__set__`_method_signature.snap | 4 +- ...a_descriptors_-_Invalid_argument_type.snap | 4 +- ...t_-_Setting_attributes_on_union_types.snap | 4 +- .../src/types/infer.rs | 68 +++++++++++-------- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap index 2914deba0c3533..8f46e05faca9c0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap @@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib # Diagnostics ``` -error: lint:invalid-assignment +error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method --> /src/mdtest_snippet.py:11:1 | 10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) 11 | instance.attr = 1 # error: [invalid-assignment] - | ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method + | ^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap index 90b4a81494efb0..0b8d3b44347014 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap @@ -29,12 +29,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib # Diagnostics ``` -error: lint:invalid-assignment +error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method --> /src/mdtest_snippet.py:12:1 | 11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter) 12 | instance.attr = "wrong" # error: [invalid-assignment] - | ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method + | ^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap index ec479c58965516..92606ccb687c32 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap @@ -37,12 +37,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib # Diagnostics ``` -error: lint:invalid-assignment +error: lint:invalid-assignment: Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]` --> /src/mdtest_snippet.py:11:5 | 10 | # TODO: The error message here could be improved to explain why the assignment fails. 11 | C1.attr = 1 # error: [invalid-assignment] - | ^^^^^^^ Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]` + | ^^^^^^^ 12 | 13 | class C2: | diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index d2649620dc0e36..175bddff08a3d3 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2442,13 +2442,17 @@ impl<'db> TypeInferenceBuilder<'db> { true } else { // TODO: This is not a very helpful error message, as it does not include the underlying reason - // why the assignment is invalid. This would be a good use case for nested diagnostics. + // why the assignment is invalid. This would be a good use case for sub-diagnostics. if emit_diagnostics { - self.context.report_lint_old(&INVALID_ASSIGNMENT, target, format_args!( - "Object of type `{}` is not assignable to attribute `{attribute}` on type `{}`", - value_ty.display(self.db()), - object_ty.display(self.db()), - )); + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + builder.into_diagnostic(format_args!( + "Object of type `{}` is not assignable \ + to attribute `{attribute}` on type `{}`", + value_ty.display(self.db()), + object_ty.display(self.db()), + )); + } } false @@ -2463,12 +2467,16 @@ impl<'db> TypeInferenceBuilder<'db> { true } else { if emit_diagnostics { - // TODO: same here, see above - self.context.report_lint_old(&INVALID_ASSIGNMENT, target, format_args!( - "Object of type `{}` is not assignable to attribute `{attribute}` on type `{}`", - value_ty.display(self.db()), - object_ty.display(self.db()), - )); + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + // TODO: same here, see above + builder.into_diagnostic(format_args!( + "Object of type `{}` is not assignable \ + to attribute `{attribute}` on type `{}`", + value_ty.display(self.db()), + object_ty.display(self.db()), + )); + } } false } @@ -2557,15 +2565,16 @@ impl<'db> TypeInferenceBuilder<'db> { .is_ok(); if !successful_call && emit_diagnostics { - // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( - "Invalid assignment to data descriptor attribute `{attribute}` on type `{}` with custom `__set__` method", - object_ty.display(db) - ), - ); + if let Some(builder) = + self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed + builder.into_diagnostic(format_args!( + "Invalid assignment to data descriptor attribute \ + `{attribute}` on type `{}` with custom `__set__` method", + object_ty.display(db) + )); + } } successful_call @@ -2695,15 +2704,16 @@ impl<'db> TypeInferenceBuilder<'db> { .is_ok(); if !successful_call && emit_diagnostics { - // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( - "Invalid assignment to data descriptor attribute `{attribute}` on type `{}` with custom `__set__` method", + if let Some(builder) = + self.context.report_lint(&INVALID_ASSIGNMENT, target) + { + // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed + builder.into_diagnostic(format_args!( + "Invalid assignment to data descriptor attribute \ + `{attribute}` on type `{}` with custom `__set__` method", object_ty.display(db) - ), - ); + )); + } } successful_call From 27a377f0772c2088d0d97b76c97d2e411247b94e Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 16 Apr 2025 11:28:18 -0400 Subject: [PATCH 0061/1161] red_knot_python_semantic: migrate `types/infer` to new diagnostic model I gave up trying to do this one lint at a time and just (mostly) mechanically translated this entire file in one go. Generally the messages stay the same (with most moving from an annotation message to the diagnostic's main message). I added a couple of `info` sub-diagnostics where it seemed to be the obvious intent. --- crates/red_knot/tests/cli.rs | 68 +- ...assignment_-_Pure_instance_attributes.snap | 4 +- ...ibute_assignment_-_Unknown_attributes.snap | 8 +- ..._-_Attribute_assignment_-_`ClassVar`s.snap | 4 +- ...ructures_-_Unresolvable_module_import.snap | 4 +- ...ures_-_Unresolvable_submodule_imports.snap | 8 +- ...vable_import_that_does_not_use_`from`.snap | 4 +- ...solvable_module_but_unresolvable_item.snap | 4 +- ...`from`_with_an_unknown_current_module.snap | 4 +- ..._`from`_with_an_unknown_nested_module.snap | 4 +- ...ng_`from`_with_an_unresolvable_module.snap | 4 +- ...ing_`from`_with_too_many_leading_dots.snap | 4 +- .../src/types/infer.rs | 826 +++++++++--------- 13 files changed, 485 insertions(+), 461 deletions(-) diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index a37468e82420d5..210063ae84ea6e 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -32,12 +32,12 @@ fn config_override_python_version() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - error: lint:unresolved-attribute + error: lint:unresolved-attribute: Type `` has no attribute `last_exc` --> /test.py:5:7 | 4 | # Access `sys.last_exc` that was only added in Python 3.12 5 | print(sys.last_exc) - | ^^^^^^^^^^^^ Type `` has no attribute `last_exc` + | ^^^^^^^^^^^^ | Found 1 diagnostic @@ -165,11 +165,11 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - error: lint:unresolved-import + error: lint:unresolved-import: Cannot resolve import `utils` --> /child/test.py:2:6 | 2 | from utils import add - | ^^^^^ Cannot resolve import `utils` + | ^^^^^ 3 | 4 | stat = add(10, 15) | @@ -265,11 +265,11 @@ fn configuration_rule_severity() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - error: lint:division-by-zero + error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero --> /test.py:2:5 | 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero + | ^^^^^ 3 | 4 | for a in range(0, int(y)): | @@ -301,11 +301,11 @@ fn configuration_rule_severity() -> anyhow::Result<()> { success: true exit_code: 0 ----- stdout ----- - warning: lint:division-by-zero + warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero --> /test.py:2:5 | 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero + | ^^^^^ 3 | 4 | for a in range(0, int(y)): | @@ -341,22 +341,22 @@ fn cli_rule_severity() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - error: lint:unresolved-import + error: lint:unresolved-import: Cannot resolve import `does_not_exit` --> /test.py:2:8 | 2 | import does_not_exit - | ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit` + | ^^^^^^^^^^^^^ 3 | 4 | y = 4 / 0 | - error: lint:division-by-zero + error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero --> /test.py:4:5 | 2 | import does_not_exit 3 | 4 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero + | ^^^^^ 5 | 6 | for a in range(0, int(y)): | @@ -388,22 +388,22 @@ fn cli_rule_severity() -> anyhow::Result<()> { success: true exit_code: 0 ----- stdout ----- - warning: lint:unresolved-import + warning: lint:unresolved-import: Cannot resolve import `does_not_exit` --> /test.py:2:8 | 2 | import does_not_exit - | ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit` + | ^^^^^^^^^^^^^ 3 | 4 | y = 4 / 0 | - warning: lint:division-by-zero + warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero --> /test.py:4:5 | 2 | import does_not_exit 3 | 4 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero + | ^^^^^ 5 | 6 | for a in range(0, int(y)): | @@ -439,11 +439,11 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - error: lint:division-by-zero + error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero --> /test.py:2:5 | 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero + | ^^^^^ 3 | 4 | for a in range(0, int(y)): | @@ -476,11 +476,11 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { success: true exit_code: 0 ----- stdout ----- - warning: lint:division-by-zero + warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero --> /test.py:2:5 | 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero + | ^^^^^ 3 | 4 | for a in range(0, int(y)): | @@ -835,11 +835,11 @@ fn user_configuration() -> anyhow::Result<()> { success: true exit_code: 0 ----- stdout ----- - warning: lint:division-by-zero + warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero --> /project/main.py:2:5 | 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero + | ^^^^^ 3 | 4 | for a in range(0, int(y)): | @@ -877,11 +877,11 @@ fn user_configuration() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - warning: lint:division-by-zero + warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero --> /project/main.py:2:5 | 2 | y = 4 / 0 - | ^^^^^ Cannot divide object of type `Literal[4]` by zero + | ^^^^^ 3 | 4 | for a in range(0, int(y)): | @@ -935,25 +935,25 @@ fn check_specific_paths() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - error: lint:unresolved-import + error: lint:unresolved-import: Cannot resolve import `does_not_exist` --> /project/tests/test_main.py:2:8 | 2 | import does_not_exist # error: unresolved-import - | ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist` + | ^^^^^^^^^^^^^^ | - error: lint:division-by-zero + error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero --> /project/main.py:2:5 | 2 | y = 4 / 0 # error: division-by-zero - | ^^^^^ Cannot divide object of type `Literal[4]` by zero + | ^^^^^ | - error: lint:unresolved-import + error: lint:unresolved-import: Cannot resolve import `main2` --> /project/other.py:2:6 | 2 | from main2 import z # error: unresolved-import - | ^^^^^ Cannot resolve import `main2` + | ^^^^^ 3 | 4 | print(z) | @@ -972,18 +972,18 @@ fn check_specific_paths() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - error: lint:unresolved-import + error: lint:unresolved-import: Cannot resolve import `does_not_exist` --> /project/tests/test_main.py:2:8 | 2 | import does_not_exist # error: unresolved-import - | ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist` + | ^^^^^^^^^^^^^^ | - error: lint:unresolved-import + error: lint:unresolved-import: Cannot resolve import `main2` --> /project/other.py:2:6 | 2 | from main2 import z # error: unresolved-import - | ^^^^^ Cannot resolve import `main2` + | ^^^^^ 3 | 4 | print(z) | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap index a1e4f9274abcb1..839dfd9fa0b698 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap @@ -40,13 +40,13 @@ error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assigna ``` ``` -error: lint:invalid-attribute-access +error: lint:invalid-attribute-access: Cannot assign to instance attribute `attr` from the class object `Literal[C]` --> /src/mdtest_snippet.py:9:1 | 7 | instance.attr = "wrong" # error: [invalid-assignment] 8 | 9 | C.attr = 1 # error: [invalid-attribute-access] - | ^^^^^^ Cannot assign to instance attribute `attr` from the class object `Literal[C]` + | ^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap index 53ef65468070f0..db88c02820195e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap @@ -23,13 +23,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib # Diagnostics ``` -error: lint:unresolved-attribute +error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `Literal[C]`. --> /src/mdtest_snippet.py:3:1 | 1 | class C: ... 2 | 3 | C.non_existent = 1 # error: [unresolved-attribute] - | ^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `Literal[C]`. + | ^^^^^^^^^^^^^^ 4 | 5 | instance = C() | @@ -37,12 +37,12 @@ error: lint:unresolved-attribute ``` ``` -error: lint:unresolved-attribute +error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `C`. --> /src/mdtest_snippet.py:6:1 | 5 | instance = C() 6 | instance.non_existent = 1 # error: [unresolved-attribute] - | ^^^^^^^^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `C`. + | ^^^^^^^^^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap index 573d8937adf928..f5f48a660d75fe 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap @@ -40,12 +40,12 @@ error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assigna ``` ``` -error: lint:invalid-attribute-access +error: lint:invalid-attribute-access: Cannot assign to ClassVar `attr` from an instance of type `C` --> /src/mdtest_snippet.py:10:1 | 9 | instance = C() 10 | instance.attr = 1 # error: [invalid-attribute-access] - | ^^^^^^^^^^^^^ Cannot assign to ClassVar `attr` from an instance of type `C` + | ^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap index 47bb7fae45c49a..b4d0d917d99e61 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap @@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md # Diagnostics ``` -error: lint:unresolved-import +error: lint:unresolved-import: Cannot resolve import `zqzqzqzqzqzqzq` --> /src/mdtest_snippet.py:1:8 | 1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`" - | ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq` + | ^^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap index bbcb0e3f40435f..fc8f6e2e6e85ce 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap @@ -27,12 +27,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md # Diagnostics ``` -error: lint:unresolved-import +error: lint:unresolved-import: Cannot resolve import `a.foo` --> /src/mdtest_snippet.py:2:8 | 1 | # Topmost component resolvable, submodule not resolvable: 2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`" - | ^^^^^ Cannot resolve import `a.foo` + | ^^^^^ 3 | 4 | # Topmost component unresolvable: | @@ -40,12 +40,12 @@ error: lint:unresolved-import ``` ``` -error: lint:unresolved-import +error: lint:unresolved-import: Cannot resolve import `b.foo` --> /src/mdtest_snippet.py:5:8 | 4 | # Topmost component unresolvable: 5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`" - | ^^^^^ Cannot resolve import `b.foo` + | ^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap index 096616ac07445e..259d63ea91203e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap @@ -20,11 +20,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso # Diagnostics ``` -error: lint:unresolved-import +error: lint:unresolved-import: Cannot resolve import `does_not_exist` --> /src/mdtest_snippet.py:1:8 | 1 | import does_not_exist # error: [unresolved-import] - | ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist` + | ^^^^^^^^^^^^^^ 2 | 3 | x = does_not_exist.foo | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap index f297f87e8aa9fa..0aa7c7109ec56a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap @@ -25,11 +25,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso # Diagnostics ``` -error: lint:unresolved-import +error: lint:unresolved-import: Module `a` has no member `does_not_exist` --> /src/mdtest_snippet.py:1:28 | 1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import] - | ^^^^^^^^^^^^^^ Module `a` has no member `does_not_exist` + | ^^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap index 88bdb9d7913370..7105afa48a1c4e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap @@ -20,11 +20,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso # Diagnostics ``` -error: lint:unresolved-import +error: lint:unresolved-import: Cannot resolve import `.does_not_exist` --> /src/mdtest_snippet.py:1:7 | 1 | from .does_not_exist import add # error: [unresolved-import] - | ^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist` + | ^^^^^^^^^^^^^^ 2 | 3 | stat = add(10, 15) | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap index 5a0c60321e850b..42cc96a5fab335 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap @@ -20,11 +20,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso # Diagnostics ``` -error: lint:unresolved-import +error: lint:unresolved-import: Cannot resolve import `.does_not_exist.foo.bar` --> /src/mdtest_snippet.py:1:7 | 1 | from .does_not_exist.foo.bar import add # error: [unresolved-import] - | ^^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist.foo.bar` + | ^^^^^^^^^^^^^^^^^^^^^^ 2 | 3 | stat = add(10, 15) | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap index e7b2303977d894..4dd7179de9b6f0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap @@ -20,11 +20,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso # Diagnostics ``` -error: lint:unresolved-import +error: lint:unresolved-import: Cannot resolve import `does_not_exist` --> /src/mdtest_snippet.py:1:6 | 1 | from does_not_exist import add # error: [unresolved-import] - | ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist` + | ^^^^^^^^^^^^^^ 2 | 3 | stat = add(10, 15) | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap index 603a2137d07a67..2d20320108721f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap @@ -32,11 +32,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso # Diagnostics ``` -error: lint:unresolved-import +error: lint:unresolved-import: Cannot resolve import `....foo` --> /src/package/subpackage/subsubpackage/__init__.py:1:10 | 1 | from ....foo import add # error: [unresolved-import] - | ^^^ Cannot resolve import `....foo` + | ^^^ 2 | 3 | stat = add(10, 15) | diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 175bddff08a3d3..cfa297d1278f06 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -749,14 +749,15 @@ impl<'db> TypeInferenceBuilder<'db> { // (1) Check that the class does not have a cyclic definition if let Some(inheritance_cycle) = class.inheritance_cycle(self.db()) { if inheritance_cycle.is_participant() { - self.context.report_lint_old( - &CYCLIC_CLASS_DEFINITION, - class_node, - format_args!( + if let Some(builder) = self + .context + .report_lint(&CYCLIC_CLASS_DEFINITION, class_node) + { + builder.into_diagnostic(format_args!( "Cyclic definition of `{}` (class cannot inherit from itself)", class.name(self.db()) - ), - ); + )); + } } // Attempting to determine the MRO of a class or if the class has a metaclass conflict // is impossible if the class is cyclically defined; there's nothing more to do here. @@ -772,13 +773,14 @@ impl<'db> TypeInferenceBuilder<'db> { for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() { let base_class = match base_class { Type::KnownInstance(KnownInstanceType::Generic) => { - // Unsubscripted `Generic` can appear in the MRO of many classes, - // but it is never valid as an explicit base class in user code. - self.context.report_lint_old( - &INVALID_BASE, - &class_node.bases()[i], - format_args!("Cannot inherit from plain `Generic`"), - ); + if let Some(builder) = self + .context + .report_lint(&INVALID_BASE, &class_node.bases()[i]) + { + // Unsubscripted `Generic` can appear in the MRO of many classes, + // but it is never valid as an explicit base class in user code. + builder.into_diagnostic("Cannot inherit from plain `Generic`"); + } continue; } Type::ClassLiteral(class) => class, @@ -790,27 +792,29 @@ impl<'db> TypeInferenceBuilder<'db> { && !(base_class.is_protocol(self.db()) || base_class.is_known(self.db(), KnownClass::Object)) { - self.context.report_lint_old( - &INVALID_PROTOCOL, - &class_node.bases()[i], - format_args!( + if let Some(builder) = self + .context + .report_lint(&INVALID_PROTOCOL, &class_node.bases()[i]) + { + builder.into_diagnostic(format_args!( "Protocol class `{}` cannot inherit from non-protocol class `{}`", class.name(self.db()), base_class.name(self.db()), - ), - ); + )); + } } if base_class.is_final(self.db()) { - self.context.report_lint_old( - &SUBCLASS_OF_FINAL_CLASS, - &class_node.bases()[i], - format_args!( + if let Some(builder) = self + .context + .report_lint(&SUBCLASS_OF_FINAL_CLASS, &class_node.bases()[i]) + { + builder.into_diagnostic(format_args!( "Class `{}` cannot inherit from final class `{}`", class.name(self.db()), base_class.name(self.db()), - ), - ); + )); + } } } @@ -821,11 +825,16 @@ impl<'db> TypeInferenceBuilder<'db> { MroErrorKind::DuplicateBases(duplicates) => { let base_nodes = class_node.bases(); for (index, duplicate) in duplicates { - self.context.report_lint_old( - &DUPLICATE_BASE, - &base_nodes[*index], - format_args!("Duplicate base class `{}`", duplicate.name(self.db())), - ); + let Some(builder) = self + .context + .report_lint(&DUPLICATE_BASE, &base_nodes[*index]) + else { + continue; + }; + builder.into_diagnostic(format_args!( + "Duplicate base class `{}`", + duplicate.name(self.db()) + )); } } MroErrorKind::InvalidBases(bases) => { @@ -837,25 +846,33 @@ impl<'db> TypeInferenceBuilder<'db> { // class will never happen. continue; } - self.context.report_lint_old( - &INVALID_BASE, - &base_nodes[*index], - format_args!( - "Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)", - base_ty.display(self.db()) - ), - ); + let Some(builder) = + self.context.report_lint(&INVALID_BASE, &base_nodes[*index]) + else { + continue; + }; + builder.into_diagnostic(format_args!( + "Invalid class base with type `{}` \ + (all bases must be a class, `Any`, `Unknown` or `Todo`)", + base_ty.display(self.db()) + )); + } + } + MroErrorKind::UnresolvableMro { bases_list } => { + if let Some(builder) = + self.context.report_lint(&INCONSISTENT_MRO, class_node) + { + builder.into_diagnostic(format_args!( + "Cannot create a consistent method resolution order (MRO) \ + for class `{}` with bases list `[{}]`", + class.name(self.db()), + bases_list + .iter() + .map(|base| base.display(self.db())) + .join(", ") + )); } } - MroErrorKind::UnresolvableMro { bases_list } => self.context.report_lint_old( - &INCONSISTENT_MRO, - class_node, - format_args!( - "Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`", - class.name(self.db()), - bases_list.iter().map(|base| base.display(self.db())).join(", ") - ), - ) } } Ok(_) => check_class_slots(&self.context, class, class_node), @@ -864,19 +881,26 @@ impl<'db> TypeInferenceBuilder<'db> { // (4) Check that the class's metaclass can be determined without error. if let Err(metaclass_error) = class.try_metaclass(self.db()) { match metaclass_error.reason() { - MetaclassErrorKind::NotCallable(ty) => self.context.report_lint_old( - &INVALID_METACLASS, - class_node, - format_args!("Metaclass type `{}` is not callable", ty.display(self.db())), - ), - MetaclassErrorKind::PartlyNotCallable(ty) => self.context.report_lint_old( - &INVALID_METACLASS, - class_node, - format_args!( - "Metaclass type `{}` is partly not callable", - ty.display(self.db()) - ), - ), + MetaclassErrorKind::NotCallable(ty) => { + if let Some(builder) = + self.context.report_lint(&INVALID_METACLASS, class_node) + { + builder.into_diagnostic(format_args!( + "Metaclass type `{}` is not callable", + ty.display(self.db()) + )); + } + } + MetaclassErrorKind::PartlyNotCallable(ty) => { + if let Some(builder) = + self.context.report_lint(&INVALID_METACLASS, class_node) + { + builder.into_diagnostic(format_args!( + "Metaclass type `{}` is partly not callable", + ty.display(self.db()) + )); + } + } MetaclassErrorKind::Conflict { candidate1: MetaclassCandidate { @@ -890,35 +914,35 @@ impl<'db> TypeInferenceBuilder<'db> { }, candidate1_is_base_class, } => { - if *candidate1_is_base_class { - self.context.report_lint_old( - &CONFLICTING_METACLASS, - class_node, - format_args!( - "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ - but `{metaclass1}` (metaclass of base class `{base1}`) and `{metaclass2}` (metaclass of base class `{base2}`) \ - have no subclass relationship", + if let Some(builder) = + self.context.report_lint(&CONFLICTING_METACLASS, class_node) + { + if *candidate1_is_base_class { + builder.into_diagnostic(format_args!( + "The metaclass of a derived class (`{class}`) \ + must be a subclass of the metaclasses of all its bases, \ + but `{metaclass1}` (metaclass of base class `{base1}`) \ + and `{metaclass2}` (metaclass of base class `{base2}`) \ + have no subclass relationship", class = class.name(self.db()), metaclass1 = metaclass1.name(self.db()), base1 = class1.name(self.db()), metaclass2 = metaclass2.name(self.db()), base2 = class2.name(self.db()), - ), - ); - } else { - self.context.report_lint_old( - &CONFLICTING_METACLASS, - class_node, - format_args!( - "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ - but `{metaclass_of_class}` (metaclass of `{class}`) and `{metaclass_of_base}` (metaclass of base class `{base}`) \ - have no subclass relationship", + )); + } else { + builder.into_diagnostic(format_args!( + "The metaclass of a derived class (`{class}`) \ + must be a subclass of the metaclasses of all its bases, \ + but `{metaclass_of_class}` (metaclass of `{class}`) \ + and `{metaclass_of_base}` (metaclass of base class `{base}`) \ + have no subclass relationship", class = class.name(self.db()), metaclass_of_class = metaclass1.name(self.db()), metaclass_of_base = metaclass2.name(self.db()), base = class2.name(self.db()), - ), - ); + )); + } } } } @@ -1054,14 +1078,12 @@ impl<'db> TypeInferenceBuilder<'db> { _ => return false, }; - self.context.report_lint_old( - &DIVISION_BY_ZERO, - node, - format_args!( + if let Some(builder) = self.context.report_lint(&DIVISION_BY_ZERO, node) { + builder.into_diagnostic(format_args!( "Cannot {op} object of type `{}` {by_zero}", left.display(self.db()) - ), - ); + )); + } true } @@ -1082,14 +1104,12 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO point out the conflicting declarations in the diagnostic? let symbol_table = self.index.symbol_table(binding.file_scope(self.db())); let symbol_name = symbol_table.symbol(binding.symbol(self.db())).name(); - self.context.report_lint_old( - &CONFLICTING_DECLARATIONS, - node, - format_args!( + if let Some(builder) = self.context.report_lint(&CONFLICTING_DECLARATIONS, node) { + builder.into_diagnostic(format_args!( "Conflicting declared types for `{symbol_name}`: {}", conflicting.display(self.db()) - ), - ); + )); + } ty.inner_type() }); if !bound_ty.is_assignable_to(self.db(), declared_ty) { @@ -1120,15 +1140,13 @@ impl<'db> TypeInferenceBuilder<'db> { let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) { ty } else { - self.context.report_lint_old( - &INVALID_DECLARATION, - node, - format_args!( + if let Some(builder) = self.context.report_lint(&INVALID_DECLARATION, node) { + builder.into_diagnostic(format_args!( "Cannot declare type `{}` for inferred type `{}`", ty.inner_type().display(self.db()), inferred_ty.display(self.db()) - ), - ); + )); + } TypeAndQualifiers::unknown() }; self.types.declarations.insert(declaration, ty); @@ -1654,13 +1672,17 @@ impl<'db> TypeInferenceBuilder<'db> { { DeclaredAndInferredType::AreTheSame(declared_ty) } else { - self.context.report_lint_old( - &INVALID_PARAMETER_DEFAULT, - parameter_with_default, - format_args!( - "Default value of type `{}` is not assignable to annotated parameter type `{}`", - default_ty.display(self.db()), declared_ty.display(self.db())), - ); + if let Some(builder) = self + .context + .report_lint(&INVALID_PARAMETER_DEFAULT, parameter_with_default) + { + builder.into_diagnostic(format_args!( + "Default value of type `{}` is not assignable \ + to annotated parameter type `{}`", + default_ty.display(self.db()), + declared_ty.display(self.db()) + )); + } DeclaredAndInferredType::AreTheSame(declared_ty) } } else { @@ -2130,11 +2152,12 @@ impl<'db> TypeInferenceBuilder<'db> { let bound_or_constraint = match bound.as_deref() { Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { if elts.len() < 2 { - self.context.report_lint_old( - &INVALID_TYPE_VARIABLE_CONSTRAINTS, - expr, - format_args!("TypeVar must have at least two constrained types"), - ); + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, expr) + { + builder.into_diagnostic("TypeVar must have at least two constrained types"); + } self.infer_expression(expr); None } else { @@ -2485,27 +2508,23 @@ impl<'db> TypeInferenceBuilder<'db> { // Super instances do not allow attribute assignment Type::Instance(instance) if instance.class().is_known(db, KnownClass::Super) => { if emit_diagnostics { - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { + builder.into_diagnostic(format_args!( "Cannot assign to attribute `{attribute}` on type `{}`", object_ty.display(self.db()), - ), - ); + )); + } } false } Type::BoundSuper(_) => { if emit_diagnostics { - self.context.report_lint_old( - &INVALID_ASSIGNMENT, - target, - format_args!( + if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { + builder.into_diagnostic(format_args!( "Cannot assign to attribute `{attribute}` on type `{}`", object_ty.display(self.db()), - ), - ); + )); + } } false } @@ -2535,14 +2554,15 @@ impl<'db> TypeInferenceBuilder<'db> { match object_ty.class_member(db, attribute.into()) { meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => { if emit_diagnostics { - self.context.report_lint_old( - &INVALID_ATTRIBUTE_ACCESS, - target, - format_args!( - "Cannot assign to ClassVar `{attribute}` from an instance of type `{ty}`", + if let Some(builder) = + self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) + { + builder.into_diagnostic(format_args!( + "Cannot assign to ClassVar `{attribute}` \ + from an instance of type `{ty}`", ty = object_ty.display(self.db()), - ), - ); + )); + } } false } @@ -2650,29 +2670,31 @@ impl<'db> TypeInferenceBuilder<'db> { Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true, Err(CallDunderError::CallError(..)) => { if emit_diagnostics { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - target, - format_args!( - "Can not assign object of `{}` to attribute `{attribute}` on type `{}` with custom `__setattr__` method.", - value_ty.display(db), - object_ty.display(db) - ), - ); + if let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) + { + builder.into_diagnostic(format_args!( + "Can not assign object of `{}` to attribute \ + `{attribute}` on type `{}` with \ + custom `__setattr__` method.", + value_ty.display(db), + object_ty.display(db) + )); + } } false } Err(CallDunderError::MethodNotAvailable) => { if emit_diagnostics { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - target, - format_args!( + if let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) + { + builder.into_diagnostic(format_args!( "Unresolved attribute `{}` on type `{}`.", attribute, object_ty.display(db) - ), - ); + )); + } } false @@ -2784,23 +2806,25 @@ impl<'db> TypeInferenceBuilder<'db> { // Attribute is declared or bound on instance. Forbid access from the class object if emit_diagnostics { if attribute_is_bound_on_instance { - self.context.report_lint_old( - &INVALID_ATTRIBUTE_ACCESS, - target, - format_args!( - "Cannot assign to instance attribute `{attribute}` from the class object `{ty}`", - ty = object_ty.display(self.db()), - )); + if let Some(builder) = + self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) + { + builder.into_diagnostic(format_args!( + "Cannot assign to instance attribute \ + `{attribute}` from the class object `{ty}`", + ty = object_ty.display(self.db()), + )); + } } else { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - target, - format_args!( + if let Some(builder) = + self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) + { + builder.into_diagnostic(format_args!( "Unresolved attribute `{}` on type `{}`.", attribute, object_ty.display(db) - ), - ); + )); + } } } @@ -2825,15 +2849,13 @@ impl<'db> TypeInferenceBuilder<'db> { false } else { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - target, - format_args!( + if let Some(builder) = self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) { + builder.into_diagnostic(format_args!( "Unresolved attribute `{}` on type `{}`.", attribute, object_ty.display(db) - ), - ); + )); + } false } @@ -3073,15 +3095,14 @@ impl<'db> TypeInferenceBuilder<'db> { let db = self.db(); let report_unsupported_augmented_op = |ctx: &mut InferContext| { - ctx.report_lint_old( - &UNSUPPORTED_OPERATOR, - assignment, - format_args!( - "Operator `{op}=` is unsupported between objects of type `{}` and `{}`", - target_type.display(db), - value_type.display(db) - ), - ); + let Some(builder) = ctx.report_lint(&UNSUPPORTED_OPERATOR, assignment) else { + return; + }; + builder.into_diagnostic(format_args!( + "Operator `{op}=` is unsupported between objects of type `{}` and `{}`", + target_type.display(db), + value_type.display(db) + )); }; // Fall back to non-augmented binary operator inference. @@ -3254,15 +3275,14 @@ impl<'db> TypeInferenceBuilder<'db> { return; } - self.context.report_lint_old( - &UNRESOLVED_IMPORT, - range, - format_args!( - "Cannot resolve import `{}{}`", - ".".repeat(level as usize), - module.unwrap_or_default() - ), - ); + let Some(builder) = self.context.report_lint(&UNRESOLVED_IMPORT, range) else { + return; + }; + builder.into_diagnostic(format_args!( + "Cannot resolve import `{}{}`", + ".".repeat(level as usize), + module.unwrap_or_default() + )); } fn infer_import_definition( @@ -3495,11 +3515,14 @@ impl<'db> TypeInferenceBuilder<'db> { if &alias.name != "*" && boundness == Boundness::PossiblyUnbound { // TODO: Consider loading _both_ the attribute and any submodule and unioning them // together if the attribute exists but is possibly-unbound. - self.context.report_lint_old( - &POSSIBLY_UNBOUND_IMPORT, - AnyNodeRef::Alias(alias), - format_args!("Member `{name}` of module `{module_name}` is possibly unbound",), - ); + if let Some(builder) = self + .context + .report_lint(&POSSIBLY_UNBOUND_IMPORT, AnyNodeRef::Alias(alias)) + { + builder.into_diagnostic(format_args!( + "Member `{name}` of module `{module_name}` is possibly unbound", + )); + } } self.add_declaration_with_binding( alias.into(), @@ -3541,11 +3564,14 @@ impl<'db> TypeInferenceBuilder<'db> { let is_import_reachable = self.is_reachable(import_from); if is_import_reachable { - self.context.report_lint_old( - &UNRESOLVED_IMPORT, - AnyNodeRef::Alias(alias), - format_args!("Module `{module_name}` has no member `{name}`",), - ); + if let Some(builder) = self + .context + .report_lint(&UNRESOLVED_IMPORT, AnyNodeRef::Alias(alias)) + { + builder.into_diagnostic(format_args!( + "Module `{module_name}` has no member `{name}`" + )); + } } } @@ -4342,29 +4368,32 @@ impl<'db> TypeInferenceBuilder<'db> { if !actual_ty .is_gradual_equivalent_to(self.db(), *asserted_ty) { - self.context.report_lint_old( - &TYPE_ASSERTION_FAILURE, - call_expression, - format_args!( - "Actual type `{}` is not the same as asserted type `{}`", - actual_ty.display(self.db()), - asserted_ty.display(self.db()), - ), - ); + if let Some(builder) = self.context.report_lint( + &TYPE_ASSERTION_FAILURE, + call_expression, + ) { + builder.into_diagnostic(format_args!( + "Actual type `{}` is not the same \ + as asserted type `{}`", + actual_ty.display(self.db()), + asserted_ty.display(self.db()), + )); + } } } } KnownFunction::AssertNever => { if let [Some(actual_ty)] = overload.parameter_types() { if !actual_ty.is_equivalent_to(self.db(), Type::Never) { - self.context.report_lint_old( + if let Some(builder) = self.context.report_lint( &TYPE_ASSERTION_FAILURE, call_expression, - format_args!( + ) { + builder.into_diagnostic(format_args!( "Expected type `Never`, got `{}` instead", actual_ty.display(self.db()), - ), - ); + )); + } } } } @@ -4395,42 +4424,42 @@ impl<'db> TypeInferenceBuilder<'db> { } }; - if !truthiness.is_always_true() { - if let Some(message) = message - .and_then(Type::into_string_literal) - .map(|s| &**s.value(self.db())) - { - self.context.report_lint_old( - &STATIC_ASSERT_ERROR, - call_expression, - format_args!( + if let Some(builder) = self + .context + .report_lint(&STATIC_ASSERT_ERROR, call_expression) + { + if !truthiness.is_always_true() { + if let Some(message) = message + .and_then(Type::into_string_literal) + .map(|s| &**s.value(self.db())) + { + builder.into_diagnostic(format_args!( "Static assertion error: {message}" - ), - ); - } else if *parameter_ty == Type::BooleanLiteral(false) { - self.context.report_lint_old( - &STATIC_ASSERT_ERROR, - call_expression, - format_args!("Static assertion error: argument evaluates to `False`"), - ); - } else if truthiness.is_always_false() { - self.context.report_lint_old( - &STATIC_ASSERT_ERROR, - call_expression, - format_args!( - "Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy", - parameter_ty=parameter_ty.display(self.db()) - ), - ); - } else { - self.context.report_lint_old( - &STATIC_ASSERT_ERROR, - call_expression, - format_args!( - "Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness", - parameter_ty=parameter_ty.display(self.db()) - ), - ); + )); + } else if *parameter_ty + == Type::BooleanLiteral(false) + { + builder.into_diagnostic( + "Static assertion error: \ + argument evaluates to `False`", + ); + } else if truthiness.is_always_false() { + builder.into_diagnostic(format_args!( + "Static assertion error: \ + argument of type `{parameter_ty}` \ + is statically known to be falsy", + parameter_ty = + parameter_ty.display(self.db()) + )); + } else { + builder.into_diagnostic(format_args!( + "Static assertion error: \ + argument of type `{parameter_ty}` \ + has an ambiguous static truthiness", + parameter_ty = + parameter_ty.display(self.db()) + )); + } } } } @@ -4445,14 +4474,15 @@ impl<'db> TypeInferenceBuilder<'db> { == casted_type.normalized(db)) && !source_type.contains_todo(db) { - self.context.report_lint_old( - &REDUNDANT_CAST, - call_expression, - format_args!( + if let Some(builder) = self + .context + .report_lint(&REDUNDANT_CAST, call_expression) + { + builder.into_diagnostic(format_args!( "Value is already of type `{}`", casted_type.display(db), - ), - ); + )); + } } } } @@ -4740,14 +4770,15 @@ impl<'db> TypeInferenceBuilder<'db> { // Still not found? It might be `reveal_type`... .or_fall_back_to(db, || { if symbol_name == "reveal_type" { - self.context.report_lint_old( - &UNDEFINED_REVEAL, - name_node, - format_args!( - "`reveal_type` used without importing it; \ - this is allowed for debugging convenience but will fail at runtime" - ), - ); + if let Some(builder) = + self.context.report_lint(&UNDEFINED_REVEAL, name_node) + { + let mut diag = + builder.into_diagnostic("`reveal_type` used without importing it"); + diag.info( + "This is allowed for debugging convenience but will fail at runtime" + ); + } typing_extensions_symbol(db, symbol_name) } else { Symbol::Unbound.into() @@ -4817,20 +4848,21 @@ impl<'db> TypeInferenceBuilder<'db> { _ => false, }; + if let Some(builder) = self + .context + .report_lint(&UNRESOLVED_ATTRIBUTE, attribute) + { if bound_on_instance { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - attribute, + builder.into_diagnostic( format_args!( - "Attribute `{}` can only be accessed on instances, not on the class object `{}` itself.", + "Attribute `{}` can only be accessed on instances, \ + not on the class object `{}` itself.", attr.id, value_type.display(db) ), ); } else { - self.context.report_lint_old( - &UNRESOLVED_ATTRIBUTE, - attribute, + builder.into_diagnostic( format_args!( "Type `{}` has no attribute `{}`", value_type.display(db), @@ -4838,6 +4870,7 @@ impl<'db> TypeInferenceBuilder<'db> { ), ); } + } } Type::unknown().into() @@ -4951,14 +4984,14 @@ impl<'db> TypeInferenceBuilder<'db> { ) { Ok(outcome) => outcome.return_type(self.db()), Err(e) => { - self.context.report_lint_old( - &UNSUPPORTED_OPERATOR, - unary, - format_args!( + if let Some(builder) = + self.context.report_lint(&UNSUPPORTED_OPERATOR, unary) + { + builder.into_diagnostic(format_args!( "Unary operator `{op}` is unsupported for type `{}`", operand_type.display(self.db()), - ), - ); + )); + } e.fallback_return_type(self.db()) } } @@ -4979,15 +5012,13 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_binary_expression_type(binary.into(), false, left_ty, right_ty, *op) .unwrap_or_else(|| { - self.context.report_lint_old( - &UNSUPPORTED_OPERATOR, - binary, - format_args!( + if let Some(builder) = self.context.report_lint(&UNSUPPORTED_OPERATOR, binary) { + builder.into_diagnostic(format_args!( "Operator `{op}` is unsupported between objects of type `{}` and `{}`", left_ty.display(self.db()), right_ty.display(self.db()) - ), - ); + )); + } Type::unknown() }) } @@ -5436,11 +5467,11 @@ impl<'db> TypeInferenceBuilder<'db> { let ty = builder .infer_binary_type_comparison(left_ty, *op, right_ty, range) .unwrap_or_else(|error| { - // Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome) - builder.context.report_lint_old( - &UNSUPPORTED_OPERATOR, - range, - format_args!( + if let Some(diagnostic_builder) = + builder.context.report_lint(&UNSUPPORTED_OPERATOR, range) + { + // Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome) + diagnostic_builder.into_diagnostic(format_args!( "Operator `{}` is not supported for types `{}` and `{}`{}", error.op, error.left_ty.display(builder.db()), @@ -5454,8 +5485,8 @@ impl<'db> TypeInferenceBuilder<'db> { right_ty.display(builder.db()) ) } - ), - ); + )); + } match op { // `in, not in, is, is not` always return bool instances @@ -6328,27 +6359,29 @@ impl<'db> TypeInferenceBuilder<'db> { ) { Ok(outcome) => return outcome.return_type(self.db()), Err(err @ CallDunderError::PossiblyUnbound { .. }) => { - self.context.report_lint_old( - &CALL_POSSIBLY_UNBOUND_METHOD, - value_node, - format_args!( + if let Some(builder) = self + .context + .report_lint(&CALL_POSSIBLY_UNBOUND_METHOD, value_node) + { + builder.into_diagnostic(format_args!( "Method `__getitem__` of type `{}` is possibly unbound", value_ty.display(self.db()), - ), - ); + )); + } return err.fallback_return_type(self.db()); } Err(CallDunderError::CallError(_, bindings)) => { - self.context.report_lint_old( - &CALL_NON_CALLABLE, - value_node, - format_args!( - "Method `__getitem__` of type `{}` is not callable on object of type `{}`", + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, value_node) + { + builder.into_diagnostic(format_args!( + "Method `__getitem__` of type `{}` \ + is not callable on object of type `{}`", bindings.callable_type().display(self.db()), value_ty.display(self.db()), - ), - ); + )); + } return bindings.return_type(self.db()); } @@ -6374,14 +6407,16 @@ impl<'db> TypeInferenceBuilder<'db> { Symbol::Unbound => {} Symbol::Type(ty, boundness) => { if boundness == Boundness::PossiblyUnbound { - self.context.report_lint_old( - &CALL_POSSIBLY_UNBOUND_METHOD, - value_node, - format_args!( - "Method `__class_getitem__` of type `{}` is possibly unbound", + if let Some(builder) = self + .context + .report_lint(&CALL_POSSIBLY_UNBOUND_METHOD, value_node) + { + builder.into_diagnostic(format_args!( + "Method `__class_getitem__` of type `{}` \ + is possibly unbound", value_ty.display(self.db()), - ), - ); + )); + } } match ty.try_call( @@ -6390,15 +6425,16 @@ impl<'db> TypeInferenceBuilder<'db> { ) { Ok(bindings) => return bindings.return_type(self.db()), Err(CallError(_, bindings)) => { - self.context.report_lint_old( - &CALL_NON_CALLABLE, - value_node, - format_args!( - "Method `__class_getitem__` of type `{}` is not callable on object of type `{}`", + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, value_node) + { + builder.into_diagnostic(format_args!( + "Method `__class_getitem__` of type `{}` \ + is not callable on object of type `{}`", bindings.callable_type().display(self.db()), value_ty.display(self.db()), - ), - ); + )); + } return bindings.return_type(self.db()); } } @@ -6564,20 +6600,19 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::Starred(starred) => self.infer_starred_expression(starred).into(), ast::Expr::BytesLiteral(bytes) => { - self.context.report_lint_old( - &BYTE_STRING_TYPE_ANNOTATION, - bytes, - format_args!("Type expressions cannot use bytes literal"), - ); + if let Some(builder) = self + .context + .report_lint(&BYTE_STRING_TYPE_ANNOTATION, bytes) + { + builder.into_diagnostic("Type expressions cannot use bytes literal"); + } TypeAndQualifiers::unknown() } ast::Expr::FString(fstring) => { - self.context.report_lint_old( - &FSTRING_TYPE_ANNOTATION, - fstring, - format_args!("Type expressions cannot use f-strings"), - ); + if let Some(builder) = self.context.report_lint(&FSTRING_TYPE_ANNOTATION, fstring) { + builder.into_diagnostic("Type expressions cannot use f-strings"); + } self.infer_fstring_expression(fstring); TypeAndQualifiers::unknown() } @@ -6651,14 +6686,15 @@ impl<'db> TypeInferenceBuilder<'db> { known_instance @ (KnownInstanceType::ClassVar | KnownInstanceType::Final), ) => match slice { ast::Expr::Tuple(..) => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Type qualifier `{type_qualifier}` expects exactly one type parameter", + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( + "Type qualifier `{type_qualifier}` \ + expects exactly one type parameter", type_qualifier = known_instance.repr(self.db()), - ), - ); + )); + } Type::unknown().into() } _ => { @@ -6751,8 +6787,9 @@ impl<'db> TypeInferenceBuilder<'db> { expression: &ast::Expr, message: std::fmt::Arguments, ) -> Type<'db> { - self.context - .report_lint_old(&INVALID_TYPE_FORM, expression, message); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, expression) { + builder.into_diagnostic(message); + } Type::unknown() } @@ -7201,11 +7238,9 @@ impl<'db> TypeInferenceBuilder<'db> { } ast::Expr::Tuple(_) => { self.infer_type_expression(slice); - self.context.report_lint_old( - &INVALID_TYPE_FORM, - slice, - format_args!("type[...] must have exactly one type argument"), - ); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, slice) { + builder.into_diagnostic("type[...] must have exactly one type argument"); + } Type::unknown() } ast::Expr::Subscript(ast::ExprSubscript { @@ -7257,11 +7292,9 @@ impl<'db> TypeInferenceBuilder<'db> { match value_ty { Type::ClassLiteral(literal) if literal.is_known(self.db(), KnownClass::Any) => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!("Type `typing.Any` expected no type parameter",), - ); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic("Type `typing.Any` expected no type parameter"); + } Type::unknown() } Type::KnownInstance(known_instance) => { @@ -7337,14 +7370,14 @@ impl<'db> TypeInferenceBuilder<'db> { Ok(ty) => ty, Err(nodes) => { for node in nodes { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - node, - format_args!( + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, node) + { + builder.into_diagnostic( "Type arguments for `Literal` must be `None`, \ - a literal value (int, bool, str, or bytes), or an enum value" - ), - ); + a literal value (int, bool, str, or bytes), or an enum value", + ); + } } Type::unknown() } @@ -7426,14 +7459,12 @@ impl<'db> TypeInferenceBuilder<'db> { // Type API special forms KnownInstanceType::Not => match arguments_slice { ast::Expr::Tuple(_) => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( "Special form `{}` expected exactly one type parameter", known_instance.repr(db) - ), - ); + )); + } Type::unknown() } _ => { @@ -7455,14 +7486,12 @@ impl<'db> TypeInferenceBuilder<'db> { } KnownInstanceType::TypeOf => match arguments_slice { ast::Expr::Tuple(_) => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( "Special form `{}` expected exactly one type parameter", known_instance.repr(db) - ), - ); + )); + } Type::unknown() } _ => { @@ -7473,14 +7502,12 @@ impl<'db> TypeInferenceBuilder<'db> { }, KnownInstanceType::CallableTypeOf => match arguments_slice { ast::Expr::Tuple(_) => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( "Special form `{}` expected exactly one type parameter", known_instance.repr(db) - ), - ); + )); + } Type::unknown() } _ => { @@ -7503,15 +7530,18 @@ impl<'db> TypeInferenceBuilder<'db> { }); let Some(signature) = signature_iter.next() else { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - arguments_slice, - format_args!( - "Expected the first argument to `{}` to be a callable object, but got an object of type `{}`", + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_FORM, arguments_slice) + { + builder.into_diagnostic(format_args!( + "Expected the first argument to `{}` \ + to be a callable object, \ + but got an object of type `{}`", known_instance.repr(db), argument_type.display(db) - ), - ); + )); + } return Type::unknown(); }; @@ -7569,14 +7599,13 @@ impl<'db> TypeInferenceBuilder<'db> { todo_type!("`NotRequired[]` type qualifier") } KnownInstanceType::ClassVar | KnownInstanceType::Final => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Type qualifier `{}` is not allowed in type expressions (only in annotation expressions)", + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "Type qualifier `{}` is not allowed in type expressions \ + (only in annotation expressions)", known_instance.repr(db) - ), - ); + )); + } self.infer_type_expression(arguments_slice) } KnownInstanceType::Required => { @@ -7612,38 +7641,33 @@ impl<'db> TypeInferenceBuilder<'db> { | KnownInstanceType::Any | KnownInstanceType::AlwaysTruthy | KnownInstanceType::AlwaysFalsy => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", known_instance.repr(db) - ), - ); + )); + } Type::unknown() } KnownInstanceType::TypingSelf | KnownInstanceType::TypeAlias | KnownInstanceType::Unknown => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( "Special form `{}` expected no type parameter", known_instance.repr(db) - ), - ); + )); + } Type::unknown() } KnownInstanceType::LiteralString => { - self.context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?", + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + let mut diag = builder.into_diagnostic(format_args!( + "Type `{}` expected no type parameter", known_instance.repr(db) - ), - ); + )); + diag.info("Did you mean to use `Literal[...]` instead?"); + } Type::unknown() } KnownInstanceType::Type => self.infer_subclass_of_type_expression(arguments_slice), @@ -7781,14 +7805,14 @@ impl<'db> TypeInferenceBuilder<'db> { return None; } _ => { - // TODO: Check whether `Expr::Name` is a ParamSpec - self.context.report_lint_old( - &INVALID_TYPE_FORM, - parameters, - format_args!( - "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`", - ), - ); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, parameters) { + // TODO: Check whether `Expr::Name` is a ParamSpec + builder.into_diagnostic(format_args!( + "The first argument to `Callable` \ + must be either a list of types, \ + ParamSpec, Concatenate, or `...`", + )); + } return None; } }) From ad5a659f29784c7350a6bac88fcf26bb669f65d3 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 18 Apr 2025 12:18:50 -0400 Subject: [PATCH 0062/1161] red_knot_python_semantic: migrate `types/string_annotation` to new diagnostics --- .../src/types/string_annotation.rs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/string_annotation.rs b/crates/red_knot_python_semantic/src/types/string_annotation.rs index 64a0d001ce04d6..0c76ad69958965 100644 --- a/crates/red_knot_python_semantic/src/types/string_annotation.rs +++ b/crates/red_knot_python_semantic/src/types/string_annotation.rs @@ -142,38 +142,38 @@ pub(crate) fn parse_string_annotation( if let Some(string_literal) = string_expr.as_single_part_string() { let prefix = string_literal.flags.prefix(); if prefix.is_raw() { - context.report_lint_old( - &RAW_STRING_TYPE_ANNOTATION, - string_literal, - format_args!("Type expressions cannot use raw string literal"), - ); + if let Some(builder) = context.report_lint(&RAW_STRING_TYPE_ANNOTATION, string_literal) + { + builder.into_diagnostic("Type expressions cannot use raw string literal"); + } // Compare the raw contents (without quotes) of the expression with the parsed contents // contained in the string literal. } else if &source[string_literal.content_range()] == string_literal.as_str() { match ruff_python_parser::parse_string_annotation(source.as_str(), string_literal) { Ok(parsed) => return Some(parsed), - Err(parse_error) => context.report_lint_old( - &INVALID_SYNTAX_IN_FORWARD_ANNOTATION, - string_literal, - format_args!("Syntax error in forward annotation: {}", parse_error.error), - ), + Err(parse_error) => { + if let Some(builder) = + context.report_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION, string_literal) + { + builder.into_diagnostic(format_args!( + "Syntax error in forward annotation: {}", + parse_error.error + )); + } + } } - } else { + } else if let Some(builder) = + context.report_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, string_expr) + { // The raw contents of the string doesn't match the parsed content. This could be the // case for annotations that contain escape sequences. - context.report_lint_old( - &ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, - string_expr, - format_args!("Type expressions cannot contain escape characters"), - ); + builder.into_diagnostic("Type expressions cannot contain escape characters"); } - } else { + } else if let Some(builder) = + context.report_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, string_expr) + { // String is implicitly concatenated. - context.report_lint_old( - &IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, - string_expr, - format_args!("Type expressions cannot span multiple string literals"), - ); + builder.into_diagnostic("Type expressions cannot span multiple string literals"); } None From 3796b13ea2088f202f749568e6f4d01ae2e21a10 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 22 Apr 2025 09:26:37 -0400 Subject: [PATCH 0063/1161] red_knot_python_semantic: migrate `types/call/bind` to new diagnostics --- ...stics_-_Calls_to_overloaded_functions.snap | 4 +- .../src/types/call/bind.rs | 107 ++++++++---------- 2 files changed, 49 insertions(+), 62 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap index c824ee37da98fe..27eba8849f757c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap @@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_mat # Diagnostics ``` -error: lint:no-matching-overload +error: lint:no-matching-overload: No overload of class `type` matches arguments --> /src/mdtest_snippet.py:1:1 | 1 | type("Foo", ()) # error: [no-matching-overload] - | ^^^^^^^^^^^^^^^ No overload of class `type` matches arguments + | ^^^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 6687bd28466053..6fb2505e87facb 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -179,24 +179,23 @@ impl<'db> Bindings<'db> { // If all union elements are not callable, report that the union as a whole is not // callable. if self.into_iter().all(|b| !b.is_callable()) { - context.report_lint_old( - &CALL_NON_CALLABLE, - node, - format_args!( + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + builder.into_diagnostic(format_args!( "Object of type `{}` is not callable", self.callable_type().display(context.db()) - ), - ); + )); + } return; } for (index, conflicting_form) in self.conflicting_forms.iter().enumerate() { if *conflicting_form { - context.report_lint_old( - &CONFLICTING_ARGUMENT_FORMS, - BindingError::get_node(node, Some(index)), - format_args!("Argument is used as both a value and a type form in call"), - ); + let node = BindingError::get_node(node, Some(index)); + if let Some(builder) = context.report_lint(&CONFLICTING_ARGUMENT_FORMS, node) { + builder.into_diagnostic( + "Argument is used as both a value and a type form in call", + ); + } } } @@ -907,43 +906,37 @@ impl<'db> CallableBinding<'db> { fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) { if !self.is_callable() { - context.report_lint_old( - &CALL_NON_CALLABLE, - node, - format_args!( + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + builder.into_diagnostic(format_args!( "Object of type `{}` is not callable", self.callable_type.display(context.db()), - ), - ); + )); + } return; } if self.dunder_call_is_possibly_unbound { - context.report_lint_old( - &CALL_NON_CALLABLE, - node, - format_args!( + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + builder.into_diagnostic(format_args!( "Object of type `{}` is not callable (possibly unbound `__call__` method)", self.callable_type.display(context.db()), - ), - ); + )); + } return; } let callable_description = CallableDescription::new(context.db(), self.callable_type); if self.overloads.len() > 1 { - context.report_lint_old( - &NO_MATCHING_OVERLOAD, - node, - format_args!( + if let Some(builder) = context.report_lint(&NO_MATCHING_OVERLOAD, node) { + builder.into_diagnostic(format_args!( "No overload{} matches arguments", if let Some(CallableDescription { kind, name }) = callable_description { format!(" of {kind} `{name}`") } else { String::new() } - ), - ); + )); + } return; } @@ -1444,10 +1437,9 @@ impl<'db> BindingError<'db> { expected_positional_count, provided_positional_count, } => { - context.report_lint_old( - &TOO_MANY_POSITIONAL_ARGUMENTS, - Self::get_node(node, *first_excess_argument_index), - format_args!( + let node = Self::get_node(node, *first_excess_argument_index); + if let Some(builder) = context.report_lint(&TOO_MANY_POSITIONAL_ARGUMENTS, node) { + builder.into_diagnostic(format_args!( "Too many positional arguments{}: expected \ {expected_positional_count}, got {provided_positional_count}", if let Some(CallableDescription { kind, name }) = callable_description { @@ -1455,75 +1447,70 @@ impl<'db> BindingError<'db> { } else { String::new() } - ), - ); + )); + } } Self::MissingArguments { parameters } => { - let s = if parameters.0.len() == 1 { "" } else { "s" }; - context.report_lint_old( - &MISSING_ARGUMENT, - node, - format_args!( + if let Some(builder) = context.report_lint(&MISSING_ARGUMENT, node) { + let s = if parameters.0.len() == 1 { "" } else { "s" }; + builder.into_diagnostic(format_args!( "No argument{s} provided for required parameter{s} {parameters}{}", if let Some(CallableDescription { kind, name }) = callable_description { format!(" of {kind} `{name}`") } else { String::new() } - ), - ); + )); + } } Self::UnknownArgument { argument_name, argument_index, } => { - context.report_lint_old( - &UNKNOWN_ARGUMENT, - Self::get_node(node, *argument_index), - format_args!( + let node = Self::get_node(node, *argument_index); + if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, node) { + builder.into_diagnostic(format_args!( "Argument `{argument_name}` does not match any known parameter{}", if let Some(CallableDescription { kind, name }) = callable_description { format!(" of {kind} `{name}`") } else { String::new() } - ), - ); + )); + } } Self::ParameterAlreadyAssigned { argument_index, parameter, } => { - context.report_lint_old( - &PARAMETER_ALREADY_ASSIGNED, - Self::get_node(node, *argument_index), - format_args!( + let node = Self::get_node(node, *argument_index); + if let Some(builder) = context.report_lint(&PARAMETER_ALREADY_ASSIGNED, node) { + builder.into_diagnostic(format_args!( "Multiple values provided for parameter {parameter}{}", if let Some(CallableDescription { kind, name }) = callable_description { format!(" of {kind} `{name}`") } else { String::new() } - ), - ); + )); + } } Self::InternalCallError(reason) => { - context.report_lint_old( - &CALL_NON_CALLABLE, - Self::get_node(node, None), - format_args!( + let node = Self::get_node(node, None); + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + builder.into_diagnostic(format_args!( "Call{} failed: {reason}", if let Some(CallableDescription { kind, name }) = callable_description { format!(" of {kind} `{name}`") } else { String::new() } - ), - ); + )); + } } } } From c12640fea84ff242b63239cda228d82731151762 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 22 Apr 2025 09:46:43 -0400 Subject: [PATCH 0064/1161] red_knot_python_semantic: migrate `types/diagnostic` to new diagnostics --- crates/red_knot/tests/cli.rs | 56 +++--- ...ignment_-_Possibly-unbound_attributes.snap | 8 +- ...or_loops_-_With_non-callable_iterator.snap | 4 +- ...n_type_-_Invalid_implicit_return_type.snap | 12 +- ...ion_return_type_-_Invalid_return_type.snap | 4 +- ...pe_-_Invalid_return_type_in_stub_file.snap | 8 +- .../src/types/diagnostic.rs | 187 +++++++++--------- 7 files changed, 134 insertions(+), 145 deletions(-) diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index 210063ae84ea6e..df8e21defbe269 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -274,13 +274,13 @@ fn configuration_rule_severity() -> anyhow::Result<()> { 4 | for a in range(0, int(y)): | - warning: lint:possibly-unresolved-reference + warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined --> /test.py:7:7 | 5 | x = a 6 | 7 | print(x) # possibly-unresolved-reference - | ^ Name `x` used when possibly not defined + | ^ | Found 2 diagnostics @@ -361,13 +361,13 @@ fn cli_rule_severity() -> anyhow::Result<()> { 6 | for a in range(0, int(y)): | - warning: lint:possibly-unresolved-reference + warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined --> /test.py:9:7 | 7 | x = a 8 | 9 | print(x) # possibly-unresolved-reference - | ^ Name `x` used when possibly not defined + | ^ | Found 3 diagnostics @@ -448,13 +448,13 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { 4 | for a in range(0, int(y)): | - warning: lint:possibly-unresolved-reference + warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined --> /test.py:7:7 | 5 | x = a 6 | 7 | print(x) # possibly-unresolved-reference - | ^ Name `x` used when possibly not defined + | ^ | Found 2 diagnostics @@ -555,11 +555,11 @@ fn exit_code_only_warnings() -> anyhow::Result<()> { success: true exit_code: 0 ----- stdout ----- - warning: lint:unresolved-reference + warning: lint:unresolved-reference: Name `x` used when not defined --> /test.py:1:7 | 1 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined + | ^ | Found 1 diagnostic @@ -638,11 +638,11 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - warning: lint:unresolved-reference + warning: lint:unresolved-reference: Name `x` used when not defined --> /test.py:1:7 | 1 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined + | ^ | Found 1 diagnostic @@ -670,11 +670,11 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any success: false exit_code: 1 ----- stdout ----- - warning: lint:unresolved-reference + warning: lint:unresolved-reference: Name `x` used when not defined --> /test.py:1:7 | 1 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined + | ^ | Found 1 diagnostic @@ -699,20 +699,20 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - warning: lint:unresolved-reference + warning: lint:unresolved-reference: Name `x` used when not defined --> /test.py:2:7 | 2 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined + | ^ 3 | print(4[1]) # [non-subscriptable] | - error: lint:non-subscriptable + error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method --> /test.py:3:7 | 2 | print(x) # [unresolved-reference] 3 | print(4[1]) # [non-subscriptable] - | ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method + | ^ | Found 2 diagnostics @@ -737,20 +737,20 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow:: success: false exit_code: 1 ----- stdout ----- - warning: lint:unresolved-reference + warning: lint:unresolved-reference: Name `x` used when not defined --> /test.py:2:7 | 2 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined + | ^ 3 | print(4[1]) # [non-subscriptable] | - error: lint:non-subscriptable + error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method --> /test.py:3:7 | 2 | print(x) # [unresolved-reference] 3 | print(4[1]) # [non-subscriptable] - | ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method + | ^ | Found 2 diagnostics @@ -775,20 +775,20 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> { success: true exit_code: 0 ----- stdout ----- - warning: lint:unresolved-reference + warning: lint:unresolved-reference: Name `x` used when not defined --> /test.py:2:7 | 2 | print(x) # [unresolved-reference] - | ^ Name `x` used when not defined + | ^ 3 | print(4[1]) # [non-subscriptable] | - error: lint:non-subscriptable + error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method --> /test.py:3:7 | 2 | print(x) # [unresolved-reference] 3 | print(4[1]) # [non-subscriptable] - | ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method + | ^ | Found 2 diagnostics @@ -844,13 +844,13 @@ fn user_configuration() -> anyhow::Result<()> { 4 | for a in range(0, int(y)): | - warning: lint:possibly-unresolved-reference + warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined --> /project/main.py:7:7 | 5 | x = a 6 | 7 | print(x) - | ^ Name `x` used when possibly not defined + | ^ | Found 2 diagnostics @@ -886,13 +886,13 @@ fn user_configuration() -> anyhow::Result<()> { 4 | for a in range(0, int(y)): | - error: lint:possibly-unresolved-reference + error: lint:possibly-unresolved-reference: Name `x` used when possibly not defined --> /project/main.py:7:7 | 5 | x = a 6 | 7 | print(x) - | ^ Name `x` used when possibly not defined + | ^ | Found 2 diagnostics diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap index 8bc9529169efa0..9d98faf548b411 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap @@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib # Diagnostics ``` -warning: lint:possibly-unbound-attribute +warning: lint:possibly-unbound-attribute: Attribute `attr` on type `Literal[C]` is possibly unbound --> /src/mdtest_snippet.py:6:5 | 4 | attr: int = 0 5 | 6 | C.attr = 1 # error: [possibly-unbound-attribute] - | ^^^^^^ Attribute `attr` on type `Literal[C]` is possibly unbound + | ^^^^^^ 7 | 8 | instance = C() | @@ -40,12 +40,12 @@ warning: lint:possibly-unbound-attribute ``` ``` -warning: lint:possibly-unbound-attribute +warning: lint:possibly-unbound-attribute: Attribute `attr` on type `C` is possibly unbound --> /src/mdtest_snippet.py:9:5 | 8 | instance = C() 9 | instance.attr = 1 # error: [possibly-unbound-attribute] - | ^^^^^^^^^^^^^ Attribute `attr` on type `C` is possibly unbound + | ^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap index 2fb989fd46909b..f85c26b8231fc5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap @@ -45,13 +45,13 @@ error: lint:not-iterable ``` ``` -warning: lint:possibly-unresolved-reference +warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined --> /src/mdtest_snippet.py:16:17 | 14 | # revealed: Unknown 15 | # error: [possibly-unresolved-reference] 16 | reveal_type(x) - | ^ Name `x` used when possibly not defined + | ^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap index e88e7a29a7de5c..1b57002c775edd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap @@ -56,12 +56,12 @@ error: lint:invalid-return-type: Return type does not match returned value ``` ``` -error: lint:invalid-return-type +error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` --> /src/mdtest_snippet.py:7:22 | 6 | # error: [invalid-return-type] 7 | def f(cond: bool) -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` + | ^^^ 8 | if cond: 9 | return 1 | @@ -69,12 +69,12 @@ error: lint:invalid-return-type ``` ``` -error: lint:invalid-return-type +error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` --> /src/mdtest_snippet.py:12:22 | 11 | # error: [invalid-return-type] 12 | def f(cond: bool) -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` + | ^^^ 13 | if cond: 14 | raise ValueError() | @@ -82,12 +82,12 @@ error: lint:invalid-return-type ``` ``` -error: lint:invalid-return-type +error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` --> /src/mdtest_snippet.py:17:22 | 16 | # error: [invalid-return-type] 17 | def f(cond: bool) -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` + | ^^^ 18 | if cond: 19 | cond = False | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap index b54a9b6abb548d..c039791eae7e44 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap @@ -35,12 +35,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty # Diagnostics ``` -error: lint:invalid-return-type +error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` --> /src/mdtest_snippet.py:2:12 | 1 | # error: [invalid-return-type] 2 | def f() -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` + | ^^^ 3 | 1 | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap index 3514f85b6cd42e..3c02500ae3832d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap @@ -45,12 +45,12 @@ error: lint:invalid-return-type: Return type does not match returned value ``` ``` -error: lint:invalid-return-type +error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` --> /src/mdtest_snippet.pyi:6:14 | 5 | # error: [invalid-return-type] 6 | def foo() -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` + | ^^^ 7 | print("...") 8 | ... | @@ -58,12 +58,12 @@ error: lint:invalid-return-type ``` ``` -error: lint:invalid-return-type +error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` --> /src/mdtest_snippet.pyi:11:14 | 10 | # error: [invalid-return-type] 11 | def foo() -> int: - | ^^^ Function can implicitly return `None`, which is not assignable to return type `int` + | ^^^ 12 | f"""{foo} is a function that ...""" 13 | ... | diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index f82eae4d0b32f5..e35f19fea99fd7 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -1075,14 +1075,13 @@ pub(super) fn report_index_out_of_bounds( length: usize, index: i64, ) { - context.report_lint_old( - &INDEX_OUT_OF_BOUNDS, - node, - format_args!( - "Index {index} is out of bounds for {kind} `{}` with length {length}", - tuple_ty.display(context.db()) - ), - ); + let Some(builder) = context.report_lint(&INDEX_OUT_OF_BOUNDS, node) else { + return; + }; + builder.into_diagnostic(format_args!( + "Index {index} is out of bounds for {kind} `{}` with length {length}", + tuple_ty.display(context.db()) + )); } /// Emit a diagnostic declaring that a type does not support subscripting. @@ -1092,22 +1091,20 @@ pub(super) fn report_non_subscriptable( non_subscriptable_ty: Type, method: &str, ) { - context.report_lint_old( - &NON_SUBSCRIPTABLE, - node, - format_args!( - "Cannot subscript object of type `{}` with no `{method}` method", - non_subscriptable_ty.display(context.db()) - ), - ); + let Some(builder) = context.report_lint(&NON_SUBSCRIPTABLE, node) else { + return; + }; + builder.into_diagnostic(format_args!( + "Cannot subscript object of type `{}` with no `{method}` method", + non_subscriptable_ty.display(context.db()) + )); } pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeRef) { - context.report_lint_old( - &ZERO_STEPSIZE_IN_SLICE, - node, - format_args!("Slice step size can not be zero"), - ); + let Some(builder) = context.report_lint(&ZERO_STEPSIZE_IN_SLICE, node) else { + return; + }; + builder.into_diagnostic("Slice step size can not be zero"); } fn report_invalid_assignment_with_message( @@ -1209,21 +1206,21 @@ pub(super) fn report_implicit_return_type( range: impl Ranged, expected_ty: Type, ) { - context.report_lint_old( - &INVALID_RETURN_TYPE, - range, - format_args!( - "Function can implicitly return `None`, which is not assignable to return type `{}`", - expected_ty.display(context.db()) - ), - ); + let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, range) else { + return; + }; + builder.into_diagnostic(format_args!( + "Function can implicitly return `None`, which is not assignable to return type `{}`", + expected_ty.display(context.db()) + )); } pub(super) fn report_invalid_type_checking_constant(context: &InferContext, node: AnyNodeRef) { - context.report_lint_old( - &INVALID_TYPE_CHECKING_CONSTANT, - node, - format_args!("The name TYPE_CHECKING is reserved for use as a flag; only False can be assigned to it.",), + let Some(builder) = context.report_lint(&INVALID_TYPE_CHECKING_CONSTANT, node) else { + return; + }; + builder.into_diagnostic( + "The name TYPE_CHECKING is reserved for use as a flag; only False can be assigned to it", ); } @@ -1231,13 +1228,12 @@ pub(super) fn report_possibly_unresolved_reference( context: &InferContext, expr_name_node: &ast::ExprName, ) { - let ast::ExprName { id, .. } = expr_name_node; + let Some(builder) = context.report_lint(&POSSIBLY_UNRESOLVED_REFERENCE, expr_name_node) else { + return; + }; - context.report_lint_old( - &POSSIBLY_UNRESOLVED_REFERENCE, - expr_name_node, - format_args!("Name `{id}` used when possibly not defined"), - ); + let ast::ExprName { id, .. } = expr_name_node; + builder.into_diagnostic(format_args!("Name `{id}` used when possibly not defined")); } pub(super) fn report_possibly_unbound_attribute( @@ -1246,93 +1242,86 @@ pub(super) fn report_possibly_unbound_attribute( attribute: &str, object_ty: Type, ) { - context.report_lint_old( - &POSSIBLY_UNBOUND_ATTRIBUTE, - target, - format_args!( - "Attribute `{attribute}` on type `{}` is possibly unbound", - object_ty.display(context.db()), - ), - ); + let Some(builder) = context.report_lint(&POSSIBLY_UNBOUND_ATTRIBUTE, target) else { + return; + }; + builder.into_diagnostic(format_args!( + "Attribute `{attribute}` on type `{}` is possibly unbound", + object_ty.display(context.db()), + )); } pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node: &ast::ExprName) { - let ast::ExprName { id, .. } = expr_name_node; + let Some(builder) = context.report_lint(&UNRESOLVED_REFERENCE, expr_name_node) else { + return; + }; - context.report_lint_old( - &UNRESOLVED_REFERENCE, - expr_name_node, - format_args!("Name `{id}` used when not defined"), - ); + let ast::ExprName { id, .. } = expr_name_node; + builder.into_diagnostic(format_args!("Name `{id}` used when not defined")); } pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) { - context.report_lint_old( - &INVALID_EXCEPTION_CAUGHT, - node, - format_args!( - "Cannot catch object of type `{}` in an exception handler \ + let Some(builder) = context.report_lint(&INVALID_EXCEPTION_CAUGHT, node) else { + return; + }; + builder.into_diagnostic(format_args!( + "Cannot catch object of type `{}` in an exception handler \ (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)", - ty.display(context.db()) - ), - ); + ty.display(context.db()) + )); } pub(crate) fn report_invalid_exception_raised(context: &InferContext, node: &ast::Expr, ty: Type) { - context.report_lint_old( - &INVALID_RAISE, - node, - format_args!( - "Cannot raise object of type `{}` (must be a `BaseException` subclass or instance)", - ty.display(context.db()) - ), - ); + let Some(builder) = context.report_lint(&INVALID_RAISE, node) else { + return; + }; + builder.into_diagnostic(format_args!( + "Cannot raise object of type `{}` (must be a `BaseException` subclass or instance)", + ty.display(context.db()) + )); } pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) { - context.report_lint_old( - &INVALID_RAISE, - node, - format_args!( - "Cannot use object of type `{}` as exception cause \ - (must be a `BaseException` subclass or instance or `None`)", - ty.display(context.db()) - ), - ); + let Some(builder) = context.report_lint(&INVALID_RAISE, node) else { + return; + }; + builder.into_diagnostic(format_args!( + "Cannot use object of type `{}` as exception cause \ + (must be a `BaseException` subclass or instance or `None`)", + ty.display(context.db()) + )); } pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node: &ast::Expr) { - context.report_lint_old( - &INCOMPATIBLE_SLOTS, - node, - format_args!("Class base has incompatible `__slots__`"), - ); + let Some(builder) = context.report_lint(&INCOMPATIBLE_SLOTS, node) else { + return; + }; + builder.into_diagnostic("Class base has incompatible `__slots__`"); } pub(crate) fn report_invalid_arguments_to_annotated( context: &InferContext, subscript: &ast::ExprSubscript, ) { - context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Special form `{}` expected at least 2 arguments (one type and at least one metadata element)", - KnownInstanceType::Annotated.repr(context.db()) - ), - ); + let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, subscript) else { + return; + }; + builder.into_diagnostic(format_args!( + "Special form `{}` expected at least 2 arguments \ + (one type and at least one metadata element)", + KnownInstanceType::Annotated.repr(context.db()) + )); } pub(crate) fn report_invalid_arguments_to_callable( context: &InferContext, subscript: &ast::ExprSubscript, ) { - context.report_lint_old( - &INVALID_TYPE_FORM, - subscript, - format_args!( - "Special form `{}` expected exactly two arguments (parameter types and return type)", - KnownInstanceType::Callable.repr(context.db()) - ), - ); + let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, subscript) else { + return; + }; + builder.into_diagnostic(format_args!( + "Special form `{}` expected exactly two arguments (parameter types and return type)", + KnownInstanceType::Callable.repr(context.db()) + )); } From 17f799424af74b9ec37834641bf8dda93ef34376 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 22 Apr 2025 10:50:00 -0400 Subject: [PATCH 0065/1161] red_knot_python_semantic: migrate `types` to new diagnostics --- ..._For_loops_-_Bad_`__getitem__`_method.snap | 4 +- ...for.md_-_For_loops_-_Invalid_iterable.snap | 4 +- ...New_over_old_style_iteration_protocol.snap | 4 +- ...hod_and_`__getitem__`_is_not_callable.snap | 4 +- ...bly-not-callable_`__getitem__`_method.snap | 8 +- ...ossibly_invalid_`__getitem__`_methods.snap | 8 +- ...-_Possibly_invalid_`__iter__`_methods.snap | 8 +- ..._-_Possibly_invalid_`__next__`_method.snap | 8 +- ..._iter__`_and_bad_`__getitem__`_method.snap | 4 +- ..._`_and_possibly_invalid_`__getitem__`.snap | 8 +- ..._`_and_possibly_unbound_`__getitem__`.snap | 4 +- ...element_has_invalid_`__iter__`_method.snap | 4 +- ...nion_element_has_no_`__iter__`_method.snap | 4 +- ...or_loops_-_With_non-callable_iterator.snap | 4 +- ...__iter__`_does_not_return_an_iterator.snap | 4 +- ...__iter__`_method_with_a_bad_signature.snap | 4 +- ...tor_with_an_invalid_`__next__`_method.snap | 8 +- ...types_with_invalid_`__bool__`_methods.snap | 4 +- ...oesn't_implement_`__bool__`_correctly.snap | 8 +- ...hat_implements_`__bool__`_incorrectly.snap | 4 +- ..._don't_implement_`__bool__`_correctly.snap | 8 +- ...that_incorrectly_implement_`__bool__`.snap | 4 +- ...that_incorrectly_implement_`__bool__`.snap | 4 +- ...acking_-_Right_hand_side_not_iterable.snap | 4 +- crates/red_knot_python_semantic/src/types.rs | 167 +++++++++--------- 25 files changed, 144 insertions(+), 151 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap index b80fd4586d5664..7c5331746e0d16 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap @@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) --> /src/mdtest_snippet.py:10:10 | 9 | # error: [not-iterable] 10 | for x in Iterable(): - | ^^^^^^^^^^ Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) + | ^^^^^^^^^^ 11 | reveal_type(x) # revealed: int | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap index a8600a15477514..29541bbdb2e429 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap @@ -20,12 +20,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method --> /src/mdtest_snippet.py:2:10 | 1 | nonsense = 123 2 | for x in nonsense: # error: [not-iterable] - | ^^^^^^^^ Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method + | ^^^^^^^^ 3 | pass | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap index 59cf2147b83927..9a6a7075309489 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap @@ -24,13 +24,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable --> /src/mdtest_snippet.py:6:10 | 4 | __iter__: None = None 5 | 6 | for x in NotIterable(): # error: [not-iterable] - | ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable + | ^^^^^^^^^^^^^ 7 | pass | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap index a9d5964a6b1df2..69aad44e938b56 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap @@ -25,12 +25,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable --> /src/mdtest_snippet.py:7:10 | 6 | # error: [not-iterable] 7 | for x in Bad(): - | ^^^^^ Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable + | ^^^^^ 8 | reveal_type(x) # revealed: Unknown | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap index 62695a6d1ca81c..aef7309c4d5496 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap @@ -46,12 +46,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable --> /src/mdtest_snippet.py:22:14 | 21 | # error: [not-iterable] 22 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable + | ^^^^^^^^^^^ 23 | # TODO... `int` might be ideal here? 24 | reveal_type(x) # revealed: int | Unknown | @@ -73,12 +73,12 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable --> /src/mdtest_snippet.py:27:14 | 26 | # error: [not-iterable] 27 | for y in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable + | ^^^^^^^^^^^ 28 | # TODO... `int` might be ideal here? 29 | reveal_type(y) # revealed: int | Unknown | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap index 1a0330059d18ce..173adfdc413cb8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap @@ -43,12 +43,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable --> /src/mdtest_snippet.py:20:14 | 19 | # error: [not-iterable] 20 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable + | ^^^^^^^^^^^ 21 | # TODO: `str` might be better 22 | reveal_type(x) # revealed: str | Unknown | @@ -70,12 +70,12 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) --> /src/mdtest_snippet.py:25:14 | 24 | # error: [not-iterable] 25 | for y in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) + | ^^^^^^^^^^^ 26 | reveal_type(y) # revealed: str | int | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap index d59db051e7f349..67b34560f8d337 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap @@ -47,12 +47,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`) --> /src/mdtest_snippet.py:17:14 | 16 | # error: [not-iterable] 17 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`) + | ^^^^^^^^^^^ 18 | reveal_type(x) # revealed: int | @@ -73,12 +73,12 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable --> /src/mdtest_snippet.py:28:14 | 27 | # error: [not-iterable] 28 | for x in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable + | ^^^^^^^^^^^ 29 | # TODO: `int` would probably be better here: 30 | reveal_type(x) # revealed: int | Unknown | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap index e4957e08fd0ed7..7e56bce301f480 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap @@ -51,12 +51,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`) --> /src/mdtest_snippet.py:28:14 | 27 | # error: [not-iterable] 28 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`) + | ^^^^^^^^^^^ 29 | reveal_type(x) # revealed: int | str | @@ -77,12 +77,12 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable --> /src/mdtest_snippet.py:32:14 | 31 | # error: [not-iterable] 32 | for y in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable + | ^^^^^^^^^^^ 33 | # TODO: `int` would probably be better here: 34 | reveal_type(y) # revealed: int | Unknown | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap index dfcb11a238d26e..4aa08e1173eda5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap @@ -36,12 +36,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) --> /src/mdtest_snippet.py:18:14 | 17 | # error: [not-iterable] 18 | for x in Iterable(): - | ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) + | ^^^^^^^^^^ 19 | reveal_type(x) # revealed: int | bytes | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap index 90a743e3008e8e..0bb17a5837b200 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap @@ -54,12 +54,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable --> /src/mdtest_snippet.py:31:14 | 30 | # error: [not-iterable] 31 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable + | ^^^^^^^^^^^ 32 | # TODO: `bytes | str` might be better 33 | reveal_type(x) # revealed: bytes | str | Unknown | @@ -81,12 +81,12 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) --> /src/mdtest_snippet.py:36:14 | 35 | # error: [not-iterable] 36 | for y in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) + | ^^^^^^^^^^^ 37 | reveal_type(y) # revealed: bytes | str | int | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap index dacd22e758d5a2..d55e7203a2a935 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap @@ -35,12 +35,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method --> /src/mdtest_snippet.py:17:14 | 16 | # error: [not-iterable] 17 | for x in Iterable(): - | ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method + | ^^^^^^^^^^ 18 | reveal_type(x) # revealed: int | bytes | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap index f5f70d0c134f59..1e708f0e989524 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap @@ -36,13 +36,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method --> /src/mdtest_snippet.py:18:14 | 16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989) 17 | # error: [not-iterable] 18 | for x in Test() if flag else Test2(): - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 19 | reveal_type(x) # revealed: int | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap index c474ec522ccab6..62fb5bc8e8edf7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap @@ -31,13 +31,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method --> /src/mdtest_snippet.py:13:14 | 11 | def _(flag: bool): 12 | # error: [not-iterable] 13 | for x in Test() if flag else 42: - | ^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method + | ^^^^^^^^^^^^^^^^^^^^^^ 14 | reveal_type(x) # revealed: int | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap index f85c26b8231fc5..bd2bfcd156f256 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap @@ -33,12 +33,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable --> /src/mdtest_snippet.py:11:14 | 10 | # error: [not-iterable] 11 | for x in NotIterable(): - | ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable + | ^^^^^^^^^^^^^ 12 | pass | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap index cf9109de802d6a..5f66eb75e7daba 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap @@ -26,12 +26,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method --> /src/mdtest_snippet.py:8:10 | 7 | # error: [not-iterable] 8 | for x in Bad(): - | ^^^^^ Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method + | ^^^^^ 9 | reveal_type(x) # revealed: Unknown | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap index 65331fe1971d04..62bcf1e730c481 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap @@ -30,12 +30,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`) --> /src/mdtest_snippet.py:12:10 | 11 | # error: [not-iterable] 12 | for x in Iterable(): - | ^^^^^^^^^^ Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`) + | ^^^^^^^^^^ 13 | reveal_type(x) # revealed: int | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap index 5807ade696e661..1d4e8e2eeda1e0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap @@ -41,12 +41,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`) --> /src/mdtest_snippet.py:19:10 | 18 | # error: [not-iterable] 19 | for x in Iterable1(): - | ^^^^^^^^^^^ Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`) + | ^^^^^^^^^^^ 20 | reveal_type(x) # revealed: int | @@ -67,12 +67,12 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable --> /src/mdtest_snippet.py:23:10 | 22 | # error: [not-iterable] 23 | for y in Iterable2(): - | ^^^^^^^^^^^ Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable + | ^^^^^^^^^^^ 24 | reveal_type(y) # revealed: Unknown | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap index cebcc8765538fb..df0e16c766752c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap @@ -24,12 +24,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.m # Diagnostics ``` -error: lint:unsupported-bool-conversion +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable --> /src/mdtest_snippet.py:7:8 | 6 | # error: [unsupported-bool-conversion] 7 | 10 and a and True - | ^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable + | ^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap index c811afea2fa0df..4412f143f79efc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap @@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc # Diagnostics ``` -error: lint:unsupported-bool-conversion +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable --> /src/mdtest_snippet.py:9:1 | 8 | # error: [unsupported-bool-conversion] 9 | 10 in WithContains() - | ^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable + | ^^^^^^^^^^^^^^^^^^^^ 10 | # error: [unsupported-bool-conversion] 11 | 10 not in WithContains() | @@ -41,13 +41,13 @@ error: lint:unsupported-bool-conversion ``` ``` -error: lint:unsupported-bool-conversion +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable --> /src/mdtest_snippet.py:11:1 | 9 | 10 in WithContains() 10 | # error: [unsupported-bool-conversion] 11 | 10 not in WithContains() - | ^^^^^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable + | ^^^^^^^^^^^^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap index 3482463acd311c..b02543d3a506ca 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap @@ -22,12 +22,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md # Diagnostics ``` -error: lint:unsupported-bool-conversion +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable --> /src/mdtest_snippet.py:5:1 | 4 | # error: [unsupported-bool-conversion] 5 | not NotBoolable() - | ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable + | ^^^^^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap index c0004ad58d0f09..ff2b61237cb3ed 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap @@ -33,12 +33,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc # Diagnostics ``` -error: lint:unsupported-bool-conversion +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable --> /src/mdtest_snippet.py:12:1 | 11 | # error: [unsupported-bool-conversion] 12 | 10 < Comparable() < 20 - | ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable + | ^^^^^^^^^^^^^^^^^ 13 | # error: [unsupported-bool-conversion] 14 | 10 < Comparable() < Comparable() | @@ -46,13 +46,13 @@ error: lint:unsupported-bool-conversion ``` ``` -error: lint:unsupported-bool-conversion +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable --> /src/mdtest_snippet.py:14:1 | 12 | 10 < Comparable() < 20 13 | # error: [unsupported-bool-conversion] 14 | 10 < Comparable() < Comparable() - | ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable + | ^^^^^^^^^^^^^^^^^ 15 | 16 | Comparable() < Comparable() # fine | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap index b741702c188372..f7ad96efef5d32 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap @@ -34,12 +34,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples. # Diagnostics ``` -error: lint:unsupported-bool-conversion +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable --> /src/mdtest_snippet.py:15:1 | 14 | # error: [unsupported-bool-conversion] 15 | a < b < b - | ^^^^^ Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable + | ^^^^^ 16 | 17 | a < b # fine | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap index 1580c863e80995..99de8491fc0787 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap @@ -26,12 +26,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples. # Diagnostics ``` -error: lint:unsupported-bool-conversion +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable --> /src/mdtest_snippet.py:9:1 | 8 | # error: [unsupported-bool-conversion] 9 | (A(),) == (A(),) - | ^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable + | ^^^^^^^^^^^^^^^^ | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap index 6f2eb7f7399375..026dd73c54b3e1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap @@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack # Diagnostics ``` -error: lint:not-iterable +error: lint:not-iterable: Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method --> /src/mdtest_snippet.py:1:8 | 1 | a, b = 1 # error: [not-iterable] - | ^ Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method + | ^ | ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 06b189a2d4ebbb..87f471526979eb 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -5029,11 +5029,10 @@ impl<'db> InvalidTypeExpressionError<'db> { } = self; if is_reachable { for error in invalid_expressions { - context.report_lint_old( - &INVALID_TYPE_FORM, - node, - format_args!("{}", error.reason(context.db())), - ); + let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, node) else { + continue; + }; + builder.into_diagnostic(error.reason(context.db())); } } fallback_type @@ -5218,6 +5217,11 @@ impl<'db> ContextManagerError<'db> { context_expression_type: Type<'db>, context_expression_node: ast::AnyNodeRef, ) { + let Some(builder) = context.report_lint(&INVALID_CONTEXT_MANAGER, context_expression_node) + else { + return; + }; + let format_call_dunder_error = |call_dunder_error: &CallDunderError<'db>, name: &str| { match call_dunder_error { CallDunderError::MethodNotAvailable => format!("it does not implement `{name}`"), @@ -5269,9 +5273,7 @@ impl<'db> ContextManagerError<'db> { } => format_call_dunder_errors(enter_error, "__enter__", exit_error, "__exit__"), }; - context.report_lint_old( - &INVALID_CONTEXT_MANAGER, - context_expression_node, + builder.into_diagnostic( format_args!( "Object of type `{context_expression}` cannot be used with `with` because {formatted_errors}", context_expression = context_expression_type.display(db) @@ -5369,10 +5371,13 @@ impl<'db> IterationError<'db> { iterable_type: Type<'db>, iterable_node: ast::AnyNodeRef, ) { + let Some(builder) = context.report_lint(&NOT_ITERABLE, iterable_node) else { + return; + }; let db = context.db(); let report_not_iterable = |arguments: std::fmt::Arguments| { - context.report_lint_old(&NOT_ITERABLE, iterable_node, arguments); + builder.into_diagnostic(arguments); }; // TODO: for all of these error variants, the "explanation" for the diagnostic @@ -5646,13 +5651,14 @@ impl<'db> BoolError<'db> { } fn report_diagnostic_impl(&self, context: &InferContext, condition: TextRange) { + let Some(builder) = context.report_lint(&UNSUPPORTED_BOOL_CONVERSION, condition) else { + return; + }; match self { Self::IncorrectArguments { not_boolable_type, .. } => { - context.report_lint_old( - &UNSUPPORTED_BOOL_CONVERSION, - condition, + builder.into_diagnostic( format_args!( "Boolean conversion is unsupported for type `{}`; it incorrectly implements `__bool__`", not_boolable_type.display(context.db()) @@ -5663,25 +5669,20 @@ impl<'db> BoolError<'db> { not_boolable_type, return_type, } => { - context.report_lint_old( - &UNSUPPORTED_BOOL_CONVERSION, - condition, - format_args!( - "Boolean conversion is unsupported for type `{not_boolable}`; the return type of its bool method (`{return_type}`) isn't assignable to `bool", - not_boolable = not_boolable_type.display(context.db()), - return_type = return_type.display(context.db()) - ), - ); + builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{not_boolable}`; \ + the return type of its bool method (`{return_type}`) \ + isn't assignable to `bool", + not_boolable = not_boolable_type.display(context.db()), + return_type = return_type.display(context.db()) + )); } Self::NotCallable { not_boolable_type } => { - context.report_lint_old( - &UNSUPPORTED_BOOL_CONVERSION, - condition, - format_args!( - "Boolean conversion is unsupported for type `{}`; its `__bool__` method isn't callable", - not_boolable_type.display(context.db()) - ), - ); + builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{}`; \ + its `__bool__` method isn't callable", + not_boolable_type.display(context.db()) + )); } Self::Union { union, .. } => { let first_error = union @@ -5690,26 +5691,20 @@ impl<'db> BoolError<'db> { .find_map(|element| element.try_bool(context.db()).err()) .unwrap(); - context.report_lint_old( - &UNSUPPORTED_BOOL_CONVERSION, - condition, - format_args!( - "Boolean conversion is unsupported for union `{}` because `{}` doesn't implement `__bool__` correctly", - Type::Union(*union).display(context.db()), - first_error.not_boolable_type().display(context.db()), - ), - ); + builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for union `{}` \ + because `{}` doesn't implement `__bool__` correctly", + Type::Union(*union).display(context.db()), + first_error.not_boolable_type().display(context.db()), + )); } Self::Other { not_boolable_type } => { - context.report_lint_old( - &UNSUPPORTED_BOOL_CONVERSION, - condition, - format_args!( - "Boolean conversion is unsupported for type `{}`; it incorrectly implements `__bool__`", - not_boolable_type.display(context.db()) - ), - ); + builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{}`; \ + it incorrectly implements `__bool__`", + not_boolable_type.display(context.db()) + )); } } } @@ -5740,27 +5735,28 @@ impl<'db> ConstructorCallError<'db> { ) { let report_init_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error { CallDunderError::MethodNotAvailable => { - // If we are using vendored typeshed, it should be impossible to have missing - // or unbound `__init__` method on a class, as all classes have `object` in MRO. - // Thus the following may only trigger if a custom typeshed is used. - context.report_lint_old( - &CALL_POSSIBLY_UNBOUND_METHOD, - context_expression_node, - format_args!( - "`__init__` method is missing on type `{}`. Make sure your `object` in typeshed has its definition.", + if let Some(builder) = + context.report_lint(&CALL_POSSIBLY_UNBOUND_METHOD, context_expression_node) + { + // If we are using vendored typeshed, it should be impossible to have missing + // or unbound `__init__` method on a class, as all classes have `object` in MRO. + // Thus the following may only trigger if a custom typeshed is used. + builder.into_diagnostic(format_args!( + "`__init__` method is missing on type `{}`. \ + Make sure your `object` in typeshed has its definition.", context_expression_type.display(context.db()), - ), - ); + )); + } } CallDunderError::PossiblyUnbound(bindings) => { - context.report_lint_old( - &CALL_POSSIBLY_UNBOUND_METHOD, - context_expression_node, - format_args!( + if let Some(builder) = + context.report_lint(&CALL_POSSIBLY_UNBOUND_METHOD, context_expression_node) + { + builder.into_diagnostic(format_args!( "Method `__init__` on type `{}` is possibly unbound.", context_expression_type.display(context.db()), - ), - ); + )); + } bindings.report_diagnostics(context, context_expression_node); } @@ -5776,14 +5772,14 @@ impl<'db> ConstructorCallError<'db> { unreachable!("`__new__` method may not be called if missing"); } CallDunderError::PossiblyUnbound(bindings) => { - context.report_lint_old( - &CALL_POSSIBLY_UNBOUND_METHOD, - context_expression_node, - format_args!( + if let Some(builder) = + context.report_lint(&CALL_POSSIBLY_UNBOUND_METHOD, context_expression_node) + { + builder.into_diagnostic(format_args!( "Method `__new__` on type `{}` is possibly unbound.", context_expression_type.display(context.db()), - ), - ); + )); + } bindings.report_diagnostics(context, context_expression_node); } @@ -7130,34 +7126,31 @@ impl BoundSuperError<'_> { pub(super) fn report_diagnostic(&self, context: &InferContext, node: AnyNodeRef) { match self { BoundSuperError::InvalidPivotClassType { pivot_class } => { - context.report_lint_old( - &INVALID_SUPER_ARGUMENT, - node, - format_args!( + if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + builder.into_diagnostic(format_args!( "`{pivot_class}` is not a valid class", pivot_class = pivot_class.display(context.db()), - ), - ); + )); + } } BoundSuperError::FailingConditionCheck { pivot_class, owner } => { - context.report_lint_old( - &INVALID_SUPER_ARGUMENT, - node, - format_args!( - "`{owner}` is not an instance or subclass of `{pivot_class}` in `super({pivot_class}, {owner})` call", + if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + builder.into_diagnostic(format_args!( + "`{owner}` is not an instance or subclass of \ + `{pivot_class}` in `super({pivot_class}, {owner})` call", pivot_class = pivot_class.display(context.db()), owner = owner.display(context.db()), - ), - ); + )); + } } BoundSuperError::UnavailableImplicitArguments => { - context.report_lint_old( - &UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, - node, - format_args!( + if let Some(builder) = + context.report_lint(&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, node) + { + builder.into_diagnostic(format_args!( "Cannot determine implicit arguments for 'super()' in this context", - ), - ); + )); + } } } } From 810478f68bb969a75406cb5d8a09722ed9b99ba2 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 22 Apr 2025 10:51:42 -0400 Subject: [PATCH 0066/1161] red_knot_python_semantic: remove last vestige of old diagnostics! --- .../src/types/context.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/context.rs b/crates/red_knot_python_semantic/src/types/context.rs index 4a6c24186c53c0..36e1e919f59d8b 100644 --- a/crates/red_knot_python_semantic/src/types/context.rs +++ b/crates/red_knot_python_semantic/src/types/context.rs @@ -81,22 +81,6 @@ impl<'db> InferContext<'db> { self.diagnostics.get_mut().extend(other); } - /// Reports a lint located at `ranged`. - pub(super) fn report_lint_old( - &self, - lint: &'static LintMetadata, - ranged: T, - message: fmt::Arguments, - ) where - T: Ranged, - { - let Some(builder) = self.report_lint(lint, ranged) else { - return; - }; - let mut diag = builder.into_diagnostic(""); - diag.set_primary_message(message); - } - /// Optionally return a builder for a lint diagnostic guard. /// /// If the current context believes a diagnostic should be reported for From 27ada26ddb7ff784bbd7cee8ba65eeaa9fe85603 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 22 Apr 2025 09:12:52 -0700 Subject: [PATCH 0067/1161] [red-knot] fix unions of literals, again (#17534) ## Summary #17451 was incomplete. `AlwaysFalsy` and `AlwaysTruthy` are not the only two types that are super-types of some literals (of a given kind) and not others. That set also includes intersections containing `AlwaysTruthy` or `AlwaysFalsy`, and intersections containing literal types of the same kind. Cover these cases as well. Fixes #17478. ## Test Plan Added mdtests. `QUICKCHECK_TESTS=1000000 cargo test -p red_knot_python_semantic -- --ignored types::property_tests::stable` failed on both `all_fully_static_type_pairs_are_subtypes_of_their_union` and `all_type_pairs_are_assignable_to_their_union` prior to this PR, passes after it. --------- Co-authored-by: Alex Waygood --- .../resources/mdtest/call/union.md | 25 +++++++++++ .../src/types/builder.rs | 43 ++++++++++++++++--- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/union.md b/crates/red_knot_python_semantic/resources/mdtest/call/union.md index 88d6ad9c378b0b..10055283a83667 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/union.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/union.md @@ -162,6 +162,31 @@ def _(flag: bool): reveal_type(f("string")) # revealed: Literal["string", "'string'"] ``` +## Unions with literals and negations + +```py +from typing import Literal, Union +from knot_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to + +static_assert(is_subtype_of(Literal["a", ""], Union[Literal["a", ""], Not[AlwaysFalsy]])) +static_assert(is_subtype_of(Not[AlwaysFalsy], Union[Literal["", "a"], Not[AlwaysFalsy]])) +static_assert(is_subtype_of(Literal["a", ""], Union[Not[AlwaysFalsy], Literal["a", ""]])) +static_assert(is_subtype_of(Not[AlwaysFalsy], Union[Not[AlwaysFalsy], Literal["a", ""]])) + +static_assert(is_subtype_of(Literal["a", ""], Union[Literal["a", ""], Not[Literal[""]]])) +static_assert(is_subtype_of(Not[Literal[""]], Union[Literal["a", ""], Not[Literal[""]]])) +static_assert(is_subtype_of(Literal["a", ""], Union[Not[Literal[""]], Literal["a", ""]])) +static_assert(is_subtype_of(Not[Literal[""]], Union[Not[Literal[""]], Literal["a", ""]])) + +def _( + x: Union[Literal["a", ""], Not[AlwaysFalsy]], + y: Union[Literal["a", ""], Not[Literal[""]]], +): + reveal_type(x) # revealed: Literal[""] | ~AlwaysFalsy + # TODO should be `object` + reveal_type(y) # revealed: Literal[""] | ~Literal[""] +``` + ## Cannot use an argument as both a value and a type form ```py diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 41d194652e3d80..6bfecfe2a92f62 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -44,6 +44,40 @@ use crate::types::{ use crate::{Db, FxOrderSet}; use smallvec::SmallVec; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum LiteralKind { + Int, + String, + Bytes, +} + +impl<'db> Type<'db> { + /// Return `true` if this type can be a supertype of some literals of `kind` and not others. + fn splits_literals(self, db: &'db dyn Db, kind: LiteralKind) -> bool { + match (self, kind) { + (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => true, + (Type::StringLiteral(_), LiteralKind::String) => true, + (Type::BytesLiteral(_), LiteralKind::Bytes) => true, + (Type::IntLiteral(_), LiteralKind::Int) => true, + (Type::Intersection(intersection), _) => { + intersection + .positive(db) + .iter() + .any(|ty| ty.splits_literals(db, kind)) + || intersection + .negative(db) + .iter() + .any(|ty| ty.splits_literals(db, kind)) + } + (Type::Union(union), _) => union + .elements(db) + .iter() + .any(|ty| ty.splits_literals(db, kind)), + _ => false, + } + } +} + enum UnionElement<'db> { IntLiterals(FxOrderSet), StringLiterals(FxOrderSet>), @@ -61,12 +95,9 @@ impl<'db> UnionElement<'db> { /// If this `UnionElement` is some other type, return `ReduceResult::Type` so `UnionBuilder` /// can perform more complex checks on it. fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> { - // `AlwaysTruthy` and `AlwaysFalsy` are the only types which can be a supertype of only - // _some_ literals of the same kind, so we need to walk the full set in this case. - let needs_filter = matches!(other_type, Type::AlwaysTruthy | Type::AlwaysFalsy); match self { UnionElement::IntLiterals(literals) => { - ReduceResult::KeepIf(if needs_filter { + ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::Int) { literals.retain(|literal| { !Type::IntLiteral(*literal).is_subtype_of(db, other_type) }); @@ -77,7 +108,7 @@ impl<'db> UnionElement<'db> { }) } UnionElement::StringLiterals(literals) => { - ReduceResult::KeepIf(if needs_filter { + ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::String) { literals.retain(|literal| { !Type::StringLiteral(*literal).is_subtype_of(db, other_type) }); @@ -88,7 +119,7 @@ impl<'db> UnionElement<'db> { }) } UnionElement::BytesLiterals(literals) => { - ReduceResult::KeepIf(if needs_filter { + ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::Bytes) { literals.retain(|literal| { !Type::BytesLiteral(*literal).is_subtype_of(db, other_type) }); From 3872d57463da8f683070c6bda1f4121af05359da Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 22 Apr 2025 09:20:53 -0700 Subject: [PATCH 0068/1161] [red-knot] add regression test for fixed cycle panic (#17535) Add a regression test for the cycle documented in https://github.com/astral-sh/ruff/issues/14767, which no longer panics (or even causes a cycle at all.) Fixes https://github.com/astral-sh/ruff/issues/14767 --- .../test/corpus/self_referential_function_annotation.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 crates/red_knot_project/resources/test/corpus/self_referential_function_annotation.py diff --git a/crates/red_knot_project/resources/test/corpus/self_referential_function_annotation.py b/crates/red_knot_project/resources/test/corpus/self_referential_function_annotation.py new file mode 100644 index 00000000000000..5bbb1beb115579 --- /dev/null +++ b/crates/red_knot_project/resources/test/corpus/self_referential_function_annotation.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +def foo(a: foo()): + pass From f9da115fdc8b89add6a846abacdbf94e72551328 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:23:08 -0400 Subject: [PATCH 0069/1161] [minor] Delete outdated TODO comment (#17565) Summary -- Delete a TODO I left that was handled in the last minor release (#16125). Test Plan -- N/a --- crates/ruff/tests/lint.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index 41b44d52923516..d9b18fcfb78a27 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -5175,8 +5175,8 @@ fn a005_module_shadowing_non_strict() -> Result<()> { } /// Test A005 with `strict-checking` unset -/// TODO(brent) This should currently match the strict version, but after the next minor -/// release it will match the non-strict version directly above +/// +/// This should match the non-strict version directly above #[test] fn a005_module_shadowing_strict_default() -> Result<()> { let tempdir = TempDir::new()?; From aa46047649b60ae04bc3578445697ffb182d17fe Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Wed, 23 Apr 2025 05:18:20 +0100 Subject: [PATCH 0070/1161] [red-knot] Surround intersections with `()` in potentially ambiguous contexts (#17568) ## Summary Add parentheses to multi-element intersections, when displayed in a context that's otherwise potentially ambiguous. ## Test Plan Update mdtest files --------- Co-authored-by: Carl Meyer --- .../resources/mdtest/comparison/integers.md | 2 +- .../mdtest/comparison/non_bool_returns.md | 2 +- .../resources/mdtest/expression/boolean.md | 8 +++--- .../resources/mdtest/intersection_types.md | 8 +++--- .../mdtest/narrow/conditionals/boolean.md | 16 +++++------ .../resources/mdtest/narrow/truthiness.md | 28 +++++++++---------- .../resources/mdtest/narrow/type.md | 2 +- crates/red_knot_python_semantic/src/types.rs | 4 +++ .../src/types/display.rs | 19 +++++++------ 9 files changed, 48 insertions(+), 41 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md index bf956e84132c7b..eeccd6a60a461b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md @@ -13,7 +13,7 @@ reveal_type(1 is not 1) # revealed: bool reveal_type(1 is 2) # revealed: Literal[False] reveal_type(1 is not 7) # revealed: Literal[True] # error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`" -reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True] +reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True] ``` ## Integer instance diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md index 6cf77e5f1e0af7..2da190aa5d6d57 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md @@ -37,7 +37,7 @@ class C: return self x = A() < B() < C() -reveal_type(x) # revealed: A & ~AlwaysTruthy | B +reveal_type(x) # revealed: (A & ~AlwaysTruthy) | B y = 0 < 1 < A() < 3 reveal_type(y) # revealed: Literal[False] | A diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md index ce3363636d7403..160189ef0c2aa8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md @@ -10,8 +10,8 @@ def _(foo: str): reveal_type(False or "z") # revealed: Literal["z"] reveal_type(False or True) # revealed: Literal[True] reveal_type(False or False) # revealed: Literal[False] - reveal_type(foo or False) # revealed: str & ~AlwaysFalsy | Literal[False] - reveal_type(foo or True) # revealed: str & ~AlwaysFalsy | Literal[True] + reveal_type(foo or False) # revealed: (str & ~AlwaysFalsy) | Literal[False] + reveal_type(foo or True) # revealed: (str & ~AlwaysFalsy) | Literal[True] ``` ## AND @@ -20,8 +20,8 @@ def _(foo: str): def _(foo: str): reveal_type(True and False) # revealed: Literal[False] reveal_type(False and True) # revealed: Literal[False] - reveal_type(foo and False) # revealed: str & ~AlwaysTruthy | Literal[False] - reveal_type(foo and True) # revealed: str & ~AlwaysTruthy | Literal[True] + reveal_type(foo and False) # revealed: (str & ~AlwaysTruthy) | Literal[False] + reveal_type(foo and True) # revealed: (str & ~AlwaysTruthy) | Literal[True] reveal_type("x" and "y" and "z") # revealed: Literal["z"] reveal_type("x" and "y" and "") # revealed: Literal[""] reveal_type("" and "y") # revealed: Literal[""] diff --git a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md b/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md index 51bf653ef6b3bb..d2f409036c58b3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md @@ -191,9 +191,9 @@ def _( i2: Intersection[P | Q | R, S], i3: Intersection[P | Q, R | S], ) -> None: - reveal_type(i1) # revealed: P & Q | P & R | P & S - reveal_type(i2) # revealed: P & S | Q & S | R & S - reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S + reveal_type(i1) # revealed: (P & Q) | (P & R) | (P & S) + reveal_type(i2) # revealed: (P & S) | (Q & S) | (R & S) + reveal_type(i3) # revealed: (P & R) | (Q & R) | (P & S) | (Q & S) def simplifications_for_same_elements( i1: Intersection[P, Q | P], @@ -216,7 +216,7 @@ def simplifications_for_same_elements( # = P & Q | P & R | Q | Q & R # = Q | P & R # (again, because Q is a supertype of P & Q and of Q & R) - reveal_type(i3) # revealed: Q | P & R + reveal_type(i3) # revealed: Q | (P & R) # (P | Q) & (P | Q) # = P & P | P & Q | Q & P | Q & Q diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md index 566ec10a78de34..8b9394a380522f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md @@ -10,7 +10,7 @@ def _(x: A | B): if isinstance(x, A) and isinstance(x, B): reveal_type(x) # revealed: A & B else: - reveal_type(x) # revealed: B & ~A | A & ~B + reveal_type(x) # revealed: (B & ~A) | (A & ~B) ``` ## Arms might not add narrowing constraints @@ -131,8 +131,8 @@ def _(x: A | B | C, y: A | B | C): # The same for `y` reveal_type(y) # revealed: A | B | C else: - reveal_type(x) # revealed: B & ~A | C & ~A - reveal_type(y) # revealed: B & ~A | C & ~A + reveal_type(x) # revealed: (B & ~A) | (C & ~A) + reveal_type(y) # revealed: (B & ~A) | (C & ~A) if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)): # Here, types of `x` and `y` can be narrowd since all `or` arms constraint them. @@ -155,7 +155,7 @@ def _(x: A | B | C): reveal_type(x) # revealed: B & ~C else: # ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C - reveal_type(x) # revealed: A & ~B | C + reveal_type(x) # revealed: (A & ~B) | C ``` ## mixing `or` and `not` @@ -167,7 +167,7 @@ class C: ... def _(x: A | B | C): if isinstance(x, B) or not isinstance(x, C): - reveal_type(x) # revealed: B | A & ~C + reveal_type(x) # revealed: B | (A & ~C) else: reveal_type(x) # revealed: C & ~B ``` @@ -181,7 +181,7 @@ class C: ... def _(x: A | B | C): if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)): - reveal_type(x) # revealed: A | B & ~C + reveal_type(x) # revealed: A | (B & ~C) else: # ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B) reveal_type(x) # revealed: C & ~A @@ -197,7 +197,7 @@ class C: ... def _(x: A | B | C): if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)): # A & (B | ~C) -> (A & B) | (A & ~C) - reveal_type(x) # revealed: A & B | A & ~C + reveal_type(x) # revealed: (A & B) | (A & ~C) else: # ~((A & B) | (A & ~C)) -> # ~(A & B) & ~(A & ~C) -> @@ -206,7 +206,7 @@ def _(x: A | B | C): # ~A | (~A & C) | (~B & C) -> # ~A | (C & ~B) -> # ~A | (C & ~B) The positive side of ~A is A | B | C -> - reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B + reveal_type(x) # revealed: (B & ~A) | (C & ~A) | (C & ~B) ``` ## Boolean expression internal narrowing diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md index f8f5ab8c5e686e..d9bf54e8f8b1f0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md @@ -82,19 +82,19 @@ class B: ... def f(x: A | B): if x: - reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy + reveal_type(x) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy) else: - reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy + reveal_type(x) # revealed: (A & ~AlwaysTruthy) | (B & ~AlwaysTruthy) if x and not x: - reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy + reveal_type(x) # revealed: (A & ~AlwaysFalsy & ~AlwaysTruthy) | (B & ~AlwaysFalsy & ~AlwaysTruthy) else: reveal_type(x) # revealed: A | B if x or not x: reveal_type(x) # revealed: A | B else: - reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy + reveal_type(x) # revealed: (A & ~AlwaysTruthy & ~AlwaysFalsy) | (B & ~AlwaysTruthy & ~AlwaysFalsy) ``` ### Truthiness of Types @@ -111,9 +111,9 @@ x = int if flag() else str reveal_type(x) # revealed: Literal[int, str] if x: - reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy + reveal_type(x) # revealed: (Literal[int] & ~AlwaysFalsy) | (Literal[str] & ~AlwaysFalsy) else: - reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy + reveal_type(x) # revealed: (Literal[int] & ~AlwaysTruthy) | (Literal[str] & ~AlwaysTruthy) ``` ## Determined Truthiness @@ -176,12 +176,12 @@ if isinstance(x, str) and not isinstance(x, B): z = x if flag() else y - reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"] + reveal_type(z) # revealed: (A & str & ~B) | Literal[0, 42, "", "hello"] if z: - reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"] + reveal_type(z) # revealed: (A & str & ~B & ~AlwaysFalsy) | Literal[42, "hello"] else: - reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""] + reveal_type(z) # revealed: (A & str & ~B & ~AlwaysTruthy) | Literal[0, ""] ``` ## Narrowing Multiple Variables @@ -264,7 +264,7 @@ def _( ): reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] if ta: - reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] & ~AlwaysFalsy + reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy) reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass] if af: @@ -296,12 +296,12 @@ def _(x: Literal[0, 1]): reveal_type(x and A()) # revealed: Literal[0] | A def _(x: str): - reveal_type(x or A()) # revealed: str & ~AlwaysFalsy | A - reveal_type(x and A()) # revealed: str & ~AlwaysTruthy | A + reveal_type(x or A()) # revealed: (str & ~AlwaysFalsy) | A + reveal_type(x and A()) # revealed: (str & ~AlwaysTruthy) | A def _(x: bool | str): - reveal_type(x or A()) # revealed: Literal[True] | str & ~AlwaysFalsy | A - reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A + reveal_type(x or A()) # revealed: Literal[True] | (str & ~AlwaysFalsy) | A + reveal_type(x and A()) # revealed: Literal[False] | (str & ~AlwaysTruthy) | A class Falsy: def __bool__(self) -> Literal[False]: 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 602265039db275..07cabd76d597c7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md @@ -127,7 +127,7 @@ class B: ... def _[T](x: A | B): if type(x) is A[str]: - reveal_type(x) # revealed: A[int] & A[Unknown] | B & A[Unknown] + reveal_type(x) # revealed: (A[int] & A[Unknown]) | (B & A[Unknown]) else: reveal_type(x) # revealed: A[int] | B ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 87f471526979eb..f65054c182ac1f 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -6993,6 +6993,10 @@ impl<'db> IntersectionType<'db> { pub fn iter_positive(&self, db: &'db dyn Db) -> impl Iterator> { self.positive(db).iter().copied() } + + pub fn has_one_element(&self, db: &'db dyn Db) -> bool { + (self.positive(db).len() + self.negative(db).len()) == 1 + } } #[salsa::interned(debug)] diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 5687b6c35c0081..70e8cd99cf42ca 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -679,14 +679,17 @@ struct DisplayMaybeParenthesizedType<'db> { impl Display for DisplayMaybeParenthesizedType<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if let Type::Callable(_) - | Type::MethodWrapper(_) - | Type::FunctionLiteral(_) - | Type::BoundMethod(_) = self.ty - { - write!(f, "({})", self.ty.display(self.db)) - } else { - self.ty.display(self.db).fmt(f) + let write_parentheses = |f: &mut Formatter<'_>| write!(f, "({})", self.ty.display(self.db)); + match self.ty { + Type::Callable(_) + | Type::MethodWrapper(_) + | Type::FunctionLiteral(_) + | Type::BoundMethod(_) + | Type::Union(_) => write_parentheses(f), + Type::Intersection(intersection) if !intersection.has_one_element(self.db) => { + write_parentheses(f) + } + _ => self.ty.display(self.db).fmt(f), } } } From e45f23b0ec1efd093c6588f9b7d56bf87c4eebae Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Wed, 23 Apr 2025 06:40:33 +0100 Subject: [PATCH 0071/1161] [red-knot] Class literal `__new__` function callable subtyping (#17533) ## Summary From https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable this covers step 2 and partially step 3 (always respecting the `__new__`) ## Test Plan Update is_subtype_of.md --------- Co-authored-by: Carl Meyer --- .../mdtest/type_properties/is_subtype_of.md | 50 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 31 +++++------- .../src/types/class.rs | 37 ++++++++++++++ 3 files changed, 99 insertions(+), 19 deletions(-) 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 6eb506e602ea05..413edf54b12d21 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 @@ -1166,6 +1166,56 @@ static_assert(is_subtype_of(TypeOf[C], Callable[[int], int])) static_assert(is_subtype_of(TypeOf[C], Callable[[], str])) ``` +#### Classes with `__new__` + +```py +from typing import Callable +from knot_extensions import TypeOf, static_assert, is_subtype_of + +class A: + def __new__(cls, a: int) -> int: + return a + +static_assert(is_subtype_of(TypeOf[A], Callable[[int], int])) +static_assert(not is_subtype_of(TypeOf[A], Callable[[], int])) + +class B: ... +class C(B): ... + +class D: + def __new__(cls) -> B: + return B() + +class E(D): + def __new__(cls) -> C: + return C() + +static_assert(is_subtype_of(TypeOf[E], Callable[[], C])) +static_assert(is_subtype_of(TypeOf[E], Callable[[], B])) +static_assert(not is_subtype_of(TypeOf[D], Callable[[], C])) +static_assert(is_subtype_of(TypeOf[D], Callable[[], B])) +``` + +#### Classes with `__call__` and `__new__` + +If `__call__` and `__new__` are both present, `__call__` takes precedence. + +```py +from typing import Callable +from knot_extensions import TypeOf, static_assert, is_subtype_of + +class MetaWithIntReturn(type): + def __call__(cls) -> int: + return super().__call__() + +class F(metaclass=MetaWithIntReturn): + def __new__(cls) -> str: + return super().__new__(cls) + +static_assert(is_subtype_of(TypeOf[F], Callable[[], int])) +static_assert(not is_subtype_of(TypeOf[F], Callable[[], str])) +``` + ### Bound methods ```py diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f65054c182ac1f..ec35873e320576 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1175,25 +1175,9 @@ impl<'db> Type<'db> { self_subclass_ty.is_subtype_of(db, target_subclass_ty) } - (Type::ClassLiteral(_), Type::Callable(_)) => { - let metaclass_call_function_symbol = self - .member_lookup_with_policy( - db, - "__call__".into(), - MemberLookupPolicy::NO_INSTANCE_FALLBACK - | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, - ) - .symbol; - - if let Symbol::Type(Type::BoundMethod(metaclass_call_function), _) = - metaclass_call_function_symbol - { - // TODO: this intentionally diverges from step 1 in - // https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable - // by always respecting the signature of the metaclass `__call__`, rather than - // using a heuristic which makes unwarranted assumptions to sometimes ignore it. - let metaclass_call_function = metaclass_call_function.into_callable_type(db); - return metaclass_call_function.is_subtype_of(db, target); + (Type::ClassLiteral(class_literal), Type::Callable(_)) => { + if let Some(callable) = class_literal.into_callable(db) { + return callable.is_subtype_of(db, target); } false } @@ -5961,6 +5945,15 @@ impl<'db> FunctionType<'db> { )) } + /// Convert the `FunctionType` into a [`Type::BoundMethod`]. + pub(crate) fn into_bound_method_type( + self, + db: &'db dyn Db, + self_instance: Type<'db>, + ) -> Type<'db> { + Type::BoundMethod(BoundMethodType::new(db, self, self_instance)) + } + /// Returns the [`FileRange`] of the function's name. pub fn focus_range(self, db: &dyn Db) -> FileRange { FileRange::new( diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index e90ef080d49bc1..8eaa0c7988e96e 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -818,6 +818,43 @@ impl<'db> ClassLiteralType<'db> { )) } + pub(super) fn into_callable(self, db: &'db dyn Db) -> Option> { + let self_ty = Type::from(self); + let metaclass_call_function_symbol = self_ty + .member_lookup_with_policy( + db, + "__call__".into(), + MemberLookupPolicy::NO_INSTANCE_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ) + .symbol; + + if let Symbol::Type(Type::BoundMethod(metaclass_call_function), _) = + metaclass_call_function_symbol + { + // TODO: this intentionally diverges from step 1 in + // https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable + // by always respecting the signature of the metaclass `__call__`, rather than + // using a heuristic which makes unwarranted assumptions to sometimes ignore it. + return Some(metaclass_call_function.into_callable_type(db)); + } + + let new_function_symbol = self_ty + .member_lookup_with_policy( + db, + "__new__".into(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ) + .symbol; + + if let Symbol::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol { + return Some(new_function.into_bound_method_type(db, self.into())); + } + // TODO handle `__init__` also + None + } + /// Returns the class member of this class named `name`. /// /// The member resolves to a member on the class itself or any of its proper superclasses. From f36262d970a76e78d725faf0ed15d2152a72dde8 Mon Sep 17 00:00:00 2001 From: David Salvisberg Date: Wed, 23 Apr 2025 09:26:00 +0200 Subject: [PATCH 0072/1161] Fixes how the checker visits `typing.cast`/`typing.NewType` arguments (#17538) --- .../fixtures/flake8_type_checking/TC006.py | 8 +++ crates/ruff_linter/src/checkers/ast/mod.rs | 51 +++++++++++++------ ...g__tests__runtime-cast-value_TC006.py.snap | 34 +++++++++++++ 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py index cf10553c49199c..a321f2a30e6822 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py @@ -89,3 +89,11 @@ def f(): int # TC006 , 6.0 ) + + +def f(): + # Keyword arguments + from typing import cast + + cast(typ=int, val=3.0) # TC006 + cast(val=3.0, typ=int) # TC006 diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index ebff124383a042..10784403e49d56 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -1539,25 +1539,37 @@ impl<'a> Visitor<'a> for Checker<'a> { } } Some(typing::Callable::Cast) => { - let mut args = arguments.args.iter(); - if let Some(arg) = args.next() { - self.visit_type_definition(arg); - - if !self.source_type.is_stub() && self.enabled(Rule::RuntimeCastValue) { - flake8_type_checking::rules::runtime_cast_value(self, arg); + for (i, arg) in arguments.arguments_source_order().enumerate() { + match (i, arg) { + (0, ArgOrKeyword::Arg(arg)) => self.visit_cast_type_argument(arg), + (_, ArgOrKeyword::Arg(arg)) => self.visit_non_type_definition(arg), + (_, ArgOrKeyword::Keyword(Keyword { arg, value, .. })) => { + if let Some(id) = arg { + if id == "typ" { + self.visit_cast_type_argument(value); + } else { + self.visit_non_type_definition(value); + } + } + } } } - for arg in args { - self.visit_expr(arg); - } } Some(typing::Callable::NewType) => { - let mut args = arguments.args.iter(); - if let Some(arg) = args.next() { - self.visit_non_type_definition(arg); - } - for arg in args { - self.visit_type_definition(arg); + for (i, arg) in arguments.arguments_source_order().enumerate() { + match (i, arg) { + (1, ArgOrKeyword::Arg(arg)) => self.visit_type_definition(arg), + (_, ArgOrKeyword::Arg(arg)) => self.visit_non_type_definition(arg), + (_, ArgOrKeyword::Keyword(Keyword { arg, value, .. })) => { + if let Some(id) = arg { + if id == "tp" { + self.visit_type_definition(value); + } else { + self.visit_non_type_definition(value); + } + } + } + } } } Some(typing::Callable::TypeVar) => { @@ -2209,6 +2221,15 @@ impl<'a> Checker<'a> { self.semantic.flags = snapshot; } + /// Visit an [`Expr`], and treat it as the `typ` argument to `typing.cast`. + fn visit_cast_type_argument(&mut self, arg: &'a Expr) { + self.visit_type_definition(arg); + + if !self.source_type.is_stub() && self.enabled(Rule::RuntimeCastValue) { + flake8_type_checking::rules::runtime_cast_value(self, arg); + } + } + /// Visit an [`Expr`], and treat it as a boolean test. This is useful for detecting whether an /// expressions return value is significant, or whether the calling context only relies on /// its truthiness. diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006.py.snap index 25348b34ab633b..f117c364f7eb5c 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-cast-value_TC006.py.snap @@ -212,3 +212,37 @@ TC006.py:89:9: TC006 [*] Add quotes to type expression in `typing.cast()` 89 |+ "int" # TC006 90 90 | , 6.0 91 91 | ) +92 92 | + +TC006.py:98:14: TC006 [*] Add quotes to type expression in `typing.cast()` + | +96 | from typing import cast +97 | +98 | cast(typ=int, val=3.0) # TC006 + | ^^^ TC006 +99 | cast(val=3.0, typ=int) # TC006 + | + = help: Add quotes + +ℹ Safe fix +95 95 | # Keyword arguments +96 96 | from typing import cast +97 97 | +98 |- cast(typ=int, val=3.0) # TC006 + 98 |+ cast(typ="int", val=3.0) # TC006 +99 99 | cast(val=3.0, typ=int) # TC006 + +TC006.py:99:23: TC006 [*] Add quotes to type expression in `typing.cast()` + | +98 | cast(typ=int, val=3.0) # TC006 +99 | cast(val=3.0, typ=int) # TC006 + | ^^^ TC006 + | + = help: Add quotes + +ℹ Safe fix +96 96 | from typing import cast +97 97 | +98 98 | cast(typ=int, val=3.0) # TC006 +99 |- cast(val=3.0, typ=int) # TC006 + 99 |+ cast(val=3.0, typ="int") # TC006 From 3fae176345d426deaba437b1c8cb8e0e5d921596 Mon Sep 17 00:00:00 2001 From: Shaygan Hooshyari Date: Wed, 23 Apr 2025 09:27:00 +0200 Subject: [PATCH 0073/1161] Remove redundant `type_to_visitor_function` entries (#17564) --- crates/ruff_python_ast/generate.py | 72 ++++++++++++++---------------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/crates/ruff_python_ast/generate.py b/crates/ruff_python_ast/generate.py index 24e344908a9e06..f6afab68cc1747 100644 --- a/crates/ruff_python_ast/generate.py +++ b/crates/ruff_python_ast/generate.py @@ -40,6 +40,23 @@ } +@dataclass +class VisitorInfo: + name: str + accepts_sequence: bool = False + + +# Map of AST node types to their corresponding visitor information. +# Only visitors that are different from the default `visit_*` method are included. +# These visitors either have a different name or accept a sequence of items. +type_to_visitor_function: dict[str, VisitorInfo] = { + "TypeParams": VisitorInfo("visit_type_params", True), + "Parameters": VisitorInfo("visit_parameters", True), + "Stmt": VisitorInfo("visit_body", True), + "Arguments": VisitorInfo("visit_arguments", True), +} + + def rustfmt(code: str) -> str: return check_output(["rustfmt", "--emit=stdout"], input=code, text=True) @@ -202,6 +219,7 @@ def extract_type_argument(rust_type_str: str) -> str: if close_bracket_index == -1 or close_bracket_index <= open_bracket_index: raise ValueError(f"Brackets are not balanced for type {rust_type_str}") inner_type = rust_type_str[open_bracket_index + 1 : close_bracket_index].strip() + inner_type = inner_type.replace("crate::", "") return inner_type @@ -766,39 +784,6 @@ def write_node(out: list[str], ast: Ast) -> None: # Source order visitor -@dataclass -class VisitorInfo: - name: str - accepts_sequence: bool = False - - -# Map of AST node types to their corresponding visitor information -type_to_visitor_function: dict[str, VisitorInfo] = { - "Decorator": VisitorInfo("visit_decorator"), - "Identifier": VisitorInfo("visit_identifier"), - "crate::TypeParams": VisitorInfo("visit_type_params", True), - "crate::Parameters": VisitorInfo("visit_parameters", True), - "Expr": VisitorInfo("visit_expr"), - "Stmt": VisitorInfo("visit_body", True), - "Arguments": VisitorInfo("visit_arguments", True), - "crate::Arguments": VisitorInfo("visit_arguments", True), - "Operator": VisitorInfo("visit_operator"), - "ElifElseClause": VisitorInfo("visit_elif_else_clause"), - "WithItem": VisitorInfo("visit_with_item"), - "MatchCase": VisitorInfo("visit_match_case"), - "ExceptHandler": VisitorInfo("visit_except_handler"), - "Alias": VisitorInfo("visit_alias"), - "UnaryOp": VisitorInfo("visit_unary_op"), - "DictItem": VisitorInfo("visit_dict_item"), - "Comprehension": VisitorInfo("visit_comprehension"), - "CmpOp": VisitorInfo("visit_cmp_op"), - "FStringValue": VisitorInfo("visit_f_string_value"), - "StringLiteralValue": VisitorInfo("visit_string_literal"), - "BytesLiteralValue": VisitorInfo("visit_bytes_literal"), -} -annotation_visitor_function = VisitorInfo("visit_annotation") - - def write_source_order(out: list[str], ast: Ast) -> None: for group in ast.groups: for node in group.nodes: @@ -816,24 +801,33 @@ def write_source_order(out: list[str], ast: Ast) -> None: fields_list += "range: _,\n" for field in node.fields_in_source_order(): - visitor = type_to_visitor_function[field.parsed_ty.inner] + visitor_name = ( + type_to_visitor_function.get( + field.parsed_ty.inner, VisitorInfo("") + ).name + or f"visit_{to_snake_case(field.parsed_ty.inner)}" + ) + visits_sequence = type_to_visitor_function.get( + field.parsed_ty.inner, VisitorInfo("") + ).accepts_sequence + if field.is_annotation: - visitor = annotation_visitor_function + visitor_name = "visit_annotation" if field.parsed_ty.optional: body += f""" if let Some({field.name}) = {field.name} {{ - visitor.{visitor.name}({field.name}); + visitor.{visitor_name}({field.name}); }}\n """ - elif not visitor.accepts_sequence and field.parsed_ty.seq: + elif not visits_sequence and field.parsed_ty.seq: body += f""" for elm in {field.name} {{ - visitor.{visitor.name}(elm); + visitor.{visitor_name}(elm); }} """ else: - body += f"visitor.{visitor.name}({field.name});\n" + body += f"visitor.{visitor_name}({field.name});\n" visitor_arg_name = "visitor" if len(node.fields_in_source_order()) == 0: From b1b8ca3bcd9fde92cd2e2ab553b557d4e73d2154 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Apr 2025 10:39:10 +0200 Subject: [PATCH 0074/1161] [red-knot] GenericAlias instances as a base class (#17575) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary We currently emit a diagnostic for code like the following: ```py from typing import Any # error: Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`) class C(tuple[Any, ...]): ... ``` The changeset here silences this diagnostic by recognizing instances of `GenericAlias` in `ClassBase::try_from_type`, and inferring a `@Todo` type for them. This is a change in preparation for #17557, because `C` previously had `Unknown` in its MRO … ```py reveal_type(C.__mro__) # tuple[Literal[C], Unknown, Literal[object]] ``` … which would cause us to think that `C` is assignable to everything. The changeset also removes some false positive `invalid-base` diagnostics across the ecosystem. ## Test Plan Updated Markdown tests. --- .../resources/mdtest/annotations/stdlib_typing_aliases.md | 6 +++--- .../resources/mdtest/generics/classes.md | 2 -- .../resources/mdtest/subscript/tuple.md | 4 +--- .../resources/mdtest/type_of/basic.md | 4 +--- crates/red_knot_python_semantic/src/types/class_base.rs | 3 +++ 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md index 7426e4c8875733..4c54139cc47bbc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -106,13 +106,13 @@ reveal_type(ChainMapSubclass.__mro__) class CounterSubclass(typing.Counter): ... # TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[CounterSubclass], Literal[Counter], Unknown, Literal[object]] +# revealed: tuple[Literal[CounterSubclass], Literal[Counter], @Todo(GenericAlias instance), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(CounterSubclass.__mro__) class DefaultDictSubclass(typing.DefaultDict): ... # TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], Unknown, Literal[object]] +# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], @Todo(GenericAlias instance), Literal[object]] reveal_type(DefaultDictSubclass.__mro__) class DequeSubclass(typing.Deque): ... @@ -124,6 +124,6 @@ reveal_type(DequeSubclass.__mro__) class OrderedDictSubclass(typing.OrderedDict): ... # TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], Unknown, Literal[object]] +# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], @Todo(GenericAlias instance), Literal[object]] reveal_type(OrderedDictSubclass.__mro__) ``` 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 63599786601160..295b44827de28d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -326,8 +326,6 @@ class Sub(Base[Sub]): ... ## Another cyclic case ```pyi -# TODO no error (generics) -# error: [invalid-base] class Derived[T](list[Derived[T]]): ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index 005e3da1ea87ff..04b39d1f504ec1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -81,13 +81,11 @@ python-version = "3.9" ``` ```py -# TODO: `tuple[int, str]` is a valid base (generics) -# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)" class A(tuple[int, str]): ... # Runtime value: `(A, tuple, object)` # TODO: Generics -reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]] +reveal_type(A.__mro__) # revealed: tuple[Literal[A], @Todo(GenericAlias instance), Literal[object]] ``` ## `typing.Tuple` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md b/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md index 1e5f4bdc5b258c..fc7f9f450bfe68 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md @@ -145,12 +145,10 @@ _: type[A, B] ## As a base class ```py -# TODO: this is a false positive -# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)" class Foo(type[int]): ... # TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]] -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] +reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], @Todo(GenericAlias instance), Literal[object]] ``` ## `@final` classes diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 6142cd322fb0da..fa5175a0ab84fe 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -78,6 +78,9 @@ impl<'db> ClassBase<'db> { Self::Class(literal.default_specialization(db)) }), Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), + Type::Instance(instance) if instance.class().is_known(db, KnownClass::GenericAlias) => { + Self::try_from_type(db, todo_type!("GenericAlias instance")) + } Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs? Type::Intersection(_) => None, // TODO -- probably incorrect? Type::Instance(_) => None, // TODO -- handle `__mro_entries__`? From a24132173530f926b3ab569e4773800461d71e0d Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Apr 2025 10:57:11 +0200 Subject: [PATCH 0075/1161] [red-knot] mypy_primer: add strawberry, print compilation errors to stderr (#17578) ## Summary mypy_primer changes included here: https://github.com/hauntsaninja/mypy_primer/compare/ebaa9fd27b51a278873b63676fd25490cec6823b..4c22d192a456e27badf85b3ea0f830707375d2b7 - Add strawberry as a `good.txt` project (was previously included in our fork) - Print Red Knot compilation errors to stderr (thanks @MichaReiser) --- .github/workflows/mypy_primer.yaml | 2 +- crates/red_knot_python_semantic/resources/primer/good.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index dab2bd4c845396..acf975c30a8452 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -46,7 +46,7 @@ jobs: - name: Install mypy_primer run: | - uv tool install "git+https://github.com/hauntsaninja/mypy_primer@ebaa9fd27b51a278873b63676fd25490cec6823b" + uv tool install "git+https://github.com/hauntsaninja/mypy_primer@4c22d192a456e27badf85b3ea0f830707375d2b7" - name: Run mypy_primer shell: bash diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/red_knot_python_semantic/resources/primer/good.txt index 2c7a36665a4043..86cec204df8f50 100644 --- a/crates/red_knot_python_semantic/resources/primer/good.txt +++ b/crates/red_knot_python_semantic/resources/primer/good.txt @@ -96,6 +96,7 @@ speedrun.com_global_scoreboard_webapp starlette static-frame stone +strawberry tornado twine typeshed-stats From 99fa850e5362d957f2249224dcecccf984faabee Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Apr 2025 11:37:30 +0200 Subject: [PATCH 0076/1161] [red-knot] Assignability for subclasses of `Any` and `Unknown` (#17557) ## Summary Allow (instances of) subclasses of `Any` and `Unknown` to be assignable to (instances of) other classes, unless they are final. This allows us to get rid of ~1000 false positives, mostly when mock-objects like `unittest.mock.MagicMock` are assigned to various targets. ## Test Plan Adapted and new Markdown tests. --- .../resources/mdtest/annotations/any.md | 31 +++++++++++++++---- .../mdtest/mdtest_custom_typeshed.md | 1 + .../type_properties/is_assignable_to.md | 7 +++++ .../src/types/class.rs | 12 +++++-- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md index b5b43a8412bac6..e0de156eb0bdd5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md @@ -50,10 +50,9 @@ y: Any = "not an Any" # error: [invalid-assignment] The spec allows you to define subclasses of `Any`. -TODO: Handle assignments correctly. `Subclass` has an unknown superclass, which might be `int`. The -assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment -to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore -assignable to `int`. +`Subclass` has an unknown superclass, which might be `int`. The assignment to `x` should not be +allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since +`Subclass` might have `int` as a superclass, and is therefore assignable to `int`. ```py from typing import Any @@ -63,13 +62,33 @@ class Subclass(Any): ... reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]] x: Subclass = 1 # error: [invalid-assignment] -# TODO: no diagnostic -y: int = Subclass() # error: [invalid-assignment] +y: int = Subclass() def _(s: Subclass): reveal_type(s) # revealed: Subclass ``` +`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly +be a subclass of `FinalClass`: + +```py +from typing import final + +@final +class FinalClass: ... + +f: FinalClass = Subclass() # error: [invalid-assignment] +``` + +A use case where this comes up is with mocking libraries, where the mock object should be assignable +to any type: + +```py +from unittest.mock import MagicMock + +x: int = MagicMock() +``` + ## Invalid `Any` cannot be parameterized: diff --git a/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md b/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md index e4255708fe373a..7c4a0abc3b6422 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md +++ b/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md @@ -22,6 +22,7 @@ We can then place custom stub files in `/typeshed/stdlib`, for example: `/typeshed/stdlib/builtins.pyi`: ```pyi +class object: ... class BuiltinClass: ... builtin_symbol: BuiltinClass diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index fdd7ddbe7c259a..9c33827733b414 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -47,6 +47,13 @@ static_assert(is_assignable_to(Unknown, Literal[1])) static_assert(is_assignable_to(Any, Literal[1])) static_assert(is_assignable_to(Literal[1], Unknown)) static_assert(is_assignable_to(Literal[1], Any)) + +class SubtypeOfAny(Any): ... + +static_assert(is_assignable_to(SubtypeOfAny, Any)) +static_assert(is_assignable_to(SubtypeOfAny, int)) +static_assert(is_assignable_to(Any, SubtypeOfAny)) +static_assert(not is_assignable_to(int, SubtypeOfAny)) ``` ## Literal types diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 8eaa0c7988e96e..f35c6a3eb2f45f 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -324,8 +324,6 @@ impl<'db> ClassType<'db> { } pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - // `is_subclass_of` is checking the subtype relation, in which gradual types do not - // participate, so we should not return `True` if we find `Any/Unknown` in the MRO. if self.is_subclass_of(db, other) { return true; } @@ -341,6 +339,16 @@ impl<'db> ClassType<'db> { } } + if self.iter_mro(db).any(|base| { + matches!( + base, + ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown) + ) + }) && !other.is_final(db) + { + return true; + } + false } From f9c7908bb74815f0f7cc4b0c62dfa2f057189b2e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 23 Apr 2025 11:03:52 +0100 Subject: [PATCH 0077/1161] [red-knot] Add more tests for protocol members (#17550) --- .../resources/mdtest/protocols.md | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 01b63d95f09d34..c011cff751bbaa 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -403,12 +403,37 @@ class Lumberjack(Protocol): reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(specialized non-generic class) ``` +A sub-protocol inherits and extends the members of its superclass protocol(s): + +```py +class Bar(Protocol): + spam: str + +class Baz(Bar, Protocol): + ham: memoryview + +# TODO: `tuple[Literal["spam", "ham"]]` or `frozenset[Literal["spam", "ham"]]` +reveal_type(get_protocol_members(Baz)) # revealed: @Todo(specialized non-generic class) + +class Baz2(Bar, Foo, Protocol): ... + +# TODO: either +# `tuple[Literal["spam"], Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]` +# or `frozenset[Literal["spam", "x", "y", "z", "method_member"]]` +reveal_type(get_protocol_members(Baz2)) # revealed: @Todo(specialized non-generic class) +``` + ## Subtyping of protocols with attribute members In the following example, the protocol class `HasX` defines an interface such that any other fully static type can be said to be a subtype of `HasX` if all inhabitants of that other type have a mutable `x` attribute of type `int`: +```toml +[environment] +python-version = "3.12" +``` + ```py from typing import Protocol from knot_extensions import static_assert, is_assignable_to, is_subtype_of @@ -548,6 +573,54 @@ def f(arg: HasXWithDefault): reveal_type(type(arg).x) # revealed: int ``` +Assignments in a class body of a protocol -- of any kind -- are not permitted by red-knot unless the +symbol being assigned to is also explicitly declared in the protocol's class body. Note that this is +stricter validation of protocol members than many other type checkers currently apply (as of +2025/04/21). + +The reason for this strict validation is that undeclared variables in the class body would lead to +an ambiguous interface being declared by the protocol. + +```py +from typing_extensions import TypeAlias, get_protocol_members + +class MyContext: + def __enter__(self) -> int: + return 42 + + def __exit__(self, *args) -> None: ... + +class LotsOfBindings(Protocol): + a: int + a = 42 # this is fine, since `a` is declared in the class body + b: int = 56 # this is also fine, by the same principle + + type c = str # this is very strange but I can't see a good reason to disallow it + d: TypeAlias = bytes # same here + + class Nested: ... # also weird, but we should also probably allow it + class NestedProtocol(Protocol): ... # same here... + e = 72 # TODO: this should error with `[invalid-protocol]` (`e` is not declared) + + f, g = (1, 2) # TODO: this should error with `[invalid-protocol]` (`f` and `g` are not declared) + + h: int = (i := 3) # TODO: this should error with `[invalid-protocol]` (`i` is not declared) + + for j in range(42): # TODO: this should error with `[invalid-protocol]` (`j` is not declared) + pass + + with MyContext() as k: # TODO: this should error with `[invalid-protocol]` (`k` is not declared) + pass + + match object(): + case l: # TODO: this should error with `[invalid-protocol]` (`l` is not declared) + ... + +# TODO: all bindings in the above class should be understood as protocol members, +# even those that we complained about with a diagnostic +reveal_type(get_protocol_members(LotsOfBindings)) # revealed: @Todo(specialized non-generic class) +``` + Attribute members are allowed to have assignments in methods on the protocol class, just like non-protocol classes. Unlike other classes, however, *implicit* instance attributes -- those that are not declared in the class body -- are not allowed: From 0a1f9d090e398c4f14a01aa83f3a150bf2254697 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 23 Apr 2025 11:13:20 +0100 Subject: [PATCH 0078/1161] [red-knot] Emit a diagnostic if a non-protocol is passed to `get_protocol_members` (#17551) --- .../resources/mdtest/protocols.md | 41 ++++++++-- ...lid_calls_to_`get_protocol_members()`.snap | 82 +++++++++++++++++++ .../src/types/diagnostic.rs | 50 ++++++++++- .../src/types/infer.rs | 16 +++- 4 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index c011cff751bbaa..966cfcebabcf2f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -375,15 +375,6 @@ class Foo(Protocol): reveal_type(get_protocol_members(Foo)) # revealed: @Todo(specialized non-generic class) ``` -Calling `get_protocol_members` on a non-protocol class raises an error at runtime: - -```py -class NotAProtocol: ... - -# TODO: should emit `[invalid-protocol]` error, should reveal `Unknown` -reveal_type(get_protocol_members(NotAProtocol)) # revealed: @Todo(specialized non-generic class) -``` - Certain special attributes and methods are not considered protocol members at runtime, and should not be considered protocol members by type checkers either: @@ -423,6 +414,38 @@ class Baz2(Bar, Foo, Protocol): ... reveal_type(get_protocol_members(Baz2)) # revealed: @Todo(specialized non-generic class) ``` +## Invalid calls to `get_protocol_members()` + + + +Calling `get_protocol_members` on a non-protocol class raises an error at runtime: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing_extensions import Protocol, get_protocol_members + +class NotAProtocol: ... + +get_protocol_members(NotAProtocol) # error: [invalid-argument-type] + +class AlsoNotAProtocol(NotAProtocol, object): ... + +get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] +``` + +The original class object must be passed to the function; a specialised version of a generic version +does not suffice: + +```py +class GenericProtocol[T](Protocol): ... + +get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549) +``` + ## Subtyping of protocols with attribute members In the following example, the protocol class `HasX` defines an interface such that any other fully diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap new file mode 100644 index 00000000000000..59d90c9e36fe59 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap @@ -0,0 +1,82 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: protocols.md - Protocols - Invalid calls to `get_protocol_members()` +mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import Protocol, get_protocol_members + 2 | + 3 | class NotAProtocol: ... + 4 | + 5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] + 6 | + 7 | class AlsoNotAProtocol(NotAProtocol, object): ... + 8 | + 9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] +10 | class GenericProtocol[T](Protocol): ... +11 | +12 | get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549) +``` + +# Diagnostics + +``` +error: lint:invalid-argument-type: Invalid argument to `get_protocol_members` + --> /src/mdtest_snippet.py:5:1 + | +3 | class NotAProtocol: ... +4 | +5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +6 | +7 | class AlsoNotAProtocol(NotAProtocol, object): ... + | +info: Only protocol classes can be passed to `get_protocol_members` +info: `NotAProtocol` is declared here, but it is not a protocol class: + --> /src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol, get_protocol_members +2 | +3 | class NotAProtocol: ... + | ^^^^^^^^^^^^ +4 | +5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] + | +info: A class is only a protocol class if it directly inherits from `typing.Protocol` or `typing_extensions.Protocol` +info: See https://typing.python.org/en/latest/spec/protocol.html# + +``` + +``` +error: lint:invalid-argument-type: Invalid argument to `get_protocol_members` + --> /src/mdtest_snippet.py:9:1 + | + 7 | class AlsoNotAProtocol(NotAProtocol, object): ... + 8 | + 9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +10 | class GenericProtocol[T](Protocol): ... + | +info: Only protocol classes can be passed to `get_protocol_members` +info: `AlsoNotAProtocol` is declared here, but it is not a protocol class: + --> /src/mdtest_snippet.py:7:7 + | +5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] +6 | +7 | class AlsoNotAProtocol(NotAProtocol, object): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +8 | +9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] + | +info: A class is only a protocol class if it directly inherits from `typing.Protocol` or `typing_extensions.Protocol` +info: See https://typing.python.org/en/latest/spec/protocol.html# + +``` diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index e35f19fea99fd7..7ae368c491bb92 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -1,4 +1,5 @@ use super::context::InferContext; +use super::ClassLiteralType; use crate::declare_lint; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; use crate::suppression::FileSuppressionId; @@ -8,9 +9,9 @@ use crate::types::string_annotation::{ RAW_STRING_TYPE_ANNOTATION, }; use crate::types::{KnownInstanceType, Type}; -use ruff_db::diagnostic::{Annotation, Diagnostic, Span}; +use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic}; use ruff_python_ast::{self as ast, AnyNodeRef}; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use std::fmt::Formatter; @@ -1313,6 +1314,51 @@ pub(crate) fn report_invalid_arguments_to_annotated( )); } +pub(crate) fn report_bad_argument_to_get_protocol_members( + context: &InferContext, + call: &ast::ExprCall, + class: ClassLiteralType, +) { + let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call) else { + return; + }; + let db = context.db(); + let mut diagnostic = builder.into_diagnostic("Invalid argument to `get_protocol_members`"); + diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + diagnostic.info("Only protocol classes can be passed to `get_protocol_members`"); + + let class_scope = class.body_scope(db); + let class_node = class_scope.node(db).expect_class(); + let class_name = &class_node.name; + let class_def_diagnostic_range = TextRange::new( + class_name.start(), + class_node + .arguments + .as_deref() + .map(Ranged::end) + .unwrap_or_else(|| class_name.end()), + ); + let mut class_def_diagnostic = SubDiagnostic::new( + Severity::Info, + format_args!("`{class_name}` is declared here, but it is not a protocol class:"), + ); + class_def_diagnostic.annotate(Annotation::primary( + Span::from(class_scope.file(db)).with_range(class_def_diagnostic_range), + )); + diagnostic.sub(class_def_diagnostic); + + diagnostic.info( + "A class is only a protocol class if it directly inherits \ + from `typing.Protocol` or `typing_extensions.Protocol`", + ); + // TODO the typing spec isn't really designed as user-facing documentation, + // but there isn't really any user-facing documentation that covers this specific issue well + // (it's not described well in the CPython docs; and PEP-544 is a snapshot of a decision taken + // years ago rather than up-to-date documentation). We should either write our own docs + // describing this well or contribute to type-checker-agnostic docs somewhere and link to those. + diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#"); +} + pub(crate) fn report_invalid_arguments_to_callable( context: &InferContext, subscript: &ast::ExprSubscript, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index cfa297d1278f06..e07f17d7ac7b2a 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -96,7 +96,8 @@ use crate::Db; use super::context::{InNoTypeCheck, InferContext}; use super::diagnostic::{ - report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause, + report_bad_argument_to_get_protocol_members, report_index_out_of_bounds, + report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_type_checking_constant, report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero, report_unresolved_reference, INVALID_METACLASS, INVALID_PROTOCOL, REDUNDANT_CAST, @@ -4486,6 +4487,19 @@ impl<'db> TypeInferenceBuilder<'db> { } } } + KnownFunction::GetProtocolMembers => { + if let [Some(Type::ClassLiteral(class))] = + overload.parameter_types() + { + if !class.is_protocol(self.db()) { + report_bad_argument_to_get_protocol_members( + &self.context, + call_expression, + *class, + ); + } + } + } _ => {} } } From 5407249467c4c07b14980f4aabfc9b9da464d032 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 23 Apr 2025 15:11:20 +0200 Subject: [PATCH 0079/1161] [red-knot] Make `BoundMethodType` a salsa interned (#17581) --- crates/red_knot_python_semantic/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ec35873e320576..7fc0cd14e2793d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -6223,7 +6223,7 @@ impl KnownFunction { /// on an instance of a class. For example, the expression `Path("a.txt").touch` creates /// a bound method object that represents the `Path.touch` method which is bound to the /// instance `Path("a.txt")`. -#[salsa::tracked(debug)] +#[salsa::interned(debug)] pub struct BoundMethodType<'db> { /// The function that is being bound. Corresponds to the `__func__` attribute on a /// bound method object From 8abf93f5fbce74232432421c59b1ba5d24dd17d4 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 23 Apr 2025 15:32:41 +0200 Subject: [PATCH 0080/1161] [red-knot] Early return from `project.is_file_open` for vendored files (#17580) --- crates/red_knot_project/src/lib.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/red_knot_project/src/lib.rs b/crates/red_knot_project/src/lib.rs index fd6545972a4664..8393aec6637ceb 100644 --- a/crates/red_knot_project/src/lib.rs +++ b/crates/red_knot_project/src/lib.rs @@ -314,20 +314,23 @@ impl Project { /// * It has a [`SystemPath`] and belongs to a package's `src` files /// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath) pub fn is_file_open(self, db: &dyn Db, file: File) -> bool { + let path = file.path(db); + + // Try to return early to avoid adding a dependency on `open_files` or `file_set` which + // both have a durability of `LOW`. + if path.is_vendored_path() { + return false; + } + if let Some(open_files) = self.open_files(db) { open_files.contains(&file) } else if file.path(db).is_system_path() { - self.contains_file(db, file) + self.files(db).contains(&file) } else { file.path(db).is_system_virtual_path() } } - /// Returns `true` if `file` is a first-party file part of this package. - pub fn contains_file(self, db: &dyn Db, file: File) -> bool { - self.files(db).contains(&file) - } - #[tracing::instrument(level = "debug", skip(self, db))] pub fn remove_file(self, db: &mut dyn Db, file: File) { tracing::debug!( From 624f5c6c229b00ac6c6d865b06c7aef23bb2e023 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 23 Apr 2025 15:47:54 +0200 Subject: [PATCH 0081/1161] Fix stale diagnostics in Ruff playground (#17583) --- playground/package-lock.json | 12 +++++----- playground/ruff/src/Editor/Diagnostics.tsx | 27 +++++++++++----------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index f25650f3f0b446..0f1af5d971b121 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -41,10 +41,10 @@ "pyodide": "^0.27.4", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable-panels": "^2.1.8", + "react-resizable-panels": "^2.1.7", "red_knot_wasm": "file:red_knot_wasm", "shared": "0.0.0", - "smol-toml": "^1.3.3" + "smol-toml": "^1.3.1" }, "devDependencies": { "vite-plugin-static-copy": "^2.3.0" @@ -6087,14 +6087,14 @@ "monaco-editor": "^0.52.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable-panels": "^2.1.8", + "react-resizable-panels": "^2.0.0", "ruff_wasm": "file:ruff_wasm", "shared": "0.0.0", - "smol-toml": "^1.3.3" + "smol-toml": "^1.3.0" } }, "ruff/ruff_wasm": { - "version": "0.11.3", + "version": "0.11.4", "license": "MIT" }, "shared": { @@ -6103,7 +6103,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.3.2", "react": "^19.0.0", - "react-resizable-panels": "^2.1.8" + "react-resizable-panels": "^2.1.7" } } } diff --git a/playground/ruff/src/Editor/Diagnostics.tsx b/playground/ruff/src/Editor/Diagnostics.tsx index 781c919205543c..427e5b24b2ccd9 100644 --- a/playground/ruff/src/Editor/Diagnostics.tsx +++ b/playground/ruff/src/Editor/Diagnostics.tsx @@ -66,27 +66,28 @@ function Items({ ); } + const uniqueIds: Map = new Map(); + return (
    - {diagnostics.map((diagnostic, index) => { + {diagnostics.map((diagnostic) => { + const row = diagnostic.start_location.row; + const column = diagnostic.start_location.column; + const mostlyUniqueId = `${row}:${column}-${diagnostic.code}`; + + const disambiguator = uniqueIds.get(mostlyUniqueId) ?? 0; + uniqueIds.set(mostlyUniqueId, disambiguator + 1); + return ( -
  • +
  • From e7f38fe74ba642cbd4cf286d8524f4172cbe30e2 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:52:58 -0400 Subject: [PATCH 0082/1161] [red-knot] Detect semantic syntax errors (#17463) Summary -- This PR extends semantic syntax error detection to red-knot. The main changes here are: 1. Adding `SemanticSyntaxChecker` and `Vec` fields to the `SemanticIndexBuilder` 2. Calling `SemanticSyntaxChecker::visit_stmt` and `visit_expr` in the `SemanticIndexBuilder`'s `visit_stmt` and `visit_expr` methods 3. Implementing `SemanticSyntaxContext` for `SemanticIndexBuilder` 4. Adding new mdtests to test the context implementation and show diagnostics (3) is definitely the trickiest and required (I think) a minor addition to the `SemanticIndexBuilder`. I tried to look around for existing code performing the necessary checks, but I definitely could have missed something or misused the existing code even when I found it. There's still one TODO around `global` statement handling. I don't think there's an existing way to look this up, but I'm happy to work on that here or in a separate PR. This currently only affects detection of one error (`LoadBeforeGlobalDeclaration` or [PLE0118](https://docs.astral.sh/ruff/rules/load-before-global-declaration/) in ruff), so it's not too big of a problem even if we leave the TODO. Test Plan -- New mdtests, as well as new errors for existing mdtests --------- Co-authored-by: Alex Waygood --- .../resources/mdtest/annotations/invalid.md | 69 ++++---- .../resources/mdtest/comprehensions/basic.md | 10 +- .../diagnostics/semantic_syntax_errors.md | 165 ++++++++++++++++++ .../resources/mdtest/import/star.md | 6 +- ...chronous_comprehensions_-_Python_3.10.snap | 46 +++++ .../src/semantic_index.rs | 8 + .../src/semantic_index/builder.rs | 141 ++++++++++++++- crates/red_knot_python_semantic/src/types.rs | 8 + .../src/types/diagnostic.rs | 4 + crates/ruff_db/src/diagnostic/mod.rs | 13 ++ crates/ruff_linter/src/checkers/ast/mod.rs | 4 - .../ruff_python_parser/src/semantic_errors.rs | 19 +- crates/ruff_python_parser/tests/fixtures.rs | 4 - 13 files changed, 431 insertions(+), 66 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md index 9c91adb52e2eb2..e2b522190f60bf 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md @@ -56,40 +56,41 @@ def _( def bar() -> None: return None -def _( - a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" - b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions" - c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions" - d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression" - e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression" - f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions" - g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions" - h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions" - i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions" - j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions" - k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions" - l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions" - m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions" - n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions" - o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions" - p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions" - q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions" - r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions" -): - reveal_type(a) # revealed: Unknown - reveal_type(b) # revealed: Unknown - reveal_type(c) # revealed: Unknown - reveal_type(d) # revealed: Unknown - reveal_type(e) # revealed: int | Unknown - reveal_type(f) # revealed: Unknown - reveal_type(g) # revealed: Unknown - reveal_type(h) # revealed: Unknown - reveal_type(i) # revealed: Unknown - reveal_type(j) # revealed: Unknown - reveal_type(k) # revealed: Unknown - reveal_type(p) # revealed: Unknown - reveal_type(q) # revealed: int | Unknown - reveal_type(r) # revealed: @Todo(unknown type subscript) +async def outer(): # avoid unrelated syntax errors on yield, yield from, and await + def _( + a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" + b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions" + c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions" + d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression" + e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression" + f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions" + g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions" + h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions" + i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions" + j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions" + k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions" + l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions" + m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions" + n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions" + o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions" + p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions" + q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions" + r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions" + ): + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown + reveal_type(d) # revealed: Unknown + reveal_type(e) # revealed: int | Unknown + reveal_type(f) # revealed: Unknown + reveal_type(g) # revealed: Unknown + reveal_type(h) # revealed: Unknown + reveal_type(i) # revealed: Unknown + reveal_type(j) # revealed: Unknown + reveal_type(k) # revealed: Unknown + reveal_type(p) # revealed: Unknown + reveal_type(q) # revealed: int | Unknown + reveal_type(r) # revealed: @Todo(unknown type subscript) ``` ## Invalid Collection based AST nodes diff --git a/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md index 8047fc078d09ec..a32244c09abb4c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md @@ -127,8 +127,9 @@ class AsyncIterable: def __aiter__(self) -> AsyncIterator: return AsyncIterator() -# revealed: @Todo(async iterables/iterators) -[reveal_type(x) async for x in AsyncIterable()] +async def _(): + # revealed: @Todo(async iterables/iterators) + [reveal_type(x) async for x in AsyncIterable()] ``` ### Invalid async comprehension @@ -145,6 +146,7 @@ class Iterable: def __iter__(self) -> Iterator: return Iterator() -# revealed: @Todo(async iterables/iterators) -[reveal_type(x) async for x in Iterable()] +async def _(): + # revealed: @Todo(async iterables/iterators) + [reveal_type(x) async for x in Iterable()] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md new file mode 100644 index 00000000000000..7dfabdef84bc8d --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -0,0 +1,165 @@ +# Semantic syntax error diagnostics + +## `async` comprehensions in synchronous comprehensions + +### Python 3.10 + + + +Before Python 3.11, `async` comprehensions could not be used within outer sync comprehensions, even +within an `async` function ([CPython issue](https://github.com/python/cpython/issues/77527)): + +```toml +[environment] +python-version = "3.10" +``` + +```py +async def elements(n): + yield n + +async def f(): + # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11)" + return {n: [x async for x in elements(n)] for n in range(3)} +``` + +If all of the comprehensions are `async`, on the other hand, the code was still valid: + +```py +async def test(): + return [[x async for x in elements(n)] async for n in range(3)] +``` + +These are a couple of tricky but valid cases to check that nested scope handling is wired up +correctly in the `SemanticSyntaxContext` trait: + +```py +async def f(): + [x for x in [1]] and [x async for x in elements(1)] + +async def f(): + def g(): + pass + [x async for x in elements(1)] +``` + +### Python 3.11 + +All of these same examples are valid after Python 3.11: + +```toml +[environment] +python-version = "3.11" +``` + +```py +async def elements(n): + yield n + +async def f(): + return {n: [x async for x in elements(n)] for n in range(3)} +``` + +## Late `__future__` import + +```py +from collections import namedtuple + +# error: [invalid-syntax] "__future__ imports must be at the top of the file" +from __future__ import print_function +``` + +## Invalid annotation + +This one might be a bit redundant with the `invalid-type-form` error. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from __future__ import annotations + +# error: [invalid-type-form] "Named expressions are not allowed in type expressions" +# error: [invalid-syntax] "named expression cannot be used within a type annotation" +def f() -> (y := 3): ... +``` + +## Duplicate `match` key + +```toml +[environment] +python-version = "3.10" +``` + +```py +match 2: + # error: [invalid-syntax] "mapping pattern checks duplicate key `"x"`" + case {"x": 1, "x": 2}: + ... +``` + +## `return`, `yield`, `yield from`, and `await` outside function + +```py +# error: [invalid-syntax] "`return` statement outside of a function" +return + +# error: [invalid-syntax] "`yield` statement outside of a function" +yield + +# error: [invalid-syntax] "`yield from` statement outside of a function" +yield from [] + +# error: [invalid-syntax] "`await` statement outside of a function" +# error: [invalid-syntax] "`await` outside of an asynchronous function" +await 1 + +def f(): + # error: [invalid-syntax] "`await` outside of an asynchronous function" + await 1 +``` + +Generators are evaluated lazily, so `await` is allowed, even outside of a function. + +```py +async def g(): + yield 1 + +(x async for x in g()) +``` + +## `await` outside async function + +This error includes `await`, `async for`, `async with`, and `async` comprehensions. + +```python +async def elements(n): + yield n + +def _(): + # error: [invalid-syntax] "`await` outside of an asynchronous function" + await 1 + # error: [invalid-syntax] "`async for` outside of an asynchronous function" + async for _ in elements(1): + ... + # error: [invalid-syntax] "`async with` outside of an asynchronous function" + async with elements(1) as x: + ... + # error: [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.9 (syntax was added in 3.11)" + # error: [invalid-syntax] "asynchronous comprehension outside of an asynchronous function" + [x async for x in elements(1)] +``` + +## Load before `global` declaration + +This should be an error, but it's not yet. + +TODO implement `SemanticSyntaxContext::global` + +```py +def f(): + x = 1 + global x +``` 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 5cad39a51e0a6a..49cf3bac004e90 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/star.md +++ b/crates/red_knot_python_semantic/resources/mdtest/import/star.md @@ -189,7 +189,7 @@ match 42: ... case [O]: ... - case P | Q: + case P | Q: # error: [invalid-syntax] "name capture `P` makes remaining patterns unreachable" ... case object(foo=R): ... @@ -289,7 +289,7 @@ match 42: ... case [D]: ... - case E | F: + case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable" ... case object(foo=G): ... @@ -357,7 +357,7 @@ match 42: ... case [D]: ... - case E | F: + case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable" ... case object(foo=G): ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap new file mode 100644 index 00000000000000..d3ec566124a9c7 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap @@ -0,0 +1,46 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: semantic_syntax_errors.md - Semantic syntax error diagnostics - `async` comprehensions in synchronous comprehensions - Python 3.10 +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | async def elements(n): + 2 | yield n + 3 | + 4 | async def f(): + 5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11)" + 6 | return {n: [x async for x in elements(n)] for n in range(3)} + 7 | async def test(): + 8 | return [[x async for x in elements(n)] async for n in range(3)] + 9 | async def f(): +10 | [x for x in [1]] and [x async for x in elements(1)] +11 | +12 | async def f(): +13 | def g(): +14 | pass +15 | [x async for x in elements(1)] +``` + +# Diagnostics + +``` +error: invalid-syntax + --> /src/mdtest_snippet.py:6:19 + | +4 | async def f(): +5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax... +6 | return {n: [x async for x in elements(n)] for n in range(3)} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) +7 | async def test(): +8 | return [[x async for x in elements(n)] async for n in range(3)] + | + +``` diff --git a/crates/red_knot_python_semantic/src/semantic_index.rs b/crates/red_knot_python_semantic/src/semantic_index.rs index 0af319bb63e1df..619172faef097a 100644 --- a/crates/red_knot_python_semantic/src/semantic_index.rs +++ b/crates/red_knot_python_semantic/src/semantic_index.rs @@ -5,6 +5,7 @@ use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_index::{IndexSlice, IndexVec}; +use ruff_python_parser::semantic_errors::SemanticSyntaxError; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use salsa::plumbing::AsId; use salsa::Update; @@ -175,6 +176,9 @@ pub(crate) struct SemanticIndex<'db> { /// Map of all of the eager bindings that appear in this file. eager_bindings: FxHashMap, + + /// List of all semantic syntax errors in this file. + semantic_syntax_errors: Vec, } impl<'db> SemanticIndex<'db> { @@ -399,6 +403,10 @@ impl<'db> SemanticIndex<'db> { None => EagerBindingsResult::NotFound, } } + + pub(crate) fn semantic_syntax_errors(&self) -> &[SemanticSyntaxError] { + &self.semantic_syntax_errors + } } pub struct AncestorsIter<'a> { diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index 1e8b26aa9ec6d4..a17e4523f360a4 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -1,3 +1,4 @@ +use std::cell::{OnceCell, RefCell}; use std::sync::Arc; use except_handlers::TryNodeContextStackManager; @@ -5,10 +6,15 @@ use rustc_hash::{FxHashMap, FxHashSet}; use ruff_db::files::File; use ruff_db::parsed::ParsedModule; +use ruff_db::source::{source_text, SourceText}; use ruff_index::IndexVec; use ruff_python_ast::name::Name; use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor}; -use ruff_python_ast::{self as ast}; +use ruff_python_ast::{self as ast, PythonVersion}; +use ruff_python_parser::semantic_errors::{ + SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, +}; +use ruff_text_size::TextRange; use crate::ast_node_ref::AstNodeRef; use crate::module_name::ModuleName; @@ -32,8 +38,8 @@ use crate::semantic_index::predicate::{ }; use crate::semantic_index::re_exports::exported_names; use crate::semantic_index::symbol::{ - FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId, - SymbolTableBuilder, + FileScopeId, NodeWithScopeKey, NodeWithScopeKind, NodeWithScopeRef, Scope, ScopeId, ScopeKind, + ScopedSymbolId, SymbolTableBuilder, }; use crate::semantic_index::use_def::{ EagerBindingsKey, FlowSnapshot, ScopedEagerBindingsId, UseDefMapBuilder, @@ -43,7 +49,7 @@ use crate::semantic_index::visibility_constraints::{ }; use crate::semantic_index::SemanticIndex; use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue}; -use crate::Db; +use crate::{Db, Program}; mod except_handlers; @@ -85,6 +91,11 @@ pub(super) struct SemanticIndexBuilder<'db> { /// Flags about the file's global scope has_future_annotations: bool, + // Used for checking semantic syntax errors + python_version: PythonVersion, + source_text: OnceCell, + semantic_checker: SemanticSyntaxChecker, + // Semantic Index fields scopes: IndexVec, scope_ids_by_scope: IndexVec>, @@ -98,6 +109,8 @@ pub(super) struct SemanticIndexBuilder<'db> { expressions_by_node: FxHashMap>, imported_modules: FxHashSet, eager_bindings: FxHashMap, + /// Errors collected by the `semantic_checker`. + semantic_syntax_errors: RefCell>, } impl<'db> SemanticIndexBuilder<'db> { @@ -129,6 +142,11 @@ impl<'db> SemanticIndexBuilder<'db> { imported_modules: FxHashSet::default(), eager_bindings: FxHashMap::default(), + + python_version: Program::get(db).python_version(db), + source_text: OnceCell::new(), + semantic_checker: SemanticSyntaxChecker::default(), + semantic_syntax_errors: RefCell::default(), }; builder.push_scope_with_parent( @@ -156,10 +174,6 @@ impl<'db> SemanticIndexBuilder<'db> { self.current_scope_info().file_scope_id } - fn current_scope_is_global_scope(&self) -> bool { - self.scope_stack.len() == 1 - } - /// Returns the scope ID of the surrounding class body scope if the current scope /// is a method inside a class body. Returns `None` otherwise, e.g. if the current /// scope is a function body outside of a class, or if the current scope is not a @@ -1050,8 +1064,20 @@ impl<'db> SemanticIndexBuilder<'db> { imported_modules: Arc::new(self.imported_modules), has_future_annotations: self.has_future_annotations, eager_bindings: self.eager_bindings, + semantic_syntax_errors: self.semantic_syntax_errors.into_inner(), } } + + fn with_semantic_checker(&mut self, f: impl FnOnce(&mut SemanticSyntaxChecker, &Self)) { + let mut checker = std::mem::take(&mut self.semantic_checker); + f(&mut checker, self); + self.semantic_checker = checker; + } + + fn source_text(&self) -> &SourceText { + self.source_text + .get_or_init(|| source_text(self.db.upcast(), self.file)) + } } impl<'db, 'ast> Visitor<'ast> for SemanticIndexBuilder<'db> @@ -1059,6 +1085,8 @@ where 'ast: 'db, { fn visit_stmt(&mut self, stmt: &'ast ast::Stmt) { + self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context)); + match stmt { ast::Stmt::FunctionDef(function_def) => { let ast::StmtFunctionDef { @@ -1254,7 +1282,7 @@ where // Wildcard imports are invalid syntax everywhere except the top-level scope, // and thus do not bind any definitions anywhere else - if !self.current_scope_is_global_scope() { + if !self.in_module_scope() { continue; } @@ -1809,6 +1837,8 @@ where } fn visit_expr(&mut self, expr: &'ast ast::Expr) { + self.with_semantic_checker(|semantic, context| semantic.visit_expr(expr, context)); + self.scopes_by_expression .insert(expr.into(), self.current_scope()); self.current_ast_ids().record_expression(expr); @@ -2268,6 +2298,99 @@ where } } +impl SemanticSyntaxContext for SemanticIndexBuilder<'_> { + fn future_annotations_or_stub(&self) -> bool { + self.has_future_annotations + } + + fn python_version(&self) -> PythonVersion { + self.python_version + } + + fn source(&self) -> &str { + self.source_text().as_str() + } + + // TODO(brent) handle looking up `global` bindings + fn global(&self, _name: &str) -> Option { + None + } + + fn in_async_context(&self) -> bool { + for scope_info in self.scope_stack.iter().rev() { + let scope = &self.scopes[scope_info.file_scope_id]; + match scope.kind() { + ScopeKind::Class | ScopeKind::Lambda => return false, + ScopeKind::Function => { + return scope.node().expect_function().is_async; + } + ScopeKind::Comprehension + | ScopeKind::Module + | ScopeKind::TypeAlias + | ScopeKind::Annotation => {} + } + } + false + } + + fn in_await_allowed_context(&self) -> bool { + for scope_info in self.scope_stack.iter().rev() { + let scope = &self.scopes[scope_info.file_scope_id]; + match scope.kind() { + ScopeKind::Class => return false, + ScopeKind::Function | ScopeKind::Lambda => return true, + ScopeKind::Comprehension + | ScopeKind::Module + | ScopeKind::TypeAlias + | ScopeKind::Annotation => {} + } + } + false + } + + fn in_sync_comprehension(&self) -> bool { + for scope_info in self.scope_stack.iter().rev() { + let scope = &self.scopes[scope_info.file_scope_id]; + let generators = match scope.node() { + NodeWithScopeKind::ListComprehension(node) => &node.generators, + NodeWithScopeKind::SetComprehension(node) => &node.generators, + NodeWithScopeKind::DictComprehension(node) => &node.generators, + _ => continue, + }; + if generators.iter().all(|gen| !gen.is_async) { + return true; + } + } + false + } + + fn in_module_scope(&self) -> bool { + self.scope_stack.len() == 1 + } + + fn in_function_scope(&self) -> bool { + let kind = self.scopes[self.current_scope()].kind(); + matches!(kind, ScopeKind::Function | ScopeKind::Lambda) + } + + fn in_generator_scope(&self) -> bool { + matches!( + self.scopes[self.current_scope()].node(), + NodeWithScopeKind::GeneratorExpression(_) + ) + } + + fn in_notebook(&self) -> bool { + self.source_text().is_notebook() + } + + fn report_semantic_error(&self, error: SemanticSyntaxError) { + if self.db.is_file_open(self.file) { + self.semantic_syntax_errors.borrow_mut().push(error); + } + } +} + #[derive(Copy, Clone, Debug, PartialEq)] enum CurrentAssignment<'a> { Assign { diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 7fc0cd14e2793d..398cd95d1eb8cc 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -10,6 +10,7 @@ use diagnostic::{ CALL_POSSIBLY_UNBOUND_METHOD, INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, }; +use ruff_db::diagnostic::create_semantic_syntax_diagnostic; use ruff_db::files::{File, FileRange}; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, AnyNodeRef}; @@ -90,6 +91,13 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics { diagnostics.extend(result.diagnostics()); } + diagnostics.extend_diagnostics( + index + .semantic_syntax_errors() + .iter() + .map(|error| create_semantic_syntax_diagnostic(file, error)), + ); + check_suppressions(db, file, &mut diagnostics); diagnostics diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 7ae368c491bb92..ec546255cfb8d8 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -1021,6 +1021,10 @@ impl TypeCheckDiagnostics { self.used_suppressions.extend(&other.used_suppressions); } + pub(super) fn extend_diagnostics(&mut self, diagnostics: impl IntoIterator) { + self.diagnostics.extend(diagnostics); + } + pub(crate) fn mark_used(&mut self, suppression_id: FileSuppressionId) { self.used_suppressions.insert(suppression_id); } diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 61a3ca68053b1a..3478d105b5024f 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -845,3 +845,16 @@ pub fn create_unsupported_syntax_diagnostic( diag.annotate(Annotation::primary(span).message(err.to_string())); diag } + +/// Creates a `Diagnostic` from a semantic syntax error. +/// +/// See [`create_parse_diagnostic`] for more details. +pub fn create_semantic_syntax_diagnostic( + file: File, + err: &ruff_python_parser::semantic_errors::SemanticSyntaxError, +) -> 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_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 10784403e49d56..5ae94f21b3709f 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -557,10 +557,6 @@ impl<'a> Checker<'a> { } impl SemanticSyntaxContext for Checker<'_> { - fn seen_docstring_boundary(&self) -> bool { - self.semantic.seen_module_docstring_boundary() - } - fn python_version(&self) -> PythonVersion { self.target_version } diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index e61c440fcf34d3..39b85e53929936 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -32,6 +32,10 @@ pub struct SemanticSyntaxChecker { /// Python considers it a syntax error to import from `__future__` after any other /// non-`__future__`-importing statements. seen_futures_boundary: bool, + + /// The checker has traversed past the module docstring boundary (i.e. seen any statement in the + /// module). + seen_module_docstring_boundary: bool, } impl SemanticSyntaxChecker { @@ -506,7 +510,7 @@ impl SemanticSyntaxChecker { // update internal state match stmt { Stmt::Expr(StmtExpr { value, .. }) - if !ctx.seen_docstring_boundary() && value.is_string_literal_expr() => {} + if !self.seen_module_docstring_boundary && value.is_string_literal_expr() => {} Stmt::ImportFrom(StmtImportFrom { module, .. }) => { // Allow __future__ imports until we see a non-__future__ import. if !matches!(module.as_deref(), Some("__future__")) { @@ -520,6 +524,8 @@ impl SemanticSyntaxChecker { self.seen_futures_boundary = true; } } + + self.seen_module_docstring_boundary = true; } /// Check `expr` for semantic syntax errors and update the checker's internal state. @@ -881,7 +887,7 @@ impl Display for SemanticSyntaxError { f.write_str("`return` statement outside of a function") } SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind) => { - write!(f, "`{kind}` outside of an asynchronous function") + write!(f, "{kind} outside of an asynchronous function") } } } @@ -1207,9 +1213,9 @@ pub enum AwaitOutsideAsyncFunctionKind { impl Display for AwaitOutsideAsyncFunctionKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { - AwaitOutsideAsyncFunctionKind::Await => "await", - AwaitOutsideAsyncFunctionKind::AsyncFor => "async for", - AwaitOutsideAsyncFunctionKind::AsyncWith => "async with", + AwaitOutsideAsyncFunctionKind::Await => "`await`", + AwaitOutsideAsyncFunctionKind::AsyncFor => "`async for`", + AwaitOutsideAsyncFunctionKind::AsyncWith => "`async with`", AwaitOutsideAsyncFunctionKind::AsyncComprehension => "asynchronous comprehension", }) } @@ -1584,9 +1590,6 @@ where /// x # here, classes break function scopes /// ``` pub trait SemanticSyntaxContext { - /// Returns `true` if a module's docstring boundary has been passed. - fn seen_docstring_boundary(&self) -> bool; - /// Returns `true` if `__future__`-style type annotations are enabled. fn future_annotations_or_stub(&self) -> bool; diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index a7b0260010b992..1ba0f39f3f33b9 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -504,10 +504,6 @@ impl<'a> SemanticSyntaxCheckerVisitor<'a> { } impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { - fn seen_docstring_boundary(&self) -> bool { - false - } - fn future_annotations_or_stub(&self) -> bool { false } From 5a719f2d6013e5b615539c847a9525993c1095a6 Mon Sep 17 00:00:00 2001 From: Navdeep K <56593523+knavdeep152002@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:19:20 +0530 Subject: [PATCH 0083/1161] [`pycodestyle`] Auto-fix redundant boolean comparison (`E712`) (#17090) This pull request fixes https://github.com/astral-sh/ruff/issues/17014 changes this ```python from __future__ import annotations flag1 = True flag2 = True if flag1 == True or flag2 == True: pass if flag1 == False and flag2 == False: pass flag3 = True if flag1 == flag3 and (flag2 == False or flag3 == True): # Should become: if flag1==flag3 and (not flag2 or flag3) pass if flag1 == True and (flag2 == False or not flag3 == True): # Should become: if flag1 and (not flag2 or not flag3) pass if flag1 != True and (flag2 != False or not flag3 == True): # Should become: if not flag1 and (flag2 or not flag3) pass flag = True while flag == True: # Should become: while flag flag = False flag = True x = 5 if flag == True and x > 0: # Should become: if flag and x > 0 print("ok") flag = True result = "yes" if flag == True else "no" # Should become: result = "yes" if flag else "no" x = flag == True < 5 x = (flag == True) == False < 5 ``` to this ```python from __future__ import annotations flag1 = True flag2 = True if flag1 or flag2: pass if not flag1 and not flag2: pass flag3 = True if flag1 == flag3 and (not flag2 or flag3): # Should become: if flag1 == flag3 and (not flag2 or flag3) pass if flag1 and (not flag2 or not flag3): # Should become: if flag1 and (not flag2 or not flag3) pass if not flag1 and (flag2 or not flag3): # Should become: if not flag1 and (flag2 or not flag3) pass flag = True while flag: # Should become: while flag flag = False flag = True x = 5 if flag and x > 0: # Should become: if flag and x > 0 print("ok") flag = True result = "yes" if flag else "no" # Should become: result = "yes" if flag else "no" x = flag is True < 5 x = (flag) is False < 5 ``` --------- Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../pycodestyle/rules/literal_comparisons.rs | 96 ++++++++++++++++--- ...les__pycodestyle__tests__E712_E712.py.snap | 22 ++--- ...pycodestyle__tests__constant_literals.snap | 9 +- 3 files changed, 100 insertions(+), 27 deletions(-) diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs index 265d0c7046a6cd..cffcde998fddf2 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -1,9 +1,9 @@ +use ruff_python_ast::parenthesize::parenthesized_range; use rustc_hash::FxHashMap; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::helpers; -use ruff_python_ast::helpers::generate_comparison; +use ruff_python_ast::helpers::{self, generate_comparison}; use ruff_python_ast::{self as ast, CmpOp, Expr}; use ruff_text_size::Ranged; @@ -170,6 +170,42 @@ impl AlwaysFixableViolation for TrueFalseComparison { } } +fn is_redundant_boolean_comparison(op: CmpOp, comparator: &Expr) -> Option { + let value = comparator.as_boolean_literal_expr()?.value; + match op { + CmpOp::Is | CmpOp::Eq => Some(value), + CmpOp::IsNot | CmpOp::NotEq => Some(!value), + _ => None, + } +} + +fn generate_redundant_comparison( + compare: &ast::ExprCompare, + comment_ranges: &ruff_python_trivia::CommentRanges, + source: &str, + comparator: &Expr, + kind: bool, + needs_wrap: bool, +) -> String { + let comparator_range = + parenthesized_range(comparator.into(), compare.into(), comment_ranges, source) + .unwrap_or(comparator.range()); + + let comparator_str = &source[comparator_range]; + + let result = if kind { + comparator_str.to_string() + } else { + format!("not {comparator_str}") + }; + + if needs_wrap { + format!("({result})") + } else { + result + } +} + /// E711, E712 pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) { // Mapping from (bad operator index) to (replacement operator). As we iterate @@ -323,7 +359,6 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) // TODO(charlie): Respect `noqa` directives. If one of the operators has a // `noqa`, but another doesn't, both will be removed here. if !bad_ops.is_empty() { - // Replace the entire comparison expression. let ops = compare .ops .iter() @@ -331,14 +366,53 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) .map(|(idx, op)| bad_ops.get(&idx).unwrap_or(op)) .copied() .collect::>(); - let content = generate_comparison( - &compare.left, - &ops, - &compare.comparators, - compare.into(), - checker.comment_ranges(), - checker.source(), - ); + + let comment_ranges = checker.comment_ranges(); + let source = checker.source(); + + let content = match (&*compare.ops, &*compare.comparators) { + ([op], [comparator]) => { + if let Some(kind) = is_redundant_boolean_comparison(*op, &compare.left) { + let needs_wrap = compare.left.range().start() != compare.range().start(); + generate_redundant_comparison( + compare, + comment_ranges, + source, + comparator, + kind, + needs_wrap, + ) + } else if let Some(kind) = is_redundant_boolean_comparison(*op, comparator) { + let needs_wrap = comparator.range().end() != compare.range().end(); + generate_redundant_comparison( + compare, + comment_ranges, + source, + &compare.left, + kind, + needs_wrap, + ) + } else { + generate_comparison( + &compare.left, + &ops, + &compare.comparators, + compare.into(), + comment_ranges, + source, + ) + } + } + _ => generate_comparison( + &compare.left, + &ops, + &compare.comparators, + compare.into(), + comment_ranges, + source, + ), + }; + for diagnostic in &mut diagnostics { diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( content.to_string(), diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap index 02f2c269516876..76d290a4b5cea7 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap @@ -14,7 +14,7 @@ E712.py:2:4: E712 [*] Avoid equality comparisons to `True`; use `if res:` for tr ℹ Unsafe fix 1 1 | #: E712 2 |-if res == True: - 2 |+if res is True: + 2 |+if res: 3 3 | pass 4 4 | #: E712 5 5 | if res != False: @@ -35,7 +35,7 @@ E712.py:5:4: E712 [*] Avoid inequality comparisons to `False`; use `if res:` for 3 3 | pass 4 4 | #: E712 5 |-if res != False: - 5 |+if res is not False: + 5 |+if res: 6 6 | pass 7 7 | #: E712 8 8 | if True != res: @@ -56,7 +56,7 @@ E712.py:8:4: E712 [*] Avoid inequality comparisons to `True`; use `if not res:` 6 6 | pass 7 7 | #: E712 8 |-if True != res: - 8 |+if True is not res: + 8 |+if not res: 9 9 | pass 10 10 | #: E712 11 11 | if False == res: @@ -77,7 +77,7 @@ E712.py:11:4: E712 [*] Avoid equality comparisons to `False`; use `if not res:` 9 9 | pass 10 10 | #: E712 11 |-if False == res: - 11 |+if False is res: + 11 |+if not res: 12 12 | pass 13 13 | #: E712 14 14 | if res[1] == True: @@ -98,7 +98,7 @@ E712.py:14:4: E712 [*] Avoid equality comparisons to `True`; use `if res[1]:` fo 12 12 | pass 13 13 | #: E712 14 |-if res[1] == True: - 14 |+if res[1] is True: + 14 |+if res[1]: 15 15 | pass 16 16 | #: E712 17 17 | if res[1] != False: @@ -119,7 +119,7 @@ E712.py:17:4: E712 [*] Avoid inequality comparisons to `False`; use `if res[1]:` 15 15 | pass 16 16 | #: E712 17 |-if res[1] != False: - 17 |+if res[1] is not False: + 17 |+if res[1]: 18 18 | pass 19 19 | #: E712 20 20 | var = 1 if cond == True else -1 if cond == False else cond @@ -140,7 +140,7 @@ E712.py:20:12: E712 [*] Avoid equality comparisons to `True`; use `if cond:` for 18 18 | pass 19 19 | #: E712 20 |-var = 1 if cond == True else -1 if cond == False else cond - 20 |+var = 1 if cond is True else -1 if cond == False else cond + 20 |+var = 1 if cond else -1 if cond == False else cond 21 21 | #: E712 22 22 | if (True) == TrueElement or x == TrueElement: 23 23 | pass @@ -161,7 +161,7 @@ E712.py:20:36: E712 [*] Avoid equality comparisons to `False`; use `if not cond: 18 18 | pass 19 19 | #: E712 20 |-var = 1 if cond == True else -1 if cond == False else cond - 20 |+var = 1 if cond == True else -1 if cond is False else cond + 20 |+var = 1 if cond == True else -1 if not cond else cond 21 21 | #: E712 22 22 | if (True) == TrueElement or x == TrueElement: 23 23 | pass @@ -181,7 +181,7 @@ E712.py:22:4: E712 [*] Avoid equality comparisons to `True`; use `if TrueElement 20 20 | var = 1 if cond == True else -1 if cond == False else cond 21 21 | #: E712 22 |-if (True) == TrueElement or x == TrueElement: - 22 |+if (True) is TrueElement or x == TrueElement: + 22 |+if (TrueElement) or x == TrueElement: 23 23 | pass 24 24 | 25 25 | if res == True != False: @@ -241,7 +241,7 @@ E712.py:28:3: E712 [*] Avoid equality comparisons to `True`; use `if TrueElement 26 26 | pass 27 27 | 28 |-if(True) == TrueElement or x == TrueElement: - 28 |+if(True) is TrueElement or x == TrueElement: + 28 |+if(TrueElement) or x == TrueElement: 29 29 | pass 30 30 | 31 31 | if (yield i) == True: @@ -261,7 +261,7 @@ E712.py:31:4: E712 [*] Avoid equality comparisons to `True`; use `if yield i:` f 29 29 | pass 30 30 | 31 |-if (yield i) == True: - 31 |+if (yield i) is True: + 31 |+if (yield i): 32 32 | print("even") 33 33 | 34 34 | #: Okay diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap index 04831e37b3bd60..7adb6732e7bf38 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs -snapshot_kind: text --- constant_literals.py:4:4: F632 [*] Use `==` to compare constant literals | @@ -123,7 +122,7 @@ constant_literals.py:14:4: E712 [*] Avoid equality comparisons to `False`; use ` 12 12 | if False is "abc": # F632 (fix, but leaves behind unfixable E712) 13 13 | pass 14 |-if False == None: # E711, E712 (fix) - 14 |+if False is None: # E711, E712 (fix) + 14 |+if not None: # E711, E712 (fix) 15 15 | pass 16 16 | if None == False: # E711, E712 (fix) 17 17 | pass @@ -144,7 +143,7 @@ constant_literals.py:14:13: E711 [*] Comparison to `None` should be `cond is Non 12 12 | if False is "abc": # F632 (fix, but leaves behind unfixable E712) 13 13 | pass 14 |-if False == None: # E711, E712 (fix) - 14 |+if False is None: # E711, E712 (fix) + 14 |+if not None: # E711, E712 (fix) 15 15 | pass 16 16 | if None == False: # E711, E712 (fix) 17 17 | pass @@ -164,7 +163,7 @@ constant_literals.py:16:4: E711 [*] Comparison to `None` should be `cond is None 14 14 | if False == None: # E711, E712 (fix) 15 15 | pass 16 |-if None == False: # E711, E712 (fix) - 16 |+if None is False: # E711, E712 (fix) + 16 |+if not None: # E711, E712 (fix) 17 17 | pass 18 18 | 19 19 | named_var = [] @@ -184,7 +183,7 @@ constant_literals.py:16:4: E712 [*] Avoid equality comparisons to `False`; use ` 14 14 | if False == None: # E711, E712 (fix) 15 15 | pass 16 |-if None == False: # E711, E712 (fix) - 16 |+if None is False: # E711, E712 (fix) + 16 |+if not None: # E711, E712 (fix) 17 17 | pass 18 18 | 19 19 | named_var = [] From b5375529272820f036c5abce5a7e36d2581fb957 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Thu, 24 Apr 2025 01:43:41 +0900 Subject: [PATCH 0084/1161] [`airflow`] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR301`) (#17355) ## Summary Apply auto fixes to cases where the names have changed in Airflow 3 ## Test Plan Add `AIR301_names_fix.py` and `AIR301_provider_names_fix.py` test fixtures --- .../test/fixtures/airflow/AIR301_names.py | 100 +-- .../test/fixtures/airflow/AIR301_names_fix.py | 45 + .../airflow/AIR301_provider_names_fix.py | 54 ++ crates/ruff_linter/src/rules/airflow/mod.rs | 2 + .../src/rules/airflow/rules/removal_in_3.rs | 173 ++-- ...sts__AIR301_AIR301_class_attribute.py.snap | 41 +- ...irflow__tests__AIR301_AIR301_names.py.snap | 774 ++++++------------ ...ow__tests__AIR301_AIR301_names_fix.py.snap | 325 ++++++++ ...__AIR301_AIR301_provider_names_fix.py.snap | 254 ++++++ 9 files changed, 1063 insertions(+), 705 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_fix.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR301_provider_names_fix.py create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names_fix.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_provider_names_fix.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py index bd9606b91819e5..ccab278a0fc18b 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py @@ -9,9 +9,7 @@ PY311, PY312, ) -from airflow.api_connexion.security import requires_access, requires_access_dataset -from airflow.auth.managers.base_auth_manager import is_authorized_dataset -from airflow.auth.managers.models.resource_details import DatasetDetails +from airflow.api_connexion.security import requires_access from airflow.configuration import ( as_dict, get, @@ -24,32 +22,13 @@ ) from airflow.contrib.aws_athena_hook import AWSAthenaHook from airflow.datasets import DatasetAliasEvent -from airflow.datasets.manager import ( - DatasetManager, - dataset_manager, - resolve_dataset_manager, -) from airflow.hooks.base_hook import BaseHook -from airflow.lineage.hook import DatasetLineageInfo -from airflow.listeners.spec.dataset import on_dataset_changed, on_dataset_created -from airflow.metrics.validators import AllowListValidator, BlockListValidator from airflow.operators.subdag import SubDagOperator -from airflow.providers.amazon.aws.auth_manager.avp.entities import AvpEntities -from airflow.providers.amazon.aws.datasets import s3 -from airflow.providers.common.io.datasets import file as common_io_file -from airflow.providers.fab.auth_manager import fab_auth_manager -from airflow.providers.google.datasets import bigquery, gcs from airflow.providers.mysql.datasets import mysql -from airflow.providers.openlineage.utils.utils import ( - DatasetInfo, - translate_airflow_dataset, -) from airflow.providers.postgres.datasets import postgres from airflow.providers.trino.datasets import trino -from airflow.secrets.local_filesystem import LocalFilesystemBackend, load_connections -from airflow.security.permissions import RESOURCE_DATASET +from airflow.secrets.local_filesystem import LocalFilesystemBackend from airflow.sensors.base_sensor_operator import BaseSensorOperator -from airflow.timetables.simple import DatasetTriggeredTimetable from airflow.triggers.external_task import TaskStateTrigger from airflow.utils import dates from airflow.utils.dag_cycle_tester import test_cycle @@ -70,19 +49,15 @@ from airflow.utils.log import secrets_masker from airflow.utils.state import SHUTDOWN, terminating_states from airflow.utils.trigger_rule import TriggerRule -from airflow.www.auth import has_access, has_access_dataset +from airflow.www.auth import has_access from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key # airflow root PY36, PY37, PY38, PY39, PY310, PY311, PY312 -DatasetFromRoot() # airflow.api_connexion.security -requires_access, requires_access_dataset +requires_access -# airflow.auth.managers -is_authorized_dataset -DatasetDetails() # airflow.configuration get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set @@ -95,76 +70,17 @@ # airflow.datasets DatasetAliasEvent() -# airflow.datasets.manager -DatasetManager() -dataset_manager -resolve_dataset_manager # airflow.hooks BaseHook() -# airflow.lineage.hook -DatasetLineageInfo() - -# airflow.listeners.spec.dataset -on_dataset_changed -on_dataset_created - -# airflow.metrics.validators -AllowListValidator() -BlockListValidator() - - -# airflow.operators.branch_operator -BaseBranchOperator() - -# airflow.operators.dagrun_operator -TriggerDagRunLink() -TriggerDagRunOperator() - -# airflow.operators.email_operator -EmailOperator() - -# airflow.operators.latest_only_operator -LatestOnlyOperator() - -# airflow.operators.python_operator -BranchPythonOperator() -PythonOperator() -PythonVirtualenvOperator() -ShortCircuitOperator() # airflow.operators.subdag.* SubDagOperator() -# airflow.providers.amazon -AvpEntities.DATASET -s3.create_dataset -s3.convert_dataset_to_openlineage -s3.sanitize_uri - -# airflow.providers.common.io -common_io_file.convert_dataset_to_openlineage -common_io_file.create_dataset -common_io_file.sanitize_uri - -# airflow.providers.fab -fab_auth_manager.is_authorized_dataset - -# airflow.providers.google -bigquery.sanitize_uri - -gcs.create_dataset -gcs.sanitize_uri -gcs.convert_dataset_to_openlineage - # airflow.providers.mysql mysql.sanitize_uri -# airflow.providers.openlineage -DatasetInfo() -translate_airflow_dataset - # airflow.providers.postgres postgres.sanitize_uri @@ -174,18 +90,12 @@ # airflow.secrets # get_connection LocalFilesystemBackend() -load_connections -# airflow.security.permissions -RESOURCE_DATASET # airflow.sensors.base_sensor_operator BaseSensorOperator() -# airflow.timetables -DatasetTriggeredTimetable() - # airflow.triggers.external_task TaskStateTrigger() @@ -233,9 +143,9 @@ TriggerRule.DUMMY TriggerRule.NONE_FAILED_OR_SKIPPED + # airflow.www.auth has_access -has_access_dataset # airflow.www.utils get_sensitive_variables_fields diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_fix.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_fix.py new file mode 100644 index 00000000000000..d7c9414c5a75ca --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_fix.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from airflow.api_connexion.security import requires_access_dataset +from airflow.auth.managers.models.resource_details import ( + DatasetDetails, + is_authorized_dataset, +) +from airflow.datasets.manager import ( + DatasetManager, + dataset_manager, + resolve_dataset_manager, +) +from airflow.lineage.hook import DatasetLineageInfo +from airflow.metrics.validators import AllowListValidator, BlockListValidator +from airflow.secrets.local_filesystm import load_connections +from airflow.security.permissions import RESOURCE_DATASET +from airflow.www.auth import has_access_dataset + +requires_access_dataset() + +DatasetDetails() +is_authorized_dataset() + +DatasetManager() +dataset_manager() +resolve_dataset_manager() + +DatasetLineageInfo() + +AllowListValidator() +BlockListValidator() + +load_connections() + +RESOURCE_DATASET + +has_access_dataset() + +from airflow.listeners.spec.dataset import ( + on_dataset_changed, + on_dataset_created, +) + +on_dataset_created() +on_dataset_changed() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_provider_names_fix.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_provider_names_fix.py new file mode 100644 index 00000000000000..99b77a7ebe181b --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_provider_names_fix.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities import DATASET +from airflow.providers.amazon.aws.datasets.s3 import ( + convert_dataset_to_openlineage as s3_convert_dataset_to_openlineage, +) +from airflow.providers.amazon.aws.datasets.s3 import create_dataset as s3_create_dataset +from airflow.providers.common.io.dataset.file import ( + convert_dataset_to_openlineage as io_convert_dataset_to_openlineage, +) +from airflow.providers.common.io.dataset.file import create_dataset as io_create_dataset +from airflow.providers.fab.auth_manager.fab_auth_manager import is_authorized_dataset as fab_is_authorized_dataset +from airflow.providers.google.datasets.bigquery import ( + create_dataset as bigquery_create_dataset, +) +from airflow.providers.google.datasets.gcs import ( + convert_dataset_to_openlineage as gcs_convert_dataset_to_openlineage, +) +from airflow.providers.google.datasets.gcs import create_dataset as gcs_create_dataset +from airflow.providers.openlineage.utils.utils import ( + DatasetInfo, + translate_airflow_dataset, +) + +DATASET + +s3_create_dataset() +s3_convert_dataset_to_openlineage() + +io_create_dataset() +io_convert_dataset_to_openlineage() + +fab_is_authorized_dataset() + +# airflow.providers.google.datasets.bigquery +bigquery_create_dataset() +# airflow.providers.google.datasets.gcs +gcs_create_dataset() +gcs_convert_dataset_to_openlineage() +# airflow.providers.openlineage.utils.utils +DatasetInfo() +translate_airflow_dataset() +# +# airflow.secrets.local_filesystem +load_connections() +# +# airflow.security.permissions +RESOURCE_DATASET + +# airflow.timetables +DatasetTriggeredTimetable() +# +# airflow.www.auth +has_access_dataset diff --git a/crates/ruff_linter/src/rules/airflow/mod.rs b/crates/ruff_linter/src/rules/airflow/mod.rs index 69a35b5dd3d264..744712b5d144e0 100644 --- a/crates/ruff_linter/src/rules/airflow/mod.rs +++ b/crates/ruff_linter/src/rules/airflow/mod.rs @@ -17,6 +17,8 @@ mod tests { #[test_case(Rule::AirflowDagNoScheduleArgument, Path::new("AIR002.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_args.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_names.py"))] + #[test_case(Rule::Airflow3Removal, Path::new("AIR301_names_fix.py"))] + #[test_case(Rule::Airflow3Removal, Path::new("AIR301_provider_names_fix.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_names_try.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_class_attribute.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_airflow_plugin.py"))] diff --git a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs index b73bb11e519cce..a70403510323d8 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs @@ -582,14 +582,16 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { // airflow.auth.managers ["airflow", "auth", "managers", "models", "resource_details", "DatasetDetails"] => { - Replacement::Name( - "airflow.api_fastapi.auth.managers.models.resource_details.AssetDetails", - ) + Replacement::AutoImport { + module: "airflow.api_fastapi.auth.managers.models.resource_details", + name: "AssetDetails", + } } ["airflow", "auth", "managers", "base_auth_manager", "is_authorized_dataset"] => { - Replacement::Name( - "airflow.api_fastapi.auth.managers.base_auth_manager.is_authorized_asset", - ) + Replacement::AutoImport { + module: "airflow.api_fastapi.auth.managers.base_auth_manager", + name: "is_authorized_asset", + } } // airflow.configuration @@ -606,9 +608,18 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { // airflow.datasets.manager ["airflow", "datasets", "manager", rest] => match *rest { - "DatasetManager" => Replacement::Name("airflow.assets.manager.AssetManager"), - "dataset_manager" => Replacement::Name("airflow.assets.manager.asset_manager"), - "resolve_dataset_manager" => Replacement::Name("airflow.assets.resolve_asset_manager"), + "DatasetManager" => Replacement::AutoImport { + module: "airflow.assets.manager", + name: "AssetManager", + }, + "dataset_manager" => Replacement::AutoImport { + module: "airflow.assets.manager", + name: "asset_manager", + }, + "resolve_dataset_manager" => Replacement::AutoImport { + module: "airflow.assets.manager", + name: "resolve_asset_manager", + }, _ => return, }, // airflow.datasets @@ -620,30 +631,35 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { } // airflow.lineage.hook - ["airflow", "lineage", "hook", "DatasetLineageInfo"] => { - Replacement::Name("airflow.lineage.hook.AssetLineageInfo") - } + ["airflow", "lineage", "hook", "DatasetLineageInfo"] => Replacement::AutoImport { + module: "airflow.lineage.hook", + name: "AssetLineageInfo", + }, // airflow.listeners.spec // TODO: this is removed ["airflow", "listeners", "spec", "dataset", rest] => match *rest { - "on_dataset_created" => { - Replacement::Name("airflow.listeners.spec.asset.on_asset_created") - } - "on_dataset_changed" => { - Replacement::Name("airflow.listeners.spec.asset.on_asset_changed") - } + "on_dataset_created" => Replacement::AutoImport { + module: "airflow.listeners.spec.asset", + name: "on_asset_created", + }, + "on_dataset_changed" => Replacement::AutoImport { + module: "airflow.listeners.spec.asset", + name: "on_asset_changed", + }, _ => return, }, // airflow.metrics.validators ["airflow", "metrics", "validators", rest] => match *rest { - "AllowListValidator" => { - Replacement::Name("airflow.metrics.validators.PatternAllowListValidator") - } - "BlockListValidator" => { - Replacement::Name("airflow.metrics.validators.PatternBlockListValidator") - } + "AllowListValidator" => Replacement::AutoImport { + module: "airflow.metrics.validators", + name: "PatternAllowListValidator", + }, + "BlockListValidator" => Replacement::AutoImport { + module: "airflow.metrics.validators", + name: "PatternBlockListValidator", + }, _ => return, }, @@ -658,14 +674,16 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { } // airflow.secrets - ["airflow", "secrets", "local_filesystem", "load_connections"] => { - Replacement::Name("airflow.secrets.local_filesystem.load_connections_dict") - } + ["airflow", "secrets", "local_filesystem", "load_connections"] => Replacement::AutoImport { + module: "airflow.secrets.local_filesystem", + name: "load_connections_dict", + }, // airflow.security - ["airflow", "security", "permissions", "RESOURCE_DATASET"] => { - Replacement::Name("airflow.security.permissions.RESOURCE_ASSET") - } + ["airflow", "security", "permissions", "RESOURCE_DATASET"] => Replacement::AutoImport { + module: "airflow.security.permissions", + name: "RESOURCE_ASSET", + }, // airflow.sensors ["airflow", "sensors", "base_sensor_operator", "BaseSensorOperator"] => { @@ -674,7 +692,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { // airflow.timetables ["airflow", "timetables", "simple", "DatasetTriggeredTimetable"] => { - Replacement::Name("airflow.timetables.simple.AssetTriggeredTimetable") + Replacement::AutoImport { + module: "airflow.timetables.simple", + name: "AssetTriggeredTimetable", + } } // airflow.triggers @@ -733,9 +754,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { ["airflow", "www", "auth", "has_access"] => { Replacement::Name("airflow.www.auth.has_access_*") } - ["airflow", "www", "auth", "has_access_dataset"] => { - Replacement::Name("airflow.www.auth.has_access_dataset") - } + ["airflow", "www", "auth", "has_access_dataset"] => Replacement::AutoImport { + module: "airflow.www.auth", + name: "has_access_asset", + }, ["airflow", "www", "utils", "get_sensitive_variables_fields"] => { Replacement::Name("airflow.utils.log.secrets_masker.get_sensitive_variables_fields") } @@ -744,31 +766,38 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { } // airflow.providers.amazon - ["airflow", "providers", "amazon", "aws", rest @ ..] => match &rest { - ["datasets", "s3", "create_dataset"] => { - Replacement::Name("airflow.providers.amazon.aws.assets.s3.create_asset") - } - ["datasets", "s3", "convert_dataset_to_openlineage"] => Replacement::Name( - "airflow.providers.amazon.aws.assets.s3.convert_asset_to_openlineage", - ), - ["datasets", "s3", "sanitize_uri"] => { + ["airflow", "providers", "amazon", "aws", "datasets", "s3", rest] => match *rest { + "create_dataset" => Replacement::AutoImport { + module: "airflow.providers.amazon.aws.assets.s3", + name: "create_asset", + }, + "convert_dataset_to_openlineage" => Replacement::AutoImport { + module: "airflow.providers.amazon.aws.assets.s3", + name: "convert_asset_to_openlineage", + }, + "sanitize_uri" => { Replacement::Name("airflow.providers.amazon.aws.assets.s3.sanitize_uri") } - ["auth_manager", "avp", "entities", "AvpEntities", "DATASET"] => Replacement::Name( - "airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities.ASSET", - ), _ => return, }, + ["airflow", "providers", "amazon", "aws", "auth_manager", "avp", "entities", "AvpEntities", "DATASET"] => { + Replacement::AutoImport { + module: "airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities", + name: "ASSET", + } + } // airflow.providers.common.io // airflow.providers.common.io.datasets.file ["airflow", "providers", "common", "io", "datasets", "file", rest] => match *rest { - "create_dataset" => { - Replacement::Name("airflow.providers.common.io.assets.file.create_asset") - } - "convert_dataset_to_openlineage" => Replacement::Name( - "airflow.providers.common.io.assets.file.convert_asset_to_openlineage", - ), + "create_dataset" => Replacement::AutoImport { + module: "airflow.providers.common.io.assets.file", + name: "create_asset", + }, + "convert_dataset_to_openlineage" => Replacement::AutoImport { + module: "airflow.providers.common.io.assets.file", + name: "convert_asset_to_openlineage", + }, "sanitize_uri" => { Replacement::Name("airflow.providers.common.io.assets.file.sanitize_uri") } @@ -777,23 +806,27 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { // airflow.providers.fab ["airflow", "providers", "fab", "auth_manager", "fab_auth_manager", "is_authorized_dataset"] => { - Replacement::Name( - "airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_asset", - ) + Replacement::AutoImport { + module: "airflow.providers.fab.auth_manager.fab_auth_manager", + name: "is_authorized_asset", + } } // airflow.providers.google // airflow.providers.google.datasets ["airflow", "providers", "google", "datasets", rest @ ..] => match &rest { - ["bigquery", "create_dataset"] => { - Replacement::Name("airflow.providers.google.assets.bigquery.create_asset") - } - ["gcs", "create_dataset"] => { - Replacement::Name("airflow.providers.google.assets.gcs.create_asset") - } - ["gcs", "convert_dataset_to_openlineage"] => Replacement::Name( - "airflow.providers.google.assets.gcs.convert_asset_to_openlineage", - ), + ["bigquery", "create_dataset"] => Replacement::AutoImport { + module: "airflow.providers.google.assets.bigquery", + name: "create_asset", + }, + ["gcs", "create_dataset"] => Replacement::AutoImport { + module: "airflow.providers.google.assets.gcs", + name: "create_asset", + }, + ["gcs", "convert_dataset_to_openlineage"] => Replacement::AutoImport { + module: "airflow.providers.google.assets.gcs", + name: "convert_asset_to_openlineage", + }, ["gcs", "sanitize_uri"] => { Replacement::Name("airflow.providers.google.assets.gcs.sanitize_uri") } @@ -813,13 +846,15 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { // airflow.providers.openlineage // airflow.providers.openlineage.utils.utils ["airflow", "providers", "openlineage", "utils", "utils", rest] => match *rest { - "DatasetInfo" => { - Replacement::Name("airflow.providers.openlineage.utils.utils.AssetInfo") - } + "DatasetInfo" => Replacement::AutoImport { + module: "airflow.providers.openlineage.utils.utils", + name: "AssetInfo", + }, - "translate_airflow_dataset" => Replacement::Name( - "airflow.providers.openlineage.utils.utils.translate_airflow_asset", - ), + "translate_airflow_dataset" => Replacement::AutoImport { + module: "airflow.providers.openlineage.utils.utils", + name: "translate_airflow_asset", + }, _ => return, }, diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap index 7d69175385d0b0..0ae63a796b1124 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_class_attribute.py.snap @@ -163,7 +163,7 @@ AIR301_class_attribute.py:39:25: AIR301 [*] `iter_dataset_aliases` is removed in 41 41 | # airflow.datasets.manager 42 42 | dm = DatasetManager() -AIR301_class_attribute.py:42:6: AIR301 `airflow.datasets.manager.DatasetManager` is removed in Airflow 3.0 +AIR301_class_attribute.py:42:6: AIR301 [*] `airflow.datasets.manager.DatasetManager` is removed in Airflow 3.0 | 41 | # airflow.datasets.manager 42 | dm = DatasetManager() @@ -173,6 +173,24 @@ AIR301_class_attribute.py:42:6: AIR301 `airflow.datasets.manager.DatasetManager` | = help: Use `airflow.assets.manager.AssetManager` instead +ℹ Safe fix +19 19 | from airflow.providers_manager import ProvidersManager +20 20 | from airflow.secrets.base_secrets import BaseSecretsBackend +21 21 | from airflow.secrets.local_filesystem import LocalFilesystemBackend + 22 |+from airflow.assets.manager import AssetManager +22 23 | +23 24 | # airflow.Dataset +24 25 | dataset_from_root = DatasetFromRoot() +-------------------------------------------------------------------------------- +39 40 | any_to_test_method_call.iter_dataset_aliases() +40 41 | +41 42 | # airflow.datasets.manager +42 |-dm = DatasetManager() + 43 |+dm = AssetManager() +43 44 | dm.register_dataset_change() +44 45 | dm.create_datasets() +45 46 | dm.notify_dataset_created() + AIR301_class_attribute.py:43:4: AIR301 [*] `register_dataset_change` is removed in Airflow 3.0 | 41 | # airflow.datasets.manager @@ -277,7 +295,7 @@ AIR301_class_attribute.py:47:4: AIR301 [*] `notify_dataset_alias_created` is rem 49 49 | # airflow.lineage.hook 50 50 | dl_info = DatasetLineageInfo() -AIR301_class_attribute.py:50:11: AIR301 `airflow.lineage.hook.DatasetLineageInfo` is removed in Airflow 3.0 +AIR301_class_attribute.py:50:11: AIR301 [*] `airflow.lineage.hook.DatasetLineageInfo` is removed in Airflow 3.0 | 49 | # airflow.lineage.hook 50 | dl_info = DatasetLineageInfo() @@ -286,6 +304,25 @@ AIR301_class_attribute.py:50:11: AIR301 `airflow.lineage.hook.DatasetLineageInfo | = help: Use `airflow.lineage.hook.AssetLineageInfo` instead +ℹ Safe fix +9 9 | DatasetAny, +10 10 | ) +11 11 | from airflow.datasets.manager import DatasetManager +12 |-from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector + 12 |+from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector, AssetLineageInfo +13 13 | from airflow.providers.amazon.auth_manager.aws_auth_manager import AwsAuthManager +14 14 | from airflow.providers.apache.beam.hooks import BeamHook, NotAir302HookError +15 15 | from airflow.providers.google.cloud.secrets.secret_manager import ( +-------------------------------------------------------------------------------- +47 47 | dm.notify_dataset_alias_created() +48 48 | +49 49 | # airflow.lineage.hook +50 |-dl_info = DatasetLineageInfo() + 50 |+dl_info = AssetLineageInfo() +51 51 | dl_info.dataset +52 52 | +53 53 | hlc = HookLineageCollector() + AIR301_class_attribute.py:51:9: AIR301 [*] `dataset` is removed in Airflow 3.0 | 49 | # airflow.lineage.hook diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap index 0943b18e5980d3..8c58bcb351eff8 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap @@ -1,741 +1,437 @@ --- source: crates/ruff_linter/src/rules/airflow/mod.rs --- -AIR301_names.py:77:1: AIR301 `airflow.PY36` is removed in Airflow 3.0 +AIR301_names.py:56:1: AIR301 `airflow.PY36` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +55 | # airflow root +56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^ AIR301 -78 | DatasetFromRoot() +57 | +58 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:7: AIR301 `airflow.PY37` is removed in Airflow 3.0 +AIR301_names.py:56:7: AIR301 `airflow.PY37` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +55 | # airflow root +56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^ AIR301 -78 | DatasetFromRoot() +57 | +58 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:13: AIR301 `airflow.PY38` is removed in Airflow 3.0 +AIR301_names.py:56:13: AIR301 `airflow.PY38` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +55 | # airflow root +56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^ AIR301 -78 | DatasetFromRoot() +57 | +58 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:19: AIR301 `airflow.PY39` is removed in Airflow 3.0 +AIR301_names.py:56:19: AIR301 `airflow.PY39` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +55 | # airflow root +56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^ AIR301 -78 | DatasetFromRoot() +57 | +58 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:25: AIR301 `airflow.PY310` is removed in Airflow 3.0 +AIR301_names.py:56:25: AIR301 `airflow.PY310` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +55 | # airflow root +56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^^ AIR301 -78 | DatasetFromRoot() +57 | +58 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:32: AIR301 `airflow.PY311` is removed in Airflow 3.0 +AIR301_names.py:56:32: AIR301 `airflow.PY311` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +55 | # airflow root +56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^^ AIR301 -78 | DatasetFromRoot() +57 | +58 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:77:39: AIR301 `airflow.PY312` is removed in Airflow 3.0 +AIR301_names.py:56:39: AIR301 `airflow.PY312` is removed in Airflow 3.0 | -76 | # airflow root -77 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 +55 | # airflow root +56 | PY36, PY37, PY38, PY39, PY310, PY311, PY312 | ^^^^^ AIR301 -78 | DatasetFromRoot() +57 | +58 | # airflow.api_connexion.security | = help: Use `sys.version_info` instead -AIR301_names.py:81:1: AIR301 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0 +AIR301_names.py:59:1: AIR301 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0 | -80 | # airflow.api_connexion.security -81 | requires_access, requires_access_dataset +58 | # airflow.api_connexion.security +59 | requires_access | ^^^^^^^^^^^^^^^ AIR301 -82 | -83 | # airflow.auth.managers | = help: Use `airflow.api_connexion.security.requires_access_*` instead -AIR301_names.py:81:18: AIR301 [*] `airflow.api_connexion.security.requires_access_dataset` is removed in Airflow 3.0 - | -80 | # airflow.api_connexion.security -81 | requires_access, requires_access_dataset - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -82 | -83 | # airflow.auth.managers - | - = help: Use `airflow.api_connexion.security.requires_access_asset` instead - -ℹ Safe fix -9 9 | PY311, -10 10 | PY312, -11 11 | ) -12 |-from airflow.api_connexion.security import requires_access, requires_access_dataset - 12 |+from airflow.api_connexion.security import requires_access, requires_access_dataset, requires_access_asset -13 13 | from airflow.auth.managers.base_auth_manager import is_authorized_dataset -14 14 | from airflow.auth.managers.models.resource_details import DatasetDetails -15 15 | from airflow.configuration import ( --------------------------------------------------------------------------------- -78 78 | DatasetFromRoot() -79 79 | -80 80 | # airflow.api_connexion.security -81 |-requires_access, requires_access_dataset - 81 |+requires_access, requires_access_asset -82 82 | -83 83 | # airflow.auth.managers -84 84 | is_authorized_dataset - -AIR301_names.py:84:1: AIR301 `airflow.auth.managers.base_auth_manager.is_authorized_dataset` is removed in Airflow 3.0 - | -83 | # airflow.auth.managers -84 | is_authorized_dataset - | ^^^^^^^^^^^^^^^^^^^^^ AIR301 -85 | DatasetDetails() - | - = help: Use `airflow.api_fastapi.auth.managers.base_auth_manager.is_authorized_asset` instead - -AIR301_names.py:85:1: AIR301 `airflow.auth.managers.models.resource_details.DatasetDetails` is removed in Airflow 3.0 - | -83 | # airflow.auth.managers -84 | is_authorized_dataset -85 | DatasetDetails() - | ^^^^^^^^^^^^^^ AIR301 -86 | -87 | # airflow.configuration - | - = help: Use `airflow.api_fastapi.auth.managers.models.resource_details.AssetDetails` instead - -AIR301_names.py:88:1: AIR301 `airflow.configuration.get` is removed in Airflow 3.0 +AIR301_names.py:63:1: AIR301 `airflow.configuration.get` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +62 | # airflow.configuration +63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set | ^^^ AIR301 | = help: Use `airflow.configuration.conf.get` instead -AIR301_names.py:88:6: AIR301 `airflow.configuration.getboolean` is removed in Airflow 3.0 +AIR301_names.py:63:6: AIR301 `airflow.configuration.getboolean` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +62 | # airflow.configuration +63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set | ^^^^^^^^^^ AIR301 | = help: Use `airflow.configuration.conf.getboolean` instead -AIR301_names.py:88:18: AIR301 `airflow.configuration.getfloat` is removed in Airflow 3.0 +AIR301_names.py:63:18: AIR301 `airflow.configuration.getfloat` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +62 | # airflow.configuration +63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set | ^^^^^^^^ AIR301 | = help: Use `airflow.configuration.conf.getfloat` instead -AIR301_names.py:88:28: AIR301 `airflow.configuration.getint` is removed in Airflow 3.0 +AIR301_names.py:63:28: AIR301 `airflow.configuration.getint` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +62 | # airflow.configuration +63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set | ^^^^^^ AIR301 | = help: Use `airflow.configuration.conf.getint` instead -AIR301_names.py:88:36: AIR301 `airflow.configuration.has_option` is removed in Airflow 3.0 +AIR301_names.py:63:36: AIR301 `airflow.configuration.has_option` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +62 | # airflow.configuration +63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set | ^^^^^^^^^^ AIR301 | = help: Use `airflow.configuration.conf.has_option` instead -AIR301_names.py:88:48: AIR301 `airflow.configuration.remove_option` is removed in Airflow 3.0 +AIR301_names.py:63:48: AIR301 `airflow.configuration.remove_option` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +62 | # airflow.configuration +63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set | ^^^^^^^^^^^^^ AIR301 | = help: Use `airflow.configuration.conf.remove_option` instead -AIR301_names.py:88:63: AIR301 `airflow.configuration.as_dict` is removed in Airflow 3.0 +AIR301_names.py:63:63: AIR301 `airflow.configuration.as_dict` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +62 | # airflow.configuration +63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set | ^^^^^^^ AIR301 | = help: Use `airflow.configuration.conf.as_dict` instead -AIR301_names.py:88:72: AIR301 `airflow.configuration.set` is removed in Airflow 3.0 +AIR301_names.py:63:72: AIR301 `airflow.configuration.set` is removed in Airflow 3.0 | -87 | # airflow.configuration -88 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set +62 | # airflow.configuration +63 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set | ^^^ AIR301 | = help: Use `airflow.configuration.conf.set` instead -AIR301_names.py:92:1: AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0; The whole `airflow.contrib` module has been removed. +AIR301_names.py:67:1: AIR301 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0; The whole `airflow.contrib` module has been removed. | -91 | # airflow.contrib.* -92 | AWSAthenaHook() +66 | # airflow.contrib.* +67 | AWSAthenaHook() | ^^^^^^^^^^^^^ AIR301 | -AIR301_names.py:96:1: AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0 +AIR301_names.py:71:1: AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0 | -95 | # airflow.datasets -96 | DatasetAliasEvent() +70 | # airflow.datasets +71 | DatasetAliasEvent() | ^^^^^^^^^^^^^^^^^ AIR301 -97 | -98 | # airflow.datasets.manager | -AIR301_names.py:99:1: AIR301 `airflow.datasets.manager.DatasetManager` is removed in Airflow 3.0 - | - 98 | # airflow.datasets.manager - 99 | DatasetManager() - | ^^^^^^^^^^^^^^ AIR301 -100 | dataset_manager -101 | resolve_dataset_manager - | - = help: Use `airflow.assets.manager.AssetManager` instead - -AIR301_names.py:100:1: AIR301 `airflow.datasets.manager.dataset_manager` is removed in Airflow 3.0 - | - 98 | # airflow.datasets.manager - 99 | DatasetManager() -100 | dataset_manager - | ^^^^^^^^^^^^^^^ AIR301 -101 | resolve_dataset_manager - | - = help: Use `airflow.assets.manager.asset_manager` instead - -AIR301_names.py:101:1: AIR301 `airflow.datasets.manager.resolve_dataset_manager` is removed in Airflow 3.0 - | - 99 | DatasetManager() -100 | dataset_manager -101 | resolve_dataset_manager - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -102 | -103 | # airflow.hooks - | - = help: Use `airflow.assets.resolve_asset_manager` instead - -AIR301_names.py:104:1: AIR301 `airflow.hooks.base_hook.BaseHook` is removed in Airflow 3.0 - | -103 | # airflow.hooks -104 | BaseHook() - | ^^^^^^^^ AIR301 -105 | -106 | # airflow.lineage.hook - | - = help: Use `airflow.hooks.base.BaseHook` instead - -AIR301_names.py:107:1: AIR301 `airflow.lineage.hook.DatasetLineageInfo` is removed in Airflow 3.0 - | -106 | # airflow.lineage.hook -107 | DatasetLineageInfo() - | ^^^^^^^^^^^^^^^^^^ AIR301 -108 | -109 | # airflow.listeners.spec.dataset - | - = help: Use `airflow.lineage.hook.AssetLineageInfo` instead - -AIR301_names.py:110:1: AIR301 `airflow.listeners.spec.dataset.on_dataset_changed` is removed in Airflow 3.0 - | -109 | # airflow.listeners.spec.dataset -110 | on_dataset_changed - | ^^^^^^^^^^^^^^^^^^ AIR301 -111 | on_dataset_created - | - = help: Use `airflow.listeners.spec.asset.on_asset_changed` instead - -AIR301_names.py:111:1: AIR301 `airflow.listeners.spec.dataset.on_dataset_created` is removed in Airflow 3.0 - | -109 | # airflow.listeners.spec.dataset -110 | on_dataset_changed -111 | on_dataset_created - | ^^^^^^^^^^^^^^^^^^ AIR301 -112 | -113 | # airflow.metrics.validators - | - = help: Use `airflow.listeners.spec.asset.on_asset_created` instead - -AIR301_names.py:114:1: AIR301 `airflow.metrics.validators.AllowListValidator` is removed in Airflow 3.0 - | -113 | # airflow.metrics.validators -114 | AllowListValidator() - | ^^^^^^^^^^^^^^^^^^ AIR301 -115 | BlockListValidator() - | - = help: Use `airflow.metrics.validators.PatternAllowListValidator` instead - -AIR301_names.py:115:1: AIR301 `airflow.metrics.validators.BlockListValidator` is removed in Airflow 3.0 - | -113 | # airflow.metrics.validators -114 | AllowListValidator() -115 | BlockListValidator() - | ^^^^^^^^^^^^^^^^^^ AIR301 - | - = help: Use `airflow.metrics.validators.PatternBlockListValidator` instead - -AIR301_names.py:138:1: AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0; The whole `airflow.subdag` module has been removed. - | -137 | # airflow.operators.subdag.* -138 | SubDagOperator() - | ^^^^^^^^^^^^^^ AIR301 -139 | -140 | # airflow.providers.amazon - | - -AIR301_names.py:141:13: AIR301 `airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities.DATASET` is removed in Airflow 3.0 - | -140 | # airflow.providers.amazon -141 | AvpEntities.DATASET - | ^^^^^^^ AIR301 -142 | s3.create_dataset -143 | s3.convert_dataset_to_openlineage - | - = help: Use `airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities.ASSET` instead - -AIR301_names.py:142:4: AIR301 `airflow.providers.amazon.aws.datasets.s3.create_dataset` is removed in Airflow 3.0 - | -140 | # airflow.providers.amazon -141 | AvpEntities.DATASET -142 | s3.create_dataset - | ^^^^^^^^^^^^^^ AIR301 -143 | s3.convert_dataset_to_openlineage -144 | s3.sanitize_uri - | - = help: Use `airflow.providers.amazon.aws.assets.s3.create_asset` instead - -AIR301_names.py:143:4: AIR301 `airflow.providers.amazon.aws.datasets.s3.convert_dataset_to_openlineage` is removed in Airflow 3.0 - | -141 | AvpEntities.DATASET -142 | s3.create_dataset -143 | s3.convert_dataset_to_openlineage - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -144 | s3.sanitize_uri - | - = help: Use `airflow.providers.amazon.aws.assets.s3.convert_asset_to_openlineage` instead - -AIR301_names.py:144:4: AIR301 `airflow.providers.amazon.aws.datasets.s3.sanitize_uri` is removed in Airflow 3.0 - | -142 | s3.create_dataset -143 | s3.convert_dataset_to_openlineage -144 | s3.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -145 | -146 | # airflow.providers.common.io - | - = help: Use `airflow.providers.amazon.aws.assets.s3.sanitize_uri` instead - -AIR301_names.py:147:16: AIR301 `airflow.providers.common.io.datasets.file.convert_dataset_to_openlineage` is removed in Airflow 3.0 - | -146 | # airflow.providers.common.io -147 | common_io_file.convert_dataset_to_openlineage - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -148 | common_io_file.create_dataset -149 | common_io_file.sanitize_uri - | - = help: Use `airflow.providers.common.io.assets.file.convert_asset_to_openlineage` instead - -AIR301_names.py:148:16: AIR301 `airflow.providers.common.io.datasets.file.create_dataset` is removed in Airflow 3.0 - | -146 | # airflow.providers.common.io -147 | common_io_file.convert_dataset_to_openlineage -148 | common_io_file.create_dataset - | ^^^^^^^^^^^^^^ AIR301 -149 | common_io_file.sanitize_uri - | - = help: Use `airflow.providers.common.io.assets.file.create_asset` instead - -AIR301_names.py:149:16: AIR301 `airflow.providers.common.io.datasets.file.sanitize_uri` is removed in Airflow 3.0 - | -147 | common_io_file.convert_dataset_to_openlineage -148 | common_io_file.create_dataset -149 | common_io_file.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -150 | -151 | # airflow.providers.fab - | - = help: Use `airflow.providers.common.io.assets.file.sanitize_uri` instead - -AIR301_names.py:152:18: AIR301 `airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_dataset` is removed in Airflow 3.0 - | -151 | # airflow.providers.fab -152 | fab_auth_manager.is_authorized_dataset - | ^^^^^^^^^^^^^^^^^^^^^ AIR301 -153 | -154 | # airflow.providers.google - | - = help: Use `airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_asset` instead - -AIR301_names.py:157:5: AIR301 `airflow.providers.google.datasets.gcs.create_dataset` is removed in Airflow 3.0 - | -155 | bigquery.sanitize_uri -156 | -157 | gcs.create_dataset - | ^^^^^^^^^^^^^^ AIR301 -158 | gcs.sanitize_uri -159 | gcs.convert_dataset_to_openlineage - | - = help: Use `airflow.providers.google.assets.gcs.create_asset` instead - -AIR301_names.py:158:5: AIR301 `airflow.providers.google.datasets.gcs.sanitize_uri` is removed in Airflow 3.0 - | -157 | gcs.create_dataset -158 | gcs.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -159 | gcs.convert_dataset_to_openlineage - | - = help: Use `airflow.providers.google.assets.gcs.sanitize_uri` instead - -AIR301_names.py:159:5: AIR301 `airflow.providers.google.datasets.gcs.convert_dataset_to_openlineage` is removed in Airflow 3.0 - | -157 | gcs.create_dataset -158 | gcs.sanitize_uri -159 | gcs.convert_dataset_to_openlineage - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -160 | -161 | # airflow.providers.mysql - | - = help: Use `airflow.providers.google.assets.gcs.convert_asset_to_openlineage` instead - -AIR301_names.py:162:7: AIR301 `airflow.providers.mysql.datasets.mysql.sanitize_uri` is removed in Airflow 3.0 - | -161 | # airflow.providers.mysql -162 | mysql.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -163 | -164 | # airflow.providers.openlineage - | - = help: Use `airflow.providers.mysql.assets.mysql.sanitize_uri` instead - -AIR301_names.py:165:1: AIR301 `airflow.providers.openlineage.utils.utils.DatasetInfo` is removed in Airflow 3.0 - | -164 | # airflow.providers.openlineage -165 | DatasetInfo() - | ^^^^^^^^^^^ AIR301 -166 | translate_airflow_dataset - | - = help: Use `airflow.providers.openlineage.utils.utils.AssetInfo` instead - -AIR301_names.py:166:1: AIR301 `airflow.providers.openlineage.utils.utils.translate_airflow_dataset` is removed in Airflow 3.0 - | -164 | # airflow.providers.openlineage -165 | DatasetInfo() -166 | translate_airflow_dataset - | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -167 | -168 | # airflow.providers.postgres - | - = help: Use `airflow.providers.openlineage.utils.utils.translate_airflow_asset` instead - -AIR301_names.py:169:10: AIR301 `airflow.providers.postgres.datasets.postgres.sanitize_uri` is removed in Airflow 3.0 - | -168 | # airflow.providers.postgres -169 | postgres.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -170 | -171 | # airflow.providers.trino - | - = help: Use `airflow.providers.postgres.assets.postgres.sanitize_uri` instead +AIR301_names.py:75:1: AIR301 `airflow.hooks.base_hook.BaseHook` is removed in Airflow 3.0 + | +74 | # airflow.hooks +75 | BaseHook() + | ^^^^^^^^ AIR301 + | + = help: Use `airflow.hooks.base.BaseHook` instead -AIR301_names.py:172:7: AIR301 `airflow.providers.trino.datasets.trino.sanitize_uri` is removed in Airflow 3.0 - | -171 | # airflow.providers.trino -172 | trino.sanitize_uri - | ^^^^^^^^^^^^ AIR301 -173 | -174 | # airflow.secrets - | - = help: Use `airflow.providers.trino.assets.trino.sanitize_uri` instead +AIR301_names.py:79:1: AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0; The whole `airflow.subdag` module has been removed. + | +78 | # airflow.operators.subdag.* +79 | SubDagOperator() + | ^^^^^^^^^^^^^^ AIR301 +80 | +81 | # airflow.providers.mysql + | -AIR301_names.py:177:1: AIR301 `airflow.secrets.local_filesystem.load_connections` is removed in Airflow 3.0 - | -175 | # get_connection -176 | LocalFilesystemBackend() -177 | load_connections - | ^^^^^^^^^^^^^^^^ AIR301 -178 | -179 | # airflow.security.permissions - | - = help: Use `airflow.secrets.local_filesystem.load_connections_dict` instead +AIR301_names.py:82:7: AIR301 `airflow.providers.mysql.datasets.mysql.sanitize_uri` is removed in Airflow 3.0 + | +81 | # airflow.providers.mysql +82 | mysql.sanitize_uri + | ^^^^^^^^^^^^ AIR301 +83 | +84 | # airflow.providers.postgres + | + = help: Use `airflow.providers.mysql.assets.mysql.sanitize_uri` instead -AIR301_names.py:180:1: AIR301 `airflow.security.permissions.RESOURCE_DATASET` is removed in Airflow 3.0 - | -179 | # airflow.security.permissions -180 | RESOURCE_DATASET - | ^^^^^^^^^^^^^^^^ AIR301 -181 | -182 | # airflow.sensors.base_sensor_operator - | - = help: Use `airflow.security.permissions.RESOURCE_ASSET` instead +AIR301_names.py:85:10: AIR301 `airflow.providers.postgres.datasets.postgres.sanitize_uri` is removed in Airflow 3.0 + | +84 | # airflow.providers.postgres +85 | postgres.sanitize_uri + | ^^^^^^^^^^^^ AIR301 +86 | +87 | # airflow.providers.trino + | + = help: Use `airflow.providers.postgres.assets.postgres.sanitize_uri` instead -AIR301_names.py:183:1: AIR301 `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0 - | -182 | # airflow.sensors.base_sensor_operator -183 | BaseSensorOperator() - | ^^^^^^^^^^^^^^^^^^ AIR301 - | - = help: Use `airflow.sdk.bases.sensor.BaseSensorOperator` instead +AIR301_names.py:88:7: AIR301 `airflow.providers.trino.datasets.trino.sanitize_uri` is removed in Airflow 3.0 + | +87 | # airflow.providers.trino +88 | trino.sanitize_uri + | ^^^^^^^^^^^^ AIR301 +89 | +90 | # airflow.secrets + | + = help: Use `airflow.providers.trino.assets.trino.sanitize_uri` instead -AIR301_names.py:187:1: AIR301 `airflow.timetables.simple.DatasetTriggeredTimetable` is removed in Airflow 3.0 - | -186 | # airflow.timetables -187 | DatasetTriggeredTimetable() - | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -188 | -189 | # airflow.triggers.external_task - | - = help: Use `airflow.timetables.simple.AssetTriggeredTimetable` instead +AIR301_names.py:96:1: AIR301 `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0 + | +95 | # airflow.sensors.base_sensor_operator +96 | BaseSensorOperator() + | ^^^^^^^^^^^^^^^^^^ AIR301 + | + = help: Use `airflow.sdk.bases.sensor.BaseSensorOperator` instead -AIR301_names.py:190:1: AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0 +AIR301_names.py:100:1: AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0 | -189 | # airflow.triggers.external_task -190 | TaskStateTrigger() + 99 | # airflow.triggers.external_task +100 | TaskStateTrigger() | ^^^^^^^^^^^^^^^^ AIR301 -191 | -192 | # airflow.utils.date +101 | +102 | # airflow.utils.date | -AIR301_names.py:193:7: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0 +AIR301_names.py:103:7: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0 | -192 | # airflow.utils.date -193 | dates.date_range +102 | # airflow.utils.date +103 | dates.date_range | ^^^^^^^^^^ AIR301 -194 | dates.days_ago +104 | dates.days_ago | -AIR301_names.py:194:7: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0 +AIR301_names.py:104:7: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0 | -192 | # airflow.utils.date -193 | dates.date_range -194 | dates.days_ago +102 | # airflow.utils.date +103 | dates.date_range +104 | dates.days_ago | ^^^^^^^^ AIR301 -195 | -196 | date_range +105 | +106 | date_range | = help: Use `pendulum.today('UTC').add(days=-N, ...)` instead -AIR301_names.py:196:1: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0 +AIR301_names.py:106:1: AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0 | -194 | dates.days_ago -195 | -196 | date_range +104 | dates.days_ago +105 | +106 | date_range | ^^^^^^^^^^ AIR301 -197 | days_ago -198 | infer_time_unit +107 | days_ago +108 | infer_time_unit | -AIR301_names.py:197:1: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0 +AIR301_names.py:107:1: AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0 | -196 | date_range -197 | days_ago +106 | date_range +107 | days_ago | ^^^^^^^^ AIR301 -198 | infer_time_unit -199 | parse_execution_date +108 | infer_time_unit +109 | parse_execution_date | = help: Use `pendulum.today('UTC').add(days=-N, ...)` instead -AIR301_names.py:198:1: AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0 +AIR301_names.py:108:1: AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0 | -196 | date_range -197 | days_ago -198 | infer_time_unit +106 | date_range +107 | days_ago +108 | infer_time_unit | ^^^^^^^^^^^^^^^ AIR301 -199 | parse_execution_date -200 | round_time +109 | parse_execution_date +110 | round_time | -AIR301_names.py:199:1: AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0 +AIR301_names.py:109:1: AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0 | -197 | days_ago -198 | infer_time_unit -199 | parse_execution_date +107 | days_ago +108 | infer_time_unit +109 | parse_execution_date | ^^^^^^^^^^^^^^^^^^^^ AIR301 -200 | round_time -201 | scale_time_units +110 | round_time +111 | scale_time_units | -AIR301_names.py:200:1: AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0 +AIR301_names.py:110:1: AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0 | -198 | infer_time_unit -199 | parse_execution_date -200 | round_time +108 | infer_time_unit +109 | parse_execution_date +110 | round_time | ^^^^^^^^^^ AIR301 -201 | scale_time_units +111 | scale_time_units | -AIR301_names.py:201:1: AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0 +AIR301_names.py:111:1: AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0 | -199 | parse_execution_date -200 | round_time -201 | scale_time_units +109 | parse_execution_date +110 | round_time +111 | scale_time_units | ^^^^^^^^^^^^^^^^ AIR301 -202 | -203 | # This one was not deprecated. +112 | +113 | # This one was not deprecated. | -AIR301_names.py:208:1: AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0 +AIR301_names.py:118:1: AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0 | -207 | # airflow.utils.dag_cycle_tester -208 | test_cycle +117 | # airflow.utils.dag_cycle_tester +118 | test_cycle | ^^^^^^^^^^ AIR301 | -AIR301_names.py:212:1: AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0 +AIR301_names.py:122:1: AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0 | -211 | # airflow.utils.db -212 | create_session +121 | # airflow.utils.db +122 | create_session | ^^^^^^^^^^^^^^ AIR301 -213 | -214 | # airflow.utils.decorators +123 | +124 | # airflow.utils.decorators | -AIR301_names.py:215:1: AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0; `apply_defaults` is now unconditionally done and can be safely removed. +AIR301_names.py:125:1: AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0; `apply_defaults` is now unconditionally done and can be safely removed. | -214 | # airflow.utils.decorators -215 | apply_defaults +124 | # airflow.utils.decorators +125 | apply_defaults | ^^^^^^^^^^^^^^ AIR301 -216 | -217 | # airflow.utils.file +126 | +127 | # airflow.utils.file | -AIR301_names.py:218:1: AIR301 `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0 +AIR301_names.py:128:1: AIR301 `airflow.utils.file.TemporaryDirectory` is removed in Airflow 3.0 | -217 | # airflow.utils.file -218 | TemporaryDirectory() +127 | # airflow.utils.file +128 | TemporaryDirectory() | ^^^^^^^^^^^^^^^^^^ AIR301 -219 | mkdirs +129 | mkdirs | = help: Use `tempfile.TemporaryDirectory` instead -AIR301_names.py:219:1: AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0 +AIR301_names.py:129:1: AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0 | -217 | # airflow.utils.file -218 | TemporaryDirectory() -219 | mkdirs +127 | # airflow.utils.file +128 | TemporaryDirectory() +129 | mkdirs | ^^^^^^ AIR301 -220 | -221 | # airflow.utils.helpers +130 | +131 | # airflow.utils.helpers | = help: Use `pathlib.Path({path}).mkdir` instead -AIR301_names.py:222:1: AIR301 `airflow.utils.helpers.chain` is removed in Airflow 3.0 +AIR301_names.py:132:1: AIR301 `airflow.utils.helpers.chain` is removed in Airflow 3.0 | -221 | # airflow.utils.helpers -222 | helper_chain +131 | # airflow.utils.helpers +132 | helper_chain | ^^^^^^^^^^^^ AIR301 -223 | helper_cross_downstream +133 | helper_cross_downstream | = help: Use `airflow.sdk.chain` instead -AIR301_names.py:223:1: AIR301 `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0 +AIR301_names.py:133:1: AIR301 `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0 | -221 | # airflow.utils.helpers -222 | helper_chain -223 | helper_cross_downstream +131 | # airflow.utils.helpers +132 | helper_chain +133 | helper_cross_downstream | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -224 | -225 | # airflow.utils.log +134 | +135 | # airflow.utils.log | = help: Use `airflow.sdk.cross_downstream` instead -AIR301_names.py:226:1: AIR301 `airflow.utils.log.secrets_masker` is removed in Airflow 3.0 +AIR301_names.py:136:1: AIR301 `airflow.utils.log.secrets_masker` is removed in Airflow 3.0 | -225 | # airflow.utils.log -226 | secrets_masker +135 | # airflow.utils.log +136 | secrets_masker | ^^^^^^^^^^^^^^ AIR301 -227 | -228 | # airflow.utils.state +137 | +138 | # airflow.utils.state | = help: Use `airflow.sdk.execution_time.secrets_masker` instead -AIR301_names.py:229:1: AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0 +AIR301_names.py:139:1: AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0 | -228 | # airflow.utils.state -229 | SHUTDOWN +138 | # airflow.utils.state +139 | SHUTDOWN | ^^^^^^^^ AIR301 -230 | terminating_states +140 | terminating_states | -AIR301_names.py:230:1: AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0 +AIR301_names.py:140:1: AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0 | -228 | # airflow.utils.state -229 | SHUTDOWN -230 | terminating_states +138 | # airflow.utils.state +139 | SHUTDOWN +140 | terminating_states | ^^^^^^^^^^^^^^^^^^ AIR301 -231 | -232 | # airflow.utils.trigger_rule +141 | +142 | # airflow.utils.trigger_rule | -AIR301_names.py:233:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0 +AIR301_names.py:143:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0 | -232 | # airflow.utils.trigger_rule -233 | TriggerRule.DUMMY +142 | # airflow.utils.trigger_rule +143 | TriggerRule.DUMMY | ^^^^^ AIR301 -234 | TriggerRule.NONE_FAILED_OR_SKIPPED +144 | TriggerRule.NONE_FAILED_OR_SKIPPED | -AIR301_names.py:234:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0 +AIR301_names.py:144:13: AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0 | -232 | # airflow.utils.trigger_rule -233 | TriggerRule.DUMMY -234 | TriggerRule.NONE_FAILED_OR_SKIPPED +142 | # airflow.utils.trigger_rule +143 | TriggerRule.DUMMY +144 | TriggerRule.NONE_FAILED_OR_SKIPPED | ^^^^^^^^^^^^^^^^^^^^^^ AIR301 -235 | -236 | # airflow.www.auth | -AIR301_names.py:237:1: AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0 +AIR301_names.py:148:1: AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0 | -236 | # airflow.www.auth -237 | has_access +147 | # airflow.www.auth +148 | has_access | ^^^^^^^^^^ AIR301 -238 | has_access_dataset +149 | +150 | # airflow.www.utils | = help: Use `airflow.www.auth.has_access_*` instead -AIR301_names.py:238:1: AIR301 `airflow.www.auth.has_access_dataset` is removed in Airflow 3.0 - | -236 | # airflow.www.auth -237 | has_access -238 | has_access_dataset - | ^^^^^^^^^^^^^^^^^^ AIR301 -239 | -240 | # airflow.www.utils - | - = help: Use `airflow.www.auth.has_access_dataset` instead - -AIR301_names.py:241:1: AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0 +AIR301_names.py:151:1: AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0 | -240 | # airflow.www.utils -241 | get_sensitive_variables_fields +150 | # airflow.www.utils +151 | get_sensitive_variables_fields | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 -242 | should_hide_value_for_key +152 | should_hide_value_for_key | = help: Use `airflow.utils.log.secrets_masker.get_sensitive_variables_fields` instead -AIR301_names.py:242:1: AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0 +AIR301_names.py:152:1: AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0 | -240 | # airflow.www.utils -241 | get_sensitive_variables_fields -242 | should_hide_value_for_key +150 | # airflow.www.utils +151 | get_sensitive_variables_fields +152 | should_hide_value_for_key | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 | = help: Use `airflow.utils.log.secrets_masker.should_hide_value_for_key` instead diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names_fix.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names_fix.py.snap new file mode 100644 index 00000000000000..ff6982d49ad41e --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names_fix.py.snap @@ -0,0 +1,325 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR301_names_fix.py:19:1: AIR301 [*] `airflow.api_connexion.security.requires_access_dataset` is removed in Airflow 3.0 + | +17 | from airflow.www.auth import has_access_dataset +18 | +19 | requires_access_dataset() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +20 | +21 | DatasetDetails() + | + = help: Use `airflow.api_connexion.security.requires_access_asset` instead + +ℹ Safe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.api_connexion.security import requires_access_dataset + 3 |+from airflow.api_connexion.security import requires_access_dataset, requires_access_asset +4 4 | from airflow.auth.managers.models.resource_details import ( +5 5 | DatasetDetails, +6 6 | is_authorized_dataset, +-------------------------------------------------------------------------------- +16 16 | from airflow.security.permissions import RESOURCE_DATASET +17 17 | from airflow.www.auth import has_access_dataset +18 18 | +19 |-requires_access_dataset() + 19 |+requires_access_asset() +20 20 | +21 21 | DatasetDetails() +22 22 | is_authorized_dataset() + +AIR301_names_fix.py:21:1: AIR301 [*] `airflow.auth.managers.models.resource_details.DatasetDetails` is removed in Airflow 3.0 + | +19 | requires_access_dataset() +20 | +21 | DatasetDetails() + | ^^^^^^^^^^^^^^ AIR301 +22 | is_authorized_dataset() + | + = help: Use `airflow.api_fastapi.auth.managers.models.resource_details.AssetDetails` instead + +ℹ Safe fix +15 15 | from airflow.secrets.local_filesystm import load_connections +16 16 | from airflow.security.permissions import RESOURCE_DATASET +17 17 | from airflow.www.auth import has_access_dataset + 18 |+from airflow.api_fastapi.auth.managers.models.resource_details import AssetDetails +18 19 | +19 20 | requires_access_dataset() +20 21 | +21 |-DatasetDetails() + 22 |+AssetDetails() +22 23 | is_authorized_dataset() +23 24 | +24 25 | DatasetManager() + +AIR301_names_fix.py:24:1: AIR301 [*] `airflow.datasets.manager.DatasetManager` is removed in Airflow 3.0 + | +22 | is_authorized_dataset() +23 | +24 | DatasetManager() + | ^^^^^^^^^^^^^^ AIR301 +25 | dataset_manager() +26 | resolve_dataset_manager() + | + = help: Use `airflow.assets.manager.AssetManager` instead + +ℹ Safe fix +15 15 | from airflow.secrets.local_filesystm import load_connections +16 16 | from airflow.security.permissions import RESOURCE_DATASET +17 17 | from airflow.www.auth import has_access_dataset + 18 |+from airflow.assets.manager import AssetManager +18 19 | +19 20 | requires_access_dataset() +20 21 | +21 22 | DatasetDetails() +22 23 | is_authorized_dataset() +23 24 | +24 |-DatasetManager() + 25 |+AssetManager() +25 26 | dataset_manager() +26 27 | resolve_dataset_manager() +27 28 | + +AIR301_names_fix.py:25:1: AIR301 [*] `airflow.datasets.manager.dataset_manager` is removed in Airflow 3.0 + | +24 | DatasetManager() +25 | dataset_manager() + | ^^^^^^^^^^^^^^^ AIR301 +26 | resolve_dataset_manager() + | + = help: Use `airflow.assets.manager.asset_manager` instead + +ℹ Safe fix +15 15 | from airflow.secrets.local_filesystm import load_connections +16 16 | from airflow.security.permissions import RESOURCE_DATASET +17 17 | from airflow.www.auth import has_access_dataset + 18 |+from airflow.assets.manager import asset_manager +18 19 | +19 20 | requires_access_dataset() +20 21 | +-------------------------------------------------------------------------------- +22 23 | is_authorized_dataset() +23 24 | +24 25 | DatasetManager() +25 |-dataset_manager() + 26 |+asset_manager() +26 27 | resolve_dataset_manager() +27 28 | +28 29 | DatasetLineageInfo() + +AIR301_names_fix.py:26:1: AIR301 [*] `airflow.datasets.manager.resolve_dataset_manager` is removed in Airflow 3.0 + | +24 | DatasetManager() +25 | dataset_manager() +26 | resolve_dataset_manager() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +27 | +28 | DatasetLineageInfo() + | + = help: Use `airflow.assets.manager.resolve_asset_manager` instead + +ℹ Safe fix +15 15 | from airflow.secrets.local_filesystm import load_connections +16 16 | from airflow.security.permissions import RESOURCE_DATASET +17 17 | from airflow.www.auth import has_access_dataset + 18 |+from airflow.assets.manager import resolve_asset_manager +18 19 | +19 20 | requires_access_dataset() +20 21 | +-------------------------------------------------------------------------------- +23 24 | +24 25 | DatasetManager() +25 26 | dataset_manager() +26 |-resolve_dataset_manager() + 27 |+resolve_asset_manager() +27 28 | +28 29 | DatasetLineageInfo() +29 30 | + +AIR301_names_fix.py:28:1: AIR301 [*] `airflow.lineage.hook.DatasetLineageInfo` is removed in Airflow 3.0 + | +26 | resolve_dataset_manager() +27 | +28 | DatasetLineageInfo() + | ^^^^^^^^^^^^^^^^^^ AIR301 +29 | +30 | AllowListValidator() + | + = help: Use `airflow.lineage.hook.AssetLineageInfo` instead + +ℹ Safe fix +10 10 | dataset_manager, +11 11 | resolve_dataset_manager, +12 12 | ) +13 |-from airflow.lineage.hook import DatasetLineageInfo + 13 |+from airflow.lineage.hook import DatasetLineageInfo, AssetLineageInfo +14 14 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +15 15 | from airflow.secrets.local_filesystm import load_connections +16 16 | from airflow.security.permissions import RESOURCE_DATASET +-------------------------------------------------------------------------------- +25 25 | dataset_manager() +26 26 | resolve_dataset_manager() +27 27 | +28 |-DatasetLineageInfo() + 28 |+AssetLineageInfo() +29 29 | +30 30 | AllowListValidator() +31 31 | BlockListValidator() + +AIR301_names_fix.py:30:1: AIR301 [*] `airflow.metrics.validators.AllowListValidator` is removed in Airflow 3.0 + | +28 | DatasetLineageInfo() +29 | +30 | AllowListValidator() + | ^^^^^^^^^^^^^^^^^^ AIR301 +31 | BlockListValidator() + | + = help: Use `airflow.metrics.validators.PatternAllowListValidator` instead + +ℹ Safe fix +11 11 | resolve_dataset_manager, +12 12 | ) +13 13 | from airflow.lineage.hook import DatasetLineageInfo +14 |-from airflow.metrics.validators import AllowListValidator, BlockListValidator + 14 |+from airflow.metrics.validators import AllowListValidator, BlockListValidator, PatternAllowListValidator +15 15 | from airflow.secrets.local_filesystm import load_connections +16 16 | from airflow.security.permissions import RESOURCE_DATASET +17 17 | from airflow.www.auth import has_access_dataset +-------------------------------------------------------------------------------- +27 27 | +28 28 | DatasetLineageInfo() +29 29 | +30 |-AllowListValidator() + 30 |+PatternAllowListValidator() +31 31 | BlockListValidator() +32 32 | +33 33 | load_connections() + +AIR301_names_fix.py:31:1: AIR301 [*] `airflow.metrics.validators.BlockListValidator` is removed in Airflow 3.0 + | +30 | AllowListValidator() +31 | BlockListValidator() + | ^^^^^^^^^^^^^^^^^^ AIR301 +32 | +33 | load_connections() + | + = help: Use `airflow.metrics.validators.PatternBlockListValidator` instead + +ℹ Safe fix +11 11 | resolve_dataset_manager, +12 12 | ) +13 13 | from airflow.lineage.hook import DatasetLineageInfo +14 |-from airflow.metrics.validators import AllowListValidator, BlockListValidator + 14 |+from airflow.metrics.validators import AllowListValidator, BlockListValidator, PatternBlockListValidator +15 15 | from airflow.secrets.local_filesystm import load_connections +16 16 | from airflow.security.permissions import RESOURCE_DATASET +17 17 | from airflow.www.auth import has_access_dataset +-------------------------------------------------------------------------------- +28 28 | DatasetLineageInfo() +29 29 | +30 30 | AllowListValidator() +31 |-BlockListValidator() + 31 |+PatternBlockListValidator() +32 32 | +33 33 | load_connections() +34 34 | + +AIR301_names_fix.py:35:1: AIR301 [*] `airflow.security.permissions.RESOURCE_DATASET` is removed in Airflow 3.0 + | +33 | load_connections() +34 | +35 | RESOURCE_DATASET + | ^^^^^^^^^^^^^^^^ AIR301 +36 | +37 | has_access_dataset() + | + = help: Use `airflow.security.permissions.RESOURCE_ASSET` instead + +ℹ Safe fix +13 13 | from airflow.lineage.hook import DatasetLineageInfo +14 14 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +15 15 | from airflow.secrets.local_filesystm import load_connections +16 |-from airflow.security.permissions import RESOURCE_DATASET + 16 |+from airflow.security.permissions import RESOURCE_DATASET, RESOURCE_ASSET +17 17 | from airflow.www.auth import has_access_dataset +18 18 | +19 19 | requires_access_dataset() +-------------------------------------------------------------------------------- +32 32 | +33 33 | load_connections() +34 34 | +35 |-RESOURCE_DATASET + 35 |+RESOURCE_ASSET +36 36 | +37 37 | has_access_dataset() +38 38 | + +AIR301_names_fix.py:37:1: AIR301 [*] `airflow.www.auth.has_access_dataset` is removed in Airflow 3.0 + | +35 | RESOURCE_DATASET +36 | +37 | has_access_dataset() + | ^^^^^^^^^^^^^^^^^^ AIR301 +38 | +39 | from airflow.listeners.spec.dataset import ( + | + = help: Use `airflow.www.auth.has_access_asset` instead + +ℹ Safe fix +14 14 | from airflow.metrics.validators import AllowListValidator, BlockListValidator +15 15 | from airflow.secrets.local_filesystm import load_connections +16 16 | from airflow.security.permissions import RESOURCE_DATASET +17 |-from airflow.www.auth import has_access_dataset + 17 |+from airflow.www.auth import has_access_dataset, has_access_asset +18 18 | +19 19 | requires_access_dataset() +20 20 | +-------------------------------------------------------------------------------- +34 34 | +35 35 | RESOURCE_DATASET +36 36 | +37 |-has_access_dataset() + 37 |+has_access_asset() +38 38 | +39 39 | from airflow.listeners.spec.dataset import ( +40 40 | on_dataset_changed, + +AIR301_names_fix.py:44:1: AIR301 [*] `airflow.listeners.spec.dataset.on_dataset_created` is removed in Airflow 3.0 + | +42 | ) +43 | +44 | on_dataset_created() + | ^^^^^^^^^^^^^^^^^^ AIR301 +45 | on_dataset_changed() + | + = help: Use `airflow.listeners.spec.asset.on_asset_created` instead + +ℹ Safe fix +40 40 | on_dataset_changed, +41 41 | on_dataset_created, +42 42 | ) + 43 |+from airflow.listeners.spec.asset import on_asset_created +43 44 | +44 |-on_dataset_created() + 45 |+on_asset_created() +45 46 | on_dataset_changed() + +AIR301_names_fix.py:45:1: AIR301 [*] `airflow.listeners.spec.dataset.on_dataset_changed` is removed in Airflow 3.0 + | +44 | on_dataset_created() +45 | on_dataset_changed() + | ^^^^^^^^^^^^^^^^^^ AIR301 + | + = help: Use `airflow.listeners.spec.asset.on_asset_changed` instead + +ℹ Safe fix +40 40 | on_dataset_changed, +41 41 | on_dataset_created, +42 42 | ) + 43 |+from airflow.listeners.spec.asset import on_asset_changed +43 44 | +44 45 | on_dataset_created() +45 |-on_dataset_changed() + 46 |+on_asset_changed() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_provider_names_fix.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_provider_names_fix.py.snap new file mode 100644 index 00000000000000..6b986b413fd04a --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_provider_names_fix.py.snap @@ -0,0 +1,254 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR301_provider_names_fix.py:25:1: AIR301 [*] `airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities.DATASET` is removed in Airflow 3.0 + | +23 | ) +24 | +25 | DATASET + | ^^^^^^^ AIR301 +26 | +27 | s3_create_dataset() + | + = help: Use `airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities.ASSET` instead + +ℹ Safe fix +1 1 | from __future__ import annotations +2 2 | +3 |-from airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities import DATASET + 3 |+from airflow.providers.amazon.aws.auth_manager.avp.entities.AvpEntities import DATASET, ASSET +4 4 | from airflow.providers.amazon.aws.datasets.s3 import ( +5 5 | convert_dataset_to_openlineage as s3_convert_dataset_to_openlineage, +6 6 | ) +-------------------------------------------------------------------------------- +22 22 | translate_airflow_dataset, +23 23 | ) +24 24 | +25 |-DATASET + 25 |+ASSET +26 26 | +27 27 | s3_create_dataset() +28 28 | s3_convert_dataset_to_openlineage() + +AIR301_provider_names_fix.py:27:1: AIR301 [*] `airflow.providers.amazon.aws.datasets.s3.create_dataset` is removed in Airflow 3.0 + | +25 | DATASET +26 | +27 | s3_create_dataset() + | ^^^^^^^^^^^^^^^^^ AIR301 +28 | s3_convert_dataset_to_openlineage() + | + = help: Use `airflow.providers.amazon.aws.assets.s3.create_asset` instead + +ℹ Safe fix +21 21 | DatasetInfo, +22 22 | translate_airflow_dataset, +23 23 | ) + 24 |+from airflow.providers.amazon.aws.assets.s3 import create_asset +24 25 | +25 26 | DATASET +26 27 | +27 |-s3_create_dataset() + 28 |+create_asset() +28 29 | s3_convert_dataset_to_openlineage() +29 30 | +30 31 | io_create_dataset() + +AIR301_provider_names_fix.py:28:1: AIR301 [*] `airflow.providers.amazon.aws.datasets.s3.convert_dataset_to_openlineage` is removed in Airflow 3.0 + | +27 | s3_create_dataset() +28 | s3_convert_dataset_to_openlineage() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +29 | +30 | io_create_dataset() + | + = help: Use `airflow.providers.amazon.aws.assets.s3.convert_asset_to_openlineage` instead + +ℹ Safe fix +21 21 | DatasetInfo, +22 22 | translate_airflow_dataset, +23 23 | ) + 24 |+from airflow.providers.amazon.aws.assets.s3 import convert_asset_to_openlineage +24 25 | +25 26 | DATASET +26 27 | +27 28 | s3_create_dataset() +28 |-s3_convert_dataset_to_openlineage() + 29 |+convert_asset_to_openlineage() +29 30 | +30 31 | io_create_dataset() +31 32 | io_convert_dataset_to_openlineage() + +AIR301_provider_names_fix.py:33:1: AIR301 [*] `airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_dataset` is removed in Airflow 3.0 + | +31 | io_convert_dataset_to_openlineage() +32 | +33 | fab_is_authorized_dataset() + | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +34 | +35 | # airflow.providers.google.datasets.bigquery + | + = help: Use `airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_asset` instead + +ℹ Safe fix +9 9 | convert_dataset_to_openlineage as io_convert_dataset_to_openlineage, +10 10 | ) +11 11 | from airflow.providers.common.io.dataset.file import create_dataset as io_create_dataset +12 |-from airflow.providers.fab.auth_manager.fab_auth_manager import is_authorized_dataset as fab_is_authorized_dataset + 12 |+from airflow.providers.fab.auth_manager.fab_auth_manager import is_authorized_dataset as fab_is_authorized_dataset, is_authorized_asset +13 13 | from airflow.providers.google.datasets.bigquery import ( +14 14 | create_dataset as bigquery_create_dataset, +15 15 | ) +-------------------------------------------------------------------------------- +30 30 | io_create_dataset() +31 31 | io_convert_dataset_to_openlineage() +32 32 | +33 |-fab_is_authorized_dataset() + 33 |+is_authorized_asset() +34 34 | +35 35 | # airflow.providers.google.datasets.bigquery +36 36 | bigquery_create_dataset() + +AIR301_provider_names_fix.py:36:1: AIR301 [*] `airflow.providers.google.datasets.bigquery.create_dataset` is removed in Airflow 3.0 + | +35 | # airflow.providers.google.datasets.bigquery +36 | bigquery_create_dataset() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +37 | # airflow.providers.google.datasets.gcs +38 | gcs_create_dataset() + | + = help: Use `airflow.providers.google.assets.bigquery.create_asset` instead + +ℹ Safe fix +21 21 | DatasetInfo, +22 22 | translate_airflow_dataset, +23 23 | ) + 24 |+from airflow.providers.google.assets.bigquery import create_asset +24 25 | +25 26 | DATASET +26 27 | +-------------------------------------------------------------------------------- +33 34 | fab_is_authorized_dataset() +34 35 | +35 36 | # airflow.providers.google.datasets.bigquery +36 |-bigquery_create_dataset() + 37 |+create_asset() +37 38 | # airflow.providers.google.datasets.gcs +38 39 | gcs_create_dataset() +39 40 | gcs_convert_dataset_to_openlineage() + +AIR301_provider_names_fix.py:38:1: AIR301 [*] `airflow.providers.google.datasets.gcs.create_dataset` is removed in Airflow 3.0 + | +36 | bigquery_create_dataset() +37 | # airflow.providers.google.datasets.gcs +38 | gcs_create_dataset() + | ^^^^^^^^^^^^^^^^^^ AIR301 +39 | gcs_convert_dataset_to_openlineage() +40 | # airflow.providers.openlineage.utils.utils + | + = help: Use `airflow.providers.google.assets.gcs.create_asset` instead + +ℹ Safe fix +21 21 | DatasetInfo, +22 22 | translate_airflow_dataset, +23 23 | ) + 24 |+from airflow.providers.google.assets.gcs import create_asset +24 25 | +25 26 | DATASET +26 27 | +-------------------------------------------------------------------------------- +35 36 | # airflow.providers.google.datasets.bigquery +36 37 | bigquery_create_dataset() +37 38 | # airflow.providers.google.datasets.gcs +38 |-gcs_create_dataset() + 39 |+create_asset() +39 40 | gcs_convert_dataset_to_openlineage() +40 41 | # airflow.providers.openlineage.utils.utils +41 42 | DatasetInfo() + +AIR301_provider_names_fix.py:39:1: AIR301 [*] `airflow.providers.google.datasets.gcs.convert_dataset_to_openlineage` is removed in Airflow 3.0 + | +37 | # airflow.providers.google.datasets.gcs +38 | gcs_create_dataset() +39 | gcs_convert_dataset_to_openlineage() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +40 | # airflow.providers.openlineage.utils.utils +41 | DatasetInfo() + | + = help: Use `airflow.providers.google.assets.gcs.convert_asset_to_openlineage` instead + +ℹ Safe fix +21 21 | DatasetInfo, +22 22 | translate_airflow_dataset, +23 23 | ) + 24 |+from airflow.providers.google.assets.gcs import convert_asset_to_openlineage +24 25 | +25 26 | DATASET +26 27 | +-------------------------------------------------------------------------------- +36 37 | bigquery_create_dataset() +37 38 | # airflow.providers.google.datasets.gcs +38 39 | gcs_create_dataset() +39 |-gcs_convert_dataset_to_openlineage() + 40 |+convert_asset_to_openlineage() +40 41 | # airflow.providers.openlineage.utils.utils +41 42 | DatasetInfo() +42 43 | translate_airflow_dataset() + +AIR301_provider_names_fix.py:41:1: AIR301 [*] `airflow.providers.openlineage.utils.utils.DatasetInfo` is removed in Airflow 3.0 + | +39 | gcs_convert_dataset_to_openlineage() +40 | # airflow.providers.openlineage.utils.utils +41 | DatasetInfo() + | ^^^^^^^^^^^ AIR301 +42 | translate_airflow_dataset() +43 | # + | + = help: Use `airflow.providers.openlineage.utils.utils.AssetInfo` instead + +ℹ Safe fix +20 20 | from airflow.providers.openlineage.utils.utils import ( +21 21 | DatasetInfo, +22 22 | translate_airflow_dataset, + 23 |+AssetInfo, +23 24 | ) +24 25 | +25 26 | DATASET +-------------------------------------------------------------------------------- +38 39 | gcs_create_dataset() +39 40 | gcs_convert_dataset_to_openlineage() +40 41 | # airflow.providers.openlineage.utils.utils +41 |-DatasetInfo() + 42 |+AssetInfo() +42 43 | translate_airflow_dataset() +43 44 | # +44 45 | # airflow.secrets.local_filesystem + +AIR301_provider_names_fix.py:42:1: AIR301 [*] `airflow.providers.openlineage.utils.utils.translate_airflow_dataset` is removed in Airflow 3.0 + | +40 | # airflow.providers.openlineage.utils.utils +41 | DatasetInfo() +42 | translate_airflow_dataset() + | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +43 | # +44 | # airflow.secrets.local_filesystem + | + = help: Use `airflow.providers.openlineage.utils.utils.translate_airflow_asset` instead + +ℹ Safe fix +20 20 | from airflow.providers.openlineage.utils.utils import ( +21 21 | DatasetInfo, +22 22 | translate_airflow_dataset, + 23 |+translate_airflow_asset, +23 24 | ) +24 25 | +25 26 | DATASET +-------------------------------------------------------------------------------- +39 40 | gcs_convert_dataset_to_openlineage() +40 41 | # airflow.providers.openlineage.utils.utils +41 42 | DatasetInfo() +42 |-translate_airflow_dataset() + 43 |+translate_airflow_asset() +43 44 | # +44 45 | # airflow.secrets.local_filesystem +45 46 | load_connections() From e91e2f49dbe3a280d1cd14644d6a9dd9a648a9f6 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Apr 2025 19:31:14 +0200 Subject: [PATCH 0085/1161] [red-knot] Trust module-level undeclared symbols in stubs (#17577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Many symbols in typeshed are defined without being declared. For example: ```pyi # builtins: IOError = OSError # types LambdaType = FunctionType NotImplementedType = _NotImplementedType # typing Text = str # random uniform = _inst.uniform # optparse make_option = Option # all over the place: _T = TypeVar("_T") ``` Here, we introduce a change that skips widening the public type of these symbols (by unioning with `Unknown`). fixes #17032 ## Ecosystem analysis This is difficult to analyze in detail, but I went over most changes and it looks very favorable to me overall. The diff on the overall numbers is: ``` errors: 1287 -> 859 (reduction by 428) warnings: 45 -> 59 (increase by 14) ``` ### Removed false positives `invalid-base` examples: ```diff - error[lint:invalid-base] /tmp/mypy_primer/projects/pip/src/pip/_vendor/rich/console.py:548:27: Invalid class base with type `Unknown | Literal[_local]` (all bases must be a class, `Any`, `Unknown` or `Todo`) - error[lint:invalid-base] /tmp/mypy_primer/projects/tornado/tornado/iostream.py:84:25: Invalid class base with type `Unknown | Literal[OSError]` (all bases must be a class, `Any`, `Unknown` or `Todo`) - error[lint:invalid-base] /tmp/mypy_primer/projects/mitmproxy/test/conftest.py:35:40: Invalid class base with type `Unknown | Literal[_UnixDefaultEventLoopPolicy]` (all bases must be a class, `Any`, `Unknown` or `Todo`) ``` `invalid-exception-caught` examples: ```diff - error[lint:invalid-exception-caught] /tmp/mypy_primer/projects/cloud-init/cloudinit/cmd/status.py:334:16: Cannot catch object of type `Literal[ProcessExecutionError]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses) - error[lint:invalid-exception-caught] /tmp/mypy_primer/projects/jinja/src/jinja2/loaders.py:537:16: Cannot catch object of type `Literal[TemplateNotFound]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses) ``` `unresolved-reference` examples https://github.com/canonical/cloud-init/blob/7a0265d36e01e649f72005548f17dca9ac0150ad/cloudinit/handlers/jinja_template.py#L120-L123 (we now understand the `isinstance` narrowing) ```diff - error[lint:unresolved-attribute] /tmp/mypy_primer/projects/cloud-init/cloudinit/handlers/jinja_template.py:123:16: Type `Exception` has no attribute `errno` ``` `unknown-argument` examples https://github.com/hauntsaninja/boostedblob/blob/master/boostedblob/request.py#L53 ```diff - error[lint:unknown-argument] /tmp/mypy_primer/projects/boostedblob/boostedblob/request.py:53:17: Argument `connect` does not match any known parameter of bound method `__init__` ``` `unknown-argument` There are a lot of `__init__`-related changes because we now understand [`@attr.s`](https://github.com/python-attrs/attrs/blob/3d42a6978ac60b487135db39218cfb742b100899/src/attr/__init__.pyi#L387) as a `@dataclass_transform` annotated symbol. For example: ```diff - error[lint:unknown-argument] /tmp/mypy_primer/projects/attrs/tests/test_hooks.py:72:18: Argument `x` does not match any known parameter of bound method `__init__` ``` ### New false positives This can happen if a symbol that previously was inferred as `X | Unknown` was assigned-to, but we don't yet understand the assignability to `X`: https://github.com/strawberry-graphql/strawberry/blob/main/strawberry/exceptions/handler.py#L90 ```diff + error[lint:invalid-assignment] /tmp/mypy_primer/projects/strawberry/strawberry/exceptions/handler.py:90:9: Object of type `def strawberry_threading_exception_handler(args: tuple[type[BaseException], BaseException | None, TracebackType | None, Thread | None]) -> None` is not assignable to attribute `excepthook` of type `(_ExceptHookArgs, /) -> Any` ``` ### New true positives https://github.com/DataDog/dd-trace-py/blob/6bbb5519fe4b3964f9ca73b21cf35df8387618b2/tests/tracer/test_span.py#L714 ```diff + error[lint:invalid-argument-type] /tmp/mypy_primer/projects/dd-trace-py/tests/tracer/test_span.py:714:33: Argument to this function is incorrect: Expected `str`, found `Literal[b"\xf0\x9f\xa4\x94"]` ``` ### Changed diagnostics A lot of changed diagnostics because we now show `@Todo(Support for `typing.TypeVar` instances in type expressions)` instead of `Unknown` for all kinds of symbols that used a `_T = TypeVar("_T")` as a type. One prominent example is the `list.__getitem__` method: `builtins.pyi`: ```pyi _T = TypeVar("_T") # previously `TypeVar | Unknown`, now just `TypeVar` # … class list(MutableSequence[_T]): # … @overload def __getitem__(self, i: SupportsIndex, /) -> _T: ... # … ``` which causes this change in diagnostics: ```py xs = [1, 2] reveal_type(xs[0]) # previously `Unknown`, now `@Todo(Support for `typing.TypeVar` instances in type expressions)` ``` ## Test Plan Updated Markdown tests --- .../resources/mdtest/scopes/eager.md | 2 +- .../resources/mdtest/subscript/lists.md | 2 +- .../mdtest/type_properties/is_singleton.md | 5 +---- .../src/semantic_index/symbol.rs | 8 ++++++++ crates/red_knot_python_semantic/src/symbol.rs | 14 ++++++++++++-- .../src/types/signatures.rs | 5 +---- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md index 533330ddb72de5..bfd11b667716cf 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md @@ -404,7 +404,7 @@ x = int class C: var: ClassVar[x] -reveal_type(C.var) # revealed: Unknown | str +reveal_type(C.var) # revealed: str x = str ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md index d074d1b82669a8..192dc4a88e10e0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md @@ -12,7 +12,7 @@ x = [1, 2, 3] reveal_type(x) # revealed: list # TODO reveal int -reveal_type(x[0]) # revealed: Unknown +reveal_type(x[0]) # revealed: @Todo(Support for `typing.TypeVar` instances in type expressions) # TODO reveal list reveal_type(x[0:1]) # revealed: @Todo(specialized non-generic class) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md index a60efdfd9e8f77..4673affed67bc0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md @@ -128,10 +128,7 @@ python-version = "3.10" import types from knot_extensions import static_assert, is_singleton -# TODO: types.NotImplementedType is a TypeAlias of builtins._NotImplementedType -# Once TypeAlias support is added, it should satisfy `is_singleton` -reveal_type(types.NotImplementedType) # revealed: Unknown | Literal[_NotImplementedType] -static_assert(not is_singleton(types.NotImplementedType)) +static_assert(is_singleton(types.NotImplementedType)) ``` ### Callables diff --git a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs index ec9ef3886eff3c..10f6e9c5d190f2 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs @@ -115,6 +115,10 @@ impl<'db> ScopeId<'db> { self.node(db).scope_kind().is_function_like() } + pub(crate) fn is_module_scope(self, db: &'db dyn Db) -> bool { + self.node(db).scope_kind().is_module() + } + pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool { self.node(db).scope_kind().is_type_parameter() } @@ -263,6 +267,10 @@ impl ScopeKind { matches!(self, ScopeKind::Class) } + pub(crate) fn is_module(self) -> bool { + matches!(self, ScopeKind::Module) + } + pub(crate) fn is_type_parameter(self) -> bool { matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias) } diff --git a/crates/red_knot_python_semantic/src/symbol.rs b/crates/red_knot_python_semantic/src/symbol.rs index dae79c3b63aba5..4e713a7fdea153 100644 --- a/crates/red_knot_python_semantic/src/symbol.rs +++ b/crates/red_knot_python_semantic/src/symbol.rs @@ -593,8 +593,18 @@ fn symbol_by_id<'db>( "__slots__" | "TYPE_CHECKING" ); - widen_type_for_undeclared_public_symbol(db, inferred, is_considered_non_modifiable) - .into() + if scope.is_module_scope(db) && scope.file(db).is_stub(db.upcast()) { + // We generally trust module-level undeclared symbols in stubs and do not union + // with `Unknown`. If we don't do this, simple aliases like `IOError = OSError` in + // stubs would result in `IOError` being a union of `OSError` and `Unknown`, which + // leads to all sorts of downstream problems. Similarly, type variables are often + // defined as `_T = TypeVar("_T")`, without being declared. + + inferred.into() + } else { + widen_type_for_undeclared_public_symbol(db, inferred, is_considered_non_modifiable) + .into() + } } // Symbol has conflicting declared types Err((declared, _)) => { diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 52b1ab827ff387..c74554fe91f445 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -1631,10 +1631,7 @@ mod tests { assert_eq!(a_name, "a"); assert_eq!(b_name, "b"); // Parameter resolution deferred; we should see B - assert_eq!( - a_annotated_ty.unwrap().display(&db).to_string(), - "Unknown | B" - ); + assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B"); assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T"); } From e170fe493dfdffbb833629d7565a5e0555c528a7 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Apr 2025 20:07:29 +0200 Subject: [PATCH 0086/1161] [red-knot] Trust all symbols in stub files (#17588) ## Summary *Generally* trust undeclared symbols in stubs, not just at the module level. Follow-up on the discussion [here](https://github.com/astral-sh/ruff/pull/17577#discussion_r2055945909). ## Test Plan New Markdown test. --- .../mdtest/boundness_declaredness/public.md | 63 +++++++++++++++++++ .../src/semantic_index/symbol.rs | 8 --- crates/red_knot_python_semantic/src/symbol.rs | 2 +- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md b/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md index 3c41c72e440da2..956fa1cc7fd82d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md +++ b/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md @@ -292,3 +292,66 @@ reveal_type(a) # revealed: Unknown # Modifications allowed in this case: a = None ``` + +## In stub files + +In stub files, we have a minor modification to the rules above: we do not union with `Unknown` for +undeclared symbols. + +### Undeclared and bound + +`mod.pyi`: + +```pyi +MyInt = int + +class C: + MyStr = str +``` + +```py +from mod import MyInt, C + +reveal_type(MyInt) # revealed: Literal[int] +reveal_type(C.MyStr) # revealed: Literal[str] +``` + +### Undeclared and possibly unbound + +`mod.pyi`: + +```pyi +def flag() -> bool: + return True + +if flag(): + MyInt = int + + class C: + MyStr = str +``` + +```py +# error: [possibly-unbound-import] +# error: [possibly-unbound-import] +from mod import MyInt, C + +reveal_type(MyInt) # revealed: Literal[int] +reveal_type(C.MyStr) # revealed: Literal[str] +``` + +### Undeclared and unbound + +`mod.pyi`: + +```pyi +if False: + MyInt = int +``` + +```py +# error: [unresolved-import] +from mod import MyInt + +reveal_type(MyInt) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs index 10f6e9c5d190f2..ec9ef3886eff3c 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs @@ -115,10 +115,6 @@ impl<'db> ScopeId<'db> { self.node(db).scope_kind().is_function_like() } - pub(crate) fn is_module_scope(self, db: &'db dyn Db) -> bool { - self.node(db).scope_kind().is_module() - } - pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool { self.node(db).scope_kind().is_type_parameter() } @@ -267,10 +263,6 @@ impl ScopeKind { matches!(self, ScopeKind::Class) } - pub(crate) fn is_module(self) -> bool { - matches!(self, ScopeKind::Module) - } - pub(crate) fn is_type_parameter(self) -> bool { matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias) } diff --git a/crates/red_knot_python_semantic/src/symbol.rs b/crates/red_knot_python_semantic/src/symbol.rs index 4e713a7fdea153..3be4ce8d02fc5f 100644 --- a/crates/red_knot_python_semantic/src/symbol.rs +++ b/crates/red_knot_python_semantic/src/symbol.rs @@ -593,7 +593,7 @@ fn symbol_by_id<'db>( "__slots__" | "TYPE_CHECKING" ); - if scope.is_module_scope(db) && scope.file(db).is_stub(db.upcast()) { + if scope.file(db).is_stub(db.upcast()) { // We generally trust module-level undeclared symbols in stubs and do not union // with `Unknown`. If we don't do this, simple aliases like `IOError = OSError` in // stubs would result in `IOError` being a union of `OSError` and `Unknown`, which From 61e73481feca429f51a3c02e08666202dce381fb Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Apr 2025 20:34:13 +0200 Subject: [PATCH 0087/1161] [red-knot] Assignability of class instances to Callable (#17590) ## Summary Model assignability of class instances with a `__call__` method to `Callable` types. This should solve some false positives related to `functools.partial` (yes, 1098 fewer diagnostics!). Reference: https://github.com/astral-sh/ruff/issues/17343#issuecomment-2824618483 ## Test Plan New Markdown tests. --- .../type_properties/is_assignable_to.md | 26 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 10 +++++++ 2 files changed, 36 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 9c33827733b414..ce7198ba697e12 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -560,4 +560,30 @@ c: Callable[..., int] = overloaded c: Callable[[int], str] = overloaded ``` +### Classes with `__call__` + +```py +from typing import Callable, Any +from knot_extensions import static_assert, is_assignable_to + +class TakesAny: + def __call__(self, a: Any) -> str: + return "" + +class ReturnsAny: + def __call__(self, a: str) -> Any: ... + +static_assert(is_assignable_to(TakesAny, Callable[[int], str])) +static_assert(not is_assignable_to(TakesAny, Callable[[int], int])) + +static_assert(is_assignable_to(ReturnsAny, Callable[[str], int])) +static_assert(not is_assignable_to(ReturnsAny, Callable[[int], int])) + +from functools import partial + +def f(x: int, y: str) -> None: ... + +c1: Callable[[int], None] = partial(f, y="a") +``` + [typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 398cd95d1eb8cc..ebb44ae9ccc08d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1460,6 +1460,16 @@ impl<'db> Type<'db> { self_callable.is_assignable_to(db, target_callable) } + (Type::Instance(_), Type::Callable(_)) => { + let call_symbol = self.member(db, "__call__").symbol; + match call_symbol { + Symbol::Type(Type::BoundMethod(call_function), _) => call_function + .into_callable_type(db) + .is_assignable_to(db, target), + _ => false, + } + } + (Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => { self_function_literal .into_callable_type(db) From 9db63fc58cbf19d97bba616b1680c5898255a7ff Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 23 Apr 2025 15:06:18 -0400 Subject: [PATCH 0088/1161] [red-knot] Handle generic constructors of generic classes (#17552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now handle generic constructor methods on generic classes correctly: ```py class C[T]: def __init__[S](self, t: T, s: S): ... x = C(1, "str") ``` Here, constructing `C` requires us to infer a specialization for the generic contexts of `C` and `__init__` at the same time. At first I thought I would need to track the full stack of nested generic contexts here (since the `[S]` context is nested within the `[T]` context). But I think this is the only way that we might need to specialize more than one generic context at once — in all other cases, a containing generic context must be specialized before we get to a nested one, and so we can just special-case this. While we're here, we also construct the generic context for a generic function lazily, when its signature is accessed, instead of eagerly when inferring the function body. --- .../resources/mdtest/generics/classes.md | 20 ++---- crates/red_knot_python_semantic/src/types.rs | 61 +++++++++++++++---- .../src/types/call/bind.rs | 47 ++++++++------ .../src/types/class.rs | 2 +- .../src/types/generics.rs | 11 ++-- .../src/types/infer.rs | 7 +-- .../src/types/signatures.rs | 14 +++++ 7 files changed, 102 insertions(+), 60 deletions(-) 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 295b44827de28d..11d1d36b353dfd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -232,21 +232,11 @@ TODO: These do not currently work yet, because we don't correctly model the nest class C[T]: def __init__[S](self, x: T, y: S) -> None: ... -# TODO: no error -# TODO: revealed: C[Literal[1]] -# error: [invalid-argument-type] -reveal_type(C(1, 1)) # revealed: C[Unknown] -# TODO: no error -# TODO: revealed: C[Literal[1]] -# error: [invalid-argument-type] -reveal_type(C(1, "string")) # revealed: C[Unknown] -# TODO: no error -# TODO: revealed: C[Literal[1]] -# error: [invalid-argument-type] -reveal_type(C(1, True)) # revealed: C[Unknown] - -# TODO: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `S`, found `Literal[1]`" +reveal_type(C(1, 1)) # revealed: C[Literal[1]] +reveal_type(C(1, "string")) # revealed: C[Literal[1]] +reveal_type(C(1, True)) # revealed: C[Literal[1]] + +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five", 1) ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ebb44ae9ccc08d..9f45c4bb85a392 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4268,13 +4268,13 @@ impl<'db> Type<'db> { .as_ref() .and_then(Bindings::single_element) .and_then(CallableBinding::matching_overload) - .and_then(|(_, binding)| binding.specialization()); + .and_then(|(_, binding)| binding.inherited_specialization()); let init_specialization = init_call_outcome .and_then(Result::ok) .as_ref() .and_then(Bindings::single_element) .and_then(CallableBinding::matching_overload) - .and_then(|(_, binding)| binding.specialization()); + .and_then(|(_, binding)| binding.inherited_specialization()); let specialization = match (new_specialization, init_specialization) { (None, None) => None, (Some(specialization), None) | (None, Some(specialization)) => { @@ -5940,8 +5940,10 @@ pub struct FunctionType<'db> { /// with `@dataclass_transformer(...)`. dataclass_transformer_params: Option, - /// The generic context of a generic function. - generic_context: Option>, + /// The inherited generic context, if this function is a class method being used to infer the + /// specialization of its generic class. If the method is itself generic, this is in addition + /// to its own generic context. + inherited_generic_context: Option>, /// A specialization that should be applied to the function's parameter and return types, /// either because the function is itself generic, or because it appears in the body of a @@ -6007,11 +6009,7 @@ impl<'db> FunctionType<'db> { /// would depend on the function's AST and rerun for every change in that file. #[salsa::tracked(return_ref)] pub(crate) fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> { - let mut internal_signature = self.internal_signature(db); - - if let Some(specialization) = self.specialization(db) { - internal_signature = internal_signature.apply_specialization(db, specialization); - } + let internal_signature = self.internal_signature(db); // The semantic model records a use for each function on the name node. This is used here // to get the previous function definition with the same name. @@ -6071,14 +6069,51 @@ impl<'db> FunctionType<'db> { let scope = self.body_scope(db); let function_stmt_node = scope.node(db).expect_function(); let definition = self.definition(db); - Signature::from_function(db, self.generic_context(db), definition, function_stmt_node) + let generic_context = function_stmt_node.type_params.as_ref().map(|type_params| { + let index = semantic_index(db, scope.file(db)); + GenericContext::from_type_params(db, index, type_params) + }); + let mut signature = Signature::from_function( + db, + generic_context, + self.inherited_generic_context(db), + definition, + function_stmt_node, + ); + if let Some(specialization) = self.specialization(db) { + signature = signature.apply_specialization(db, specialization); + } + signature } pub(crate) fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool { self.known(db) == Some(known_function) } - fn with_generic_context(self, db: &'db dyn Db, generic_context: GenericContext<'db>) -> Self { + fn with_dataclass_transformer_params( + self, + db: &'db dyn Db, + params: DataclassTransformerParams, + ) -> Self { + Self::new( + db, + self.name(db).clone(), + self.known(db), + self.body_scope(db), + self.decorators(db), + Some(params), + self.inherited_generic_context(db), + self.specialization(db), + ) + } + + fn with_inherited_generic_context( + self, + db: &'db dyn Db, + inherited_generic_context: GenericContext<'db>, + ) -> Self { + // A function cannot inherit more than one generic context from its containing class. + debug_assert!(self.inherited_generic_context(db).is_none()); Self::new( db, self.name(db).clone(), @@ -6086,7 +6121,7 @@ impl<'db> FunctionType<'db> { self.body_scope(db), self.decorators(db), self.dataclass_transformer_params(db), - Some(generic_context), + Some(inherited_generic_context), self.specialization(db), ) } @@ -6103,7 +6138,7 @@ impl<'db> FunctionType<'db> { self.body_scope(db), self.decorators(db), self.dataclass_transformer_params(db), - self.generic_context(db), + self.inherited_generic_context(db), Some(specialization), ) } diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 6fb2505e87facb..4a2b5a7bfdae93 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -19,9 +19,9 @@ use crate::types::diagnostic::{ use crate::types::generics::{Specialization, SpecializationBuilder}; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ - BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, FunctionType, - KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, - UnionType, WrapperDescriptorKind, + BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, KnownClass, + KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, UnionType, + WrapperDescriptorKind, }; use ruff_db::diagnostic::{Annotation, Severity, Span, SubDiagnostic}; use ruff_python_ast as ast; @@ -424,16 +424,9 @@ impl<'db> Bindings<'db> { Type::DataclassTransformer(params) => { if let [Some(Type::FunctionLiteral(function))] = overload.parameter_types() { - overload.set_return_type(Type::FunctionLiteral(FunctionType::new( - db, - function.name(db), - function.known(db), - function.body_scope(db), - function.decorators(db), - Some(params), - function.generic_context(db), - function.specialization(db), - ))); + overload.set_return_type(Type::FunctionLiteral( + function.with_dataclass_transformer_params(db, params), + )); } } @@ -961,6 +954,10 @@ pub(crate) struct Binding<'db> { /// The specialization that was inferred from the argument types, if the callable is generic. specialization: Option>, + /// The specialization that was inferred for a class method's containing generic class, if it + /// is being used to infer a specialization for the class. + inherited_specialization: Option>, + /// The formal parameter that each argument is matched with, in argument source order, or /// `None` if the argument was not matched to any parameter. argument_parameters: Box<[Option]>, @@ -1097,6 +1094,7 @@ impl<'db> Binding<'db> { Self { return_ty: signature.return_ty.unwrap_or(Type::unknown()), specialization: None, + inherited_specialization: None, argument_parameters: argument_parameters.into_boxed_slice(), parameter_tys: vec![None; parameters.len()].into_boxed_slice(), errors, @@ -1112,8 +1110,8 @@ impl<'db> Binding<'db> { // If this overload is generic, first see if we can infer a specialization of the function // from the arguments that were passed in. let parameters = signature.parameters(); - self.specialization = signature.generic_context.map(|generic_context| { - let mut builder = SpecializationBuilder::new(db, generic_context); + if signature.generic_context.is_some() || signature.inherited_generic_context.is_some() { + let mut builder = SpecializationBuilder::new(db); for (argument_index, (_, argument_type)) in argument_types.iter().enumerate() { let Some(parameter_index) = self.argument_parameters[argument_index] else { // There was an error with argument when matching parameters, so don't bother @@ -1126,8 +1124,11 @@ impl<'db> Binding<'db> { }; builder.infer(expected_type, argument_type); } - builder.build() - }); + self.specialization = signature.generic_context.map(|gc| builder.build(gc)); + self.inherited_specialization = signature + .inherited_generic_context + .map(|gc| builder.build(gc)); + } let mut num_synthetic_args = 0; let get_argument_index = |argument_index: usize, num_synthetic_args: usize| { @@ -1155,6 +1156,9 @@ impl<'db> Binding<'db> { if let Some(specialization) = self.specialization { expected_ty = expected_ty.apply_specialization(db, specialization); } + if let Some(inherited_specialization) = self.inherited_specialization { + expected_ty = expected_ty.apply_specialization(db, inherited_specialization); + } if !argument_type.is_assignable_to(db, expected_ty) { let positional = matches!(argument, Argument::Positional | Argument::Synthetic) && !parameter.is_variadic(); @@ -1180,6 +1184,11 @@ impl<'db> Binding<'db> { if let Some(specialization) = self.specialization { self.return_ty = self.return_ty.apply_specialization(db, specialization); } + if let Some(inherited_specialization) = self.inherited_specialization { + self.return_ty = self + .return_ty + .apply_specialization(db, inherited_specialization); + } } pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) { @@ -1190,8 +1199,8 @@ impl<'db> Binding<'db> { self.return_ty } - pub(crate) fn specialization(&self) -> Option> { - self.specialization + pub(crate) fn inherited_specialization(&self) -> Option> { + self.inherited_specialization } pub(crate) fn parameter_types(&self) -> &[Option>] { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index f35c6a3eb2f45f..85aa50aa0dadf4 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -1017,7 +1017,7 @@ impl<'db> ClassLiteralType<'db> { Some(_), "__new__" | "__init__", ) => Type::FunctionLiteral( - function.with_generic_context(db, origin.generic_context(db)), + function.with_inherited_generic_context(db, origin.generic_context(db)), ), _ => ty, } diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index ef5d66a772eed5..66cd67f263e300 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -299,22 +299,19 @@ impl<'db> Specialization<'db> { /// specialization of a generic function. pub(crate) struct SpecializationBuilder<'db> { db: &'db dyn Db, - generic_context: GenericContext<'db>, types: FxHashMap, UnionBuilder<'db>>, } impl<'db> SpecializationBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db, generic_context: GenericContext<'db>) -> Self { + pub(crate) fn new(db: &'db dyn Db) -> Self { Self { db, - generic_context, types: FxHashMap::default(), } } - pub(crate) fn build(mut self) -> Specialization<'db> { - let types: Box<[_]> = self - .generic_context + pub(crate) fn build(&mut self, generic_context: GenericContext<'db>) -> Specialization<'db> { + let types: Box<[_]> = generic_context .variables(self.db) .iter() .map(|variable| { @@ -324,7 +321,7 @@ impl<'db> SpecializationBuilder<'db> { .unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown())) }) .collect(); - Specialization::new(self.db, self.generic_context, types) + Specialization::new(self.db, generic_context, types) } fn add_type_mapping(&mut self, typevar: TypeVarInstance<'db>, ty: Type<'db>) { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index e07f17d7ac7b2a..8dcfe141675414 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1525,10 +1525,6 @@ impl<'db> TypeInferenceBuilder<'db> { } } - let generic_context = type_params.as_ref().map(|type_params| { - GenericContext::from_type_params(self.db(), self.index, type_params) - }); - let function_kind = KnownFunction::try_from_definition_and_name(self.db(), definition, name); @@ -1537,6 +1533,7 @@ impl<'db> TypeInferenceBuilder<'db> { .node_scope(NodeWithScopeRef::Function(function)) .to_scope_id(self.db(), self.file()); + let inherited_generic_context = None; let specialization = None; let mut inferred_ty = Type::FunctionLiteral(FunctionType::new( @@ -1546,7 +1543,7 @@ impl<'db> TypeInferenceBuilder<'db> { body_scope, function_decorators, dataclass_transformer_params, - generic_context, + inherited_generic_context, specialization, )); diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index c74554fe91f445..5e931c5d96570a 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -166,6 +166,7 @@ impl<'db> CallableSignature<'db> { pub(crate) fn dynamic(signature_type: Type<'db>) -> Self { let signature = Signature { generic_context: None, + inherited_generic_context: None, parameters: Parameters::gradual_form(), return_ty: Some(signature_type), }; @@ -178,6 +179,7 @@ impl<'db> CallableSignature<'db> { let signature_type = todo_type!(reason); let signature = Signature { generic_context: None, + inherited_generic_context: None, parameters: Parameters::todo(), return_ty: Some(signature_type), }; @@ -215,6 +217,11 @@ pub struct Signature<'db> { /// The generic context for this overload, if it is generic. pub(crate) generic_context: Option>, + /// The inherited generic context, if this function is a class method being used to infer the + /// specialization of its generic class. If the method is itself generic, this is in addition + /// to its own generic context. + pub(crate) inherited_generic_context: Option>, + /// Parameters, in source order. /// /// The ordering of parameters in a valid signature must be: first positional-only parameters, @@ -233,6 +240,7 @@ impl<'db> Signature<'db> { pub(crate) fn new(parameters: Parameters<'db>, return_ty: Option>) -> Self { Self { generic_context: None, + inherited_generic_context: None, parameters, return_ty, } @@ -245,6 +253,7 @@ impl<'db> Signature<'db> { ) -> Self { Self { generic_context, + inherited_generic_context: None, parameters, return_ty, } @@ -254,6 +263,7 @@ impl<'db> Signature<'db> { pub(super) fn from_function( db: &'db dyn Db, generic_context: Option>, + inherited_generic_context: Option>, definition: Definition<'db>, function_node: &ast::StmtFunctionDef, ) -> Self { @@ -267,6 +277,7 @@ impl<'db> Signature<'db> { Self { generic_context, + inherited_generic_context, parameters: Parameters::from_parameters( db, definition, @@ -279,6 +290,7 @@ impl<'db> Signature<'db> { pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self { Self { generic_context: self.generic_context, + inherited_generic_context: self.inherited_generic_context, parameters: self .parameters .iter() @@ -295,6 +307,7 @@ impl<'db> Signature<'db> { ) -> Self { Self { generic_context: self.generic_context, + inherited_generic_context: self.inherited_generic_context, parameters: self.parameters.apply_specialization(db, specialization), return_ty: self .return_ty @@ -310,6 +323,7 @@ impl<'db> Signature<'db> { pub(crate) fn bind_self(&self) -> Self { Self { generic_context: self.generic_context, + inherited_generic_context: self.inherited_generic_context, parameters: Parameters::new(self.parameters().iter().skip(1).cloned()), return_ty: self.return_ty, } From d5410ef9feaf9b3c35f7da98d8314c1a30af7fa9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Wed, 23 Apr 2025 15:45:51 -0400 Subject: [PATCH 0089/1161] [syntax-errors] Make duplicate parameter names a semantic error (#17131) Status -- This is a pretty minor change, but it was breaking a red-knot mdtest until #17463 landed. Now this should close #11934 as the last syntax error being tracked there! Summary -- Moves `Parser::validate_parameters` to `SemanticSyntaxChecker::duplicate_parameter_name`. Test Plan -- Existing tests, with `## Errors` replaced with `## Semantic Syntax Errors`. --- crates/ruff_linter/src/checkers/ast/mod.rs | 3 +- crates/ruff_python_parser/src/error.rs | 5 -- .../src/parser/statement.rs | 25 -------- .../ruff_python_parser/src/semantic_errors.rs | 60 ++++++++++++++++++- ...sions__lambda_duplicate_parameters.py.snap | 18 +++--- ...alid_syntax@params_duplicate_names.py.snap | 3 +- 6 files changed, 70 insertions(+), 44 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 5ae94f21b3709f..87169d7a910285 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -615,7 +615,8 @@ impl SemanticSyntaxContext for Checker<'_> { | SemanticSyntaxErrorKind::DuplicateMatchKey(_) | SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_) | SemanticSyntaxErrorKind::InvalidStarExpression - | SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_) => { + | SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_) + | SemanticSyntaxErrorKind::DuplicateParameter(_) => { if self.settings.preview.is_enabled() { self.semantic_errors.borrow_mut().push(error); } diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 3673ded43a3a96..bc170ba838be5b 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -126,8 +126,6 @@ pub enum ParseErrorType { /// A default value was found for a `*` or `**` parameter. VarParameterWithDefault, - /// A duplicate parameter was found in a function definition or lambda expression. - DuplicateParameter(String), /// A keyword argument was repeated. DuplicateKeywordArgumentError(String), @@ -285,9 +283,6 @@ impl std::fmt::Display for ParseErrorType { f.write_str("Invalid augmented assignment target") } ParseErrorType::InvalidDeleteTarget => f.write_str("Invalid delete target"), - ParseErrorType::DuplicateParameter(arg_name) => { - write!(f, "Duplicate parameter {arg_name:?}") - } ParseErrorType::DuplicateKeywordArgumentError(arg_name) => { write!(f, "Duplicate keyword argument {arg_name:?}") } diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 86d6fde4c15402..2361b252e6b8a5 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -1,8 +1,6 @@ use compact_str::CompactString; use std::fmt::{Display, Write}; -use rustc_hash::{FxBuildHasher, FxHashSet}; - use ruff_python_ast::name::Name; use ruff_python_ast::{ self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, PythonVersion, Stmt, @@ -3339,10 +3337,6 @@ impl<'src> Parser<'src> { parameters.range = self.node_range(start); - // test_err params_duplicate_names - // def foo(a, a=10, *a, a, a: str, **a): ... - self.validate_parameters(¶meters); - parameters } @@ -3630,25 +3624,6 @@ impl<'src> Parser<'src> { } } - /// Validate that the given parameters doesn't have any duplicate names. - /// - /// Report errors for all the duplicate names found. - fn validate_parameters(&mut self, parameters: &ast::Parameters) { - let mut all_arg_names = - FxHashSet::with_capacity_and_hasher(parameters.len(), FxBuildHasher); - - for parameter in parameters { - let range = parameter.name().range(); - let param_name = parameter.name().as_str(); - if !all_arg_names.insert(param_name) { - self.add_error( - ParseErrorType::DuplicateParameter(param_name.to_string()), - range, - ); - } - } - } - /// Classify the `match` soft keyword token. /// /// # Panics diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 39b85e53929936..fd41ea21879f9e 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -13,7 +13,7 @@ use ruff_python_ast::{ StmtImportFrom, }; use ruff_text_size::{Ranged, TextRange, TextSize}; -use rustc_hash::FxHashSet; +use rustc_hash::{FxBuildHasher, FxHashSet}; #[derive(Debug, Default)] pub struct SemanticSyntaxChecker { @@ -74,8 +74,17 @@ impl SemanticSyntaxChecker { visitor.visit_pattern(&case.pattern); } } - Stmt::FunctionDef(ast::StmtFunctionDef { type_params, .. }) - | Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) + Stmt::FunctionDef(ast::StmtFunctionDef { + type_params, + parameters, + .. + }) => { + if let Some(type_params) = type_params { + Self::duplicate_type_parameter_name(type_params, ctx); + } + Self::duplicate_parameter_name(parameters, ctx); + } + Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) | Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => { if let Some(type_params) = type_params { Self::duplicate_type_parameter_name(type_params, ctx); @@ -453,6 +462,32 @@ impl SemanticSyntaxChecker { } } + fn duplicate_parameter_name( + parameters: &ast::Parameters, + ctx: &Ctx, + ) { + if parameters.len() < 2 { + return; + } + + let mut all_arg_names = + FxHashSet::with_capacity_and_hasher(parameters.len(), FxBuildHasher); + + for parameter in parameters { + let range = parameter.name().range(); + let param_name = parameter.name().as_str(); + if !all_arg_names.insert(param_name) { + // test_err params_duplicate_names + // def foo(a, a=10, *a, a, a: str, **a): ... + Self::add_error( + ctx, + SemanticSyntaxErrorKind::DuplicateParameter(param_name.to_string()), + range, + ); + } + } + } + fn irrefutable_match_case(stmt: &ast::StmtMatch, ctx: &Ctx) { // test_ok irrefutable_case_pattern_at_end // match x: @@ -646,6 +681,12 @@ impl SemanticSyntaxChecker { Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await); Self::await_outside_async_function(ctx, expr, AwaitOutsideAsyncFunctionKind::Await); } + Expr::Lambda(ast::ExprLambda { + parameters: Some(parameters), + .. + }) => { + Self::duplicate_parameter_name(parameters, ctx); + } _ => {} } } @@ -889,6 +930,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind) => { write!(f, "{kind} outside of an asynchronous function") } + SemanticSyntaxErrorKind::DuplicateParameter(name) => { + write!(f, r#"Duplicate parameter "{name}""#) + } } } } @@ -1200,6 +1244,16 @@ pub enum SemanticSyntaxErrorKind { /// async with x: ... # error /// ``` AwaitOutsideAsyncFunction(AwaitOutsideAsyncFunctionKind), + + /// Represents a duplicate parameter name in a function or lambda expression. + /// + /// ## Examples + /// + /// ```python + /// def f(x, x): ... + /// lambda x, x: ... + /// ``` + DuplicateParameter(String), } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap index 04b08939f39db4..6ea4a192edaf1e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap @@ -284,6 +284,16 @@ Module( ``` ## Errors + | +7 | lambda a, *a: 1 +8 | +9 | lambda a, *, **a: 1 + | ^^^ Syntax Error: Expected one or more keyword parameter after '*' separator + | + + +## Semantic Syntax Errors + | 1 | lambda a, a: 1 | ^ Syntax Error: Duplicate parameter "a" @@ -322,14 +332,6 @@ Module( | - | -7 | lambda a, *a: 1 -8 | -9 | lambda a, *, **a: 1 - | ^^^ Syntax Error: Expected one or more keyword parameter after '*' separator - | - - | 7 | lambda a, *a: 1 8 | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_duplicate_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_duplicate_names.py.snap index f7bfc720440aca..50e5e76378f269 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_duplicate_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_duplicate_names.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/params_duplicate_names.py -snapshot_kind: text --- ## AST @@ -132,7 +131,7 @@ Module( }, ) ``` -## Errors +## Semantic Syntax Errors | 1 | def foo(a, a=10, *a, a, a: str, **a): ... From bfc1650198a12db8e6667720e41087332c8c4ce5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:18:42 -0400 Subject: [PATCH 0090/1161] [red-knot] Add mdtests for `global` statement (#17563) ## Summary This is a first step toward `global` support in red-knot (#15385). I went through all the matches for `global` in the `mypy/test-data` directory, but I didn't find anything too interesting that wasn't already covered by @carljm's suggestions on Discord. I still pulled in a couple of cases for a little extra variety. I also included a section from the [PLE0118](https://docs.astral.sh/ruff/rules/load-before-global-declaration/) tests in ruff that will become syntax errors once #17463 is merged and we handle `global` statements. I don't think I figured out how to use `@Todo` properly, so please let me know if I need to fix that. I hope this is a good start to the test suite otherwise. --------- Co-authored-by: Carl Meyer --- .../resources/mdtest/scopes/global.md | 177 ++++++++++++++++++ .../resources/mdtest/scopes/nonlocal.md | 11 -- 2 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/scopes/global.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md new file mode 100644 index 00000000000000..ef5c1b6a72a63f --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md @@ -0,0 +1,177 @@ +# `global` references + +## Implicit global in function + +A name reference to a never-defined symbol in a function is implicitly a global lookup. + +```py +x = 1 + +def f(): + reveal_type(x) # revealed: Unknown | Literal[1] +``` + +## Explicit global in function + +```py +x = 1 + +def f(): + global x + reveal_type(x) # revealed: Unknown | Literal[1] +``` + +## Unassignable type in function + +```py +x: int = 1 + +def f(): + y: int = 1 + # error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`" + y = "" + + global x + # TODO: error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`" + x = "" +``` + +## Nested intervening scope + +A `global` statement causes lookup to skip any bindings in intervening scopes: + +```py +x: int = 1 + +def outer(): + x: str = "" + + def inner(): + global x + # TODO: revealed: int + reveal_type(x) # revealed: str +``` + +## Narrowing + +An assignment following a `global` statement should narrow the type in the local scope after the +assignment. + +```py +x: int | None + +def f(): + global x + x = 1 + reveal_type(x) # revealed: Literal[1] +``` + +## `nonlocal` and `global` + +A binding cannot be both `nonlocal` and `global`. This should emit a semantic syntax error. CPython +marks the `nonlocal` line, while `mypy`, `pyright`, and `ruff` (`PLE0115`) mark the `global` line. + +```py +x = 1 + +def f(): + x = 1 + def g() -> None: + nonlocal x + global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global" + x = None +``` + +## Global declaration after `global` statement + +```py +def f(): + global x + # TODO this should also not be an error + y = x # error: [unresolved-reference] "Name `x` used when not defined" + x = 1 # No error. + +x = 2 +``` + +## Semantic syntax errors + +Using a name prior to its `global` declaration in the same scope is a syntax error. + +```py +x = 1 + +def f(): + print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x + print(x) + +def f(): + global x + print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x + print(x) + +def f(): + print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x, y + print(x) + +def f(): + global x, y + print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x, y + print(x) + +def f(): + x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x + x = 1 + +def f(): + global x + x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x + x = 1 + +def f(): + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x, y + del x + +def f(): + global x, y + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x, y + del x + +def f(): + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x + del x + +def f(): + global x + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x + del x + +def f(): + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x, y + del x + +def f(): + global x, y + del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x, y + del x + +def f(): + print(f"{x=}") # TODO: error: [invalid-syntax] name `x` is used prior to global declaration + global x + +# still an error in module scope +x = None # TODO: error: [invalid-syntax] name `x` is used prior to global declaration +global x +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md index 7aa16794c3553d..862757ce95e0ee 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md @@ -43,14 +43,3 @@ def f(): def h(): reveal_type(x) # revealed: Unknown | Literal[1] ``` - -## Implicit global in function - -A name reference to a never-defined symbol in a function is implicitly a global lookup. - -```py -x = 1 - -def f(): - reveal_type(x) # revealed: Unknown | Literal[1] -``` From 7b6222700b9eee040293d1c692add4c3357b70ea Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 24 Apr 2025 02:57:05 +0530 Subject: [PATCH 0091/1161] [red-knot] Add `FunctionType::to_overloaded` (#17585) ## Summary This PR adds a new method `FunctionType::to_overloaded` which converts a `FunctionType` into an `OverloadedFunction` which contains all the `@overload`-ed `FunctionType` and the implementation `FunctionType` if it exists. There's a big caveat here (it's the way overloads work) which is that this method can only "see" all the overloads that comes _before_ itself. Consider the following example: ```py from typing import overload @overload def foo() -> None: ... @overload def foo(x: int) -> int: ... def foo(x: int | None) -> int | None: return x ``` Here, when the `to_overloaded` method is invoked on the 1. first `foo` definition, it would only contain a single overload which is itself and no implementation. 2. second `foo` definition, it would contain both overloads and still no implementation 3. third `foo` definition, it would contain both overloads and the implementation which is itself ### Usages This method will be used in the logic for checking invalid overload usages. It can also be used for #17541. ## Test Plan Make sure that existing tests pass. --- crates/red_knot_python_semantic/src/types.rs | 169 ++++++++++++++----- 1 file changed, 127 insertions(+), 42 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 9f45c4bb85a392..8e9d3edb5ae492 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -5921,6 +5921,19 @@ impl<'db> IntoIterator for &'db FunctionSignature<'db> { } } +/// An overloaded function. +/// +/// This is created by the [`to_overloaded`] method on [`FunctionType`]. +/// +/// [`to_overloaded`]: FunctionType::to_overloaded +#[derive(Debug, PartialEq, Eq, salsa::Update)] +struct OverloadedFunction<'db> { + /// The overloads of this function. + overloads: Vec>, + /// The implementation of this overloaded function, if any. + implementation: Option>, +} + #[salsa::interned(debug)] pub struct FunctionType<'db> { /// Name of the function at definition. @@ -6009,49 +6022,20 @@ impl<'db> FunctionType<'db> { /// would depend on the function's AST and rerun for every change in that file. #[salsa::tracked(return_ref)] pub(crate) fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> { - let internal_signature = self.internal_signature(db); - - // The semantic model records a use for each function on the name node. This is used here - // to get the previous function definition with the same name. - let scope = self.definition(db).scope(db); - let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db)); - let use_id = self - .body_scope(db) - .node(db) - .expect_function() - .name - .scoped_use_id(db, scope); - - if let Symbol::Type(Type::FunctionLiteral(function_literal), Boundness::Bound) = - symbol_from_bindings(db, use_def.bindings_at_use(use_id)) - { - match function_literal.signature(db) { - FunctionSignature::Single(_) => { - debug_assert!( - !function_literal.has_known_decorator(db, FunctionDecorators::OVERLOAD), - "Expected `FunctionSignature::Overloaded` if the previous function was an overload" - ); - } - FunctionSignature::Overloaded(_, Some(_)) => { - // If the previous overloaded function already has an implementation, then this - // new signature completely replaces it. - } - FunctionSignature::Overloaded(signatures, None) => { - return if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) { - let mut signatures = signatures.clone(); - signatures.push(internal_signature); - FunctionSignature::Overloaded(signatures, None) - } else { - FunctionSignature::Overloaded(signatures.clone(), Some(internal_signature)) - }; - } - } - } - - if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) { - FunctionSignature::Overloaded(vec![internal_signature], None) + if let Some(overloaded) = self.to_overloaded(db) { + FunctionSignature::Overloaded( + overloaded + .overloads + .iter() + .copied() + .map(|overload| overload.internal_signature(db)) + .collect(), + overloaded + .implementation + .map(|implementation| implementation.internal_signature(db)), + ) } else { - FunctionSignature::Single(internal_signature) + FunctionSignature::Single(self.internal_signature(db)) } } @@ -6142,6 +6126,107 @@ impl<'db> FunctionType<'db> { Some(specialization), ) } + + /// Returns `self` as [`OverloadedFunction`] if it is overloaded, [`None`] otherwise. + /// + /// ## Note + /// + /// The way this method works only allows us to "see" the overloads that are defined before + /// this function definition. This is because the semantic model records a use for each + /// function on the name node which is used to get the previous function definition with the + /// same name. This means that [`OverloadedFunction`] would only include the functions that + /// comes before this function definition. Consider the following example: + /// + /// ```py + /// from typing import overload + /// + /// @overload + /// def foo() -> None: ... + /// @overload + /// def foo(x: int) -> int: ... + /// def foo(x: int | None) -> int | None: + /// return x + /// ``` + /// + /// Here, when the `to_overloaded` method is invoked on the + /// 1. first `foo` definition, it would only contain a single overload which is itself and no + /// implementation + /// 2. second `foo` definition, it would contain both overloads and still no implementation + /// 3. third `foo` definition, it would contain both overloads and the implementation which is + /// itself + fn to_overloaded(self, db: &'db dyn Db) -> Option<&'db OverloadedFunction<'db>> { + #[allow(clippy::ref_option)] // TODO: Remove once salsa supports deref (https://github.com/salsa-rs/salsa/pull/772) + #[salsa::tracked(return_ref)] + fn to_overloaded_impl<'db>( + db: &'db dyn Db, + function: FunctionType<'db>, + ) -> Option> { + // The semantic model records a use for each function on the name node. This is used here + // to get the previous function definition with the same name. + let scope = function.definition(db).scope(db); + let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db)); + let use_id = function + .body_scope(db) + .node(db) + .expect_function() + .name + .scoped_use_id(db, scope); + + if let Symbol::Type(Type::FunctionLiteral(function_literal), Boundness::Bound) = + symbol_from_bindings(db, use_def.bindings_at_use(use_id)) + { + match function_literal.to_overloaded(db) { + None => { + debug_assert!( + !function_literal.has_known_decorator(db, FunctionDecorators::OVERLOAD), + "Expected `Some(OverloadedFunction)` if the previous function was an overload" + ); + } + Some(OverloadedFunction { + implementation: Some(_), + .. + }) => { + // If the previous overloaded function already has an implementation, then this + // new signature completely replaces it. + } + Some(OverloadedFunction { + overloads, + implementation: None, + }) => { + return Some( + if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) { + let mut overloads = overloads.clone(); + overloads.push(function); + OverloadedFunction { + overloads, + implementation: None, + } + } else { + OverloadedFunction { + overloads: overloads.clone(), + implementation: Some(function), + } + }, + ); + } + } + } + + if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) { + Some(OverloadedFunction { + overloads: vec![function], + implementation: None, + }) + } else { + None + } + } + + // HACK: This is required because salsa doesn't support returning `Option<&T>` from tracked + // functions yet. Refer to https://github.com/salsa-rs/salsa/pull/772. Remove the inner + // function once it's supported. + to_overloaded_impl(db, self).as_ref() + } } /// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might From 00e73dc331ee9fc07325e453580f2bc908c7f2d9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 23 Apr 2025 22:36:12 +0100 Subject: [PATCH 0092/1161] [red-knot] Infer the members of a protocol class (#17556) --- .../resources/mdtest/protocols.md | 78 +++++++---- .../src/semantic_index/use_def.rs | 9 ++ .../src/types/call/bind.rs | 20 ++- .../src/types/class.rs | 126 ++++++++++++++++++ 4 files changed, 205 insertions(+), 28 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 966cfcebabcf2f..9988ea761fae5c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -315,7 +315,7 @@ reveal_type(Protocol()) # revealed: Unknown class MyProtocol(Protocol): x: int -# error +# TODO: should emit error reveal_type(MyProtocol()) # revealed: MyProtocol ``` @@ -363,16 +363,8 @@ class Foo(Protocol): def method_member(self) -> bytes: return b"foo" -# TODO: at runtime, `get_protocol_members` returns a `frozenset`, -# but for now we might pretend it returns a `tuple`, as we support heterogeneous `tuple` types -# but not yet generic `frozenset`s -# -# So this should either be -# -# `tuple[Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]` -# -# `frozenset[Literal["x", "y", "z", "method_member"]]` -reveal_type(get_protocol_members(Foo)) # revealed: @Todo(specialized non-generic class) +# TODO: actually a frozenset (requires support for legacy generics) +reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["method_member"], Literal["x"], Literal["y"], Literal["z"]] ``` Certain special attributes and methods are not considered protocol members at runtime, and should @@ -390,8 +382,8 @@ class Lumberjack(Protocol): def __init__(self, x: int) -> None: self.x = x -# TODO: `tuple[Literal["x"]]` or `frozenset[Literal["x"]]` -reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(specialized non-generic class) +# TODO: actually a frozenset +reveal_type(get_protocol_members(Lumberjack)) # revealed: tuple[Literal["x"]] ``` A sub-protocol inherits and extends the members of its superclass protocol(s): @@ -403,15 +395,42 @@ class Bar(Protocol): class Baz(Bar, Protocol): ham: memoryview -# TODO: `tuple[Literal["spam", "ham"]]` or `frozenset[Literal["spam", "ham"]]` -reveal_type(get_protocol_members(Baz)) # revealed: @Todo(specialized non-generic class) +# TODO: actually a frozenset +reveal_type(get_protocol_members(Baz)) # revealed: tuple[Literal["ham"], Literal["spam"]] class Baz2(Bar, Foo, Protocol): ... -# TODO: either -# `tuple[Literal["spam"], Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]` -# or `frozenset[Literal["spam", "x", "y", "z", "method_member"]]` -reveal_type(get_protocol_members(Baz2)) # revealed: @Todo(specialized non-generic class) +# TODO: actually a frozenset +# revealed: tuple[Literal["method_member"], Literal["spam"], Literal["x"], Literal["y"], Literal["z"]] +reveal_type(get_protocol_members(Baz2)) +``` + +## Protocol members in statically known branches + +The list of protocol members does not include any members declared in branches that are statically +known to be unreachable: + +```toml +[environment] +python-version = "3.9" +``` + +```py +import sys +from typing_extensions import Protocol, get_protocol_members + +class Foo(Protocol): + if sys.version_info >= (3, 10): + a: int + b = 42 + def c(self) -> None: ... + else: + d: int + e = 56 + def f(self) -> None: ... + +# TODO: actually a frozenset +reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["d"], Literal["e"], Literal["f"]] ``` ## Invalid calls to `get_protocol_members()` @@ -639,14 +658,14 @@ class LotsOfBindings(Protocol): case l: # TODO: this should error with `[invalid-protocol]` (`l` is not declared) ... -# TODO: all bindings in the above class should be understood as protocol members, -# even those that we complained about with a diagnostic -reveal_type(get_protocol_members(LotsOfBindings)) # revealed: @Todo(specialized non-generic class) +# TODO: actually a frozenset +# revealed: tuple[Literal["Nested"], Literal["NestedProtocol"], Literal["a"], Literal["b"], Literal["c"], Literal["d"], Literal["e"], Literal["f"], Literal["g"], Literal["h"], Literal["i"], Literal["j"], Literal["k"], Literal["l"]] +reveal_type(get_protocol_members(LotsOfBindings)) ``` Attribute members are allowed to have assignments in methods on the protocol class, just like -non-protocol classes. Unlike other classes, however, *implicit* instance attributes -- those that -are not declared in the class body -- are not allowed: +non-protocol classes. Unlike other classes, however, instance attributes that are not declared in +the class body are disallowed: ```py class Foo(Protocol): @@ -655,11 +674,18 @@ class Foo(Protocol): def __init__(self) -> None: self.x = 42 # fine - self.a = 56 # error + self.a = 56 # TODO: should emit diagnostic + self.b: int = 128 # TODO: should emit diagnostic def non_init_method(self) -> None: self.y = 64 # fine - self.b = 72 # error + self.c = 72 # TODO: should emit diagnostic + +# Note: the list of members does not include `a`, `b` or `c`, +# as none of these attributes is declared in the class body. +# +# TODO: actually a frozenset +reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["non_init_method"], Literal["x"], Literal["y"]] ``` If a protocol has 0 members, then all other types are assignable to it, and all fully static types diff --git a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs b/crates/red_knot_python_semantic/src/semantic_index/use_def.rs index ed540c380a28b5..fe7f4ed0db9a11 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/use_def.rs @@ -437,6 +437,15 @@ impl<'db> UseDefMap<'db> { .map(|symbol_id| (symbol_id, self.public_declarations(symbol_id))) } + pub(crate) fn all_public_bindings<'map>( + &'map self, + ) -> impl Iterator)> + 'map + { + (0..self.public_symbols.len()) + .map(ScopedSymbolId::from_usize) + .map(|symbol_id| (symbol_id, self.public_bindings(symbol_id))) + } + /// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`. pub(crate) fn can_implicit_return(&self, db: &dyn crate::Db) -> bool { !self diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 4a2b5a7bfdae93..4a36ab726c8779 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -20,8 +20,8 @@ use crate::types::generics::{Specialization, SpecializationBuilder}; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, KnownClass, - KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, UnionType, - WrapperDescriptorKind, + KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, TupleType, + UnionType, WrapperDescriptorKind, }; use ruff_db::diagnostic::{Annotation, Severity, Span, SubDiagnostic}; use ruff_python_ast as ast; @@ -561,6 +561,22 @@ impl<'db> Bindings<'db> { } } + Some(KnownFunction::GetProtocolMembers) => { + if let [Some(Type::ClassLiteral(class))] = overload.parameter_types() { + if let Some(protocol_class) = class.into_protocol_class(db) { + // TODO: actually a frozenset at runtime (requires support for legacy generic classes) + overload.set_return_type(Type::Tuple(TupleType::new( + db, + protocol_class + .protocol_members(db) + .iter() + .map(|member| Type::string_literal(db, member)) + .collect::]>>(), + ))); + } + } + } + Some(KnownFunction::Overload) => { // TODO: This can be removed once we understand legacy generics because the // typeshed definition for `typing.overload` is an identity function. diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 85aa50aa0dadf4..82f88a35deaa26 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -1,4 +1,5 @@ use std::hash::BuildHasherDefault; +use std::ops::Deref; use std::sync::{LazyLock, Mutex}; use super::{ @@ -13,6 +14,7 @@ use crate::types::signatures::{Parameter, Parameters}; use crate::types::{ CallableType, DataclassParams, DataclassTransformerParams, KnownInstanceType, Signature, }; +use crate::FxOrderSet; use crate::{ module_resolver::file_to_module, semantic_index::{ @@ -1710,6 +1712,11 @@ impl<'db> ClassLiteralType<'db> { Some(InheritanceCycle::Inherited) } } + + /// Returns `Some` if this is a protocol class, `None` otherwise. + pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option> { + self.is_protocol(db).then_some(ProtocolClassLiteral(self)) + } } impl<'db> From> for Type<'db> { @@ -1721,6 +1728,125 @@ impl<'db> From> for Type<'db> { } } +/// Representation of a single `Protocol` class definition. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) struct ProtocolClassLiteral<'db>(ClassLiteralType<'db>); + +impl<'db> ProtocolClassLiteral<'db> { + /// Returns the protocol members of this class. + /// + /// A protocol's members define the interface declared by the protocol. + /// They therefore determine how the protocol should behave with regards to + /// assignability and subtyping. + /// + /// The list of members consists of all bindings and declarations that take place + /// in the protocol's class body, except for a list of excluded attributes which should + /// not be taken into account. (This list includes `__init__` and `__new__`, which can + /// legally be defined on protocol classes but do not constitute protocol members.) + /// + /// It is illegal for a protocol class to have any instance attributes that are not declared + /// in the protocol's class body. If any are assigned to, they are not taken into account in + /// the protocol's list of members. + pub(super) fn protocol_members(self, db: &'db dyn Db) -> &'db ordermap::set::Slice { + /// The list of excluded members is subject to change between Python versions, + /// especially for dunders, but it probably doesn't matter *too* much if this + /// list goes out of date. It's up to date as of Python commit 87b1ea016b1454b1e83b9113fa9435849b7743aa + /// () + fn excluded_from_proto_members(member: &str) -> bool { + matches!( + member, + "_is_protocol" + | "__non_callable_proto_members__" + | "__static_attributes__" + | "__orig_class__" + | "__match_args__" + | "__weakref__" + | "__doc__" + | "__parameters__" + | "__module__" + | "_MutableMapping__marker" + | "__slots__" + | "__dict__" + | "__new__" + | "__protocol_attrs__" + | "__init__" + | "__class_getitem__" + | "__firstlineno__" + | "__abstractmethods__" + | "__orig_bases__" + | "_is_runtime_protocol" + | "__subclasshook__" + | "__type_params__" + | "__annotations__" + | "__annotate__" + | "__annotate_func__" + | "__annotations_cache__" + ) + } + + #[salsa::tracked(return_ref)] + fn cached_protocol_members<'db>( + db: &'db dyn Db, + class: ClassLiteralType<'db>, + ) -> Box> { + let mut members = FxOrderSet::default(); + + for parent_protocol in class + .iter_mro(db, None) + .filter_map(ClassBase::into_class) + .filter_map(|class| class.class_literal(db).0.into_protocol_class(db)) + { + let parent_scope = parent_protocol.body_scope(db); + let use_def_map = use_def_map(db, parent_scope); + let symbol_table = symbol_table(db, parent_scope); + + members.extend( + use_def_map + .all_public_declarations() + .flat_map(|(symbol_id, declarations)| { + symbol_from_declarations(db, declarations) + .map(|symbol| (symbol_id, symbol)) + }) + .filter_map(|(symbol_id, symbol)| { + symbol.symbol.ignore_possibly_unbound().map(|_| symbol_id) + }) + // Bindings in the class body that are not declared in the class body + // are not valid protocol members, and we plan to emit diagnostics for them + // elsewhere. Invalid or not, however, it's important that we still consider + // them to be protocol members. The implementation of `issubclass()` and + // `isinstance()` for runtime-checkable protocols considers them to be protocol + // members at runtime, and it's important that we accurately understand + // type narrowing that uses `isinstance()` or `issubclass()` with + // runtime-checkable protocols. + .chain(use_def_map.all_public_bindings().filter_map( + |(symbol_id, bindings)| { + symbol_from_bindings(db, bindings) + .ignore_possibly_unbound() + .map(|_| symbol_id) + }, + )) + .map(|symbol_id| symbol_table.symbol(symbol_id).name()) + .filter(|name| !excluded_from_proto_members(name)) + .cloned(), + ); + } + + members.sort(); + members.into_boxed_slice() + } + + cached_protocol_members(db, *self) + } +} + +impl<'db> Deref for ProtocolClassLiteral<'db> { + type Target = ClassLiteralType<'db>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(super) enum InheritanceCycle { /// The class is cyclically defined and is a participant in the cycle. From e897f37911ccc195299607dead61ed111a936664 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 23 Apr 2025 22:40:23 +0100 Subject: [PATCH 0093/1161] [red-knot] Emit diagnostics for isinstance() and issubclass() calls where a non-runtime-checkable protocol is the second argument (#17561) --- .../resources/mdtest/protocols.md | 17 +- ..._-_Protocols_-_Narrowing_of_protocols.snap | 241 ++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 6 +- .../src/types/class.rs | 19 +- .../src/types/diagnostic.rs | 48 +++- .../src/types/infer.rs | 21 +- 6 files changed, 336 insertions(+), 16 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 9988ea761fae5c..abba908825da84 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -1199,23 +1199,25 @@ static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty)) # er ## Narrowing of protocols + + By default, a protocol class cannot be used as the second argument to `isinstance()` or `issubclass()`, and a type checker must emit an error on such calls. However, we still narrow the type inside these branches (this matches the behaviour of other type checkers): ```py -from typing import Protocol +from typing_extensions import Protocol, reveal_type class HasX(Protocol): x: int def f(arg: object, arg2: type): - if isinstance(arg, HasX): # error + if isinstance(arg, HasX): # error: [invalid-argument-type] reveal_type(arg) # revealed: HasX else: reveal_type(arg) # revealed: ~HasX - if issubclass(arg2, HasX): # error + if issubclass(arg2, HasX): # error: [invalid-argument-type] reveal_type(arg2) # revealed: type[HasX] else: reveal_type(arg2) # revealed: type & ~type[HasX] @@ -1250,10 +1252,10 @@ class OnlyMethodMembers(Protocol): def method(self) -> None: ... def f(arg1: type, arg2: type): - if issubclass(arg1, OnlyMethodMembers): # error - reveal_type(arg1) # revealed: type[OnlyMethodMembers] + if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members) + reveal_type(arg1) # revealed: type[RuntimeCheckableHasX] else: - reveal_type(arg1) # revealed: type & ~type[OnlyMethodMembers] + reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX] if issubclass(arg2, OnlyMethodMembers): # no error! reveal_type(arg2) # revealed: type[OnlyMethodMembers] @@ -1289,8 +1291,6 @@ def _(some_list: list, some_tuple: tuple[int, str], some_sized: Sized): Add tests for: -- Assignments without declarations in protocol class bodies. And various weird ways of creating - attributes in a class body or instance method. [Example mypy tests][mypy_weird_protocols]. - More tests for protocols inside `type[]`. [Spec reference][protocols_inside_type_spec]. - Protocols with instance-method members - Protocols with `@classmethod` and `@staticmethod` @@ -1313,7 +1313,6 @@ Add tests for: [mypy_protocol_docs]: https://mypy.readthedocs.io/en/stable/protocols.html#protocols-and-structural-subtyping [mypy_protocol_tests]: https://github.com/python/mypy/blob/master/test-data/unit/check-protocols.test -[mypy_weird_protocols]: https://github.com/python/mypy/blob/a3ce6d5307e99a1b6c181eaa7c5cf134c53b7d8b/test-data/unit/check-protocols.test#L2131-L2132 [protocol conformance tests]: https://github.com/python/typing/tree/main/conformance/tests [protocols_inside_type_spec]: https://typing.python.org/en/latest/spec/protocol.html#type-and-class-objects-vs-protocols [recursive_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#recursive-protocols diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap new file mode 100644 index 00000000000000..5767ef3dbf4f8a --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap @@ -0,0 +1,241 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: protocols.md - Protocols - Narrowing of protocols +mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import Protocol, reveal_type + 2 | + 3 | class HasX(Protocol): + 4 | x: int + 5 | + 6 | def f(arg: object, arg2: type): + 7 | if isinstance(arg, HasX): # error: [invalid-argument-type] + 8 | reveal_type(arg) # revealed: HasX + 9 | else: +10 | reveal_type(arg) # revealed: ~HasX +11 | +12 | if issubclass(arg2, HasX): # error: [invalid-argument-type] +13 | reveal_type(arg2) # revealed: type[HasX] +14 | else: +15 | reveal_type(arg2) # revealed: type & ~type[HasX] +16 | from typing import runtime_checkable +17 | +18 | @runtime_checkable +19 | class RuntimeCheckableHasX(Protocol): +20 | x: int +21 | +22 | def f(arg: object): +23 | if isinstance(arg, RuntimeCheckableHasX): # no error! +24 | reveal_type(arg) # revealed: RuntimeCheckableHasX +25 | else: +26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX +27 | @runtime_checkable +28 | class OnlyMethodMembers(Protocol): +29 | def method(self) -> None: ... +30 | +31 | def f(arg1: type, arg2: type): +32 | if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members) +33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX] +34 | else: +35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX] +36 | +37 | if issubclass(arg2, OnlyMethodMembers): # no error! +38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers] +39 | else: +40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers] +``` + +# Diagnostics + +``` +error: lint:invalid-argument-type: Class `HasX` cannot be used as the second argument to `isinstance` + --> /src/mdtest_snippet.py:7:8 + | +6 | def f(arg: object, arg2: type): +7 | if isinstance(arg, HasX): # error: [invalid-argument-type] + | ^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +8 | reveal_type(arg) # revealed: HasX +9 | else: + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> /src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol, reveal_type +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `isinstance` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:8:9 + | + 6 | def f(arg: object, arg2: type): + 7 | if isinstance(arg, HasX): # error: [invalid-argument-type] + 8 | reveal_type(arg) # revealed: HasX + | ^^^^^^^^^^^^^^^^ `HasX` + 9 | else: +10 | reveal_type(arg) # revealed: ~HasX + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:10:9 + | + 8 | reveal_type(arg) # revealed: HasX + 9 | else: +10 | reveal_type(arg) # revealed: ~HasX + | ^^^^^^^^^^^^^^^^ `~HasX` +11 | +12 | if issubclass(arg2, HasX): # error: [invalid-argument-type] + | + +``` + +``` +error: lint:invalid-argument-type: Class `HasX` cannot be used as the second argument to `issubclass` + --> /src/mdtest_snippet.py:12:8 + | +10 | reveal_type(arg) # revealed: ~HasX +11 | +12 | if issubclass(arg2, HasX): # error: [invalid-argument-type] + | ^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +13 | reveal_type(arg2) # revealed: type[HasX] +14 | else: + | +info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable + --> /src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import Protocol, reveal_type +2 | +3 | class HasX(Protocol): + | ^^^^^^^^^^^^^^ `HasX` declared here +4 | x: int + | +info: A protocol class can only be used in `issubclass` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable` +info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:13:9 + | +12 | if issubclass(arg2, HasX): # error: [invalid-argument-type] +13 | reveal_type(arg2) # revealed: type[HasX] + | ^^^^^^^^^^^^^^^^^ `type[HasX]` +14 | else: +15 | reveal_type(arg2) # revealed: type & ~type[HasX] + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:15:9 + | +13 | reveal_type(arg2) # revealed: type[HasX] +14 | else: +15 | reveal_type(arg2) # revealed: type & ~type[HasX] + | ^^^^^^^^^^^^^^^^^ `type & ~type[HasX]` +16 | from typing import runtime_checkable + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:24:9 + | +22 | def f(arg: object): +23 | if isinstance(arg, RuntimeCheckableHasX): # no error! +24 | reveal_type(arg) # revealed: RuntimeCheckableHasX + | ^^^^^^^^^^^^^^^^ `RuntimeCheckableHasX` +25 | else: +26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:26:9 + | +24 | reveal_type(arg) # revealed: RuntimeCheckableHasX +25 | else: +26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX + | ^^^^^^^^^^^^^^^^ `~RuntimeCheckableHasX` +27 | @runtime_checkable +28 | class OnlyMethodMembers(Protocol): + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:33:9 + | +31 | def f(arg1: type, arg2: type): +32 | if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members) +33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX] + | ^^^^^^^^^^^^^^^^^ `type[RuntimeCheckableHasX]` +34 | else: +35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX] + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:35:9 + | +33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX] +34 | else: +35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX] + | ^^^^^^^^^^^^^^^^^ `type & ~type[RuntimeCheckableHasX]` +36 | +37 | if issubclass(arg2, OnlyMethodMembers): # no error! + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:38:9 + | +37 | if issubclass(arg2, OnlyMethodMembers): # no error! +38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers] + | ^^^^^^^^^^^^^^^^^ `type[OnlyMethodMembers]` +39 | else: +40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers] + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:40:9 + | +38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers] +39 | else: +40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers] + | ^^^^^^^^^^^^^^^^^ `type & ~type[OnlyMethodMembers]` + | + +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 8e9d3edb5ae492..ef386b35854990 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -6231,9 +6231,11 @@ impl<'db> FunctionType<'db> { /// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might /// have special behavior. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, strum_macros::EnumString)] +#[derive( + Debug, Copy, Clone, PartialEq, Eq, Hash, strum_macros::EnumString, strum_macros::IntoStaticStr, +)] #[strum(serialize_all = "snake_case")] -#[cfg_attr(test, derive(strum_macros::EnumIter, strum_macros::IntoStaticStr))] +#[cfg_attr(test, derive(strum_macros::EnumIter))] pub enum KnownFunction { /// `builtins.isinstance` #[strum(serialize = "isinstance")] diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 82f88a35deaa26..20e0624dd35ac5 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -629,12 +629,20 @@ impl<'db> ClassLiteralType<'db> { .collect() } - /// Is this class final? - pub(super) fn is_final(self, db: &'db dyn Db) -> bool { + fn known_function_decorators( + self, + db: &'db dyn Db, + ) -> impl Iterator + 'db { self.decorators(db) .iter() .filter_map(|deco| deco.into_function_literal()) - .any(|decorator| decorator.is_known(db, KnownFunction::Final)) + .filter_map(|decorator| decorator.known(db)) + } + + /// Is this class final? + pub(super) fn is_final(self, db: &'db dyn Db) -> bool { + self.known_function_decorators(db) + .contains(&KnownFunction::Final) } /// Attempt to resolve the [method resolution order] ("MRO") for this class. @@ -1837,6 +1845,11 @@ impl<'db> ProtocolClassLiteral<'db> { cached_protocol_members(db, *self) } + + pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool { + self.known_function_decorators(db) + .contains(&KnownFunction::RuntimeCheckable) + } } impl<'db> Deref for ProtocolClassLiteral<'db> { diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index ec546255cfb8d8..c4b7d4f87b31a1 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -8,7 +8,7 @@ use crate::types::string_annotation::{ IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, RAW_STRING_TYPE_ANNOTATION, }; -use crate::types::{KnownInstanceType, Type}; +use crate::types::{class::ProtocolClassLiteral, KnownFunction, KnownInstanceType, Type}; use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic}; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextRange}; @@ -1375,3 +1375,49 @@ pub(crate) fn report_invalid_arguments_to_callable( KnownInstanceType::Callable.repr(context.db()) )); } + +pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( + context: &InferContext, + call: &ast::ExprCall, + protocol: ProtocolClassLiteral, + function: KnownFunction, +) { + let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call) else { + return; + }; + let db = context.db(); + let class_name = protocol.name(db); + let function_name: &'static str = function.into(); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Class `{class_name}` cannot be used as the second argument to `{function_name}`", + )); + diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + + let class_scope = protocol.body_scope(db); + let class_node = class_scope.node(db).expect_class(); + let class_def_arguments = class_node + .arguments + .as_ref() + .expect("A `Protocol` class should always have at least one explicit base"); + let mut class_def_diagnostic = SubDiagnostic::new( + Severity::Info, + format_args!( + "`{class_name}` is declared as a protocol class, \ + but it is not declared as runtime-checkable" + ), + ); + class_def_diagnostic.annotate( + Annotation::primary(Span::from(class_scope.file(db)).with_range(TextRange::new( + class_node.name.start(), + class_def_arguments.end(), + ))) + .message(format_args!("`{class_name}` declared here")), + ); + diagnostic.sub(class_def_diagnostic); + + diagnostic.info(format_args!( + "A protocol class can only be used in `{function_name}` checks if it is decorated \ + with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable`" + )); + diagnostic.info("See https://docs.python.org/3/library/typing.html#typing.runtime_checkable"); +} diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 8dcfe141675414..9ca92e493eaadb 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -99,7 +99,8 @@ use super::diagnostic::{ report_bad_argument_to_get_protocol_members, report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_type_checking_constant, - report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero, + report_non_subscriptable, report_possibly_unresolved_reference, + report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero, report_unresolved_reference, INVALID_METACLASS, INVALID_PROTOCOL, REDUNDANT_CAST, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, }; @@ -4497,6 +4498,24 @@ impl<'db> TypeInferenceBuilder<'db> { } } } + KnownFunction::IsInstance | KnownFunction::IsSubclass => { + if let [_, Some(Type::ClassLiteral(class))] = + overload.parameter_types() + { + if let Some(protocol_class) = + class.into_protocol_class(self.db()) + { + if !protocol_class.is_runtime_checkable(self.db()) { + report_runtime_check_against_non_runtime_checkable_protocol( + &self.context, + call_expression, + protocol_class, + known_function + ); + } + } + } + } _ => {} } } From 1796ca97d5785ba7de812736c66d1c788e5bc625 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 24 Apr 2025 03:54:52 +0530 Subject: [PATCH 0094/1161] [red-knot] Special case `@abstractmethod` for function type (#17591) ## Summary This is required because otherwise the inferred type is not going to be `Type::FunctionLiteral` but a todo type because we don't recognize `TypeVar` yet: ```py _FuncT = TypeVar("_FuncT", bound=Callable[..., Any]) def abstractmethod(funcobj: _FuncT) -> _FuncT: ... ``` This is mainly required to raise diagnostic when only some (and not all) `@overload`-ed functions are decorated with `@abstractmethod`. --- crates/red_knot_python_semantic/src/types.rs | 6 ++- .../src/types/call/bind.rs | 8 ++++ .../src/types/infer.rs | 45 ++++++++++++------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ef386b35854990..1050446ad2bb78 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -5876,10 +5876,12 @@ bitflags! { pub struct FunctionDecorators: u8 { /// `@classmethod` const CLASSMETHOD = 1 << 0; - /// `@no_type_check` + /// `@typing.no_type_check` const NO_TYPE_CHECK = 1 << 1; - /// `@overload` + /// `@typing.overload` const OVERLOAD = 1 << 2; + /// `@abc.abstractmethod` + const ABSTRACT_METHOD = 1 << 3; } } diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 4a36ab726c8779..93217b08693735 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -585,6 +585,14 @@ impl<'db> Bindings<'db> { } } + Some(KnownFunction::AbstractMethod) => { + // TODO: This can be removed once we understand legacy generics because the + // typeshed definition for `abc.abstractmethod` is an identity function. + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(*ty); + } + } + Some(KnownFunction::GetattrStatic) => { let [Some(instance_ty), Some(attr_name), default] = overload.parameter_types() diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 9ca92e493eaadb..dfbf928f830a89 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1482,24 +1482,37 @@ impl<'db> TypeInferenceBuilder<'db> { for decorator in decorator_list { let decorator_ty = self.infer_decorator(decorator); - if let Type::FunctionLiteral(function) = decorator_ty { - if function.is_known(self.db(), KnownFunction::NoTypeCheck) { - // If the function is decorated with the `no_type_check` decorator, - // we need to suppress any errors that come after the decorators. - self.context.set_in_no_type_check(InNoTypeCheck::Yes); - function_decorators |= FunctionDecorators::NO_TYPE_CHECK; - continue; - } else if function.is_known(self.db(), KnownFunction::Overload) { - function_decorators |= FunctionDecorators::OVERLOAD; - continue; + match decorator_ty { + Type::FunctionLiteral(function) => { + match function.known(self.db()) { + Some(KnownFunction::NoTypeCheck) => { + // If the function is decorated with the `no_type_check` decorator, + // we need to suppress any errors that come after the decorators. + self.context.set_in_no_type_check(InNoTypeCheck::Yes); + function_decorators |= FunctionDecorators::NO_TYPE_CHECK; + continue; + } + Some(KnownFunction::Overload) => { + function_decorators |= FunctionDecorators::OVERLOAD; + continue; + } + Some(KnownFunction::AbstractMethod) => { + function_decorators |= FunctionDecorators::ABSTRACT_METHOD; + continue; + } + _ => {} + } } - } else if let Type::ClassLiteral(class) = decorator_ty { - if class.is_known(self.db(), KnownClass::Classmethod) { - function_decorators |= FunctionDecorators::CLASSMETHOD; - continue; + Type::ClassLiteral(class) => { + if class.is_known(self.db(), KnownClass::Classmethod) { + function_decorators |= FunctionDecorators::CLASSMETHOD; + continue; + } } - } else if let Type::DataclassTransformer(params) = decorator_ty { - dataclass_transformer_params = Some(params); + Type::DataclassTransformer(params) => { + dataclass_transformer_params = Some(params); + } + _ => {} } decorator_types_and_nodes.push((decorator_ty, decorator)); From 48a85c4ed464dfdf26ebfdf52dae7f4aaabbcf16 Mon Sep 17 00:00:00 2001 From: camper42 Date: Thu, 24 Apr 2025 14:06:32 +0800 Subject: [PATCH 0095/1161] [`airflow`] fix typos (`AIR302`, `AIR312`) (#17574) --- .../airflow/rules/moved_to_provider_in_3.rs | 4 +- .../suggested_to_move_to_provider_in_3.rs | 4 +- ...les__airflow__tests__AIR302_AIR302.py.snap | 308 +++++++++--------- ...les__airflow__tests__AIR312_AIR312.py.snap | 68 ++-- 4 files changed, 192 insertions(+), 192 deletions(-) diff --git a/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs index 319464903e5a24..e5d97e8cba33d3 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs @@ -65,7 +65,7 @@ impl Violation for Airflow3MovedToProvider { version, } => { Some(format!( - "Install `apache-airflow-provider-{provider}>={version}` and use `{name}` instead." + "Install `apache-airflow-providers-{provider}>={version}` and use `{name}` instead." )) }, ProviderReplacement::SourceModuleMovedToProvider { @@ -74,7 +74,7 @@ impl Violation for Airflow3MovedToProvider { provider, version, } => { - Some(format!("Install `apache-airflow-provider-{provider}>={version}` and use `{module}.{name}` instead.")) + Some(format!("Install `apache-airflow-providers-{provider}>={version}` and use `{module}.{name}` instead.")) } , } } diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs index 3fb23f6879d51c..45b9648083cd6b 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs @@ -68,7 +68,7 @@ impl Violation for Airflow3SuggestedToMoveToProvider { version, } => { Some(format!( - "Install `apache-airflow-provider-{provider}>={version}` and use `{name}` instead." + "Install `apache-airflow-providers-{provider}>={version}` and use `{name}` instead." )) }, ProviderReplacement::SourceModuleMovedToProvider { @@ -77,7 +77,7 @@ impl Violation for Airflow3SuggestedToMoveToProvider { provider, version, } => { - Some(format!("Install `apache-airflow-provider-{provider}>={version}` and use `{module}.{name}` instead.")) + Some(format!("Install `apache-airflow-providers-{provider}>={version}` and use `{module}.{name}` instead.")) } } } diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap index 3019d36239f77e..6f5e159a46a9fc 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap @@ -9,7 +9,7 @@ AIR302.py:226:1: AIR302 `airflow.hooks.S3_hook.provide_bucket_name` is moved int 227 | GCSToS3Operator() 228 | GoogleApiToS3Operator() | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.hooks.s3.provide_bucket_name` instead. + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.hooks.s3.provide_bucket_name` instead. AIR302.py:227:1: AIR302 `airflow.operators.gcs_to_s3.GCSToS3Operator` is moved into `amazon` provider in Airflow 3.0; | @@ -20,7 +20,7 @@ AIR302.py:227:1: AIR302 `airflow.operators.gcs_to_s3.GCSToS3Operator` is moved i 228 | GoogleApiToS3Operator() 229 | GoogleApiToS3Transfer() | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.gcs_to_s3.GCSToS3Operator` instead. + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.gcs_to_s3.GCSToS3Operator` instead. AIR302.py:228:1: AIR302 `airflow.operators.google_api_to_s3_transfer.GoogleApiToS3Operator` is moved into `amazon` provider in Airflow 3.0; | @@ -31,7 +31,7 @@ AIR302.py:228:1: AIR302 `airflow.operators.google_api_to_s3_transfer.GoogleApiTo 229 | GoogleApiToS3Transfer() 230 | RedshiftToS3Operator() | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator` instead. + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator` instead. AIR302.py:229:1: AIR302 `airflow.operators.google_api_to_s3_transfer.GoogleApiToS3Transfer` is moved into `amazon` provider in Airflow 3.0; | @@ -42,7 +42,7 @@ AIR302.py:229:1: AIR302 `airflow.operators.google_api_to_s3_transfer.GoogleApiTo 230 | RedshiftToS3Operator() 231 | RedshiftToS3Transfer() | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator` instead. + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator` instead. AIR302.py:230:1: AIR302 `airflow.operators.redshift_to_s3_operator.RedshiftToS3Operator` is moved into `amazon` provider in Airflow 3.0; | @@ -53,7 +53,7 @@ AIR302.py:230:1: AIR302 `airflow.operators.redshift_to_s3_operator.RedshiftToS3O 231 | RedshiftToS3Transfer() 232 | S3FileTransformOperator() | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator` instead. + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator` instead. AIR302.py:231:1: AIR302 `airflow.operators.redshift_to_s3_operator.RedshiftToS3Transfer` is moved into `amazon` provider in Airflow 3.0; | @@ -64,7 +64,7 @@ AIR302.py:231:1: AIR302 `airflow.operators.redshift_to_s3_operator.RedshiftToS3T 232 | S3FileTransformOperator() 233 | S3Hook() | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator` instead. + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator` instead. AIR302.py:232:1: AIR302 `airflow.operators.s3_file_transform_operator.S3FileTransformOperator` is moved into `amazon` provider in Airflow 3.0; | @@ -75,7 +75,7 @@ AIR302.py:232:1: AIR302 `airflow.operators.s3_file_transform_operator.S3FileTran 233 | S3Hook() 234 | SSQLTableCheckOperator3KeySensor() | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.operators.s3_file_transform.S3FileTransformOperator` instead. + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.operators.s3_file_transform.S3FileTransformOperator` instead. AIR302.py:233:1: AIR302 `airflow.hooks.S3_hook.S3Hook` is moved into `amazon` provider in Airflow 3.0; | @@ -86,7 +86,7 @@ AIR302.py:233:1: AIR302 `airflow.hooks.S3_hook.S3Hook` is moved into `amazon` pr 234 | SSQLTableCheckOperator3KeySensor() 235 | S3ToRedshiftOperator() | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.hooks.s3.S3Hook` instead. + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.hooks.s3.S3Hook` instead. AIR302.py:235:1: AIR302 `airflow.operators.s3_to_redshift_operator.S3ToRedshiftOperator` is moved into `amazon` provider in Airflow 3.0; | @@ -96,7 +96,7 @@ AIR302.py:235:1: AIR302 `airflow.operators.s3_to_redshift_operator.S3ToRedshiftO | ^^^^^^^^^^^^^^^^^^^^ AIR302 236 | S3ToRedshiftTransfer() | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator` instead. + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator` instead. AIR302.py:236:1: AIR302 `airflow.operators.s3_to_redshift_operator.S3ToRedshiftTransfer` is moved into `amazon` provider in Airflow 3.0; | @@ -107,7 +107,7 @@ AIR302.py:236:1: AIR302 `airflow.operators.s3_to_redshift_operator.S3ToRedshiftT 237 | 238 | # apache-airflow-providers-celery | - = help: Install `apache-airflow-provider-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator` instead. + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator` instead. AIR302.py:239:1: AIR302 `airflow.config_templates.default_celery.DEFAULT_CELERY_CONFIG` is moved into `celery` provider in Airflow 3.0; | @@ -117,7 +117,7 @@ AIR302.py:239:1: AIR302 `airflow.config_templates.default_celery.DEFAULT_CELERY_ 240 | app 241 | CeleryExecutor() | - = help: Install `apache-airflow-provider-celery>=3.3.0` and use `airflow.providers.celery.executors.default_celery.DEFAULT_CELERY_CONFIG` instead. + = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.default_celery.DEFAULT_CELERY_CONFIG` instead. AIR302.py:240:1: AIR302 `airflow.executors.celery_executor.app` is moved into `celery` provider in Airflow 3.0; | @@ -128,7 +128,7 @@ AIR302.py:240:1: AIR302 `airflow.executors.celery_executor.app` is moved into `c 241 | CeleryExecutor() 242 | CeleryKubernetesExecutor() | - = help: Install `apache-airflow-provider-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_executor_utils.app` instead. + = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_executor_utils.app` instead. AIR302.py:241:1: AIR302 `airflow.executors.celery_executor.CeleryExecutor` is moved into `celery` provider in Airflow 3.0; | @@ -138,7 +138,7 @@ AIR302.py:241:1: AIR302 `airflow.executors.celery_executor.CeleryExecutor` is mo | ^^^^^^^^^^^^^^ AIR302 242 | CeleryKubernetesExecutor() | - = help: Install `apache-airflow-provider-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_executor.CeleryExecutor` instead. + = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_executor.CeleryExecutor` instead. AIR302.py:242:1: AIR302 `airflow.executors.celery_kubernetes_executor.CeleryKubernetesExecutor` is moved into `celery` provider in Airflow 3.0; | @@ -149,7 +149,7 @@ AIR302.py:242:1: AIR302 `airflow.executors.celery_kubernetes_executor.CeleryKube 243 | 244 | # apache-airflow-providers-common-sql | - = help: Install `apache-airflow-provider-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_kubernetes_executor.CeleryKubernetesExecutor` instead. + = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_kubernetes_executor.CeleryKubernetesExecutor` instead. AIR302.py:245:1: AIR302 `airflow.operators.sql._convert_to_float_if_possible` is moved into `common-sql` provider in Airflow 3.0; | @@ -159,7 +159,7 @@ AIR302.py:245:1: AIR302 `airflow.operators.sql._convert_to_float_if_possible` is 246 | parse_boolean() 247 | BaseSQLOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql._convert_to_float_if_possible` instead. + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql._convert_to_float_if_possible` instead. AIR302.py:246:1: AIR302 `airflow.operators.sql.parse_boolean` is moved into `common-sql` provider in Airflow 3.0; | @@ -170,7 +170,7 @@ AIR302.py:246:1: AIR302 `airflow.operators.sql.parse_boolean` is moved into `com 247 | BaseSQLOperator() 248 | BashOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.parse_boolean` instead. + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.parse_boolean` instead. AIR302.py:247:1: AIR302 `airflow.operators.sql.BaseSQLOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -181,7 +181,7 @@ AIR302.py:247:1: AIR302 `airflow.operators.sql.BaseSQLOperator` is moved into `c 248 | BashOperator() 249 | LegacyBashOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BaseSQLOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BaseSQLOperator` instead. AIR302.py:249:1: AIR302 `airflow.operators.bash_operator.BashOperator` is moved into `standard` provider in Airflow 3.0; | @@ -192,7 +192,7 @@ AIR302.py:249:1: AIR302 `airflow.operators.bash_operator.BashOperator` is moved 250 | BranchSQLOperator() 251 | CheckOperator() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.bash.BashOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.bash.BashOperator` instead. AIR302.py:250:1: AIR302 `airflow.operators.sql.BranchSQLOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -203,7 +203,7 @@ AIR302.py:250:1: AIR302 `airflow.operators.sql.BranchSQLOperator` is moved into 251 | CheckOperator() 252 | ConnectorProtocol() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BranchSQLOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BranchSQLOperator` instead. AIR302.py:251:1: AIR302 `airflow.operators.check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -214,7 +214,7 @@ AIR302.py:251:1: AIR302 `airflow.operators.check_operator.CheckOperator` is move 252 | ConnectorProtocol() 253 | DbApiHook() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. AIR302.py:252:1: AIR302 `airflow.hooks.dbapi.ConnectorProtocol` is moved into `common-sql` provider in Airflow 3.0; | @@ -225,7 +225,7 @@ AIR302.py:252:1: AIR302 `airflow.hooks.dbapi.ConnectorProtocol` is moved into `c 253 | DbApiHook() 254 | DbApiHook2() | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.ConnectorProtocol` instead. + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.ConnectorProtocol` instead. AIR302.py:253:1: AIR302 `airflow.hooks.dbapi.DbApiHook` is moved into `common-sql` provider in Airflow 3.0; | @@ -236,7 +236,7 @@ AIR302.py:253:1: AIR302 `airflow.hooks.dbapi.DbApiHook` is moved into `common-sq 254 | DbApiHook2() 255 | IntervalCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.DbApiHook` instead. + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.DbApiHook` instead. AIR302.py:254:1: AIR302 `airflow.hooks.dbapi_hook.DbApiHook` is moved into `common-sql` provider in Airflow 3.0; | @@ -247,7 +247,7 @@ AIR302.py:254:1: AIR302 `airflow.hooks.dbapi_hook.DbApiHook` is moved into `comm 255 | IntervalCheckOperator() 256 | PrestoCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.DbApiHook` instead. + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.DbApiHook` instead. AIR302.py:255:1: AIR302 `airflow.operators.check_operator.IntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -258,7 +258,7 @@ AIR302.py:255:1: AIR302 `airflow.operators.check_operator.IntervalCheckOperator` 256 | PrestoCheckOperator() 257 | PrestoIntervalCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. AIR302.py:256:1: AIR302 `airflow.operators.presto_check_operator.PrestoCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -269,7 +269,7 @@ AIR302.py:256:1: AIR302 `airflow.operators.presto_check_operator.PrestoCheckOper 257 | PrestoIntervalCheckOperator() 258 | PrestoValueCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. AIR302.py:257:1: AIR302 `airflow.operators.presto_check_operator.PrestoIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -280,7 +280,7 @@ AIR302.py:257:1: AIR302 `airflow.operators.presto_check_operator.PrestoIntervalC 258 | PrestoValueCheckOperator() 259 | SQLCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. AIR302.py:258:1: AIR302 `airflow.operators.presto_check_operator.PrestoValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -291,7 +291,7 @@ AIR302.py:258:1: AIR302 `airflow.operators.presto_check_operator.PrestoValueChec 259 | SQLCheckOperator() 260 | SQLCheckOperator2() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. AIR302.py:259:1: AIR302 `airflow.operators.check_operator.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -302,7 +302,7 @@ AIR302.py:259:1: AIR302 `airflow.operators.check_operator.SQLCheckOperator` is m 260 | SQLCheckOperator2() 261 | SQLCheckOperator3() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. AIR302.py:260:1: AIR302 `airflow.operators.presto_check_operator.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -313,7 +313,7 @@ AIR302.py:260:1: AIR302 `airflow.operators.presto_check_operator.SQLCheckOperato 261 | SQLCheckOperator3() 262 | SQLColumnCheckOperator2() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. AIR302.py:261:1: AIR302 `airflow.operators.sql.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -324,7 +324,7 @@ AIR302.py:261:1: AIR302 `airflow.operators.sql.SQLCheckOperator` is moved into ` 262 | SQLColumnCheckOperator2() 263 | SQLIntervalCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. AIR302.py:262:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -335,7 +335,7 @@ AIR302.py:262:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator` is moved 263 | SQLIntervalCheckOperator() 264 | SQLIntervalCheckOperator2() | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.SQLColumnCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.SQLColumnCheckOperator` instead. AIR302.py:263:1: AIR302 `airflow.operators.check_operator.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -346,7 +346,7 @@ AIR302.py:263:1: AIR302 `airflow.operators.check_operator.SQLIntervalCheckOperat 264 | SQLIntervalCheckOperator2() 265 | SQLIntervalCheckOperator3() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. AIR302.py:264:1: AIR302 `airflow.operators.presto_check_operator.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -357,7 +357,7 @@ AIR302.py:264:1: AIR302 `airflow.operators.presto_check_operator.SQLIntervalChec 265 | SQLIntervalCheckOperator3() 266 | SQLTableCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. AIR302.py:265:1: AIR302 `airflow.operators.sql.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -368,7 +368,7 @@ AIR302.py:265:1: AIR302 `airflow.operators.sql.SQLIntervalCheckOperator` is move 266 | SQLTableCheckOperator() 267 | SQLThresholdCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. AIR302.py:267:1: AIR302 `airflow.operators.check_operator.SQLThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -379,7 +379,7 @@ AIR302.py:267:1: AIR302 `airflow.operators.check_operator.SQLThresholdCheckOpera 268 | SQLThresholdCheckOperator2() 269 | SQLValueCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. AIR302.py:268:1: AIR302 `airflow.operators.sql.SQLThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -390,7 +390,7 @@ AIR302.py:268:1: AIR302 `airflow.operators.sql.SQLThresholdCheckOperator` is mov 269 | SQLValueCheckOperator() 270 | SQLValueCheckOperator2() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. AIR302.py:269:1: AIR302 `airflow.operators.check_operator.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -401,7 +401,7 @@ AIR302.py:269:1: AIR302 `airflow.operators.check_operator.SQLValueCheckOperator` 270 | SQLValueCheckOperator2() 271 | SQLValueCheckOperator3() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. AIR302.py:270:1: AIR302 `airflow.operators.presto_check_operator.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -412,7 +412,7 @@ AIR302.py:270:1: AIR302 `airflow.operators.presto_check_operator.SQLValueCheckOp 271 | SQLValueCheckOperator3() 272 | SqlSensor() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. AIR302.py:271:1: AIR302 `airflow.operators.sql.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -423,7 +423,7 @@ AIR302.py:271:1: AIR302 `airflow.operators.sql.SQLValueCheckOperator` is moved i 272 | SqlSensor() 273 | SqlSensor2() | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. AIR302.py:272:1: AIR302 `airflow.sensors.sql.SqlSensor` is moved into `common-sql` provider in Airflow 3.0; | @@ -434,7 +434,7 @@ AIR302.py:272:1: AIR302 `airflow.sensors.sql.SqlSensor` is moved into `common-sq 273 | SqlSensor2() 274 | ThresholdCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.0.0` and use `airflow.providers.common.sql.sensors.sql.SqlSensor` instead. + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.sensors.sql.SqlSensor` instead. AIR302.py:274:1: AIR302 `airflow.operators.check_operator.ThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -444,7 +444,7 @@ AIR302.py:274:1: AIR302 `airflow.operators.check_operator.ThresholdCheckOperator | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 275 | ValueCheckOperator() | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. AIR302.py:275:1: AIR302 `airflow.operators.check_operator.ValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | @@ -455,7 +455,7 @@ AIR302.py:275:1: AIR302 `airflow.operators.check_operator.ValueCheckOperator` is 276 | 277 | # apache-airflow-providers-daskexecutor | - = help: Install `apache-airflow-provider-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. AIR302.py:278:1: AIR302 `airflow.executors.dask_executor.DaskExecutor` is moved into `daskexecutor` provider in Airflow 3.0; | @@ -465,7 +465,7 @@ AIR302.py:278:1: AIR302 `airflow.executors.dask_executor.DaskExecutor` is moved 279 | 280 | # apache-airflow-providers-docker | - = help: Install `apache-airflow-provider-daskexecutor>=1.0.0` and use `airflow.providers.daskexecutor.executors.dask_executor.DaskExecutor` instead. + = help: Install `apache-airflow-providers-daskexecutor>=1.0.0` and use `airflow.providers.daskexecutor.executors.dask_executor.DaskExecutor` instead. AIR302.py:281:1: AIR302 `airflow.hooks.docker_hook.DockerHook` is moved into `docker` provider in Airflow 3.0; | @@ -474,7 +474,7 @@ AIR302.py:281:1: AIR302 `airflow.hooks.docker_hook.DockerHook` is moved into `do | ^^^^^^^^^^ AIR302 282 | DockerOperator() | - = help: Install `apache-airflow-provider-docker>=1.0.0` and use `airflow.providers.docker.hooks.docker.DockerHook` instead. + = help: Install `apache-airflow-providers-docker>=1.0.0` and use `airflow.providers.docker.hooks.docker.DockerHook` instead. AIR302.py:282:1: AIR302 `airflow.operators.docker_operator.DockerOperator` is moved into `docker` provider in Airflow 3.0; | @@ -485,7 +485,7 @@ AIR302.py:282:1: AIR302 `airflow.operators.docker_operator.DockerOperator` is mo 283 | 284 | # apache-airflow-providers-apache-druid | - = help: Install `apache-airflow-provider-docker>=1.0.0` and use `airflow.providers.docker.operators.docker.DockerOperator` instead. + = help: Install `apache-airflow-providers-docker>=1.0.0` and use `airflow.providers.docker.operators.docker.DockerOperator` instead. AIR302.py:285:1: AIR302 `airflow.hooks.druid_hook.DruidDbApiHook` is moved into `apache-druid` provider in Airflow 3.0; | @@ -495,7 +495,7 @@ AIR302.py:285:1: AIR302 `airflow.hooks.druid_hook.DruidDbApiHook` is moved into 286 | DruidHook() 287 | DruidCheckOperator() | - = help: Install `apache-airflow-provider-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidDbApiHook` instead. + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidDbApiHook` instead. AIR302.py:286:1: AIR302 `airflow.hooks.druid_hook.DruidHook` is moved into `apache-druid` provider in Airflow 3.0; | @@ -505,7 +505,7 @@ AIR302.py:286:1: AIR302 `airflow.hooks.druid_hook.DruidHook` is moved into `apac | ^^^^^^^^^ AIR302 287 | DruidCheckOperator() | - = help: Install `apache-airflow-provider-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidHook` instead. + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidHook` instead. AIR302.py:287:1: AIR302 `airflow.operators.druid_check_operator.DruidCheckOperator` is moved into `apache-druid` provider in Airflow 3.0; | @@ -516,7 +516,7 @@ AIR302.py:287:1: AIR302 `airflow.operators.druid_check_operator.DruidCheckOperat 288 | 289 | # apache-airflow-providers-apache-hdfs | - = help: Install `apache-airflow-provider-apache-druid>=1.0.0` and use `DruidCheckOperator` instead. + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `DruidCheckOperator` instead. AIR302.py:290:1: AIR302 `airflow.hooks.webhdfs_hook.WebHDFSHook` is moved into `apache-hdfs` provider in Airflow 3.0; | @@ -525,7 +525,7 @@ AIR302.py:290:1: AIR302 `airflow.hooks.webhdfs_hook.WebHDFSHook` is moved into ` | ^^^^^^^^^^^ AIR302 291 | WebHdfsSensor() | - = help: Install `apache-airflow-provider-apache-hdfs>=1.0.0` and use `airflow.providers.apache.hdfs.hooks.webhdfs.WebHDFSHook` instead. + = help: Install `apache-airflow-providers-apache-hdfs>=1.0.0` and use `airflow.providers.apache.hdfs.hooks.webhdfs.WebHDFSHook` instead. AIR302.py:291:1: AIR302 `airflow.sensors.web_hdfs_sensor.WebHdfsSensor` is moved into `apache-hdfs` provider in Airflow 3.0; | @@ -536,7 +536,7 @@ AIR302.py:291:1: AIR302 `airflow.sensors.web_hdfs_sensor.WebHdfsSensor` is moved 292 | 293 | # apache-airflow-providers-apache-hive | - = help: Install `apache-airflow-provider-apache-hdfs>=1.0.0` and use `airflow.providers.apache.hdfs.sensors.web_hdfs.WebHdfsSensor` instead. + = help: Install `apache-airflow-providers-apache-hdfs>=1.0.0` and use `airflow.providers.apache.hdfs.sensors.web_hdfs.WebHdfsSensor` instead. AIR302.py:294:1: AIR302 `airflow.hooks.hive_hooks.HIVE_QUEUE_PRIORITIES` is moved into `apache-hive` provider in Airflow 3.0; | @@ -546,7 +546,7 @@ AIR302.py:294:1: AIR302 `airflow.hooks.hive_hooks.HIVE_QUEUE_PRIORITIES` is move 295 | closest_ds_partition() 296 | max_partition() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HIVE_QUEUE_PRIORITIES` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HIVE_QUEUE_PRIORITIES` instead. AIR302.py:295:1: AIR302 `airflow.macros.hive.closest_ds_partition` is moved into `apache-hive` provider in Airflow 3.0; | @@ -557,7 +557,7 @@ AIR302.py:295:1: AIR302 `airflow.macros.hive.closest_ds_partition` is moved into 296 | max_partition() 297 | HiveCliHook() | - = help: Install `apache-airflow-provider-apache-hive>=5.1.0` and use `airflow.providers.apache.hive.macros.hive.closest_ds_partition` instead. + = help: Install `apache-airflow-providers-apache-hive>=5.1.0` and use `airflow.providers.apache.hive.macros.hive.closest_ds_partition` instead. AIR302.py:296:1: AIR302 `airflow.macros.hive.max_partition` is moved into `apache-hive` provider in Airflow 3.0; | @@ -568,7 +568,7 @@ AIR302.py:296:1: AIR302 `airflow.macros.hive.max_partition` is moved into `apach 297 | HiveCliHook() 298 | HiveMetastoreHook() | - = help: Install `apache-airflow-provider-apache-hive>=5.1.0` and use `airflow.providers.apache.hive.macros.hive.max_partition` instead. + = help: Install `apache-airflow-providers-apache-hive>=5.1.0` and use `airflow.providers.apache.hive.macros.hive.max_partition` instead. AIR302.py:297:1: AIR302 `airflow.hooks.hive_hooks.HiveCliHook` is moved into `apache-hive` provider in Airflow 3.0; | @@ -579,7 +579,7 @@ AIR302.py:297:1: AIR302 `airflow.hooks.hive_hooks.HiveCliHook` is moved into `ap 298 | HiveMetastoreHook() 299 | HiveOperator() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveCliHook` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveCliHook` instead. AIR302.py:298:1: AIR302 `airflow.hooks.hive_hooks.HiveMetastoreHook` is moved into `apache-hive` provider in Airflow 3.0; | @@ -590,7 +590,7 @@ AIR302.py:298:1: AIR302 `airflow.hooks.hive_hooks.HiveMetastoreHook` is moved in 299 | HiveOperator() 300 | HivePartitionSensor() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveMetastoreHook` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveMetastoreHook` instead. AIR302.py:299:1: AIR302 `airflow.operators.hive_operator.HiveOperator` is moved into `apache-hive` provider in Airflow 3.0; | @@ -601,7 +601,7 @@ AIR302.py:299:1: AIR302 `airflow.operators.hive_operator.HiveOperator` is moved 300 | HivePartitionSensor() 301 | HiveServer2Hook() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.operators.hive.HiveOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.operators.hive.HiveOperator` instead. AIR302.py:300:1: AIR302 `airflow.sensors.hive_partition_sensor.HivePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; | @@ -612,7 +612,7 @@ AIR302.py:300:1: AIR302 `airflow.sensors.hive_partition_sensor.HivePartitionSens 301 | HiveServer2Hook() 302 | HiveStatsCollectionOperator() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.hive_partition.HivePartitionSensor` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.hive_partition.HivePartitionSensor` instead. AIR302.py:301:1: AIR302 `airflow.hooks.hive_hooks.HiveServer2Hook` is moved into `apache-hive` provider in Airflow 3.0; | @@ -623,7 +623,7 @@ AIR302.py:301:1: AIR302 `airflow.hooks.hive_hooks.HiveServer2Hook` is moved into 302 | HiveStatsCollectionOperator() 303 | HiveToDruidOperator() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveServer2Hook` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveServer2Hook` instead. AIR302.py:302:1: AIR302 `airflow.operators.hive_stats_operator.HiveStatsCollectionOperator` is moved into `apache-hive` provider in Airflow 3.0; | @@ -634,7 +634,7 @@ AIR302.py:302:1: AIR302 `airflow.operators.hive_stats_operator.HiveStatsCollecti 303 | HiveToDruidOperator() 304 | HiveToDruidTransfer() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.operators.hive_stats.HiveStatsCollectionOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.operators.hive_stats.HiveStatsCollectionOperator` instead. AIR302.py:303:1: AIR302 `airflow.operators.hive_to_druid.HiveToDruidOperator` is moved into `apache-druid` provider in Airflow 3.0; | @@ -645,7 +645,7 @@ AIR302.py:303:1: AIR302 `airflow.operators.hive_to_druid.HiveToDruidOperator` is 304 | HiveToDruidTransfer() 305 | HiveToSambaOperator() | - = help: Install `apache-airflow-provider-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator` instead. + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator` instead. AIR302.py:304:1: AIR302 `airflow.operators.hive_to_druid.HiveToDruidTransfer` is moved into `apache-druid` provider in Airflow 3.0; | @@ -656,7 +656,7 @@ AIR302.py:304:1: AIR302 `airflow.operators.hive_to_druid.HiveToDruidTransfer` is 305 | HiveToSambaOperator() 306 | S3ToHiveOperator() | - = help: Install `apache-airflow-provider-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator` instead. + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator` instead. AIR302.py:305:1: AIR302 `airflow.operators.hive_to_samba_operator.HiveToSambaOperator` is moved into `apache-hive` provider in Airflow 3.0; | @@ -667,7 +667,7 @@ AIR302.py:305:1: AIR302 `airflow.operators.hive_to_samba_operator.HiveToSambaOpe 306 | S3ToHiveOperator() 307 | S3ToHiveTransfer() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `HiveToSambaOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveToSambaOperator` instead. AIR302.py:306:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; | @@ -678,7 +678,7 @@ AIR302.py:306:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveOperator` 307 | S3ToHiveTransfer() 308 | MetastorePartitionSensor() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator` instead. AIR302.py:307:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; | @@ -689,7 +689,7 @@ AIR302.py:307:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveTransfer` 308 | MetastorePartitionSensor() 309 | NamedHivePartitionSensor() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator` instead. AIR302.py:308:1: AIR302 `airflow.sensors.metastore_partition_sensor.MetastorePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; | @@ -699,7 +699,7 @@ AIR302.py:308:1: AIR302 `airflow.sensors.metastore_partition_sensor.MetastorePar | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 309 | NamedHivePartitionSensor() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.metastore_partition.MetastorePartitionSensor` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.metastore_partition.MetastorePartitionSensor` instead. AIR302.py:309:1: AIR302 `airflow.sensors.named_hive_partition_sensor.NamedHivePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; | @@ -710,7 +710,7 @@ AIR302.py:309:1: AIR302 `airflow.sensors.named_hive_partition_sensor.NamedHivePa 310 | 311 | # apache-airflow-providers-http | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.named_hive_partition.NamedHivePartitionSensor` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.named_hive_partition.NamedHivePartitionSensor` instead. AIR302.py:312:1: AIR302 `airflow.hooks.http_hook.HttpHook` is moved into `http` provider in Airflow 3.0; | @@ -720,7 +720,7 @@ AIR302.py:312:1: AIR302 `airflow.hooks.http_hook.HttpHook` is moved into `http` 313 | HttpSensor() 314 | SimpleHttpOperator() | - = help: Install `apache-airflow-provider-http>=1.0.0` and use `airflow.providers.http.hooks.http.HttpHook` instead. + = help: Install `apache-airflow-providers-http>=1.0.0` and use `airflow.providers.http.hooks.http.HttpHook` instead. AIR302.py:313:1: AIR302 `airflow.sensors.http_sensor.HttpSensor` is moved into `http` provider in Airflow 3.0; | @@ -730,7 +730,7 @@ AIR302.py:313:1: AIR302 `airflow.sensors.http_sensor.HttpSensor` is moved into ` | ^^^^^^^^^^ AIR302 314 | SimpleHttpOperator() | - = help: Install `apache-airflow-provider-http>=1.0.0` and use `airflow.providers.http.sensors.http.HttpSensor` instead. + = help: Install `apache-airflow-providers-http>=1.0.0` and use `airflow.providers.http.sensors.http.HttpSensor` instead. AIR302.py:314:1: AIR302 `airflow.operators.http_operator.SimpleHttpOperator` is moved into `http` provider in Airflow 3.0; | @@ -741,7 +741,7 @@ AIR302.py:314:1: AIR302 `airflow.operators.http_operator.SimpleHttpOperator` is 315 | 316 | # apache-airflow-providers-jdbc | - = help: Install `apache-airflow-provider-http>=1.0.0` and use `airflow.providers.http.operators.http.SimpleHttpOperator` instead. + = help: Install `apache-airflow-providers-http>=1.0.0` and use `airflow.providers.http.operators.http.SimpleHttpOperator` instead. AIR302.py:317:1: AIR302 `airflow.hooks.jdbc_hook.jaydebeapi` is moved into `jdbc` provider in Airflow 3.0; | @@ -751,7 +751,7 @@ AIR302.py:317:1: AIR302 `airflow.hooks.jdbc_hook.jaydebeapi` is moved into `jdbc 318 | JdbcHook() 319 | JdbcOperator() | - = help: Install `apache-airflow-provider-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.jaydebeapi` instead. + = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.jaydebeapi` instead. AIR302.py:318:1: AIR302 `airflow.hooks.jdbc_hook.JdbcHook` is moved into `jdbc` provider in Airflow 3.0; | @@ -761,7 +761,7 @@ AIR302.py:318:1: AIR302 `airflow.hooks.jdbc_hook.JdbcHook` is moved into `jdbc` | ^^^^^^^^ AIR302 319 | JdbcOperator() | - = help: Install `apache-airflow-provider-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.JdbcHook` instead. + = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.JdbcHook` instead. AIR302.py:319:1: AIR302 `airflow.operators.jdbc_operator.JdbcOperator` is moved into `jdbc` provider in Airflow 3.0; | @@ -772,7 +772,7 @@ AIR302.py:319:1: AIR302 `airflow.operators.jdbc_operator.JdbcOperator` is moved 320 | 321 | # apache-airflow-providers-fab | - = help: Install `apache-airflow-provider-jdbc>=1.0.0` and use `airflow.providers.jdbc.operators.jdbc.JdbcOperator` instead. + = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `airflow.providers.jdbc.operators.jdbc.JdbcOperator` instead. AIR302.py:322:12: AIR302 `airflow.api.auth.backend.basic_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; | @@ -782,7 +782,7 @@ AIR302.py:322:12: AIR302 `airflow.api.auth.backend.basic_auth.CLIENT_AUTH` is mo 323 | basic_auth.init_app 324 | basic_auth.auth_current_user | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.CLIENT_AUTH` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.CLIENT_AUTH` instead. AIR302.py:323:12: AIR302 `airflow.api.auth.backend.basic_auth.init_app` is moved into `fab` provider in Airflow 3.0; | @@ -793,7 +793,7 @@ AIR302.py:323:12: AIR302 `airflow.api.auth.backend.basic_auth.init_app` is moved 324 | basic_auth.auth_current_user 325 | basic_auth.requires_authentication | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.init_app` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.init_app` instead. AIR302.py:324:12: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; | @@ -803,7 +803,7 @@ AIR302.py:324:12: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` | ^^^^^^^^^^^^^^^^^ AIR302 325 | basic_auth.requires_authentication | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. AIR302.py:325:12: AIR302 `airflow.api.auth.backend.basic_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; | @@ -814,7 +814,7 @@ AIR302.py:325:12: AIR302 `airflow.api.auth.backend.basic_auth.requires_authentic 326 | 327 | kerberos_auth.log | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.requires_authentication` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.requires_authentication` instead. AIR302.py:327:15: AIR302 `airflow.api.auth.backend.kerberos_auth.log` is moved into `fab` provider in Airflow 3.0; | @@ -825,7 +825,7 @@ AIR302.py:327:15: AIR302 `airflow.api.auth.backend.kerberos_auth.log` is moved i 328 | kerberos_auth.CLIENT_AUTH 329 | kerberos_auth.find_user | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.log` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.log` instead. AIR302.py:328:15: AIR302 `airflow.api.auth.backend.kerberos_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; | @@ -835,7 +835,7 @@ AIR302.py:328:15: AIR302 `airflow.api.auth.backend.kerberos_auth.CLIENT_AUTH` is 329 | kerberos_auth.find_user 330 | kerberos_auth.init_app | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.CLIENT_AUTH` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.CLIENT_AUTH` instead. AIR302.py:329:15: AIR302 `airflow.api.auth.backend.kerberos_auth.find_user` is moved into `fab` provider in Airflow 3.0; | @@ -846,7 +846,7 @@ AIR302.py:329:15: AIR302 `airflow.api.auth.backend.kerberos_auth.find_user` is m 330 | kerberos_auth.init_app 331 | kerberos_auth.requires_authentication | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.find_user` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.find_user` instead. AIR302.py:330:15: AIR302 `airflow.api.auth.backend.kerberos_auth.init_app` is moved into `fab` provider in Airflow 3.0; | @@ -857,7 +857,7 @@ AIR302.py:330:15: AIR302 `airflow.api.auth.backend.kerberos_auth.init_app` is mo 331 | kerberos_auth.requires_authentication 332 | auth_current_user | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.init_app` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.init_app` instead. AIR302.py:331:15: AIR302 `airflow.api.auth.backend.kerberos_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; | @@ -868,7 +868,7 @@ AIR302.py:331:15: AIR302 `airflow.api.auth.backend.kerberos_auth.requires_authen 332 | auth_current_user 333 | backend_kerberos_auth | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.requires_authentication` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.requires_authentication` instead. AIR302.py:332:1: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; | @@ -879,7 +879,7 @@ AIR302.py:332:1: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` 333 | backend_kerberos_auth 334 | fab_override | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. AIR302.py:335:1: AIR302 `airflow.auth.managers.fab.fab_auth_manager.FabAuthManager` is moved into `fab` provider in Airflow 3.0; | @@ -889,7 +889,7 @@ AIR302.py:335:1: AIR302 `airflow.auth.managers.fab.fab_auth_manager.FabAuthManag | ^^^^^^^^^^^^^^ AIR302 336 | FabAirflowSecurityManagerOverride() | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.FabAuthManager` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.FabAuthManager` instead. AIR302.py:336:1: AIR302 `airflow.www.security.FabAirflowSecurityManagerOverride` is moved into `fab` provider in Airflow 3.0; | @@ -900,7 +900,7 @@ AIR302.py:336:1: AIR302 `airflow.www.security.FabAirflowSecurityManagerOverride` 337 | 338 | # check whether attribute access | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.override.FabAirflowSecurityManagerOverride` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.override.FabAirflowSecurityManagerOverride` instead. AIR302.py:339:12: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; | @@ -910,7 +910,7 @@ AIR302.py:339:12: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` 340 | 341 | # apache-airflow-providers-cncf-kubernetes | - = help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. AIR302.py:342:1: AIR302 `airflow.executors.kubernetes_executor_types.ALL_NAMESPACES` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -920,7 +920,7 @@ AIR302.py:342:1: AIR302 `airflow.executors.kubernetes_executor_types.ALL_NAMESPA 343 | POD_EXECUTOR_DONE_KEY 344 | _disable_verify_ssl() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.ALL_NAMESPACES` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.ALL_NAMESPACES` instead. AIR302.py:343:1: AIR302 `airflow.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -931,7 +931,7 @@ AIR302.py:343:1: AIR302 `airflow.executors.kubernetes_executor_types.POD_EXECUTO 344 | _disable_verify_ssl() 345 | _enable_tcp_keepalive() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` instead. AIR302.py:344:1: AIR302 `airflow.kubernetes.kube_client._disable_verify_ssl` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -942,7 +942,7 @@ AIR302.py:344:1: AIR302 `airflow.kubernetes.kube_client._disable_verify_ssl` is 345 | _enable_tcp_keepalive() 346 | append_to_pod() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client._disable_verify_ssl` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client._disable_verify_ssl` instead. AIR302.py:345:1: AIR302 `airflow.kubernetes.kube_client._enable_tcp_keepalive` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -953,7 +953,7 @@ AIR302.py:345:1: AIR302 `airflow.kubernetes.kube_client._enable_tcp_keepalive` i 346 | append_to_pod() 347 | annotations_for_logging_task_metadata() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client._enable_tcp_keepalive` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client._enable_tcp_keepalive` instead. AIR302.py:346:1: AIR302 `airflow.kubernetes.k8s_model.append_to_pod` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -964,7 +964,7 @@ AIR302.py:346:1: AIR302 `airflow.kubernetes.k8s_model.append_to_pod` is moved in 347 | annotations_for_logging_task_metadata() 348 | annotations_to_key() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.append_to_pod` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.append_to_pod` instead. AIR302.py:347:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -975,7 +975,7 @@ AIR302.py:347:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.annotati 348 | annotations_to_key() 349 | create_pod_id() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata` instead. AIR302.py:348:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.annotations_to_key` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -986,7 +986,7 @@ AIR302.py:348:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.annotati 349 | create_pod_id() 350 | datetime_to_label_safe_datestring() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_to_key` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_to_key` instead. AIR302.py:349:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.create_pod_id` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -997,7 +997,7 @@ AIR302.py:349:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.create_p 350 | datetime_to_label_safe_datestring() 351 | extend_object_field() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.create_pod_id` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.create_pod_id` instead. AIR302.py:350:1: AIR302 `airflow.kubernetes.pod_generator.datetime_to_label_safe_datestring` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1008,7 +1008,7 @@ AIR302.py:350:1: AIR302 `airflow.kubernetes.pod_generator.datetime_to_label_safe 351 | extend_object_field() 352 | get_logs_task_metadata() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.datetime_to_label_safe_datestring` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.datetime_to_label_safe_datestring` instead. AIR302.py:351:1: AIR302 `airflow.kubernetes.pod_generator.extend_object_field` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1019,7 +1019,7 @@ AIR302.py:351:1: AIR302 `airflow.kubernetes.pod_generator.extend_object_field` i 352 | get_logs_task_metadata() 353 | label_safe_datestring_to_datetime() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.extend_object_field` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.extend_object_field` instead. AIR302.py:352:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.get_logs_task_metadata` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1030,7 +1030,7 @@ AIR302.py:352:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.get_logs 353 | label_safe_datestring_to_datetime() 354 | merge_objects() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.get_logs_task_metadata` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.get_logs_task_metadata` instead. AIR302.py:353:1: AIR302 `airflow.kubernetes.pod_generator.label_safe_datestring_to_datetime` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1041,7 +1041,7 @@ AIR302.py:353:1: AIR302 `airflow.kubernetes.pod_generator.label_safe_datestring_ 354 | merge_objects() 355 | Port() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.label_safe_datestring_to_datetime` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.label_safe_datestring_to_datetime` instead. AIR302.py:354:1: AIR302 `airflow.kubernetes.pod_generator.merge_objects` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1052,7 +1052,7 @@ AIR302.py:354:1: AIR302 `airflow.kubernetes.pod_generator.merge_objects` is move 355 | Port() 356 | Resources() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.merge_objects` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.merge_objects` instead. AIR302.py:355:1: AIR302 `airflow.kubernetes.pod.Port` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1063,7 +1063,7 @@ AIR302.py:355:1: AIR302 `airflow.kubernetes.pod.Port` is moved into `cncf-kubern 356 | Resources() 357 | PodRuntimeInfoEnv() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1ContainerPort` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1ContainerPort` instead. AIR302.py:356:1: AIR302 `airflow.kubernetes.pod.Resources` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1074,7 +1074,7 @@ AIR302.py:356:1: AIR302 `airflow.kubernetes.pod.Resources` is moved into `cncf-k 357 | PodRuntimeInfoEnv() 358 | PodGeneratorDeprecated() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1ResourceRequirements` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1ResourceRequirements` instead. AIR302.py:357:1: AIR302 `airflow.kubernetes.pod_runtime_info_env.PodRuntimeInfoEnv` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1085,7 +1085,7 @@ AIR302.py:357:1: AIR302 `airflow.kubernetes.pod_runtime_info_env.PodRuntimeInfoE 358 | PodGeneratorDeprecated() 359 | Volume() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1EnvVar` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1EnvVar` instead. AIR302.py:358:1: AIR302 `airflow.kubernetes.pod_generator.PodGeneratorDeprecated` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1096,7 +1096,7 @@ AIR302.py:358:1: AIR302 `airflow.kubernetes.pod_generator.PodGeneratorDeprecated 359 | Volume() 360 | VolumeMount() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. AIR302.py:359:1: AIR302 `airflow.kubernetes.volume.Volume` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1107,7 +1107,7 @@ AIR302.py:359:1: AIR302 `airflow.kubernetes.volume.Volume` is moved into `cncf-k 360 | VolumeMount() 361 | Secret() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1Volume` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1Volume` instead. AIR302.py:360:1: AIR302 `airflow.kubernetes.volume_mount.VolumeMount` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1117,7 +1117,7 @@ AIR302.py:360:1: AIR302 `airflow.kubernetes.volume_mount.VolumeMount` is moved i | ^^^^^^^^^^^ AIR302 361 | Secret() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1VolumeMount` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1VolumeMount` instead. AIR302.py:361:1: AIR302 `airflow.kubernetes.secret.Secret` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1128,7 +1128,7 @@ AIR302.py:361:1: AIR302 `airflow.kubernetes.secret.Secret` is moved into `cncf-k 362 | 363 | add_pod_suffix() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.secret.Secret` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.secret.Secret` instead. AIR302.py:363:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1139,7 +1139,7 @@ AIR302.py:363:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.add_pod_ 364 | add_pod_suffix2() 365 | get_kube_client() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_pod_suffix` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_pod_suffix` instead. AIR302.py:364:1: AIR302 `airflow.kubernetes.pod_generator.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1149,7 +1149,7 @@ AIR302.py:364:1: AIR302 `airflow.kubernetes.pod_generator.add_pod_suffix` is mov 365 | get_kube_client() 366 | get_kube_client2() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_pod_suffix` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_pod_suffix` instead. AIR302.py:365:1: AIR302 `airflow.kubernetes.kube_client.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1160,7 +1160,7 @@ AIR302.py:365:1: AIR302 `airflow.kubernetes.kube_client.get_kube_client` is move 366 | get_kube_client2() 367 | make_safe_label_value() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. AIR302.py:366:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1171,7 +1171,7 @@ AIR302.py:366:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.get_kube_cli 367 | make_safe_label_value() 368 | make_safe_label_value2() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. AIR302.py:367:1: AIR302 `airflow.kubernetes.pod_generator.make_safe_label_value` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1182,7 +1182,7 @@ AIR302.py:367:1: AIR302 `airflow.kubernetes.pod_generator.make_safe_label_value` 368 | make_safe_label_value2() 369 | rand_str() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.make_safe_label_value` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.make_safe_label_value` instead. AIR302.py:368:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.make_safe_label_value` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1193,7 +1193,7 @@ AIR302.py:368:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.make_safe_l 369 | rand_str() 370 | rand_str2() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.make_safe_label_value` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.make_safe_label_value` instead. AIR302.py:369:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1204,7 +1204,7 @@ AIR302.py:369:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.rand_str 370 | rand_str2() 371 | K8SModel() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str` instead. AIR302.py:370:1: AIR302 `airflow.kubernetes.pod_generator.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1215,7 +1215,7 @@ AIR302.py:370:1: AIR302 `airflow.kubernetes.pod_generator.rand_str` is moved int 371 | K8SModel() 372 | K8SModel2() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str` instead. AIR302.py:371:1: AIR302 `airflow.kubernetes.k8s_model.K8SModel` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1226,7 +1226,7 @@ AIR302.py:371:1: AIR302 `airflow.kubernetes.k8s_model.K8SModel` is moved into `c 372 | K8SModel2() 373 | PodLauncher() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.K8SModel` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.K8SModel` instead. AIR302.py:373:1: AIR302 `airflow.kubernetes.pod_launcher.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1237,7 +1237,7 @@ AIR302.py:373:1: AIR302 `airflow.kubernetes.pod_launcher.PodLauncher` is moved i 374 | PodLauncher2() 375 | PodStatus() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodLauncher` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodLauncher` instead. AIR302.py:374:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1248,7 +1248,7 @@ AIR302.py:374:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodLauncher` 375 | PodStatus() 376 | PodStatus2() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodLauncher` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodLauncher` instead. AIR302.py:375:1: AIR302 `airflow.kubernetes.pod_launcher.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1259,7 +1259,7 @@ AIR302.py:375:1: AIR302 `airflow.kubernetes.pod_launcher.PodStatus` is moved int 376 | PodStatus2() 377 | PodDefaults() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodStatus` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodStatus` instead. AIR302.py:376:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1270,7 +1270,7 @@ AIR302.py:376:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodStatus` i 377 | PodDefaults() 378 | PodDefaults2() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodStatus` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodStatus` instead. AIR302.py:377:1: AIR302 `airflow.kubernetes.pod_generator.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1281,7 +1281,7 @@ AIR302.py:377:1: AIR302 `airflow.kubernetes.pod_generator.PodDefaults` is moved 378 | PodDefaults2() 379 | PodDefaults3() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodDefaults` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodDefaults` instead. AIR302.py:378:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1292,7 +1292,7 @@ AIR302.py:378:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodDefaults` 379 | PodDefaults3() 380 | PodGenerator() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodDefaults` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodDefaults` instead. AIR302.py:379:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1303,7 +1303,7 @@ AIR302.py:379:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodDefaults 380 | PodGenerator() 381 | PodGenerator2() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodDefaults` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodDefaults` instead. AIR302.py:380:1: AIR302 `airflow.kubernetes.pod_generator.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1313,7 +1313,7 @@ AIR302.py:380:1: AIR302 `airflow.kubernetes.pod_generator.PodGenerator` is moved | ^^^^^^^^^^^^ AIR302 381 | PodGenerator2() | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. AIR302.py:381:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1322,7 +1322,7 @@ AIR302.py:381:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodGenerato 381 | PodGenerator2() | ^^^^^^^^^^^^^ AIR302 | - = help: Install `apache-airflow-provider-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodGenerator` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodGenerator` instead. AIR302.py:385:1: AIR302 `airflow.hooks.mssql_hook.MsSqlHook` is moved into `microsoft-mssql` provider in Airflow 3.0; | @@ -1332,7 +1332,7 @@ AIR302.py:385:1: AIR302 `airflow.hooks.mssql_hook.MsSqlHook` is moved into `micr 386 | MsSqlOperator() 387 | MsSqlToHiveOperator() | - = help: Install `apache-airflow-provider-microsoft-mssql>=1.0.0` and use `airflow.providers.microsoft.mssql.hooks.mssql.MsSqlHook` instead. + = help: Install `apache-airflow-providers-microsoft-mssql>=1.0.0` and use `airflow.providers.microsoft.mssql.hooks.mssql.MsSqlHook` instead. AIR302.py:386:1: AIR302 `airflow.operators.mssql_operator.MsSqlOperator` is moved into `microsoft-mssql` provider in Airflow 3.0; | @@ -1343,7 +1343,7 @@ AIR302.py:386:1: AIR302 `airflow.operators.mssql_operator.MsSqlOperator` is move 387 | MsSqlToHiveOperator() 388 | MsSqlToHiveTransfer() | - = help: Install `apache-airflow-provider-microsoft-mssql>=1.0.0` and use `airflow.providers.microsoft.mssql.operators.mssql.MsSqlOperator` instead. + = help: Install `apache-airflow-providers-microsoft-mssql>=1.0.0` and use `airflow.providers.microsoft.mssql.operators.mssql.MsSqlOperator` instead. AIR302.py:387:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; | @@ -1353,7 +1353,7 @@ AIR302.py:387:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveOperator` is | ^^^^^^^^^^^^^^^^^^^ AIR302 388 | MsSqlToHiveTransfer() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator` instead. AIR302.py:388:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; | @@ -1364,7 +1364,7 @@ AIR302.py:388:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveTransfer` is 389 | 390 | # apache-airflow-providers-mysql | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator` instead. AIR302.py:391:1: AIR302 `airflow.operators.hive_to_mysql.HiveToMySqlOperator` is moved into `apache-hive` provider in Airflow 3.0; | @@ -1374,7 +1374,7 @@ AIR302.py:391:1: AIR302 `airflow.operators.hive_to_mysql.HiveToMySqlOperator` is 392 | HiveToMySqlTransfer() 393 | MySqlHook() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator` instead. AIR302.py:392:1: AIR302 `airflow.operators.hive_to_mysql.HiveToMySqlTransfer` is moved into `apache-hive` provider in Airflow 3.0; | @@ -1385,7 +1385,7 @@ AIR302.py:392:1: AIR302 `airflow.operators.hive_to_mysql.HiveToMySqlTransfer` is 393 | MySqlHook() 394 | MySqlOperator() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator` instead. AIR302.py:393:1: AIR302 `airflow.hooks.mysql_hook.MySqlHook` is moved into `mysql` provider in Airflow 3.0; | @@ -1396,7 +1396,7 @@ AIR302.py:393:1: AIR302 `airflow.hooks.mysql_hook.MySqlHook` is moved into `mysq 394 | MySqlOperator() 395 | MySqlToHiveOperator() | - = help: Install `apache-airflow-provider-mysql>=1.0.0` and use `airflow.providers.mysql.hooks.mysql.MySqlHook` instead. + = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.hooks.mysql.MySqlHook` instead. AIR302.py:394:1: AIR302 `airflow.operators.mysql_operator.MySqlOperator` is moved into `mysql` provider in Airflow 3.0; | @@ -1407,7 +1407,7 @@ AIR302.py:394:1: AIR302 `airflow.operators.mysql_operator.MySqlOperator` is move 395 | MySqlToHiveOperator() 396 | MySqlToHiveTransfer() | - = help: Install `apache-airflow-provider-mysql>=1.0.0` and use `airflow.providers.mysql.operators.mysql.MySqlOperator` instead. + = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.operators.mysql.MySqlOperator` instead. AIR302.py:395:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; | @@ -1418,7 +1418,7 @@ AIR302.py:395:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveOperator` is 396 | MySqlToHiveTransfer() 397 | PrestoToMySqlOperator() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator` instead. AIR302.py:396:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; | @@ -1429,7 +1429,7 @@ AIR302.py:396:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveTransfer` is 397 | PrestoToMySqlOperator() 398 | PrestoToMySqlTransfer() | - = help: Install `apache-airflow-provider-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator` instead. AIR302.py:397:1: AIR302 `airflow.operators.presto_to_mysql.PrestoToMySqlOperator` is moved into `mysql` provider in Airflow 3.0; | @@ -1439,7 +1439,7 @@ AIR302.py:397:1: AIR302 `airflow.operators.presto_to_mysql.PrestoToMySqlOperator | ^^^^^^^^^^^^^^^^^^^^^ AIR302 398 | PrestoToMySqlTransfer() | - = help: Install `apache-airflow-provider-mysql>=1.0.0` and use `airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator` instead. + = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator` instead. AIR302.py:398:1: AIR302 `airflow.operators.presto_to_mysql.PrestoToMySqlTransfer` is moved into `mysql` provider in Airflow 3.0; | @@ -1450,7 +1450,7 @@ AIR302.py:398:1: AIR302 `airflow.operators.presto_to_mysql.PrestoToMySqlTransfer 399 | 400 | # apache-airflow-providers-oracle | - = help: Install `apache-airflow-provider-mysql>=1.0.0` and use `airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator` instead. + = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator` instead. AIR302.py:401:1: AIR302 `airflow.hooks.oracle_hook.OracleHook` is moved into `oracle` provider in Airflow 3.0; | @@ -1459,7 +1459,7 @@ AIR302.py:401:1: AIR302 `airflow.hooks.oracle_hook.OracleHook` is moved into `or | ^^^^^^^^^^ AIR302 402 | OracleOperator() | - = help: Install `apache-airflow-provider-oracle>=1.0.0` and use `airflow.providers.oracle.hooks.oracle.OracleHook` instead. + = help: Install `apache-airflow-providers-oracle>=1.0.0` and use `airflow.providers.oracle.hooks.oracle.OracleHook` instead. AIR302.py:402:1: AIR302 `airflow.operators.oracle_operator.OracleOperator` is moved into `oracle` provider in Airflow 3.0; | @@ -1470,7 +1470,7 @@ AIR302.py:402:1: AIR302 `airflow.operators.oracle_operator.OracleOperator` is mo 403 | 404 | # apache-airflow-providers-papermill | - = help: Install `apache-airflow-provider-oracle>=1.0.0` and use `airflow.providers.oracle.operators.oracle.OracleOperator` instead. + = help: Install `apache-airflow-providers-oracle>=1.0.0` and use `airflow.providers.oracle.operators.oracle.OracleOperator` instead. AIR302.py:405:1: AIR302 `airflow.operators.papermill_operator.PapermillOperator` is moved into `papermill` provider in Airflow 3.0; | @@ -1480,7 +1480,7 @@ AIR302.py:405:1: AIR302 `airflow.operators.papermill_operator.PapermillOperator` 406 | 407 | # apache-airflow-providers-apache-pig | - = help: Install `apache-airflow-provider-papermill>=1.0.0` and use `airflow.providers.papermill.operators.papermill.PapermillOperator` instead. + = help: Install `apache-airflow-providers-papermill>=1.0.0` and use `airflow.providers.papermill.operators.papermill.PapermillOperator` instead. AIR302.py:408:1: AIR302 `airflow.hooks.pig_hook.PigCliHook` is moved into `apache-pig` provider in Airflow 3.0; | @@ -1489,7 +1489,7 @@ AIR302.py:408:1: AIR302 `airflow.hooks.pig_hook.PigCliHook` is moved into `apach | ^^^^^^^^^^ AIR302 409 | PigOperator() | - = help: Install `apache-airflow-provider-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.hooks.pig.PigCliHook` instead. + = help: Install `apache-airflow-providers-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.hooks.pig.PigCliHook` instead. AIR302.py:409:1: AIR302 `airflow.operators.pig_operator.PigOperator` is moved into `apache-pig` provider in Airflow 3.0; | @@ -1500,7 +1500,7 @@ AIR302.py:409:1: AIR302 `airflow.operators.pig_operator.PigOperator` is moved in 410 | 411 | # apache-airflow-providers-postgres | - = help: Install `apache-airflow-provider-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.operators.pig.PigOperator` instead. + = help: Install `apache-airflow-providers-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.operators.pig.PigOperator` instead. AIR302.py:412:1: AIR302 `airflow.operators.postgres_operator.Mapping` is moved into `postgres` provider in Airflow 3.0; | @@ -1510,7 +1510,7 @@ AIR302.py:412:1: AIR302 `airflow.operators.postgres_operator.Mapping` is moved i 413 | PostgresHook() 414 | PostgresOperator() | - = help: Install `apache-airflow-provider-postgres>=1.0.0` and use `airflow.providers.postgres.operators.postgres.Mapping` instead. + = help: Install `apache-airflow-providers-postgres>=1.0.0` and use `airflow.providers.postgres.operators.postgres.Mapping` instead. AIR302.py:413:1: AIR302 `airflow.hooks.postgres_hook.PostgresHook` is moved into `postgres` provider in Airflow 3.0; | @@ -1520,7 +1520,7 @@ AIR302.py:413:1: AIR302 `airflow.hooks.postgres_hook.PostgresHook` is moved into | ^^^^^^^^^^^^ AIR302 414 | PostgresOperator() | - = help: Install `apache-airflow-provider-postgres>=1.0.0` and use `airflow.providers.postgres.hooks.postgres.PostgresHook` instead. + = help: Install `apache-airflow-providers-postgres>=1.0.0` and use `airflow.providers.postgres.hooks.postgres.PostgresHook` instead. AIR302.py:414:1: AIR302 `airflow.operators.postgres_operator.PostgresOperator` is moved into `postgres` provider in Airflow 3.0; | @@ -1531,7 +1531,7 @@ AIR302.py:414:1: AIR302 `airflow.operators.postgres_operator.PostgresOperator` i 415 | 416 | # apache-airflow-providers-presto | - = help: Install `apache-airflow-provider-postgres>=1.0.0` and use `airflow.providers.postgres.operators.postgres.PostgresOperator` instead. + = help: Install `apache-airflow-providers-postgres>=1.0.0` and use `airflow.providers.postgres.operators.postgres.PostgresOperator` instead. AIR302.py:417:1: AIR302 `airflow.hooks.presto_hook.PrestoHook` is moved into `presto` provider in Airflow 3.0; | @@ -1541,7 +1541,7 @@ AIR302.py:417:1: AIR302 `airflow.hooks.presto_hook.PrestoHook` is moved into `pr 418 | 419 | # apache-airflow-providers-samba | - = help: Install `apache-airflow-provider-presto>=1.0.0` and use `airflow.providers.presto.hooks.presto.PrestoHook` instead. + = help: Install `apache-airflow-providers-presto>=1.0.0` and use `airflow.providers.presto.hooks.presto.PrestoHook` instead. AIR302.py:420:1: AIR302 `airflow.hooks.samba_hook.SambaHook` is moved into `samba` provider in Airflow 3.0; | @@ -1551,7 +1551,7 @@ AIR302.py:420:1: AIR302 `airflow.hooks.samba_hook.SambaHook` is moved into `samb 421 | 422 | # apache-airflow-providers-slack | - = help: Install `apache-airflow-provider-samba>=1.0.0` and use `airflow.providers.samba.hooks.samba.SambaHook` instead. + = help: Install `apache-airflow-providers-samba>=1.0.0` and use `airflow.providers.samba.hooks.samba.SambaHook` instead. AIR302.py:423:1: AIR302 `airflow.hooks.slack_hook.SlackHook` is moved into `slack` provider in Airflow 3.0; | @@ -1561,7 +1561,7 @@ AIR302.py:423:1: AIR302 `airflow.hooks.slack_hook.SlackHook` is moved into `slac 424 | SlackAPIOperator() 425 | SlackAPIPostOperator() | - = help: Install `apache-airflow-provider-slack>=1.0.0` and use `airflow.providers.slack.hooks.slack.SlackHook` instead. + = help: Install `apache-airflow-providers-slack>=1.0.0` and use `airflow.providers.slack.hooks.slack.SlackHook` instead. AIR302.py:424:1: AIR302 `airflow.operators.slack_operator.SlackAPIOperator` is moved into `slack` provider in Airflow 3.0; | @@ -1571,7 +1571,7 @@ AIR302.py:424:1: AIR302 `airflow.operators.slack_operator.SlackAPIOperator` is m | ^^^^^^^^^^^^^^^^ AIR302 425 | SlackAPIPostOperator() | - = help: Install `apache-airflow-provider-slack>=1.0.0` and use `airflow.providers.slack.operators.slack.SlackAPIOperator` instead. + = help: Install `apache-airflow-providers-slack>=1.0.0` and use `airflow.providers.slack.operators.slack.SlackAPIOperator` instead. AIR302.py:425:1: AIR302 `airflow.operators.slack_operator.SlackAPIPostOperator` is moved into `slack` provider in Airflow 3.0; | @@ -1582,7 +1582,7 @@ AIR302.py:425:1: AIR302 `airflow.operators.slack_operator.SlackAPIPostOperator` 426 | 427 | # apache-airflow-providers-sqlite | - = help: Install `apache-airflow-provider-slack>=1.0.0` and use `airflow.providers.slack.operators.slack.SlackAPIPostOperator` instead. + = help: Install `apache-airflow-providers-slack>=1.0.0` and use `airflow.providers.slack.operators.slack.SlackAPIPostOperator` instead. AIR302.py:428:1: AIR302 `airflow.hooks.sqlite_hook.SqliteHook` is moved into `sqlite` provider in Airflow 3.0; | @@ -1591,7 +1591,7 @@ AIR302.py:428:1: AIR302 `airflow.hooks.sqlite_hook.SqliteHook` is moved into `sq | ^^^^^^^^^^ AIR302 429 | SqliteOperator() | - = help: Install `apache-airflow-provider-sqlite>=1.0.0` and use `airflow.providers.sqlite.hooks.sqlite.SqliteHook` instead. + = help: Install `apache-airflow-providers-sqlite>=1.0.0` and use `airflow.providers.sqlite.hooks.sqlite.SqliteHook` instead. AIR302.py:429:1: AIR302 `airflow.operators.sqlite_operator.SqliteOperator` is moved into `sqlite` provider in Airflow 3.0; | @@ -1602,7 +1602,7 @@ AIR302.py:429:1: AIR302 `airflow.operators.sqlite_operator.SqliteOperator` is mo 430 | 431 | # apache-airflow-providers-zendesk | - = help: Install `apache-airflow-provider-sqlite>=1.0.0` and use `airflow.providers.sqlite.operators.sqlite.SqliteOperator` instead. + = help: Install `apache-airflow-providers-sqlite>=1.0.0` and use `airflow.providers.sqlite.operators.sqlite.SqliteOperator` instead. AIR302.py:432:1: AIR302 `airflow.hooks.zendesk_hook.ZendeskHook` is moved into `zendesk` provider in Airflow 3.0; | @@ -1612,7 +1612,7 @@ AIR302.py:432:1: AIR302 `airflow.hooks.zendesk_hook.ZendeskHook` is moved into ` 433 | 434 | # apache-airflow-providers-smtp | - = help: Install `apache-airflow-provider-zendesk>=1.0.0` and use `airflow.providers.zendesk.hooks.zendesk.ZendeskHook` instead. + = help: Install `apache-airflow-providers-zendesk>=1.0.0` and use `airflow.providers.zendesk.hooks.zendesk.ZendeskHook` instead. AIR302.py:435:1: AIR302 `airflow.operators.email_operator.EmailOperator` is moved into `smtp` provider in Airflow 3.0; | @@ -1622,7 +1622,7 @@ AIR302.py:435:1: AIR302 `airflow.operators.email_operator.EmailOperator` is move 436 | 437 | # apache-airflow-providers-standard | - = help: Install `apache-airflow-provider-smtp>=1.0.0` and use `airflow.providers.smtp.operators.smtp.EmailOperator` instead. + = help: Install `apache-airflow-providers-smtp>=1.0.0` and use `airflow.providers.smtp.operators.smtp.EmailOperator` instead. AIR302.py:451:1: AIR302 `airflow.operators.dummy.DummyOperator` is moved into `standard` provider in Airflow 3.0; | @@ -1633,7 +1633,7 @@ AIR302.py:451:1: AIR302 `airflow.operators.dummy.DummyOperator` is moved into `s 452 | EmptyOperator() 453 | ExternalTaskMarker() | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. AIR302.py:452:1: AIR302 `airflow.operators.dummy.EmptyOperator` is moved into `standard` provider in Airflow 3.0; | @@ -1644,4 +1644,4 @@ AIR302.py:452:1: AIR302 `airflow.operators.dummy.EmptyOperator` is moved into `s 453 | ExternalTaskMarker() 454 | ExternalTaskSensor() | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap index 71877501cdc175..ce7b397db792be 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap @@ -10,7 +10,7 @@ AIR312.py:32:1: AIR312 `airflow.hooks.filesystem.FSHook` is deprecated and moved 33 | PackageIndexHook() 34 | SubprocessHook(), SubprocessResult(), working_directory() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.hooks.filesystem.FSHook` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.hooks.filesystem.FSHook` instead. AIR312.py:33:1: AIR312 `airflow.hooks.package_index.PackageIndexHook` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -20,7 +20,7 @@ AIR312.py:33:1: AIR312 `airflow.hooks.package_index.PackageIndexHook` is depreca 34 | SubprocessHook(), SubprocessResult(), working_directory() 35 | BashOperator() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.hooks.package_index.PackageIndexHook` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.hooks.package_index.PackageIndexHook` instead. AIR312.py:34:1: AIR312 `airflow.hooks.subprocess.SubprocessHook` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -31,7 +31,7 @@ AIR312.py:34:1: AIR312 `airflow.hooks.subprocess.SubprocessHook` is deprecated a 35 | BashOperator() 36 | BranchDateTimeOperator(), target_times_as_dates() | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.hooks.subprocess.SubprocessHook` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.hooks.subprocess.SubprocessHook` instead. AIR312.py:34:19: AIR312 `airflow.hooks.subprocess.SubprocessResult` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -42,7 +42,7 @@ AIR312.py:34:19: AIR312 `airflow.hooks.subprocess.SubprocessResult` is deprecate 35 | BashOperator() 36 | BranchDateTimeOperator(), target_times_as_dates() | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.hooks.subprocess.SubprocessResult` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.hooks.subprocess.SubprocessResult` instead. AIR312.py:34:39: AIR312 `airflow.hooks.subprocess.working_directory` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -53,7 +53,7 @@ AIR312.py:34:39: AIR312 `airflow.hooks.subprocess.working_directory` is deprecat 35 | BashOperator() 36 | BranchDateTimeOperator(), target_times_as_dates() | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.hooks.subprocess.working_directory` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.hooks.subprocess.working_directory` instead. AIR312.py:35:1: AIR312 `airflow.operators.bash.BashOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -64,7 +64,7 @@ AIR312.py:35:1: AIR312 `airflow.operators.bash.BashOperator` is deprecated and m 36 | BranchDateTimeOperator(), target_times_as_dates() 37 | TriggerDagRunLink(), TriggerDagRunOperator() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.bash.BashOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.bash.BashOperator` instead. AIR312.py:36:1: AIR312 `airflow.operators.datetime.BranchDateTimeOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -75,7 +75,7 @@ AIR312.py:36:1: AIR312 `airflow.operators.datetime.BranchDateTimeOperator` is de 37 | TriggerDagRunLink(), TriggerDagRunOperator() 38 | EmptyOperator() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.operators.datetime.BranchDateTimeOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.operators.datetime.BranchDateTimeOperator` instead. AIR312.py:36:27: AIR312 `airflow.operators.datetime.target_times_as_dates` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -86,7 +86,7 @@ AIR312.py:36:27: AIR312 `airflow.operators.datetime.target_times_as_dates` is de 37 | TriggerDagRunLink(), TriggerDagRunOperator() 38 | EmptyOperator() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.operators.datetime.target_times_as_dates` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.operators.datetime.target_times_as_dates` instead. AIR312.py:37:1: AIR312 `airflow.operators.trigger_dagrun.TriggerDagRunLink` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -97,7 +97,7 @@ AIR312.py:37:1: AIR312 `airflow.operators.trigger_dagrun.TriggerDagRunLink` is d 38 | EmptyOperator() 39 | LatestOnlyOperator() | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunLink` instead. + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunLink` instead. AIR312.py:37:22: AIR312 `airflow.operators.trigger_dagrun.TriggerDagRunOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -108,7 +108,7 @@ AIR312.py:37:22: AIR312 `airflow.operators.trigger_dagrun.TriggerDagRunOperator` 38 | EmptyOperator() 39 | LatestOnlyOperator() | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunOperator` instead. AIR312.py:38:1: AIR312 `airflow.operators.empty.EmptyOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -119,7 +119,7 @@ AIR312.py:38:1: AIR312 `airflow.operators.empty.EmptyOperator` is deprecated and 39 | LatestOnlyOperator() 40 | ( | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. AIR312.py:39:1: AIR312 `airflow.operators.latest_only.LatestOnlyOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -130,7 +130,7 @@ AIR312.py:39:1: AIR312 `airflow.operators.latest_only.LatestOnlyOperator` is dep 40 | ( 41 | BranchPythonOperator(), | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.operators.latest_only.LatestOnlyOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.operators.latest_only.LatestOnlyOperator` instead. AIR312.py:41:5: AIR312 `airflow.operators.python.BranchPythonOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -141,7 +141,7 @@ AIR312.py:41:5: AIR312 `airflow.operators.python.BranchPythonOperator` is deprec 42 | PythonOperator(), 43 | PythonVirtualenvOperator(), | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.python.BranchPythonOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.python.BranchPythonOperator` instead. AIR312.py:42:5: AIR312 `airflow.operators.python.PythonOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -152,7 +152,7 @@ AIR312.py:42:5: AIR312 `airflow.operators.python.PythonOperator` is deprecated a 43 | PythonVirtualenvOperator(), 44 | ShortCircuitOperator(), | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.python.PythonOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.python.PythonOperator` instead. AIR312.py:43:5: AIR312 `airflow.operators.python.PythonVirtualenvOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -163,7 +163,7 @@ AIR312.py:43:5: AIR312 `airflow.operators.python.PythonVirtualenvOperator` is de 44 | ShortCircuitOperator(), 45 | ) | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.python.PythonVirtualenvOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.python.PythonVirtualenvOperator` instead. AIR312.py:44:5: AIR312 `airflow.operators.python.ShortCircuitOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -174,7 +174,7 @@ AIR312.py:44:5: AIR312 `airflow.operators.python.ShortCircuitOperator` is deprec 45 | ) 46 | BranchDayOfWeekOperator() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.operators.python.ShortCircuitOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.python.ShortCircuitOperator` instead. AIR312.py:46:1: AIR312 `airflow.operators.weekday.BranchDayOfWeekOperator` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -185,7 +185,7 @@ AIR312.py:46:1: AIR312 `airflow.operators.weekday.BranchDayOfWeekOperator` is de 47 | DateTimeSensor(), DateTimeSensorAsync() 48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.operators.weekday.BranchDayOfWeekOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.operators.weekday.BranchDayOfWeekOperator` instead. AIR312.py:47:1: AIR312 `airflow.sensors.date_time.DateTimeSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -196,7 +196,7 @@ AIR312.py:47:1: AIR312 `airflow.sensors.date_time.DateTimeSensor` is deprecated 48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() 49 | FileSensor() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.date_time.DateTimeSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.date_time.DateTimeSensor` instead. AIR312.py:47:19: AIR312 `airflow.sensors.date_time.DateTimeSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -207,7 +207,7 @@ AIR312.py:47:19: AIR312 `airflow.sensors.date_time.DateTimeSensorAsync` is depre 48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() 49 | FileSensor() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.date_time.DateTimeSensorAsync` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.date_time.DateTimeSensorAsync` instead. AIR312.py:48:1: AIR312 `airflow.sensors.external_task.ExternalTaskMarker` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -218,7 +218,7 @@ AIR312.py:48:1: AIR312 `airflow.sensors.external_task.ExternalTaskMarker` is dep 49 | FileSensor() 50 | TimeSensor(), TimeSensorAsync() | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskMarker` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskMarker` instead. AIR312.py:48:23: AIR312 `airflow.sensors.external_task.ExternalTaskSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -229,7 +229,7 @@ AIR312.py:48:23: AIR312 `airflow.sensors.external_task.ExternalTaskSensor` is de 49 | FileSensor() 50 | TimeSensor(), TimeSensorAsync() | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskSensor` instead. AIR312.py:48:45: AIR312 `airflow.sensors.external_task.ExternalTaskSensorLink` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -240,7 +240,7 @@ AIR312.py:48:45: AIR312 `airflow.sensors.external_task.ExternalTaskSensorLink` i 49 | FileSensor() 50 | TimeSensor(), TimeSensorAsync() | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskSensorLink` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskSensorLink` instead. AIR312.py:49:1: AIR312 `airflow.sensors.filesystem.FileSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -251,7 +251,7 @@ AIR312.py:49:1: AIR312 `airflow.sensors.filesystem.FileSensor` is deprecated and 50 | TimeSensor(), TimeSensorAsync() 51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() | - = help: Install `apache-airflow-provider-standard>=0.0.2` and use `airflow.providers.standard.sensors.filesystem.FileSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.sensors.filesystem.FileSensor` instead. AIR312.py:50:1: AIR312 `airflow.sensors.time_sensor.TimeSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -262,7 +262,7 @@ AIR312.py:50:1: AIR312 `airflow.sensors.time_sensor.TimeSensor` is deprecated an 51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() 52 | DayOfWeekSensor() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time.TimeSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time.TimeSensor` instead. AIR312.py:50:15: AIR312 `airflow.sensors.time_sensor.TimeSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -273,7 +273,7 @@ AIR312.py:50:15: AIR312 `airflow.sensors.time_sensor.TimeSensorAsync` is depreca 51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() 52 | DayOfWeekSensor() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time.TimeSensorAsync` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time.TimeSensorAsync` instead. AIR312.py:51:1: AIR312 `airflow.sensors.time_delta.TimeDeltaSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -284,7 +284,7 @@ AIR312.py:51:1: AIR312 `airflow.sensors.time_delta.TimeDeltaSensor` is deprecate 52 | DayOfWeekSensor() 53 | DagStateTrigger(), WorkflowTrigger() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.TimeDeltaSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.TimeDeltaSensor` instead. AIR312.py:51:20: AIR312 `airflow.sensors.time_delta.TimeDeltaSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -295,7 +295,7 @@ AIR312.py:51:20: AIR312 `airflow.sensors.time_delta.TimeDeltaSensorAsync` is dep 52 | DayOfWeekSensor() 53 | DagStateTrigger(), WorkflowTrigger() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.TimeDeltaSensorAsync` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.TimeDeltaSensorAsync` instead. AIR312.py:51:44: AIR312 `airflow.sensors.time_delta.WaitSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -306,7 +306,7 @@ AIR312.py:51:44: AIR312 `airflow.sensors.time_delta.WaitSensor` is deprecated an 52 | DayOfWeekSensor() 53 | DagStateTrigger(), WorkflowTrigger() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.WaitSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.WaitSensor` instead. AIR312.py:52:1: AIR312 `airflow.sensors.weekday.DayOfWeekSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -317,7 +317,7 @@ AIR312.py:52:1: AIR312 `airflow.sensors.weekday.DayOfWeekSensor` is deprecated a 53 | DagStateTrigger(), WorkflowTrigger() 54 | FileTrigger() | - = help: Install `apache-airflow-provider-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.weekday.DayOfWeekSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.weekday.DayOfWeekSensor` instead. AIR312.py:53:1: AIR312 `airflow.triggers.external_task.DagStateTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -328,7 +328,7 @@ AIR312.py:53:1: AIR312 `airflow.triggers.external_task.DagStateTrigger` is depre 54 | FileTrigger() 55 | DateTimeTrigger(), TimeDeltaTrigger() | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.triggers.external_task.DagStateTrigger` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.triggers.external_task.DagStateTrigger` instead. AIR312.py:53:20: AIR312 `airflow.triggers.external_task.WorkflowTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -339,7 +339,7 @@ AIR312.py:53:20: AIR312 `airflow.triggers.external_task.WorkflowTrigger` is depr 54 | FileTrigger() 55 | DateTimeTrigger(), TimeDeltaTrigger() | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.triggers.external_task.WorkflowTrigger` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.triggers.external_task.WorkflowTrigger` instead. AIR312.py:54:1: AIR312 `airflow.triggers.file.FileTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -349,7 +349,7 @@ AIR312.py:54:1: AIR312 `airflow.triggers.file.FileTrigger` is deprecated and mov | ^^^^^^^^^^^ AIR312 55 | DateTimeTrigger(), TimeDeltaTrigger() | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.triggers.file.FileTrigger` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.triggers.file.FileTrigger` instead. AIR312.py:55:1: AIR312 `airflow.triggers.temporal.DateTimeTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -358,7 +358,7 @@ AIR312.py:55:1: AIR312 `airflow.triggers.temporal.DateTimeTrigger` is deprecated 55 | DateTimeTrigger(), TimeDeltaTrigger() | ^^^^^^^^^^^^^^^ AIR312 | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.triggers.temporal.DateTimeTrigger` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.triggers.temporal.DateTimeTrigger` instead. AIR312.py:55:20: AIR312 `airflow.triggers.temporal.TimeDeltaTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -367,4 +367,4 @@ AIR312.py:55:20: AIR312 `airflow.triggers.temporal.TimeDeltaTrigger` is deprecat 55 | DateTimeTrigger(), TimeDeltaTrigger() | ^^^^^^^^^^^^^^^^ AIR312 | - = help: Install `apache-airflow-provider-standard>=0.0.3` and use `airflow.providers.standard.triggers.temporal.TimeDeltaTrigger` instead. + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.triggers.temporal.TimeDeltaTrigger` instead. From a01f25107a6c8d3e33b544c1e30d028869f244f0 Mon Sep 17 00:00:00 2001 From: Max Mynter <32773644+maxmynter@users.noreply.github.com> Date: Thu, 24 Apr 2025 08:48:02 +0200 Subject: [PATCH 0096/1161] [`pyupgrade`] Preserve parenthesis when fixing native literals containing newlines (`UP018`) (#17220) --- .gitattributes | 6 +++ .../test/fixtures/pyupgrade/.editorconfig | 5 +++ .../test/fixtures/pyupgrade/UP018_CR.py | 1 + .../test/fixtures/pyupgrade/UP018_LF.py | 7 ++++ .../test/fixtures/ruff/.editorconfig | 5 +++ .../resources/test/fixtures/ruff/RUF046_CR.py | 1 + .../resources/test/fixtures/ruff/RUF046_LF.py | 3 ++ crates/ruff_linter/src/rules/pyupgrade/mod.rs | 2 + .../rules/pyupgrade/rules/native_literals.rs | 4 ++ ..._rules__pyupgrade__tests__UP018_CR.py.snap | 22 ++++++++++ ..._rules__pyupgrade__tests__UP018_LF.py.snap | 41 +++++++++++++++++++ crates/ruff_linter/src/rules/ruff/mod.rs | 2 + ...les__ruff__tests__RUF046_RUF046_CR.py.snap | 12 ++++++ ...les__ruff__tests__RUF046_RUF046_LF.py.snap | 17 ++++++++ 14 files changed, 128 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/.editorconfig create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/.editorconfig create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_CR.py.snap create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_LF.py.snap create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_CR.py.snap create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_LF.py.snap diff --git a/.gitattributes b/.gitattributes index ebc35c67454587..966aa80ecf018b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,6 +12,12 @@ crates/ruff_python_parser/resources/invalid/re_lexing/line_continuation_windows_ crates/ruff_python_parser/resources/invalid/re_lex_logical_token_windows_eol.py text eol=crlf crates/ruff_python_parser/resources/invalid/re_lex_logical_token_mac_eol.py text eol=cr +crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py text eol=cr +crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py text eol=lf + +crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py text eol=cr +crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py text eol=lf + crates/ruff_python_parser/resources/inline linguist-generated=true ruff.schema.json -diff linguist-generated=true text=auto eol=lf diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/.editorconfig b/crates/ruff_linter/resources/test/fixtures/pyupgrade/.editorconfig new file mode 100644 index 00000000000000..048d4deec14e7b --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/.editorconfig @@ -0,0 +1,5 @@ +# These rules test for reformatting with specific line endings. +# Using a formatter fixes that and breaks the tests +[UP018_{CR,LF}.py] +generated_code = true +ij_formatter_enabled = false diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py new file mode 100644 index 00000000000000..7535093fa14876 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py @@ -0,0 +1 @@ +# Keep parenthesis around preserved CR int(- 1) int(+ 1) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py new file mode 100644 index 00000000000000..3a2d43335e505a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py @@ -0,0 +1,7 @@ +# Keep parentheses around preserved \n + +int(- + 1) + +int(+ + 1) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/.editorconfig b/crates/ruff_linter/resources/test/fixtures/ruff/.editorconfig new file mode 100644 index 00000000000000..bf4811bd1a7a2a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/.editorconfig @@ -0,0 +1,5 @@ +# These rules test for reformatting with specific line endings. +# Using a formatter fixes that and breaks the tests +[RUF046_{CR,LF}.py] +generated_code = true +ij_formatter_enabled = false diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py new file mode 100644 index 00000000000000..9723c347dcf0c9 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py @@ -0,0 +1 @@ +int(- 1) # Carriage return as newline \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py new file mode 100644 index 00000000000000..53a879a56e7148 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py @@ -0,0 +1,3 @@ +# \n as newline +int(- + 1) diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 822e680acfceb4..6a7003cabc1ab1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -36,6 +36,8 @@ mod tests { #[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_1.py"))] #[test_case(Rule::LRUCacheWithoutParameters, Path::new("UP011.py"))] #[test_case(Rule::NativeLiterals, Path::new("UP018.py"))] + #[test_case(Rule::NativeLiterals, Path::new("UP018_CR.py"))] + #[test_case(Rule::NativeLiterals, Path::new("UP018_LF.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_0.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_1.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_2.py"))] diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs index a1d8bf31deb379..14e31351ca614c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{self as ast, Expr, Int, LiteralExpressionRef, OperatorPrecedence, UnaryOp}; +use ruff_source_file::find_newline; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -244,6 +245,9 @@ pub(crate) fn native_literals( let arg_code = checker.locator().slice(arg); let content = match (parent_expr, literal_type, has_unary_op) { + // Expressions including newlines must be parenthesised to be valid syntax + (_, _, true) if find_newline(arg_code).is_some() => format!("({arg_code})"), + // Attribute access on an integer requires the integer to be parenthesized to disambiguate from a float // Ex) `(7).denominator` is valid but `7.denominator` is not // Note that floats do not have this problem diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_CR.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_CR.py.snap new file mode 100644 index 00000000000000..6dae74bfe53b66 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_CR.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP018_CR.py:2:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) + | +1 | # Keep parenthesis around preserved CR int(- 1) int(+ 1) + | ^^^^^^^^^^^ UP018 + | + = help: Replace with integer literal + +ℹ Safe fix +1 1 | # Keep parenthesis around preserved CR 2 |-int(- 2 |+(- 3 3 | 1) 4 4 | int(+ 5 5 | 1) + +UP018_CR.py:4:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) + | +2 | int(- 1) int(+ 1) + | ^^^^^^^^^^^ UP018 + | + = help: Replace with integer literal + +ℹ Safe fix +1 1 | # Keep parenthesis around preserved CR 2 2 | int(- 3 3 | 1) 4 |-int(+ 4 |+(+ 5 5 | 1) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_LF.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_LF.py.snap new file mode 100644 index 00000000000000..d3a530d273c713 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018_LF.py.snap @@ -0,0 +1,41 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP018_LF.py:3:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) + | +1 | # Keep parentheses around preserved \n +2 | +3 | / int(- +4 | | 1) + | |______^ UP018 +5 | +6 | int(+ + | + = help: Replace with integer literal + +ℹ Safe fix +1 1 | # Keep parentheses around preserved \n +2 2 | +3 |-int(- + 3 |+(- +4 4 | 1) +5 5 | +6 6 | int(+ + +UP018_LF.py:6:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) + | +4 | 1) +5 | +6 | / int(+ +7 | | 1) + | |______^ UP018 + | + = help: Replace with integer literal + +ℹ Safe fix +3 3 | int(- +4 4 | 1) +5 5 | +6 |-int(+ + 6 |+(+ +7 7 | 1) diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index c3543aeb90d830..f1dc5714f1c5d7 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -84,6 +84,8 @@ mod tests { #[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.py"))] #[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.pyi"))] #[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))] + #[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046_CR.py"))] + #[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046_LF.py"))] #[test_case(Rule::NeedlessElse, Path::new("RUF047_if.py"))] #[test_case(Rule::NeedlessElse, Path::new("RUF047_for.py"))] #[test_case(Rule::NeedlessElse, Path::new("RUF047_while.py"))] diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_CR.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_CR.py.snap new file mode 100644 index 00000000000000..61b9115357d0a2 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_CR.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF046_CR.py:1:1: RUF046 [*] Value being cast to `int` is already an integer + | +1 | int(- 1) # Carriage return as newline + | ^^^^^^^^^^^ RUF046 + | + = help: Remove unnecessary `int` call + +ℹ Safe fix +1 |-int(- 1 |+(- 2 2 | 1) # Carriage return as newline diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_LF.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_LF.py.snap new file mode 100644 index 00000000000000..97d44c191d4df6 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046_LF.py.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF046_LF.py:2:1: RUF046 [*] Value being cast to `int` is already an integer + | +1 | # \n as newline +2 | / int(- +3 | | 1) + | |______^ RUF046 + | + = help: Remove unnecessary `int` call + +ℹ Safe fix +1 1 | # \n as newline +2 |-int(- + 2 |+(- +3 3 | 1) From 21fd28d713d6990240b6bc71e09813b164f5e01e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 24 Apr 2025 10:31:35 +0100 Subject: [PATCH 0097/1161] [red-knot] Ban direct instantiations of `Protocol` classes (#17597) --- .../resources/mdtest/protocols.md | 6 +- ...Protocols_-_Calls_to_protocol_classes.snap | 117 ++++++++++++++++++ .../src/types/class.rs | 24 ++++ .../src/types/diagnostic.rs | 60 +++++---- .../src/types/infer.rs | 18 ++- 5 files changed, 194 insertions(+), 31 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index abba908825da84..7f19249d77bdd3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -304,10 +304,12 @@ reveal_type(typing.Protocol is not typing_extensions.Protocol) # revealed: bool ## Calls to protocol classes + + Neither `Protocol`, nor any protocol class, can be directly instantiated: ```py -from typing import Protocol +from typing_extensions import Protocol, reveal_type # error: [call-non-callable] reveal_type(Protocol()) # revealed: Unknown @@ -315,7 +317,7 @@ reveal_type(Protocol()) # revealed: Unknown class MyProtocol(Protocol): x: int -# TODO: should emit error +# error: [call-non-callable] "Cannot instantiate class `MyProtocol`" reveal_type(MyProtocol()) # revealed: MyProtocol ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap new file mode 100644 index 00000000000000..92abc59799e3ef --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap @@ -0,0 +1,117 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: protocols.md - Protocols - Calls to protocol classes +mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import Protocol, reveal_type + 2 | + 3 | # error: [call-non-callable] + 4 | reveal_type(Protocol()) # revealed: Unknown + 5 | + 6 | class MyProtocol(Protocol): + 7 | x: int + 8 | + 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" +10 | reveal_type(MyProtocol()) # revealed: MyProtocol +11 | class SubclassOfMyProtocol(MyProtocol): ... +12 | +13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol +14 | def f(x: type[MyProtocol]): +15 | reveal_type(x()) # revealed: MyProtocol +``` + +# Diagnostics + +``` +error: lint:call-non-callable: Object of type `typing.Protocol` is not callable + --> /src/mdtest_snippet.py:4:13 + | +3 | # error: [call-non-callable] +4 | reveal_type(Protocol()) # revealed: Unknown + | ^^^^^^^^^^ +5 | +6 | class MyProtocol(Protocol): + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:4:1 + | +3 | # error: [call-non-callable] +4 | reveal_type(Protocol()) # revealed: Unknown + | ^^^^^^^^^^^^^^^^^^^^^^^ `Unknown` +5 | +6 | class MyProtocol(Protocol): + | + +``` + +``` +error: lint:call-non-callable: Cannot instantiate class `MyProtocol` + --> /src/mdtest_snippet.py:10:13 + | + 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" +10 | reveal_type(MyProtocol()) # revealed: MyProtocol + | ^^^^^^^^^^^^ This call will raise `TypeError` at runtime +11 | class SubclassOfMyProtocol(MyProtocol): ... + | +info: Protocol classes cannot be instantiated + --> /src/mdtest_snippet.py:6:7 + | +4 | reveal_type(Protocol()) # revealed: Unknown +5 | +6 | class MyProtocol(Protocol): + | ^^^^^^^^^^^^^^^^^^^^ `MyProtocol` declared as a protocol here +7 | x: int + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:10:1 + | + 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" +10 | reveal_type(MyProtocol()) # revealed: MyProtocol + | ^^^^^^^^^^^^^^^^^^^^^^^^^ `MyProtocol` +11 | class SubclassOfMyProtocol(MyProtocol): ... + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:13:1 + | +11 | class SubclassOfMyProtocol(MyProtocol): ... +12 | +13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfMyProtocol` +14 | def f(x: type[MyProtocol]): +15 | reveal_type(x()) # revealed: MyProtocol + | + +``` + +``` +info: revealed-type: Revealed type + --> /src/mdtest_snippet.py:15:5 + | +13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol +14 | def f(x: type[MyProtocol]): +15 | reveal_type(x()) # revealed: MyProtocol + | ^^^^^^^^^^^^^^^^ `MyProtocol` + | + +``` diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 20e0624dd35ac5..9f7e2f37388d74 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -37,9 +37,11 @@ use crate::{ }; use indexmap::IndexSet; use itertools::Itertools as _; +use ruff_db::diagnostic::Span; use ruff_db::files::File; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, PythonVersion}; +use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashSet, FxHasher}; type FxOrderMap = ordermap::map::OrderMap>; @@ -1725,6 +1727,28 @@ impl<'db> ClassLiteralType<'db> { pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option> { self.is_protocol(db).then_some(ProtocolClassLiteral(self)) } + + /// Returns the [`Span`] of the class's "header": the class name + /// and any arguments passed to the `class` statement. E.g. + /// + /// ```ignore + /// class Foo(Bar, metaclass=Baz): ... + /// ^^^^^^^^^^^^^^^^^^^^^^^ + /// ``` + pub(super) fn header_span(self, db: &'db dyn Db) -> Span { + let class_scope = self.body_scope(db); + let class_node = class_scope.node(db).expect_class(); + let class_name = &class_node.name; + let header_range = TextRange::new( + class_name.start(), + class_node + .arguments + .as_deref() + .map(Ranged::end) + .unwrap_or_else(|| class_name.end()), + ); + Span::from(class_scope.file(db)).with_range(header_range) + } } impl<'db> From> for Type<'db> { diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index c4b7d4f87b31a1..6cd6b3da483a1d 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -11,7 +11,7 @@ use crate::types::string_annotation::{ use crate::types::{class::ProtocolClassLiteral, KnownFunction, KnownInstanceType, Type}; use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic}; use ruff_python_ast::{self as ast, AnyNodeRef}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::Ranged; use rustc_hash::FxHashSet; use std::fmt::Formatter; @@ -1331,24 +1331,14 @@ pub(crate) fn report_bad_argument_to_get_protocol_members( diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); diagnostic.info("Only protocol classes can be passed to `get_protocol_members`"); - let class_scope = class.body_scope(db); - let class_node = class_scope.node(db).expect_class(); - let class_name = &class_node.name; - let class_def_diagnostic_range = TextRange::new( - class_name.start(), - class_node - .arguments - .as_deref() - .map(Ranged::end) - .unwrap_or_else(|| class_name.end()), - ); let mut class_def_diagnostic = SubDiagnostic::new( Severity::Info, - format_args!("`{class_name}` is declared here, but it is not a protocol class:"), + format_args!( + "`{}` is declared here, but it is not a protocol class:", + class.name(db) + ), ); - class_def_diagnostic.annotate(Annotation::primary( - Span::from(class_scope.file(db)).with_range(class_def_diagnostic_range), - )); + class_def_diagnostic.annotate(Annotation::primary(class.header_span(db))); diagnostic.sub(class_def_diagnostic); diagnostic.info( @@ -1393,12 +1383,6 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( )); diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); - let class_scope = protocol.body_scope(db); - let class_node = class_scope.node(db).expect_class(); - let class_def_arguments = class_node - .arguments - .as_ref() - .expect("A `Protocol` class should always have at least one explicit base"); let mut class_def_diagnostic = SubDiagnostic::new( Severity::Info, format_args!( @@ -1407,11 +1391,8 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( ), ); class_def_diagnostic.annotate( - Annotation::primary(Span::from(class_scope.file(db)).with_range(TextRange::new( - class_node.name.start(), - class_def_arguments.end(), - ))) - .message(format_args!("`{class_name}` declared here")), + Annotation::primary(protocol.header_span(db)) + .message(format_args!("`{class_name}` declared here")), ); diagnostic.sub(class_def_diagnostic); @@ -1421,3 +1402,28 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( )); diagnostic.info("See https://docs.python.org/3/library/typing.html#typing.runtime_checkable"); } + +pub(crate) fn report_attempted_protocol_instantiation( + context: &InferContext, + call: &ast::ExprCall, + protocol: ProtocolClassLiteral, +) { + let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, call) else { + return; + }; + let db = context.db(); + let class_name = protocol.name(db); + let mut diagnostic = + builder.into_diagnostic(format_args!("Cannot instantiate class `{class_name}`",)); + diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + + let mut class_def_diagnostic = SubDiagnostic::new( + Severity::Info, + format_args!("Protocol classes cannot be instantiated"), + ); + class_def_diagnostic.annotate( + Annotation::primary(protocol.header_span(db)) + .message(format_args!("`{class_name}` declared as a protocol here")), + ); + diagnostic.sub(class_def_diagnostic); +} diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index dfbf928f830a89..8bda7530d7f2b1 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -96,8 +96,8 @@ use crate::Db; use super::context::{InNoTypeCheck, InferContext}; use super::diagnostic::{ - report_bad_argument_to_get_protocol_members, report_index_out_of_bounds, - report_invalid_exception_caught, report_invalid_exception_cause, + report_attempted_protocol_instantiation, report_bad_argument_to_get_protocol_members, + report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_type_checking_constant, report_non_subscriptable, report_possibly_unresolved_reference, report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero, @@ -4293,6 +4293,20 @@ impl<'db> TypeInferenceBuilder<'db> { let mut call_arguments = Self::parse_arguments(arguments); let callable_type = self.infer_expression(func); + // It might look odd here that we emit an error for class-literals but not `type[]` types. + // But it's deliberate! The typing spec explicitly mandates that `type[]` types can be called + // even though class-literals cannot. This is because even though a protocol class `SomeProtocol` + // is always an abstract class, `type[SomeProtocol]` can be a concrete subclass of that protocol + // -- and indeed, according to the spec, type checkers must disallow abstract subclasses of the + // protocol to be passed to parameters that accept `type[SomeProtocol]`. + // . + if let Some(protocol_class) = callable_type + .into_class_literal() + .and_then(|class| class.into_protocol_class(self.db())) + { + report_attempted_protocol_instantiation(&self.context, call_expression, protocol_class); + } + // For class literals we model the entire class instantiation logic, so it is handled // in a separate function. For some known classes we have manual signatures defined and use // the `try_call` path below. From e93fa7062c7c8879ee3d658be6fdd159c6a25992 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 24 Apr 2025 13:11:31 +0100 Subject: [PATCH 0098/1161] [red-knot] Add more tests for protocols (#17603) --- .../resources/mdtest/protocols.md | 165 +++++++++++++++++- 1 file changed, 159 insertions(+), 6 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 7f19249d77bdd3..0af5a8b58cdd95 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -242,7 +242,7 @@ def f( Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime: ```py -# TODO: should be `Literal[True]` +# Could also be `Literal[True]`, but `bool` is fine: reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool ``` @@ -667,7 +667,11 @@ reveal_type(get_protocol_members(LotsOfBindings)) Attribute members are allowed to have assignments in methods on the protocol class, just like non-protocol classes. Unlike other classes, however, instance attributes that are not declared in -the class body are disallowed: +the class body are disallowed. This is mandated by [the spec][spec_protocol_members]: + +> Additional attributes *only* defined in the body of a method by assignment via `self` are not +> allowed. The rationale for this is that the protocol class implementation is often not shared by +> subtypes, so the interface should not depend on the default implementation. ```py class Foo(Protocol): @@ -690,6 +694,21 @@ class Foo(Protocol): reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["non_init_method"], Literal["x"], Literal["y"]] ``` +If a member is declared in a superclass of a protocol class, it is fine for it to be assigned to in +the sub-protocol class without a redeclaration: + +```py +class Super(Protocol): + x: int + +class Sub(Super, Protocol): + x = 42 # no error here, since it's declared in the superclass + +# TODO: actually frozensets +reveal_type(get_protocol_members(Super)) # revealed: tuple[Literal["x"]] +reveal_type(get_protocol_members(Sub)) # revealed: tuple[Literal["x"]] +``` + If a protocol has 0 members, then all other types are assignable to it, and all fully static types are subtypes of it: @@ -1265,6 +1284,140 @@ def f(arg1: type, arg2: type): reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers] ``` +## Truthiness of protocol instance + +An instance of a protocol type generally has ambiguous truthiness: + +```py +from typing import Protocol + +class Foo(Protocol): + x: int + +def f(foo: Foo): + reveal_type(bool(foo)) # revealed: bool +``` + +But this is not the case if the protocol has a `__bool__` method member that returns `Literal[True]` +or `Literal[False]`: + +```py +from typing import Literal + +class Truthy(Protocol): + def __bool__(self) -> Literal[True]: ... + +class FalsyFoo(Foo, Protocol): + def __bool__(self) -> Literal[False]: ... + +class FalsyFooSubclass(FalsyFoo, Protocol): + y: str + +def g(a: Truthy, b: FalsyFoo, c: FalsyFooSubclass): + reveal_type(bool(a)) # revealed: Literal[True] + reveal_type(bool(b)) # revealed: Literal[False] + reveal_type(bool(c)) # revealed: Literal[False] +``` + +It is not sufficient for a protocol to have a callable `__bool__` instance member that returns +`Literal[True]` for it to be considered always truthy. Dunder methods are looked up on the class +rather than the instance. If a protocol `X` has an instance-attribute `__bool__` member, it is +unknowable whether that attribute can be accessed on the type of an object that satisfies `X`'s +interface: + +```py +from typing import Callable + +class InstanceAttrBool(Protocol): + __bool__: Callable[[], Literal[True]] + +def h(obj: InstanceAttrBool): + reveal_type(bool(obj)) # revealed: bool +``` + +## Fully static protocols; gradual protocols + +A protocol is only fully static if all of its members are fully static: + +```py +from typing import Protocol, Any +from knot_extensions import is_fully_static, static_assert + +class FullyStatic(Protocol): + x: int + +class NotFullyStatic(Protocol): + x: Any + +static_assert(is_fully_static(FullyStatic)) + +# TODO: should pass +static_assert(not is_fully_static(NotFullyStatic)) # error: [static-assert-error] +``` + +Non-fully-static protocols do not participate in subtyping, only assignability: + +```py +from knot_extensions import is_subtype_of, is_assignable_to + +class NominalWithX: + x: int = 42 + +# TODO: these should pass +static_assert(is_assignable_to(NominalWithX, FullyStatic)) # error: [static-assert-error] +static_assert(is_assignable_to(NominalWithX, NotFullyStatic)) # error: [static-assert-error] +static_assert(is_subtype_of(NominalWithX, FullyStatic)) # error: [static-assert-error] + +static_assert(not is_subtype_of(NominalWithX, NotFullyStatic)) +``` + +Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to +the nominal type `object` (as described above): + +```py +class Empty(Protocol): ... + +static_assert(is_fully_static(Empty)) +``` + +A method member is only considered fully static if all its parameter annotations and its return +annotation are fully static: + +```py +class FullyStaticMethodMember(Protocol): + def method(self, x: int) -> str: ... + +class DynamicParameter(Protocol): + def method(self, x: Any) -> str: ... + +class DynamicReturn(Protocol): + def method(self, x: int) -> Any: ... + +static_assert(is_fully_static(FullyStaticMethodMember)) + +# TODO: these should pass +static_assert(not is_fully_static(DynamicParameter)) # error: [static-assert-error] +static_assert(not is_fully_static(DynamicReturn)) # error: [static-assert-error] +``` + +The [typing spec][spec_protocol_members] states: + +> If any parameters of a protocol method are not annotated, then their types are assumed to be `Any` + +Thus, a partially unannotated method member can also not be considered to be fully static: + +```py +class NoParameterAnnotation(Protocol): + def method(self, x) -> str: ... + +class NoReturnAnnotation(Protocol): + def method(self, x: int): ... + +# TODO: these should pass +static_assert(not is_fully_static(NoParameterAnnotation)) # error: [static-assert-error] +static_assert(not is_fully_static(NoReturnAnnotation)) # error: [static-assert-error] +``` + ## `typing.SupportsIndex` and `typing.Sized` `typing.SupportsIndex` is already somewhat supported through some special-casing in red-knot. @@ -1294,7 +1447,9 @@ def _(some_list: list, some_tuple: tuple[int, str], some_sized: Sized): Add tests for: - More tests for protocols inside `type[]`. [Spec reference][protocols_inside_type_spec]. -- Protocols with instance-method members +- Protocols with instance-method members, including: + - Protocols with methods that have parameters or the return type unannotated + - Protocols with methods that have parameters or the return type annotated with `Any` - Protocols with `@classmethod` and `@staticmethod` - Assignability of non-instance types to protocols with instance-method members (e.g. a class-literal type can be a subtype of `Sized` if its metaclass has a `__len__` method) @@ -1308,9 +1463,6 @@ Add tests for: - Protocols with instance attributes annotated with `Callable` (can a nominal type with a method satisfy that protocol, and if so in what cases?) - Protocols decorated with `@final` -- Protocols with attribute members annotated with `Any` -- Protocols with methods that have parameters or the return type unannotated -- Protocols with methods that have parameters or the return type annotated with `Any` - Equivalence and subtyping between `Callable` types and protocols that define `__call__` [mypy_protocol_docs]: https://mypy.readthedocs.io/en/stable/protocols.html#protocols-and-structural-subtyping @@ -1319,4 +1471,5 @@ Add tests for: [protocols_inside_type_spec]: https://typing.python.org/en/latest/spec/protocol.html#type-and-class-objects-vs-protocols [recursive_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#recursive-protocols [self_types_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#self-types-in-protocols +[spec_protocol_members]: https://typing.python.org/en/latest/spec/protocol.html#protocol-members [typing_spec_protocols]: https://typing.python.org/en/latest/spec/protocol.html From ac6219ec38a76394d35e2bf6813a768b11b03638 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 24 Apr 2025 06:55:05 -0700 Subject: [PATCH 0099/1161] [red-knot] fix collapsing literal and its negation to object (#17605) ## Summary Another follow-up to the unions-of-large-literals optimization. Restore the behavior that e.g. `Literal[""] | ~Literal[""]` collapses to `object`. ## Test Plan Added mdtests. --- .../resources/mdtest/call/union.md | 41 ++++-- .../src/types/builder.rs | 128 ++++++++++++++---- 2 files changed, 125 insertions(+), 44 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/union.md b/crates/red_knot_python_semantic/resources/mdtest/call/union.md index 10055283a83667..eef65c4557a422 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/union.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/union.md @@ -165,26 +165,39 @@ def _(flag: bool): ## Unions with literals and negations ```py -from typing import Literal, Union +from typing import Literal from knot_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to -static_assert(is_subtype_of(Literal["a", ""], Union[Literal["a", ""], Not[AlwaysFalsy]])) -static_assert(is_subtype_of(Not[AlwaysFalsy], Union[Literal["", "a"], Not[AlwaysFalsy]])) -static_assert(is_subtype_of(Literal["a", ""], Union[Not[AlwaysFalsy], Literal["a", ""]])) -static_assert(is_subtype_of(Not[AlwaysFalsy], Union[Not[AlwaysFalsy], Literal["a", ""]])) +static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[AlwaysFalsy])) +static_assert(is_subtype_of(Not[AlwaysFalsy], Literal["", "a"] | Not[AlwaysFalsy])) +static_assert(is_subtype_of(Literal["a", ""], Not[AlwaysFalsy] | Literal["a", ""])) +static_assert(is_subtype_of(Not[AlwaysFalsy], Not[AlwaysFalsy] | Literal["a", ""])) -static_assert(is_subtype_of(Literal["a", ""], Union[Literal["a", ""], Not[Literal[""]]])) -static_assert(is_subtype_of(Not[Literal[""]], Union[Literal["a", ""], Not[Literal[""]]])) -static_assert(is_subtype_of(Literal["a", ""], Union[Not[Literal[""]], Literal["a", ""]])) -static_assert(is_subtype_of(Not[Literal[""]], Union[Not[Literal[""]], Literal["a", ""]])) +static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[Literal[""]])) +static_assert(is_subtype_of(Not[Literal[""]], Literal["a", ""] | Not[Literal[""]])) +static_assert(is_subtype_of(Literal["a", ""], Not[Literal[""]] | Literal["a", ""])) +static_assert(is_subtype_of(Not[Literal[""]], Not[Literal[""]] | Literal["a", ""])) def _( - x: Union[Literal["a", ""], Not[AlwaysFalsy]], - y: Union[Literal["a", ""], Not[Literal[""]]], + a: Literal["a", ""] | Not[AlwaysFalsy], + b: Literal["a", ""] | Not[Literal[""]], + c: Literal[""] | Not[Literal[""]], + d: Not[Literal[""]] | Literal[""], + e: Literal["a"] | Not[Literal["a"]], + f: Literal[b"b"] | Not[Literal[b"b"]], + g: Not[Literal[b"b"]] | Literal[b"b"], + h: Literal[42] | Not[Literal[42]], + i: Not[Literal[42]] | Literal[42], ): - reveal_type(x) # revealed: Literal[""] | ~AlwaysFalsy - # TODO should be `object` - reveal_type(y) # revealed: Literal[""] | ~Literal[""] + reveal_type(a) # revealed: Literal[""] | ~AlwaysFalsy + reveal_type(b) # revealed: object + reveal_type(c) # revealed: object + reveal_type(d) # revealed: object + reveal_type(e) # revealed: object + reveal_type(f) # revealed: object + reveal_type(g) # revealed: object + reveal_type(h) # revealed: object + reveal_type(i) # revealed: object ``` ## Cannot use an argument as both a value and a type form diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 6bfecfe2a92f62..8f81db8d3f6dc1 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -97,37 +97,70 @@ impl<'db> UnionElement<'db> { fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> { match self { UnionElement::IntLiterals(literals) => { - ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::Int) { + if other_type.splits_literals(db, LiteralKind::Int) { + let mut collapse = false; + let negated = other_type.negate(db); literals.retain(|literal| { - !Type::IntLiteral(*literal).is_subtype_of(db, other_type) + let ty = Type::IntLiteral(*literal); + if negated.is_subtype_of(db, ty) { + collapse = true; + } + !ty.is_subtype_of(db, other_type) }); - !literals.is_empty() + if collapse { + ReduceResult::CollapseToObject + } else { + ReduceResult::KeepIf(!literals.is_empty()) + } } else { - // SAFETY: All `UnionElement` literal kinds must always be non-empty - !Type::IntLiteral(literals[0]).is_subtype_of(db, other_type) - }) + ReduceResult::KeepIf( + !Type::IntLiteral(literals[0]).is_subtype_of(db, other_type), + ) + } } UnionElement::StringLiterals(literals) => { - ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::String) { + if other_type.splits_literals(db, LiteralKind::String) { + let mut collapse = false; + let negated = other_type.negate(db); literals.retain(|literal| { - !Type::StringLiteral(*literal).is_subtype_of(db, other_type) + let ty = Type::StringLiteral(*literal); + if negated.is_subtype_of(db, ty) { + collapse = true; + } + !ty.is_subtype_of(db, other_type) }); - !literals.is_empty() + if collapse { + ReduceResult::CollapseToObject + } else { + ReduceResult::KeepIf(!literals.is_empty()) + } } else { - // SAFETY: All `UnionElement` literal kinds must always be non-empty - !Type::StringLiteral(literals[0]).is_subtype_of(db, other_type) - }) + ReduceResult::KeepIf( + !Type::StringLiteral(literals[0]).is_subtype_of(db, other_type), + ) + } } UnionElement::BytesLiterals(literals) => { - ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::Bytes) { + if other_type.splits_literals(db, LiteralKind::Bytes) { + let mut collapse = false; + let negated = other_type.negate(db); literals.retain(|literal| { - !Type::BytesLiteral(*literal).is_subtype_of(db, other_type) + let ty = Type::BytesLiteral(*literal); + if negated.is_subtype_of(db, ty) { + collapse = true; + } + !ty.is_subtype_of(db, other_type) }); - !literals.is_empty() + if collapse { + ReduceResult::CollapseToObject + } else { + ReduceResult::KeepIf(!literals.is_empty()) + } } else { - // SAFETY: All `UnionElement` literal kinds must always be non-empty - !Type::BytesLiteral(literals[0]).is_subtype_of(db, other_type) - }) + ReduceResult::KeepIf( + !Type::BytesLiteral(literals[0]).is_subtype_of(db, other_type), + ) + } } UnionElement::Type(existing) => ReduceResult::Type(*existing), } @@ -138,6 +171,8 @@ enum ReduceResult<'db> { /// Reduction of this `UnionElement` is complete; keep it in the union if the nested /// boolean is true, eliminate it from the union if false. KeepIf(bool), + /// Collapse this entire union to `object`. + CollapseToObject, /// The given `Type` can stand-in for the entire `UnionElement` for further union /// simplification checks. Type(Type<'db>), @@ -195,6 +230,7 @@ impl<'db> UnionBuilder<'db> { // containing it. Type::StringLiteral(literal) => { let mut found = false; + let ty_negated = ty.negate(self.db); for element in &mut self.elements { match element { UnionElement::StringLiterals(literals) => { @@ -207,8 +243,16 @@ impl<'db> UnionBuilder<'db> { found = true; break; } - UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => { - return; + UnionElement::Type(existing) => { + if ty.is_subtype_of(self.db, *existing) { + return; + } + if ty_negated.is_subtype_of(self.db, *existing) { + // The type that includes both this new element, and its negation + // (or a supertype of its negation), must be simply `object`. + self.collapse_to_object(); + return; + } } _ => {} } @@ -223,6 +267,7 @@ impl<'db> UnionBuilder<'db> { // Same for bytes literals as for string literals, above. Type::BytesLiteral(literal) => { let mut found = false; + let ty_negated = ty.negate(self.db); for element in &mut self.elements { match element { UnionElement::BytesLiterals(literals) => { @@ -235,8 +280,16 @@ impl<'db> UnionBuilder<'db> { found = true; break; } - UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => { - return; + UnionElement::Type(existing) => { + if ty.is_subtype_of(self.db, *existing) { + return; + } + if ty_negated.is_subtype_of(self.db, *existing) { + // The type that includes both this new element, and its negation + // (or a supertype of its negation), must be simply `object`. + self.collapse_to_object(); + return; + } } _ => {} } @@ -251,6 +304,7 @@ impl<'db> UnionBuilder<'db> { // And same for int literals as well. Type::IntLiteral(literal) => { let mut found = false; + let ty_negated = ty.negate(self.db); for element in &mut self.elements { match element { UnionElement::IntLiterals(literals) => { @@ -263,8 +317,16 @@ impl<'db> UnionBuilder<'db> { found = true; break; } - UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => { - return; + UnionElement::Type(existing) => { + if ty.is_subtype_of(self.db, *existing) { + return; + } + if ty_negated.is_subtype_of(self.db, *existing) { + // The type that includes both this new element, and its negation + // (or a supertype of its negation), must be simply `object`. + self.collapse_to_object(); + return; + } } _ => {} } @@ -298,6 +360,10 @@ impl<'db> UnionBuilder<'db> { continue; } ReduceResult::Type(ty) => ty, + ReduceResult::CollapseToObject => { + self.collapse_to_object(); + return; + } }; if Some(element_type) == bool_pair { to_add = KnownClass::Bool.to_instance(self.db); @@ -317,12 +383,14 @@ impl<'db> UnionBuilder<'db> { } else if element_type.is_subtype_of(self.db, ty) { to_remove.push(index); } else if ty_negated.is_subtype_of(self.db, element_type) { - // We add `ty` to the union. We just checked that `~ty` is a subtype of an existing `element`. - // This also means that `~ty | ty` is a subtype of `element | ty`, because both elements in the - // first union are subtypes of the corresponding elements in the second union. But `~ty | ty` is - // just `object`. Since `object` is a subtype of `element | ty`, we can only conclude that - // `element | ty` must be `object` (object has no other supertypes). This means we can simplify - // the whole union to just `object`, since all other potential elements would also be subtypes of + // We add `ty` to the union. We just checked that `~ty` is a subtype of an + // existing `element`. This also means that `~ty | ty` is a subtype of + // `element | ty`, because both elements in the first union are subtypes of + // the corresponding elements in the second union. But `~ty | ty` is just + // `object`. Since `object` is a subtype of `element | ty`, we can only + // conclude that `element | ty` must be `object` (object has no other + // supertypes). This means we can simplify the whole union to just + // `object`, since all other potential elements would also be subtypes of // `object`. self.collapse_to_object(); return; From e71f3ed2c589976aaae5de69639351ab132790db Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Thu, 24 Apr 2025 15:56:39 +0100 Subject: [PATCH 0100/1161] [red-knot] Update `==` and `!=` narrowing (#17567) ## Summary Historically we have avoided narrowing on `==` tests because in many cases it's unsound, since subclasses of a type could compare equal to who-knows-what. But there are a lot of types (literals and unions of them, as well as some known instances like `None` -- single-valued types) whose `__eq__` behavior we know, and which we can safely narrow away based on equality comparisons. This PR implements equality narrowing in the cases where it is sound. The most elegant way to do this (and the way that is most in-line with our approach up until now) would be to introduce new Type variants `NeverEqualTo[...]` and `AlwaysEqualTo[...]`, and then implement all type relations for those variants, narrow by intersection, and let union and intersection simplification sort it all out. This is analogous to our existing handling for `AlwaysFalse` and `AlwaysTrue`. But I'm reluctant to add new `Type` variants for this, mostly because they could end up un-simplified in some types and make types even more complex. So let's try this approach, where we handle more of the narrowing logic as a special case. ## Test Plan Updated and added tests. --------- Co-authored-by: Carl Meyer Co-authored-by: Carl Meyer Co-authored-by: Alex Waygood --- .../resources/mdtest/narrow/assert.md | 2 +- .../mdtest/narrow/conditionals/elif_else.md | 15 +- .../narrow/conditionals/{not_eq.md => eq.md} | 71 +++++++-- .../mdtest/narrow/conditionals/nested.md | 9 +- crates/red_knot_python_semantic/src/types.rs | 14 +- .../src/types/narrow.rs | 149 ++++++++++++++++-- 6 files changed, 220 insertions(+), 40 deletions(-) rename crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/{not_eq.md => eq.md} (52%) diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md index 1fad9290a51155..c452e3c71dc76a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md @@ -29,7 +29,7 @@ def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]): assert x is 2 reveal_type(x) # revealed: Literal[2] assert y == 2 - reveal_type(y) # revealed: Literal[1, 2, 3] + reveal_type(y) # revealed: Literal[2] ``` ## `assert` with `isinstance` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md index 376c24f1e97f92..bfa741428bb8f3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md @@ -20,11 +20,9 @@ def _(flag1: bool, flag2: bool): x = 1 if flag1 else 2 if flag2 else 3 if x == 1: - # TODO should be Literal[1] - reveal_type(x) # revealed: Literal[1, 2, 3] + reveal_type(x) # revealed: Literal[1] elif x == 2: - # TODO should be Literal[2] - reveal_type(x) # revealed: Literal[2, 3] + reveal_type(x) # revealed: Literal[2] else: reveal_type(x) # revealed: Literal[3] ``` @@ -38,14 +36,11 @@ def _(flag1: bool, flag2: bool): if x != 1: reveal_type(x) # revealed: Literal[2, 3] elif x != 2: - # TODO should be `Literal[1]` - reveal_type(x) # revealed: Literal[1, 3] + reveal_type(x) # revealed: Literal[1] elif x == 3: - # TODO should be Never - reveal_type(x) # revealed: Literal[1, 2, 3] + reveal_type(x) # revealed: Never else: - # TODO should be Never - reveal_type(x) # revealed: Literal[1, 2] + reveal_type(x) # revealed: Never ``` ## Assignment expressions diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/eq.md similarity index 52% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md rename to crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/eq.md index 20f25d9ed4190f..bf3184b048d86f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -9,8 +9,7 @@ def _(flag: bool): if x != None: reveal_type(x) # revealed: Literal[1] else: - # TODO should be None - reveal_type(x) # revealed: None | Literal[1] + reveal_type(x) # revealed: None ``` ## `!=` for other singleton types @@ -22,8 +21,7 @@ def _(flag: bool): if x != False: reveal_type(x) # revealed: Literal[True] else: - # TODO should be Literal[False] - reveal_type(x) # revealed: bool + reveal_type(x) # revealed: Literal[False] ``` ## `x != y` where `y` is of literal type @@ -47,8 +45,7 @@ def _(flag: bool): if C != A: reveal_type(C) # revealed: Literal[B] else: - # TODO should be Literal[A] - reveal_type(C) # revealed: Literal[A, B] + reveal_type(C) # revealed: Literal[A] ``` ## `x != y` where `y` has multiple single-valued options @@ -61,8 +58,7 @@ def _(flag1: bool, flag2: bool): if x != y: reveal_type(x) # revealed: Literal[1, 2] else: - # TODO should be Literal[2] - reveal_type(x) # revealed: Literal[1, 2] + reveal_type(x) # revealed: Literal[2] ``` ## `!=` for non-single-valued types @@ -101,6 +97,61 @@ def f() -> Literal[1, 2, 3]: if (x := f()) != 1: reveal_type(x) # revealed: Literal[2, 3] else: - # TODO should be Literal[1] - reveal_type(x) # revealed: Literal[1, 2, 3] + reveal_type(x) # revealed: Literal[1] +``` + +## Union with `Any` + +```py +from typing import Any + +def _(x: Any | None, y: Any | None): + if x != 1: + reveal_type(x) # revealed: (Any & ~Literal[1]) | None + if y == 1: + reveal_type(y) # revealed: Any & ~None +``` + +## Booleans and integers + +```py +from typing import Literal + +def _(b: bool, i: Literal[1, 2]): + if b == 1: + reveal_type(b) # revealed: Literal[True] + else: + reveal_type(b) # revealed: Literal[False] + + if b == 6: + reveal_type(b) # revealed: Never + else: + reveal_type(b) # revealed: bool + + if b == 0: + reveal_type(b) # revealed: Literal[False] + else: + reveal_type(b) # revealed: Literal[True] + + if i == True: + reveal_type(i) # revealed: Literal[1] + else: + reveal_type(i) # revealed: Literal[2] +``` + +## Narrowing `LiteralString` in union + +```py +from typing_extensions import Literal, LiteralString, Any + +def _(s: LiteralString | None, t: LiteralString | Any): + if s == "foo": + reveal_type(s) # revealed: Literal["foo"] + + if s == 1: + reveal_type(s) # revealed: Never + + if t == "foo": + # TODO could be `Literal["foo"] | Any` + reveal_type(t) # revealed: LiteralString | Any ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md index fa69fe8863bc0d..ab026cef6733ee 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md @@ -31,17 +31,14 @@ def _(flag1: bool, flag2: bool): if x != 1: reveal_type(x) # revealed: Literal[2, 3] if x == 2: - # TODO should be `Literal[2]` - reveal_type(x) # revealed: Literal[2, 3] + reveal_type(x) # revealed: Literal[2] elif x == 3: reveal_type(x) # revealed: Literal[3] else: reveal_type(x) # revealed: Never elif x != 2: - # TODO should be Literal[1] - reveal_type(x) # revealed: Literal[1, 3] + reveal_type(x) # revealed: Literal[1] else: - # TODO should be Never - reveal_type(x) # revealed: Literal[1, 2, 3] + reveal_type(x) # revealed: Never ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1050446ad2bb78..695353ef7bf8a0 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -542,6 +542,11 @@ impl<'db> Type<'db> { .is_some_and(|instance| instance.class().is_known(db, KnownClass::NoneType)) } + fn is_bool(&self, db: &'db dyn Db) -> bool { + self.into_instance() + .is_some_and(|instance| instance.class().is_known(db, KnownClass::Bool)) + } + pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { self.into_instance().is_some_and(|instance| { instance @@ -776,8 +781,13 @@ impl<'db> Type<'db> { } pub fn is_union_of_single_valued(&self, db: &'db dyn Db) -> bool { - self.into_union() - .is_some_and(|union| union.elements(db).iter().all(|ty| ty.is_single_valued(db))) + self.into_union().is_some_and(|union| { + union + .elements(db) + .iter() + .all(|ty| ty.is_single_valued(db) || ty.is_bool(db) || ty.is_literal_string()) + }) || self.is_bool(db) + || self.is_literal_string() } pub const fn into_int_literal(self) -> Option { diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index b884d1da850ac9..1dfac5b0413e41 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -394,6 +394,142 @@ impl<'db> NarrowingConstraintsBuilder<'db> { } } + fn evaluate_expr_eq(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { + // We can only narrow on equality checks against single-valued types. + if rhs_ty.is_single_valued(self.db) || rhs_ty.is_union_of_single_valued(self.db) { + // The fully-general (and more efficient) approach here would be to introduce a + // `NeverEqualTo` type that can wrap a single-valued type, and then simply return + // `~NeverEqualTo(rhs_ty)` here and let union/intersection builder sort it out. This is + // how we handle `AlwaysTruthy` and `AlwaysFalsy`. But this means we have to deal with + // this type everywhere, and possibly have it show up unsimplified in some cases, and + // so we instead prefer to just do the simplification here. (Another hybrid option that + // would be similar to this, but more efficient, would be to allow narrowing to return + // something that is not a type, and handle this not-a-type in `symbol_from_bindings`, + // instead of intersecting with a type.) + + // Return `true` if it is possible for any two inhabitants of the given types to + // compare equal to each other; otherwise return `false`. + fn could_compare_equal<'db>( + db: &'db dyn Db, + left_ty: Type<'db>, + right_ty: Type<'db>, + ) -> bool { + if !left_ty.is_disjoint_from(db, right_ty) { + // If types overlap, they have inhabitants in common; it's definitely possible + // for an object to compare equal to itself. + return true; + } + match (left_ty, right_ty) { + // In order to be sure a union type cannot compare equal to another type, it + // must be true that no element of the union can compare equal to that type. + (Type::Union(union), _) => union + .elements(db) + .iter() + .any(|ty| could_compare_equal(db, *ty, right_ty)), + (_, Type::Union(union)) => union + .elements(db) + .iter() + .any(|ty| could_compare_equal(db, left_ty, *ty)), + // Boolean literals and int literals are disjoint, and single valued, and yet + // `True == 1` and `False == 0`. + (Type::BooleanLiteral(b), Type::IntLiteral(i)) + | (Type::IntLiteral(i), Type::BooleanLiteral(b)) => i64::from(b) == i, + // Other than the above cases, two single-valued disjoint types cannot compare + // equal. + _ => !(left_ty.is_single_valued(db) && right_ty.is_single_valued(db)), + } + } + + // Return `true` if `lhs_ty` consists only of `LiteralString` and types that cannot + // compare equal to `rhs_ty`. + fn can_narrow_to_rhs<'db>( + db: &'db dyn Db, + lhs_ty: Type<'db>, + rhs_ty: Type<'db>, + ) -> bool { + match lhs_ty { + Type::Union(union) => union + .elements(db) + .iter() + .all(|ty| can_narrow_to_rhs(db, *ty, rhs_ty)), + // Either `rhs_ty` is a string literal, in which case we can narrow to it (no + // other string literal could compare equal to it), or it is not a string + // literal, in which case (given that it is single-valued), LiteralString + // cannot compare equal to it. + Type::LiteralString => true, + _ => !could_compare_equal(db, lhs_ty, rhs_ty), + } + } + + // Filter `ty` to just the types that cannot be equal to `rhs_ty`. + fn filter_to_cannot_be_equal<'db>( + db: &'db dyn Db, + ty: Type<'db>, + rhs_ty: Type<'db>, + ) -> Type<'db> { + match ty { + Type::Union(union) => { + union.map(db, |ty| filter_to_cannot_be_equal(db, *ty, rhs_ty)) + } + // Treat `bool` as `Literal[True, False]`. + Type::Instance(instance) if instance.class().is_known(db, KnownClass::Bool) => { + UnionType::from_elements( + db, + [Type::BooleanLiteral(true), Type::BooleanLiteral(false)] + .into_iter() + .map(|ty| filter_to_cannot_be_equal(db, ty, rhs_ty)), + ) + } + _ => { + if ty.is_single_valued(db) && !could_compare_equal(db, ty, rhs_ty) { + ty + } else { + Type::Never + } + } + } + } + Some(if can_narrow_to_rhs(self.db, lhs_ty, rhs_ty) { + rhs_ty + } else { + filter_to_cannot_be_equal(self.db, lhs_ty, rhs_ty).negate(self.db) + }) + } else { + None + } + } + + fn evaluate_expr_ne(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { + match (lhs_ty, rhs_ty) { + (Type::Instance(instance), Type::IntLiteral(i)) + if instance.class().is_known(self.db, KnownClass::Bool) => + { + if i == 0 { + Some(Type::BooleanLiteral(false).negate(self.db)) + } else if i == 1 { + Some(Type::BooleanLiteral(true).negate(self.db)) + } else { + None + } + } + (_, Type::BooleanLiteral(b)) => { + if b { + Some( + UnionType::from_elements(self.db, [rhs_ty, Type::IntLiteral(1)]) + .negate(self.db), + ) + } else { + Some( + UnionType::from_elements(self.db, [rhs_ty, Type::IntLiteral(0)]) + .negate(self.db), + ) + } + } + _ if rhs_ty.is_single_valued(self.db) => Some(rhs_ty.negate(self.db)), + _ => None, + } + } + fn evaluate_expr_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) { match rhs_ty { @@ -435,17 +571,8 @@ impl<'db> NarrowingConstraintsBuilder<'db> { } } ast::CmpOp::Is => Some(rhs_ty), - ast::CmpOp::NotEq => { - if rhs_ty.is_single_valued(self.db) { - let ty = IntersectionBuilder::new(self.db) - .add_negative(rhs_ty) - .build(); - Some(ty) - } else { - None - } - } - ast::CmpOp::Eq if lhs_ty.is_literal_string() => Some(rhs_ty), + ast::CmpOp::Eq => self.evaluate_expr_eq(lhs_ty, rhs_ty), + ast::CmpOp::NotEq => self.evaluate_expr_ne(lhs_ty, rhs_ty), ast::CmpOp::In => self.evaluate_expr_in(lhs_ty, rhs_ty), ast::CmpOp::NotIn => self .evaluate_expr_in(lhs_ty, rhs_ty) From 25c3be51d24e4436654baabe8f4ae79dfc31fa02 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 24 Apr 2025 08:11:45 -0700 Subject: [PATCH 0101/1161] [red-knot] simplify != narrowing (#17610) ## Summary Follow-up from review comment in https://github.com/astral-sh/ruff/pull/17567#discussion_r2058649527 ## Test Plan Existing tests. --- .../src/types/narrow.rs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index 1dfac5b0413e41..3c49626461faab 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -512,19 +512,10 @@ impl<'db> NarrowingConstraintsBuilder<'db> { None } } - (_, Type::BooleanLiteral(b)) => { - if b { - Some( - UnionType::from_elements(self.db, [rhs_ty, Type::IntLiteral(1)]) - .negate(self.db), - ) - } else { - Some( - UnionType::from_elements(self.db, [rhs_ty, Type::IntLiteral(0)]) - .negate(self.db), - ) - } - } + (_, Type::BooleanLiteral(b)) => Some( + UnionType::from_elements(self.db, [rhs_ty, Type::IntLiteral(i64::from(b))]) + .negate(self.db), + ), _ if rhs_ty.is_single_valued(self.db) => Some(rhs_ty.negate(self.db)), _ => None, } From 9a54ee3a1cb030027dc83e6257599ed06e6f28ba Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 23 Apr 2025 10:42:36 -0400 Subject: [PATCH 0102/1161] red_knot_python_semantic: add snapshot tests for unsupported boolean conversions This just captures the status quo before we try to improve them. --- .../unsupported_bool_conversion.md | 61 +++++++++++++++++++ ...l__`_attribute,_but_it's_not_callable.snap | 35 +++++++++++ ...hod,_but_has_an_incorrect_return_type.snap | 36 +++++++++++ ..._method,_but_has_incorrect_parameters.snap | 36 +++++++++++ ...ember_has_incorrect_`__bool__`_method.snap | 43 +++++++++++++ crates/red_knot_python_semantic/src/types.rs | 11 ++-- 6 files changed, 216 insertions(+), 6 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md new file mode 100644 index 00000000000000..4a0d52ce98eb74 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md @@ -0,0 +1,61 @@ + + +# Different ways that `UNSUPPORTED_BOOL_CONVERSION` can occur + +## Has a `__bool__` method, but has incorrect parameters + +```py +class NotBoolable: + def __bool__(self, foo): + return False + +a = NotBoolable() + +# error: [unsupported-bool-conversion] +10 and a and True +``` + +## Has a `__bool__` method, but has an incorrect return type + +```py +class NotBoolable: + def __bool__(self) -> str: + return "wat" + +a = NotBoolable() + +# error: [unsupported-bool-conversion] +10 and a and True +``` + +## Has a `__bool__` attribute, but it's not callable + +```py +class NotBoolable: + __bool__: int = 3 + +a = NotBoolable() + +# error: [unsupported-bool-conversion] +10 and a and True +``` + +## Part of a union where at least one member has incorrect `__bool__` method + +```py +class NotBoolable1: + def __bool__(self) -> str: + return "wat" + +class NotBoolable2: + pass + +class NotBoolable3: + __bool__: int = 3 + +def get() -> NotBoolable1 | NotBoolable2 | NotBoolable3: + return NotBoolable2() + +# error: [unsupported-bool-conversion] +10 and get() and True +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap new file mode 100644 index 00000000000000..2ef879a16f02de --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap @@ -0,0 +1,35 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unsupported_bool_conversion.md - Different ways that `UNSUPPORTED_BOOL_CONVERSION` can occur - Has a `__bool__` attribute, but it's not callable +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class NotBoolable: +2 | __bool__: int = 3 +3 | +4 | a = NotBoolable() +5 | +6 | # error: [unsupported-bool-conversion] +7 | 10 and a and True +``` + +# Diagnostics + +``` +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable + --> /src/mdtest_snippet.py:7:8 + | +6 | # error: [unsupported-bool-conversion] +7 | 10 and a and True + | ^ + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap new file mode 100644 index 00000000000000..2b69db7bae63b2 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap @@ -0,0 +1,36 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unsupported_bool_conversion.md - Different ways that `UNSUPPORTED_BOOL_CONVERSION` can occur - Has a `__bool__` method, but has an incorrect return type +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class NotBoolable: +2 | def __bool__(self) -> str: +3 | return "wat" +4 | +5 | a = NotBoolable() +6 | +7 | # error: [unsupported-bool-conversion] +8 | 10 and a and True +``` + +# Diagnostics + +``` +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; the return type of its bool method (`str`) isn't assignable to `bool + --> /src/mdtest_snippet.py:8:8 + | +7 | # error: [unsupported-bool-conversion] +8 | 10 and a and True + | ^ + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap new file mode 100644 index 00000000000000..a47dc7ee5f9c33 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap @@ -0,0 +1,36 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unsupported_bool_conversion.md - Different ways that `UNSUPPORTED_BOOL_CONVERSION` can occur - Has a `__bool__` method, but has incorrect parameters +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class NotBoolable: +2 | def __bool__(self, foo): +3 | return False +4 | +5 | a = NotBoolable() +6 | +7 | # error: [unsupported-bool-conversion] +8 | 10 and a and True +``` + +# Diagnostics + +``` +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; it incorrectly implements `__bool__` + --> /src/mdtest_snippet.py:8:8 + | +7 | # error: [unsupported-bool-conversion] +8 | 10 and a and True + | ^ + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap new file mode 100644 index 00000000000000..8d33395fd2391d --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap @@ -0,0 +1,43 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unsupported_bool_conversion.md - Different ways that `UNSUPPORTED_BOOL_CONVERSION` can occur - Part of a union where at least one member has incorrect `__bool__` method +mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | class NotBoolable1: + 2 | def __bool__(self) -> str: + 3 | return "wat" + 4 | + 5 | class NotBoolable2: + 6 | pass + 7 | + 8 | class NotBoolable3: + 9 | __bool__: int = 3 +10 | +11 | def get() -> NotBoolable1 | NotBoolable2 | NotBoolable3: +12 | return NotBoolable2() +13 | +14 | # error: [unsupported-bool-conversion] +15 | 10 and get() and True +``` + +# Diagnostics + +``` +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for union `NotBoolable1 | NotBoolable2 | NotBoolable3` because `NotBoolable1` doesn't implement `__bool__` correctly + --> /src/mdtest_snippet.py:15:8 + | +14 | # error: [unsupported-bool-conversion] +15 | 10 and get() and True + | ^^^^^ + | + +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 695353ef7bf8a0..19195f561fa06e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -5670,12 +5670,11 @@ impl<'db> BoolError<'db> { Self::IncorrectArguments { not_boolable_type, .. } => { - builder.into_diagnostic( - format_args!( - "Boolean conversion is unsupported for type `{}`; it incorrectly implements `__bool__`", - not_boolable_type.display(context.db()) - ), - ); + builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{}`; \ + it incorrectly implements `__bool__`", + not_boolable_type.display(context.db()) + )); } Self::IncorrectReturnType { not_boolable_type, From 43bd0437559a5c267b1ac8b44dfc436d7fcff3bb Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 23 Apr 2025 11:23:51 -0400 Subject: [PATCH 0103/1161] ruff_db: add a `From` impl for `FileRange` to `Span` These types are almost equivalent. The only difference is that a `Span`'s range is optional. --- crates/ruff_db/src/diagnostic/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 3478d105b5024f..02b298afbb0509 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -3,7 +3,7 @@ use std::{fmt::Formatter, sync::Arc}; use thiserror::Error; use ruff_annotate_snippets::Level as AnnotateLevel; -use ruff_text_size::TextRange; +use ruff_text_size::{Ranged, TextRange}; pub use self::render::DisplayDiagnostic; use crate::files::File; @@ -601,6 +601,12 @@ impl From for Span { } } +impl From for Span { + fn from(file_range: crate::files::FileRange) -> Span { + Span::from(file_range.file()).with_range(file_range.range()) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] pub enum Severity { Info, From a45a0a92bd1a9cea2a48e6c00c44c206e56da6b5 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 23 Apr 2025 11:23:00 -0400 Subject: [PATCH 0104/1161] red_knot_python_semantic: move parameter span helper method I wanted to use this method in other places, so I moved it to what appears to be a God-type. I also made it slightly more versatile: callers can ask for the entire parameter list by omitting a specific parameter index. --- crates/red_knot_python_semantic/src/types.rs | 54 +++++++++++++++++++ .../src/types/call/bind.rs | 46 +--------------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 19195f561fa06e..784eb643c1909d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4903,6 +4903,60 @@ impl<'db> Type<'db> { | Self::AlwaysFalsy => None, } } + + /// Returns a tuple of two spans. The first is + /// the span for the identifier of the function + /// definition for `self`. The second is + /// the span for the parameter in the function + /// definition for `self`. + /// + /// If there are no meaningful spans, then this + /// returns `None`. For example, when this type + /// isn't callable. + /// + /// When `parameter_index` is `None`, then the + /// second span returned covers the entire parameter + /// list. + /// + /// # Performance + /// + /// Note that this may introduce cross-module + /// dependencies. This can have an impact on + /// the effectiveness of incremental caching + /// and should therefore be used judiciously. + /// + /// An example of a good use case is to improve + /// a diagnostic. + fn parameter_span( + &self, + db: &'db dyn Db, + parameter_index: Option, + ) -> Option<(Span, Span)> { + match *self { + Type::FunctionLiteral(function) => { + let function_scope = function.body_scope(db); + let span = Span::from(function_scope.file(db)); + let node = function_scope.node(db); + let func_def = node.as_function()?; + let range = parameter_index + .and_then(|parameter_index| { + func_def + .parameters + .iter() + .nth(parameter_index) + .map(|param| param.range()) + }) + .unwrap_or(func_def.parameters.range); + let name_span = span.clone().with_range(func_def.name.range); + let parameter_span = span.with_range(range); + Some((name_span, parameter_span)) + } + Type::BoundMethod(bound_method) => { + Type::FunctionLiteral(bound_method.function(db)).parameter_span(db, parameter_index) + } + _ => None, + } + } } impl<'db> From<&Type<'db>> for Type<'db> { diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 93217b08693735..3e519a6a835e6f 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -23,9 +23,8 @@ use crate::types::{ KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, TupleType, UnionType, WrapperDescriptorKind, }; -use ruff_db::diagnostic::{Annotation, Severity, Span, SubDiagnostic}; +use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic}; use ruff_python_ast as ast; -use ruff_text_size::Ranged; /// Binding information for a possible union of callables. At a call site, the arguments must be /// compatible with _all_ of the types in the union for the call to be valid. @@ -1386,47 +1385,6 @@ pub(crate) enum BindingError<'db> { } impl<'db> BindingError<'db> { - /// Returns a tuple of two spans. The first is - /// the span for the identifier of the function - /// definition for `callable_ty`. The second is - /// the span for the parameter in the function - /// definition for `callable_ty`. - /// - /// If there are no meaningful spans, then this - /// returns `None`. - fn parameter_span_from_index( - db: &'db dyn Db, - callable_ty: Type<'db>, - parameter_index: usize, - ) -> Option<(Span, Span)> { - match callable_ty { - Type::FunctionLiteral(function) => { - let function_scope = function.body_scope(db); - let span = Span::from(function_scope.file(db)); - let node = function_scope.node(db); - if let Some(func_def) = node.as_function() { - let range = func_def - .parameters - .iter() - .nth(parameter_index) - .map(|param| param.range()) - .unwrap_or(func_def.parameters.range); - let name_span = span.clone().with_range(func_def.name.range); - let parameter_span = span.with_range(range); - Some((name_span, parameter_span)) - } else { - None - } - } - Type::BoundMethod(bound_method) => Self::parameter_span_from_index( - db, - Type::FunctionLiteral(bound_method.function(db)), - parameter_index, - ), - _ => None, - } - } - pub(super) fn report_diagnostic( &self, context: &InferContext<'db>, @@ -1454,7 +1412,7 @@ impl<'db> BindingError<'db> { "Expected `{expected_ty_display}`, found `{provided_ty_display}`" )); if let Some((name_span, parameter_span)) = - Self::parameter_span_from_index(context.db(), callable_ty, parameter.index) + callable_ty.parameter_span(context.db(), Some(parameter.index)) { let mut sub = SubDiagnostic::new(Severity::Info, "Function defined here"); sub.annotate(Annotation::primary(name_span)); From eb1d2518c131e31ab1a1faee9061f79ed23b3eff Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 23 Apr 2025 11:55:53 -0400 Subject: [PATCH 0105/1161] red_knot_python_semantic: add "return type span" helper method This is very similar to querying for the span of a parameter in a function definition, but instead we look for the span of a return type. --- crates/red_knot_python_semantic/src/types.rs | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 784eb643c1909d..48792f95ca6972 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4904,6 +4904,45 @@ impl<'db> Type<'db> { } } + /// Returns a tuple of two spans. The first is + /// the span for the identifier of the function + /// definition for `self`. The second is + /// the span for the return type in the function + /// definition for `self`. + /// + /// If there are no meaningful spans, then this + /// returns `None`. For example, when this type + /// isn't callable or if the function has no + /// declared return type. + /// + /// # Performance + /// + /// Note that this may introduce cross-module + /// dependencies. This can have an impact on + /// the effectiveness of incremental caching + /// and should therefore be used judiciously. + /// + /// An example of a good use case is to improve + /// a diagnostic. + fn return_type_span(&self, db: &'db dyn Db) -> Option<(Span, Span)> { + match *self { + Type::FunctionLiteral(function) => { + let function_scope = function.body_scope(db); + let span = Span::from(function_scope.file(db)); + let node = function_scope.node(db); + let func_def = node.as_function()?; + let return_type_range = func_def.returns.as_ref()?.range(); + let name_span = span.clone().with_range(func_def.name.range); + let return_type_span = span.with_range(return_type_range); + Some((name_span, return_type_span)) + } + Type::BoundMethod(bound_method) => { + Type::FunctionLiteral(bound_method.function(db)).return_type_span(db) + } + _ => None, + } + } + /// Returns a tuple of two spans. The first is /// the span for the identifier of the function /// definition for `self`. The second is From 0f4781076864e60db0dabd52c8e0cd8955b7e2a9 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 23 Apr 2025 12:28:49 -0400 Subject: [PATCH 0106/1161] red_knot_python_semantic: improve diagnostics for unsupported boolean conversions This mostly only improves things for incorrect arguments and for an incorrect return type. It doesn't do much to improve the case where `__bool__` isn't callable and leaves the union/other cases untouched completely. I picked this one because, at first glance, this _looked_ like a lower hanging fruit. The conceptual improvement here is pretty straight-forward: add annotations for relevant data. But it took me a bit to figure out how to connect all of the pieces. --- .../mdtest/conditional/if_expression.md | 2 +- .../mdtest/conditional/if_statement.md | 4 +- .../resources/mdtest/conditional/match.md | 2 +- .../resources/mdtest/expression/assert.md | 2 +- .../resources/mdtest/expression/boolean.md | 6 +- .../resources/mdtest/loops/while_loop.md | 2 +- .../resources/mdtest/narrow/truthiness.md | 2 +- ...types_with_invalid_`__bool__`_methods.snap | 3 +- ...oesn't_implement_`__bool__`_correctly.snap | 6 +- ...hat_implements_`__bool__`_incorrectly.snap | 3 +- ..._don't_implement_`__bool__`_correctly.snap | 6 +- ...that_incorrectly_implement_`__bool__`.snap | 3 +- ...that_incorrectly_implement_`__bool__`.snap | 3 +- ...l__`_attribute,_but_it's_not_callable.snap | 3 +- ...hod,_but_has_an_incorrect_return_type.snap | 12 +++- ..._method,_but_has_incorrect_parameters.snap | 12 +++- .../resources/mdtest/type_api.md | 2 +- .../resources/mdtest/unary/not.md | 2 +- crates/red_knot_python_semantic/src/types.rs | 67 +++++++++++++++---- 19 files changed, 107 insertions(+), 35 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md index b14d358ea04bd7..48b912cce13f9d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md @@ -42,6 +42,6 @@ def _(flag: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" 3 if NotBoolable() else 4 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md index 9a3fc4f8f4c897..c7a8c7732b4dd3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md @@ -154,10 +154,10 @@ def _(flag: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" if NotBoolable(): ... -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" elif NotBoolable(): ... ``` 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 8b8a3dca342351..4947de76b9f9d6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md @@ -292,7 +292,7 @@ class NotBoolable: def _(target: int, flag: NotBoolable): y = 1 match target: - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" + # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" case 1 if flag: y = 2 case 2: diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md b/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md index 54073f9170fe33..ddb429a576e099 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md @@ -4,6 +4,6 @@ class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" assert NotBoolable() ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md index 160189ef0c2aa8..7a9f25f637403c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md @@ -123,7 +123,7 @@ if NotBoolable(): class NotBoolable: __bool__: None = None -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" if NotBoolable(): ... ``` @@ -135,7 +135,7 @@ def test(cond: bool): class NotBoolable: __bool__: int | None = None if cond else 3 - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" + # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" if NotBoolable(): ... ``` @@ -149,7 +149,7 @@ def test(cond: bool): a = 10 if cond else NotBoolable() - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`; its `__bool__` method isn't callable" + # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`" if a: ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md b/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md index 397a06b742dacf..5a5784b85daf10 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md @@ -123,7 +123,7 @@ def _(flag: bool, flag2: bool): class NotBoolable: __bool__: int = 3 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`" while NotBoolable(): ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md index d9bf54e8f8b1f0..d6a8684b38f4f8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md @@ -270,7 +270,7 @@ def _( if af: reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy - # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`; the return type of its bool method (`MetaAmbiguous`) isn't assignable to `bool" + # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`" if d: # TODO: Should be `Unknown` reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap index df0e16c766752c..9366971e470be1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap @@ -24,12 +24,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.m # Diagnostics ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` --> /src/mdtest_snippet.py:7:8 | 6 | # error: [unsupported-bool-conversion] 7 | 10 and a and True | ^ | +info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap index 4412f143f79efc..6dc5ab6afd4349 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap @@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc # Diagnostics ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` --> /src/mdtest_snippet.py:9:1 | 8 | # error: [unsupported-bool-conversion] @@ -37,11 +37,12 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t 10 | # error: [unsupported-bool-conversion] 11 | 10 not in WithContains() | +info: `__bool__` on `NotBoolable` must be callable ``` ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` --> /src/mdtest_snippet.py:11:1 | 9 | 10 in WithContains() @@ -49,5 +50,6 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t 11 | 10 not in WithContains() | ^^^^^^^^^^^^^^^^^^^^^^^^ | +info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap index b02543d3a506ca..21d48d1a7a0aaa 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap @@ -22,12 +22,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md # Diagnostics ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` --> /src/mdtest_snippet.py:5:1 | 4 | # error: [unsupported-bool-conversion] 5 | not NotBoolable() | ^^^^^^^^^^^^^^^^^ | +info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap index ff2b61237cb3ed..baf8e5b0e07258 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap @@ -33,7 +33,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc # Diagnostics ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` --> /src/mdtest_snippet.py:12:1 | 11 | # error: [unsupported-bool-conversion] @@ -42,11 +42,12 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t 13 | # error: [unsupported-bool-conversion] 14 | 10 < Comparable() < Comparable() | +info: `__bool__` on `NotBoolable` must be callable ``` ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` --> /src/mdtest_snippet.py:14:1 | 12 | 10 < Comparable() < 20 @@ -56,5 +57,6 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t 15 | 16 | Comparable() < Comparable() # fine | +info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap index f7ad96efef5d32..db99489b41552b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap @@ -34,7 +34,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples. # Diagnostics ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable | Literal[False]` --> /src/mdtest_snippet.py:15:1 | 14 | # error: [unsupported-bool-conversion] @@ -43,5 +43,6 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t 16 | 17 | a < b # fine | +info: `__bool__` on `NotBoolable | Literal[False]` must be callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap index 99de8491fc0787..972e7ed6d80c29 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap @@ -26,12 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples. # Diagnostics ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` --> /src/mdtest_snippet.py:9:1 | 8 | # error: [unsupported-bool-conversion] 9 | (A(),) == (A(),) | ^^^^^^^^^^^^^^^^ | +info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap index 2ef879a16f02de..d239ebaaea5b04 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap @@ -24,12 +24,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupp # Diagnostics ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` --> /src/mdtest_snippet.py:7:8 | 6 | # error: [unsupported-bool-conversion] 7 | 10 and a and True | ^ | +info: `__bool__` must be callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap index 2b69db7bae63b2..7cbee215c3da3e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap @@ -25,12 +25,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupp # Diagnostics ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; the return type of its bool method (`str`) isn't assignable to `bool +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` --> /src/mdtest_snippet.py:8:8 | 7 | # error: [unsupported-bool-conversion] 8 | 10 and a and True | ^ | +info: `str` is not assignable to `bool` + --> /src/mdtest_snippet.py:2:9 + | +1 | class NotBoolable: +2 | def __bool__(self) -> str: + | -------- ^^^ Incorrect return type + | | + | Method defined here +3 | return "wat" + | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap index a47dc7ee5f9c33..6a2b93d51efc97 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap @@ -25,12 +25,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupp # Diagnostics ``` -error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; it incorrectly implements `__bool__` +error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` --> /src/mdtest_snippet.py:8:8 | 7 | # error: [unsupported-bool-conversion] 8 | 10 and a and True | ^ | +info: `__bool__` methods must only have a `self` parameter + --> /src/mdtest_snippet.py:2:9 + | +1 | class NotBoolable: +2 | def __bool__(self, foo): + | --------^^^^^^^^^^^ Incorrect parameters + | | + | Method defined here +3 | return False + | ``` 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 c76f0f13740a3f..c767fe98a0008f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_api.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_api.md @@ -235,7 +235,7 @@ class InvalidBoolDunder: def __bool__(self) -> int: return 1 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`; the return type of its bool method (`int`) isn't assignable to `bool" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`" static_assert(InvalidBoolDunder()) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md b/crates/red_knot_python_semantic/resources/mdtest/unary/not.md index 82f589517af48e..e01796a9f7506e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unary/not.md @@ -187,7 +187,7 @@ class MethodBoolInvalid: def __bool__(self) -> int: return 0 -# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MethodBoolInvalid`; the return type of its bool method (`int`) isn't assignable to `bool" +# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MethodBoolInvalid`" # revealed: bool reveal_type(not MethodBoolInvalid()) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 48792f95ca6972..4297b552212db6 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -10,7 +10,9 @@ use diagnostic::{ CALL_POSSIBLY_UNBOUND_METHOD, INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, }; -use ruff_db::diagnostic::create_semantic_syntax_diagnostic; +use ruff_db::diagnostic::{ + create_semantic_syntax_diagnostic, Annotation, Severity, Span, SubDiagnostic, +}; use ruff_db::files::{File, FileRange}; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, AnyNodeRef}; @@ -5763,30 +5765,71 @@ impl<'db> BoolError<'db> { Self::IncorrectArguments { not_boolable_type, .. } => { - builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{}`; \ - it incorrectly implements `__bool__`", + let mut diag = builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{}`", not_boolable_type.display(context.db()) )); + let mut sub = SubDiagnostic::new( + Severity::Info, + "`__bool__` methods must only have a `self` parameter", + ); + if let Some((func_span, parameter_span)) = not_boolable_type + .member(context.db(), "__bool__") + .into_lookup_result() + .ok() + .and_then(|quals| quals.inner_type().parameter_span(context.db(), None)) + { + sub.annotate( + Annotation::primary(parameter_span).message("Incorrect parameters"), + ); + sub.annotate(Annotation::secondary(func_span).message("Method defined here")); + } + diag.sub(sub); } Self::IncorrectReturnType { not_boolable_type, return_type, } => { - builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{not_boolable}`; \ - the return type of its bool method (`{return_type}`) \ - isn't assignable to `bool", + let mut diag = builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{not_boolable}`", not_boolable = not_boolable_type.display(context.db()), - return_type = return_type.display(context.db()) )); + let mut sub = SubDiagnostic::new( + Severity::Info, + format_args!( + "`{return_type}` is not assignable to `bool`", + return_type = return_type.display(context.db()), + ), + ); + if let Some((func_span, return_type_span)) = not_boolable_type + .member(context.db(), "__bool__") + .into_lookup_result() + .ok() + .and_then(|quals| quals.inner_type().return_type_span(context.db())) + { + sub.annotate( + Annotation::primary(return_type_span).message("Incorrect return type"), + ); + sub.annotate(Annotation::secondary(func_span).message("Method defined here")); + } + diag.sub(sub); } Self::NotCallable { not_boolable_type } => { - builder.into_diagnostic(format_args!( - "Boolean conversion is unsupported for type `{}`; \ - its `__bool__` method isn't callable", + let mut diag = builder.into_diagnostic(format_args!( + "Boolean conversion is unsupported for type `{}`", not_boolable_type.display(context.db()) )); + let sub = SubDiagnostic::new( + Severity::Info, + format_args!( + "`__bool__` on `{}` must be callable", + not_boolable_type.display(context.db()) + ), + ); + // TODO: It would be nice to create an annotation here for + // where `__bool__` is defined. At time of writing, I couldn't + // figure out a straight-forward way of doing this. ---AG + diag.sub(sub); } Self::Union { union, .. } => { let first_error = union From 8d2c79276d167fcfcf9143a2bc1b328bb9d0f876 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Thu, 24 Apr 2025 09:20:21 -0400 Subject: [PATCH 0107/1161] red_knot_python_semantic: avoid Rust's screaming snake case convention in mdtest --- .../mdtest/diagnostics/unsupported_bool_conversion.md | 2 +- ..._-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap} | 4 ++-- ..._`__bool__`_method,_but_has_an_incorrect_return_type.snap} | 2 +- ...as_a_`__bool__`_method,_but_has_incorrect_parameters.snap} | 2 +- ..._at_least_one_member_has_incorrect_`__bool__`_method.snap} | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename crates/red_knot_python_semantic/resources/mdtest/snapshots/{unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap => unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap} (86%) rename crates/red_knot_python_semantic/resources/mdtest/snapshots/{unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap => unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap} (94%) rename crates/red_knot_python_semantic/resources/mdtest/snapshots/{unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap => unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap} (94%) rename crates/red_knot_python_semantic/resources/mdtest/snapshots/{unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap => unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap} (94%) diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md index 4a0d52ce98eb74..9cb91b89fdaff2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md @@ -1,6 +1,6 @@ -# Different ways that `UNSUPPORTED_BOOL_CONVERSION` can occur +# Different ways that `unsupported-bool-conversion` can occur ## Has a `__bool__` method, but has incorrect parameters diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap rename to crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap index d239ebaaea5b04..ca93c74cd0449a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap @@ -3,7 +3,7 @@ source: crates/red_knot_test/src/lib.rs expression: snapshot --- --- -mdtest name: unsupported_bool_conversion.md - Different ways that `UNSUPPORTED_BOOL_CONVERSION` can occur - Has a `__bool__` attribute, but it's not callable +mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Has a `__bool__` attribute, but it's not callable mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md --- @@ -31,6 +31,6 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t 7 | 10 and a and True | ^ | -info: `__bool__` must be callable +info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap rename to crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap index 7cbee215c3da3e..5db16801026396 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap @@ -3,7 +3,7 @@ source: crates/red_knot_test/src/lib.rs expression: snapshot --- --- -mdtest name: unsupported_bool_conversion.md - Different ways that `UNSUPPORTED_BOOL_CONVERSION` can occur - Has a `__bool__` method, but has an incorrect return type +mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Has a `__bool__` method, but has an incorrect return type mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md --- diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap rename to crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap index 6a2b93d51efc97..89c05b6193a58f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap @@ -3,7 +3,7 @@ source: crates/red_knot_test/src/lib.rs expression: snapshot --- --- -mdtest name: unsupported_bool_conversion.md - Different ways that `UNSUPPORTED_BOOL_CONVERSION` can occur - Has a `__bool__` method, but has incorrect parameters +mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Has a `__bool__` method, but has incorrect parameters mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md --- diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap rename to crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap index 8d33395fd2391d..113522db4985a9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap @@ -3,7 +3,7 @@ source: crates/red_knot_test/src/lib.rs expression: snapshot --- --- -mdtest name: unsupported_bool_conversion.md - Different ways that `UNSUPPORTED_BOOL_CONVERSION` can occur - Part of a union where at least one member has incorrect `__bool__` method +mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Part of a union where at least one member has incorrect `__bool__` method mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md --- From 99370647615c853e1fdd5bebb3fdff221a826d15 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 24 Apr 2025 22:23:50 +0530 Subject: [PATCH 0108/1161] [red-knot] Use iterative approach to collect overloads (#17607) ## Summary This PR updates the `to_overloaded` method to use an iterative approach instead of a recursive one. Refer to https://github.com/astral-sh/ruff/pull/17585#discussion_r2056804587 for context. The main benefit here is that it avoids calling the `to_overloaded` function in a recursive manner which is a salsa query. So, this is a bit hand wavy but we should also see less memory used because the cache will only contain a single entry which should be the entire overload chain. Previously, the recursive approach would mean that each of the function involved in an overload chain would have a cache entry. This reduce in memory shouldn't be too much and I haven't looked at the actual data for it. ## Test Plan Existing test cases should pass. --- crates/red_knot_python_semantic/src/types.rs | 96 +++++++++----------- 1 file changed, 43 insertions(+), 53 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 4297b552212db6..44ce5f1561f08e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -6308,64 +6308,54 @@ impl<'db> FunctionType<'db> { db: &'db dyn Db, function: FunctionType<'db>, ) -> Option> { - // The semantic model records a use for each function on the name node. This is used here - // to get the previous function definition with the same name. - let scope = function.definition(db).scope(db); - let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db)); - let use_id = function - .body_scope(db) - .node(db) - .expect_function() - .name - .scoped_use_id(db, scope); - - if let Symbol::Type(Type::FunctionLiteral(function_literal), Boundness::Bound) = - symbol_from_bindings(db, use_def.bindings_at_use(use_id)) - { - match function_literal.to_overloaded(db) { - None => { - debug_assert!( - !function_literal.has_known_decorator(db, FunctionDecorators::OVERLOAD), - "Expected `Some(OverloadedFunction)` if the previous function was an overload" - ); - } - Some(OverloadedFunction { - implementation: Some(_), - .. - }) => { - // If the previous overloaded function already has an implementation, then this - // new signature completely replaces it. - } - Some(OverloadedFunction { - overloads, - implementation: None, - }) => { - return Some( - if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) { - let mut overloads = overloads.clone(); - overloads.push(function); - OverloadedFunction { - overloads, - implementation: None, - } - } else { - OverloadedFunction { - overloads: overloads.clone(), - implementation: Some(function), - } - }, - ); - } + let mut current = function; + let mut overloads = vec![]; + + loop { + // The semantic model records a use for each function on the name node. This is used + // here to get the previous function definition with the same name. + let scope = current.definition(db).scope(db); + let use_def = + semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db)); + let use_id = current + .body_scope(db) + .node(db) + .expect_function() + .name + .scoped_use_id(db, scope); + + let Symbol::Type(Type::FunctionLiteral(previous), Boundness::Bound) = + symbol_from_bindings(db, use_def.bindings_at_use(use_id)) + else { + break; + }; + + if previous.has_known_decorator(db, FunctionDecorators::OVERLOAD) { + overloads.push(previous); + } else { + break; } + + current = previous; } - if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) { - Some(OverloadedFunction { - overloads: vec![function], - implementation: None, - }) + // Overloads are inserted in reverse order, from bottom to top. + overloads.reverse(); + + let implementation = if function.has_known_decorator(db, FunctionDecorators::OVERLOAD) { + overloads.push(function); + None } else { + Some(function) + }; + + if overloads.is_empty() { None + } else { + Some(OverloadedFunction { + overloads, + implementation, + }) } } From f7b48510b58026f73c153ecb57720754365ba92e Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 24 Apr 2025 13:06:38 -0500 Subject: [PATCH 0109/1161] Bump 0.11.7 (#17613) --- CHANGELOG.md | 25 +++++++++++++++++++++++++ Cargo.lock | 6 +++--- README.md | 6 +++--- crates/ruff/Cargo.toml | 2 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_wasm/Cargo.toml | 2 +- docs/integrations.md | 8 ++++---- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- 10 files changed, 41 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e57f3f6e0569..5e5ddb546035fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 0.11.7 + +### Preview features + +- \[`airflow`\] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR301`) ([#17355](https://github.com/astral-sh/ruff/pull/17355)) +- \[`perflint`\] Implement fix for `manual-dict-comprehension` (`PERF403`) ([#16719](https://github.com/astral-sh/ruff/pull/16719)) +- [syntax-errors] Make duplicate parameter names a semantic error ([#17131](https://github.com/astral-sh/ruff/pull/17131)) + +### Bug fixes + +- \[`airflow`\] Fix typos in provider package names (`AIR302`, `AIR312`) ([#17574](https://github.com/astral-sh/ruff/pull/17574)) +- \[`flake8-type-checking`\] Visit keyword arguments in checks involving `typing.cast`/`typing.NewType` arguments ([#17538](https://github.com/astral-sh/ruff/pull/17538)) +- \[`pyupgrade`\] Preserve parenthesis when fixing native literals containing newlines (`UP018`) ([#17220](https://github.com/astral-sh/ruff/pull/17220)) +- \[`refurb`\] Mark the `FURB161` fix unsafe except for integers and booleans ([#17240](https://github.com/astral-sh/ruff/pull/17240)) + +### Rule changes + +- \[`perflint`\] Allow list function calls to be replaced with a comprehension (`PERF401`) ([#17519](https://github.com/astral-sh/ruff/pull/17519)) +- \[`pycodestyle`\] Auto-fix redundant boolean comparison (`E712`) ([#17090](https://github.com/astral-sh/ruff/pull/17090)) +- \[`pylint`\] make fix unsafe if delete comments (`PLR1730`) ([#17459](https://github.com/astral-sh/ruff/pull/17459)) + +### Documentation + +- Add fix safety sections to docs for several rules ([#17410](https://github.com/astral-sh/ruff/pull/17410),[#17440](https://github.com/astral-sh/ruff/pull/17440),[#17441](https://github.com/astral-sh/ruff/pull/17441),[#17443](https://github.com/astral-sh/ruff/pull/17443),[#17444](https://github.com/astral-sh/ruff/pull/17444)) + ## 0.11.6 ### Preview features diff --git a/Cargo.lock b/Cargo.lock index aa0234297c0ce3..941984daf80268 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2772,7 +2772,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.11.6" +version = "0.11.7" dependencies = [ "anyhow", "argfile", @@ -3007,7 +3007,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.11.6" +version = "0.11.7" dependencies = [ "aho-corasick", "anyhow", @@ -3333,7 +3333,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.11.6" +version = "0.11.7" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/README.md b/README.md index 3d82736d8148d8..7978b3abfc3c7b 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" # For a specific version. -curl -LsSf https://astral.sh/ruff/0.11.6/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.11.6/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.11.7/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.11.7/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.11.7 hooks: # Run the linter. - id: ruff diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 96db8ece8f2e1c..ced197e12399c8 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.11.6" +version = "0.11.7" publish = true authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 1cc1fbe68016a2..63c6455ae6e8c2 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.11.6" +version = "0.11.7" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index 0bfc980a903001..bfe2823928a0e6 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.11.6" +version = "0.11.7" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/integrations.md b/docs/integrations.md index fe5cbf528690cc..77b379996c456f 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma stage: build interruptible: true image: - name: ghcr.io/astral-sh/ruff:0.11.6-alpine + name: ghcr.io/astral-sh/ruff:0.11.7-alpine before_script: - cd $CI_PROJECT_DIR - ruff --version @@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.11.7 hooks: # Run the linter. - id: ruff @@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.11.7 hooks: # Run the linter. - id: ruff @@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.11.7 hooks: # Run the linter. - id: ruff diff --git a/docs/tutorial.md b/docs/tutorial.md index 531d3c132a8842..33538951696666 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.11.7 hooks: # Run the linter. - id: ruff diff --git a/pyproject.toml b/pyproject.toml index 5b8626e935f3b5..8b8011be234c85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.11.6" +version = "0.11.7" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 1670ea1e767f21..68b6acf4c2e88e 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.11.6" +version = "0.11.7" description = "" authors = ["Charles Marsh "] From 92ecfc908be2ed0d61477ddecf1d51608d3b35d7 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:45:54 -0400 Subject: [PATCH 0110/1161] [syntax-errors] Make `async-comprehension-in-sync-comprehension` more specific (#17460) ## Summary While adding semantic error support to red-knot, I noticed duplicate diagnostics for code like this: ```py # error: [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.9 (syntax was added in 3.11)" # error: [invalid-syntax] "`asynchronous comprehension` outside of an asynchronous function" [reveal_type(x) async for x in AsyncIterable()] ``` Beyond the duplication, the first error message doesn't make much sense because this syntax is _not_ allowed on Python 3.11 either. To fix this, this PR renames the `async-comprehension-outside-async-function` semantic syntax error to `async-comprehension-in-sync-comprehension` and fixes the rule to avoid applying outside of sync comprehensions at all. ## Test Plan New linter test demonstrating the false positive. The mdtests from my red-knot PR also reflect this change. --- .../diagnostics/semantic_syntax_errors.md | 2 +- ...nchronous_comprehensions_-_Python_3.10.snap | 6 +++--- crates/ruff_linter/src/checkers/ast/mod.rs | 2 +- crates/ruff_linter/src/linter.rs | 1 + ...n_sync_comprehension_error_on_310_3.10.snap | 2 +- ...sync_comprehension_false_positive_3.10.snap | 3 +++ ...on_in_sync_comprehension_notebook_3.10.snap | 2 +- .../ruff_python_parser/src/semantic_errors.rs | 18 +++++++++--------- ...ax@nested_async_comprehension_py310.py.snap | 10 +++++----- 9 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md index 7dfabdef84bc8d..4c755ac6ccfa77 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -19,7 +19,7 @@ async def elements(n): yield n async def f(): - # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11)" + # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)" return {n: [x async for x in elements(n)] for n in range(3)} ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap index d3ec566124a9c7..974595ccfa42fa 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap @@ -16,7 +16,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/semant 2 | yield n 3 | 4 | async def f(): - 5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11)" + 5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)" 6 | return {n: [x async for x in elements(n)] for n in range(3)} 7 | async def test(): 8 | return [[x async for x in elements(n)] async for n in range(3)] @@ -36,9 +36,9 @@ error: invalid-syntax --> /src/mdtest_snippet.py:6:19 | 4 | async def f(): -5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax... +5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (synt... 6 | return {n: [x async for x in elements(n)] for n in range(3)} - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) 7 | async def test(): 8 | return [[x async for x in elements(n)] async for n in range(3)] | diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 87169d7a910285..7178db784f94ac 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -615,7 +615,7 @@ impl SemanticSyntaxContext for Checker<'_> { | SemanticSyntaxErrorKind::DuplicateMatchKey(_) | SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_) | SemanticSyntaxErrorKind::InvalidStarExpression - | SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_) + | SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_) | SemanticSyntaxErrorKind::DuplicateParameter(_) => { if self.settings.preview.is_enabled() { self.semantic_errors.borrow_mut().push(error); diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index b266a82e54198b..4af0010c3879c3 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1022,6 +1022,7 @@ mod tests { ", PythonVersion::PY310 )] + #[test_case("false_positive", "[x async for x in y]", PythonVersion::PY310)] fn test_async_comprehension_in_sync_comprehension( name: &str, contents: &str, diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap index 86380f014a57fe..9fd781f021060a 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/linter.rs --- -:1:27: SyntaxError: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) +:1:27: SyntaxError: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) | 1 | async def f(): return [[x async for x in foo(n)] for n in range(3)] | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap new file mode 100644 index 00000000000000..8d0a8faf7ae2a7 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap @@ -0,0 +1,3 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap index d55d10cfc65947..c573573f7063c2 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_notebook_3.10.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/linter.rs --- -resources/test/fixtures/syntax_errors/async_comprehension.ipynb:3:5: SyntaxError: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) +resources/test/fixtures/syntax_errors/async_comprehension.ipynb:3:5: SyntaxError: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) | 1 | async def elements(n): yield n 2 | [x async for x in elements(5)] # okay, async at top level diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index fd41ea21879f9e..a12b61cb06db7d 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -573,7 +573,7 @@ impl SemanticSyntaxChecker { elt, generators, .. }) => { Self::check_generator_expr(elt, generators, ctx); - Self::async_comprehension_outside_async_function(ctx, generators); + Self::async_comprehension_in_sync_comprehension(ctx, generators); for generator in generators.iter().filter(|g| g.is_async) { Self::await_outside_async_function( ctx, @@ -590,7 +590,7 @@ impl SemanticSyntaxChecker { }) => { Self::check_generator_expr(key, generators, ctx); Self::check_generator_expr(value, generators, ctx); - Self::async_comprehension_outside_async_function(ctx, generators); + Self::async_comprehension_in_sync_comprehension(ctx, generators); for generator in generators.iter().filter(|g| g.is_async) { Self::await_outside_async_function( ctx, @@ -801,7 +801,7 @@ impl SemanticSyntaxChecker { } } - fn async_comprehension_outside_async_function( + fn async_comprehension_in_sync_comprehension( ctx: &Ctx, generators: &[ast::Comprehension], ) { @@ -813,7 +813,7 @@ impl SemanticSyntaxChecker { if ctx.in_notebook() && ctx.in_module_scope() { return; } - if ctx.in_async_context() && !ctx.in_sync_comprehension() { + if !ctx.in_sync_comprehension() { return; } for generator in generators.iter().filter(|gen| gen.is_async) { @@ -845,7 +845,7 @@ impl SemanticSyntaxChecker { // async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] Self::add_error( ctx, - SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(python_version), + SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version), generator.range, ); } @@ -914,11 +914,11 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::InvalidStarExpression => { f.write_str("can't use starred expression here") } - SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(python_version) => { + SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version) => { write!( f, - "cannot use an asynchronous comprehension outside of an asynchronous \ - function on Python {python_version} (syntax was added in 3.11)", + "cannot use an asynchronous comprehension inside of a synchronous comprehension \ + on Python {python_version} (syntax was added in 3.11)", ) } SemanticSyntaxErrorKind::YieldOutsideFunction(kind) => { @@ -1187,7 +1187,7 @@ pub enum SemanticSyntaxErrorKind { /// This was discussed in [BPO 33346] and fixed in Python 3.11. /// /// [BPO 33346]: https://github.com/python/cpython/issues/77527 - AsyncComprehensionOutsideAsyncFunction(PythonVersion), + AsyncComprehensionInSyncComprehension(PythonVersion), /// Represents the use of `yield`, `yield from`, or `await` outside of a function scope. /// diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap index 465ef924e78d6b..cec50a8151184e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_async_comprehension_py310.py.snap @@ -780,7 +780,7 @@ Module( | 1 | # parse_options: {"target-version": "3.10"} 2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list - | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set | @@ -790,7 +790,7 @@ Module( 1 | # parse_options: {"target-version": "3.10"} 2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict - | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] | @@ -800,7 +800,7 @@ Module( 2 | async def f(): return [[x async for x in foo(n)] for n in range(3)] # list 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set - | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] 6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] | @@ -810,7 +810,7 @@ Module( 3 | async def g(): return [{x: 1 async for x in foo(n)} for n in range(3)] # dict 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] - | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) 6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] | @@ -819,5 +819,5 @@ Module( 4 | async def h(): return [{x async for x in foo(n)} for n in range(3)] # set 5 | async def i(): return [([y async for y in range(1)], [z for z in range(2)]) for x in range(5)] 6 | async def j(): return [([y for y in range(1)], [z async for z in range(2)]) for x in range(5)] - | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11) + | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) | From 538393d1f37e2340c80d7f462f939f6c4b22bcd1 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 25 Apr 2025 04:48:54 +0900 Subject: [PATCH 0111/1161] [`airflow`] Apply auto fix to cases where name has been changed in Airflow 3 (`AIR311`) (#17571) ## Summary Apply auto fix to cases where the name has been changed in Airflow 3 (`AIR311`) ## Test Plan The test features has been updated --- .../airflow/rules/suggested_to_update_3_0.rs | 46 ++++++--- ...irflow__tests__AIR311_AIR311_names.py.snap | 99 ++++++++++++++++++- 2 files changed, 126 insertions(+), 19 deletions(-) diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs index 9d500df6601114..097ef27bb01980 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs @@ -194,9 +194,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { let replacement = match qualified_name.segments() { // airflow.datasets.metadata - ["airflow", "datasets", "metadata", "Metadata"] => { - Replacement::Name("airflow.sdk.Metadata") - } + ["airflow", "datasets", "metadata", "Metadata"] => Replacement::AutoImport { + module: "airflow.sdk", + name: "Metadata", + }, // airflow.datasets ["airflow", "Dataset"] | ["airflow", "datasets", "Dataset"] => Replacement::AutoImport { module: "airflow.sdk", @@ -204,10 +205,22 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { }, ["airflow", "datasets", rest] => match *rest { "DatasetAliasEvent" => Replacement::None, - "DatasetAlias" => Replacement::Name("airflow.sdk.AssetAlias"), - "DatasetAll" => Replacement::Name("airflow.sdk.AssetAll"), - "DatasetAny" => Replacement::Name("airflow.sdk.AssetAny"), - "expand_alias_to_datasets" => Replacement::Name("airflow.sdk.expand_alias_to_assets"), + "DatasetAlias" => Replacement::AutoImport { + module: "airflow.sdk", + name: "AssetAlias", + }, + "DatasetAll" => Replacement::AutoImport { + module: "airflow.sdk", + name: "AssetAll", + }, + "DatasetAny" => Replacement::AutoImport { + module: "airflow.sdk", + name: "AssetAny", + }, + "expand_alias_to_datasets" => Replacement::AutoImport { + module: "airflow.sdk", + name: "expand_alias_to_assets", + }, _ => return, }, @@ -235,9 +248,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { module: "airflow.sdk", name: (*rest).to_string(), }, - "BaseOperatorLink" => { - Replacement::Name("airflow.sdk.definitions.baseoperatorlink.BaseOperatorLink") - } + "BaseOperatorLink" => Replacement::AutoImport { + module: "airflow.sdk.definitions.baseoperatorlink", + name: "BaseOperatorLink", + }, _ => return, }, // airflow.model..DAG @@ -246,12 +260,16 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { name: "DAG".to_string(), }, // airflow.timetables - ["airflow", "timetables", "datasets", "DatasetOrTimeSchedule"] => { - Replacement::Name("airflow.timetables.assets.AssetOrTimeSchedule") - } + ["airflow", "timetables", "datasets", "DatasetOrTimeSchedule"] => Replacement::AutoImport { + module: "airflow.timetables.assets", + name: "AssetOrTimeSchedule", + }, // airflow.utils ["airflow", "utils", "dag_parsing_context", "get_parsing_context"] => { - Replacement::Name("airflow.sdk.get_parsing_context") + Replacement::AutoImport { + module: "airflow.sdk", + name: "get_parsing_context", + } } _ => return, diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap index 09cf57bee8a548..b291f225c2f25e 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR311_AIR311_names.py.snap @@ -50,7 +50,7 @@ AIR311_names.py:26:1: AIR311 [*] `airflow.datasets.Dataset` is removed in Airflo 28 29 | DatasetAll() 29 30 | DatasetAny() -AIR311_names.py:27:1: AIR311 `airflow.datasets.DatasetAlias` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:27:1: AIR311 [*] `airflow.datasets.DatasetAlias` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | 25 | # airflow.datasets 26 | Dataset() @@ -61,7 +61,24 @@ AIR311_names.py:27:1: AIR311 `airflow.datasets.DatasetAlias` is removed in Airfl | = help: Use `airflow.sdk.AssetAlias` instead -AIR311_names.py:28:1: AIR311 `airflow.datasets.DatasetAll` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +ℹ Safe fix +18 18 | from airflow.models.dag import DAG as DAGFromDag +19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule +20 20 | from airflow.utils.dag_parsing_context import get_parsing_context + 21 |+from airflow.sdk import AssetAlias +21 22 | +22 23 | # airflow +23 24 | DatasetFromRoot() +24 25 | +25 26 | # airflow.datasets +26 27 | Dataset() +27 |-DatasetAlias() + 28 |+AssetAlias() +28 29 | DatasetAll() +29 30 | DatasetAny() +30 31 | Metadata() + +AIR311_names.py:28:1: AIR311 [*] `airflow.datasets.DatasetAll` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | 26 | Dataset() 27 | DatasetAlias() @@ -72,7 +89,25 @@ AIR311_names.py:28:1: AIR311 `airflow.datasets.DatasetAll` is removed in Airflow | = help: Use `airflow.sdk.AssetAll` instead -AIR311_names.py:29:1: AIR311 `airflow.datasets.DatasetAny` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +ℹ Safe fix +18 18 | from airflow.models.dag import DAG as DAGFromDag +19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule +20 20 | from airflow.utils.dag_parsing_context import get_parsing_context + 21 |+from airflow.sdk import AssetAll +21 22 | +22 23 | # airflow +23 24 | DatasetFromRoot() +-------------------------------------------------------------------------------- +25 26 | # airflow.datasets +26 27 | Dataset() +27 28 | DatasetAlias() +28 |-DatasetAll() + 29 |+AssetAll() +29 30 | DatasetAny() +30 31 | Metadata() +31 32 | expand_alias_to_datasets() + +AIR311_names.py:29:1: AIR311 [*] `airflow.datasets.DatasetAny` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | 27 | DatasetAlias() 28 | DatasetAll() @@ -83,6 +118,24 @@ AIR311_names.py:29:1: AIR311 `airflow.datasets.DatasetAny` is removed in Airflow | = help: Use `airflow.sdk.AssetAny` instead +ℹ Safe fix +18 18 | from airflow.models.dag import DAG as DAGFromDag +19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule +20 20 | from airflow.utils.dag_parsing_context import get_parsing_context + 21 |+from airflow.sdk import AssetAny +21 22 | +22 23 | # airflow +23 24 | DatasetFromRoot() +-------------------------------------------------------------------------------- +26 27 | Dataset() +27 28 | DatasetAlias() +28 29 | DatasetAll() +29 |-DatasetAny() + 30 |+AssetAny() +30 31 | Metadata() +31 32 | expand_alias_to_datasets() +32 33 | + AIR311_names.py:30:1: AIR311 `airflow.datasets.metadata.Metadata` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | 28 | DatasetAll() @@ -93,7 +146,7 @@ AIR311_names.py:30:1: AIR311 `airflow.datasets.metadata.Metadata` is removed in | = help: Use `airflow.sdk.Metadata` instead -AIR311_names.py:31:1: AIR311 `airflow.datasets.expand_alias_to_datasets` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:31:1: AIR311 [*] `airflow.datasets.expand_alias_to_datasets` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | 29 | DatasetAny() 30 | Metadata() @@ -104,6 +157,24 @@ AIR311_names.py:31:1: AIR311 `airflow.datasets.expand_alias_to_datasets` is remo | = help: Use `airflow.sdk.expand_alias_to_assets` instead +ℹ Safe fix +18 18 | from airflow.models.dag import DAG as DAGFromDag +19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule +20 20 | from airflow.utils.dag_parsing_context import get_parsing_context + 21 |+from airflow.sdk import expand_alias_to_assets +21 22 | +22 23 | # airflow +23 24 | DatasetFromRoot() +-------------------------------------------------------------------------------- +28 29 | DatasetAll() +29 30 | DatasetAny() +30 31 | Metadata() +31 |-expand_alias_to_datasets() + 32 |+expand_alias_to_assets() +32 33 | +33 34 | # airflow.decorators +34 35 | dag() + AIR311_names.py:34:1: AIR311 `airflow.decorators.dag` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | 33 | # airflow.decorators @@ -228,7 +299,7 @@ AIR311_names.py:56:1: AIR311 `airflow.models.dag.DAG` is removed in Airflow 3.0; | = help: Use `airflow.sdk.DAG` instead -AIR311_names.py:58:1: AIR311 `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. +AIR311_names.py:58:1: AIR311 [*] `airflow.timetables.datasets.DatasetOrTimeSchedule` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | 56 | DAGFromDag() 57 | # airflow.timetables.datasets @@ -239,6 +310,24 @@ AIR311_names.py:58:1: AIR311 `airflow.timetables.datasets.DatasetOrTimeSchedule` | = help: Use `airflow.timetables.assets.AssetOrTimeSchedule` instead +ℹ Safe fix +18 18 | from airflow.models.dag import DAG as DAGFromDag +19 19 | from airflow.timetables.datasets import DatasetOrTimeSchedule +20 20 | from airflow.utils.dag_parsing_context import get_parsing_context + 21 |+from airflow.timetables.assets import AssetOrTimeSchedule +21 22 | +22 23 | # airflow +23 24 | DatasetFromRoot() +-------------------------------------------------------------------------------- +55 56 | # airflow.models.dag +56 57 | DAGFromDag() +57 58 | # airflow.timetables.datasets +58 |-DatasetOrTimeSchedule() + 59 |+AssetOrTimeSchedule() +59 60 | +60 61 | # airflow.utils.dag_parsing_context +61 62 | get_parsing_context() + AIR311_names.py:61:1: AIR311 `airflow.utils.dag_parsing_context.get_parsing_context` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | 60 | # airflow.utils.dag_parsing_context From cf59cee928c8429c5b88eda8f752af92e5e5f4c8 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Bodas <55339528+abhijeetbodas2001@users.noreply.github.com> Date: Fri, 25 Apr 2025 01:41:46 +0530 Subject: [PATCH 0112/1161] [syntax-errors] `nonlocal` declaration at module level (#17559) ## Summary Part of #17412 Add a new compile-time syntax error for detecting `nonlocal` declarations at a module level. ## Test Plan - Added new inline tests for the syntax error - Updated existing tests for `nonlocal` statement parsing to be inside a function scope Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- crates/ruff_linter/src/checkers/ast/mod.rs | 3 +- .../nonlocal_declaration_at_module_level.py | 2 + .../inline/err/nonlocal_stmt_empty.py | 3 +- .../inline/err/nonlocal_stmt_expression.py | 3 +- .../err/nonlocal_stmt_trailing_comma.py | 7 +- .../nonlocal_declaration_at_module_level.py | 2 + .../resources/inline/ok/nonlocal_stmt.py | 5 +- .../src/parser/statement.rs | 18 +-- .../ruff_python_parser/src/semantic_errors.rs | 22 ++++ crates/ruff_python_parser/tests/fixtures.rs | 2 +- ...nlocal_declaration_at_module_level.py.snap | 55 ++++++++ ...invalid_syntax@nonlocal_stmt_empty.py.snap | 39 ++++-- ...id_syntax@nonlocal_stmt_expression.py.snap | 85 ++++++++----- ...yntax@nonlocal_stmt_trailing_comma.py.snap | 119 +++++++++++------- ...nlocal_declaration_at_module_level.py.snap | 49 ++++++++ .../valid_syntax@nonlocal_stmt.py.snap | 80 +++++++----- 16 files changed, 363 insertions(+), 131 deletions(-) create mode 100644 crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 7178db784f94ac..6d0c519dbf49de 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -616,7 +616,8 @@ impl SemanticSyntaxContext for Checker<'_> { | SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_) | SemanticSyntaxErrorKind::InvalidStarExpression | SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_) - | SemanticSyntaxErrorKind::DuplicateParameter(_) => { + | SemanticSyntaxErrorKind::DuplicateParameter(_) + | SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => { if self.settings.preview.is_enabled() { self.semantic_errors.borrow_mut().push(error); } diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py new file mode 100644 index 00000000000000..b7ed1ba1b0b9eb --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py @@ -0,0 +1,2 @@ +nonlocal x +nonlocal x, y diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py index 07127b5f052d5b..f6f2b5577aca3e 100644 --- a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py @@ -1 +1,2 @@ -nonlocal +def _(): + nonlocal diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py index 303cb88b61d23d..46854180075b6b 100644 --- a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py @@ -1 +1,2 @@ -nonlocal x + 1 +def _(): + nonlocal x + 1 diff --git a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py index 24acf55245280e..d0742c873124d0 100644 --- a/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py +++ b/crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py @@ -1,3 +1,4 @@ -nonlocal , -nonlocal x, -nonlocal x, y, +def _(): + nonlocal , + nonlocal x, + nonlocal x, y, diff --git a/crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py b/crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py new file mode 100644 index 00000000000000..8f51257d0e03b3 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py @@ -0,0 +1,2 @@ +def _(): + nonlocal x diff --git a/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py b/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py index 7f652bb0a69f10..81caa434cb26f4 100644 --- a/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py +++ b/crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py @@ -1,2 +1,3 @@ -nonlocal x -nonlocal x, y, z +def _(): + nonlocal x + nonlocal x, y, z diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 2361b252e6b8a5..cb1d4d5e570ba2 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -897,12 +897,14 @@ impl<'src> Parser<'src> { self.bump(TokenKind::Nonlocal); // test_err nonlocal_stmt_trailing_comma - // nonlocal , - // nonlocal x, - // nonlocal x, y, + // def _(): + // nonlocal , + // nonlocal x, + // nonlocal x, y, // test_err nonlocal_stmt_expression - // nonlocal x + 1 + // def _(): + // nonlocal x + 1 let names = self.parse_comma_separated_list_into_vec( RecoveryContextKind::Identifiers, Parser::parse_identifier, @@ -910,7 +912,8 @@ impl<'src> Parser<'src> { if names.is_empty() { // test_err nonlocal_stmt_empty - // nonlocal + // def _(): + // nonlocal self.add_error( ParseErrorType::EmptyNonlocalNames, self.current_token_range(), @@ -918,8 +921,9 @@ impl<'src> Parser<'src> { } // test_ok nonlocal_stmt - // nonlocal x - // nonlocal x, y, z + // def _(): + // nonlocal x + // nonlocal x, y, z ast::StmtNonlocal { range: self.node_range(start), names, diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index a12b61cb06db7d..149292b3b9b854 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -142,6 +142,22 @@ impl SemanticSyntaxChecker { AwaitOutsideAsyncFunctionKind::AsyncWith, ); } + Stmt::Nonlocal(ast::StmtNonlocal { range, .. }) => { + // test_ok nonlocal_declaration_at_module_level + // def _(): + // nonlocal x + + // test_err nonlocal_declaration_at_module_level + // nonlocal x + // nonlocal x, y + if ctx.in_module_scope() { + Self::add_error( + ctx, + SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel, + *range, + ); + } + } _ => {} } @@ -933,6 +949,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::DuplicateParameter(name) => { write!(f, r#"Duplicate parameter "{name}""#) } + SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => { + write!(f, "nonlocal declaration not allowed at module level") + } } } } @@ -1254,6 +1273,9 @@ pub enum SemanticSyntaxErrorKind { /// lambda x, x: ... /// ``` DuplicateParameter(String), + + /// Represents a nonlocal declaration at module level + NonlocalDeclarationAtModuleLevel, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 1ba0f39f3f33b9..f81cbcd41e0941 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -538,7 +538,7 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> { } fn in_module_scope(&self) -> bool { - true + self.scopes.len() == 1 } fn in_function_scope(&self) -> bool { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap new file mode 100644 index 00000000000000..8b63405c852398 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_declaration_at_module_level.py.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_declaration_at_module_level.py +--- +## AST + +``` +Module( + ModModule { + range: 0..25, + body: [ + Nonlocal( + StmtNonlocal { + range: 0..10, + names: [ + Identifier { + id: Name("x"), + range: 9..10, + }, + ], + }, + ), + Nonlocal( + StmtNonlocal { + range: 11..24, + names: [ + Identifier { + id: Name("x"), + range: 20..21, + }, + Identifier { + id: Name("y"), + range: 23..24, + }, + ], + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | nonlocal x + | ^^^^^^^^^^ Syntax Error: nonlocal declaration not allowed at module level +2 | nonlocal x, y + | + + + | +1 | nonlocal x +2 | nonlocal x, y + | ^^^^^^^^^^^^^ Syntax Error: nonlocal declaration not allowed at module level + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap index 08015c08ab20e3..67c9ca6335fa2b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_empty.py.snap @@ -1,19 +1,41 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_empty.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..9, + range: 0..22, body: [ - Nonlocal( - StmtNonlocal { - range: 0..8, - names: [], + FunctionDef( + StmtFunctionDef { + range: 0..21, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + }, + type_params: None, + parameters: Parameters { + range: 5..7, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + range: 13..21, + names: [], + }, + ), + ], }, ), ], @@ -23,6 +45,7 @@ Module( ## Errors | -1 | nonlocal - | ^ Syntax Error: Nonlocal statement must have at least one name +1 | def _(): +2 | nonlocal + | ^ Syntax Error: Nonlocal statement must have at least one name | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap index 7903a99470370b..b5d768d9a32e32 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_expression.py.snap @@ -1,45 +1,67 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_expression.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..15, + range: 0..28, body: [ - Nonlocal( - StmtNonlocal { - range: 0..10, - names: [ - Identifier { - id: Name("x"), - range: 9..10, - }, + FunctionDef( + StmtFunctionDef { + range: 0..27, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + }, + type_params: None, + parameters: Parameters { + range: 5..7, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + range: 13..23, + names: [ + Identifier { + id: Name("x"), + range: 22..23, + }, + ], + }, + ), + Expr( + StmtExpr { + range: 24..27, + value: UnaryOp( + ExprUnaryOp { + range: 24..27, + op: UAdd, + operand: NumberLiteral( + ExprNumberLiteral { + range: 26..27, + value: Int( + 1, + ), + }, + ), + }, + ), + }, + ), ], }, ), - Expr( - StmtExpr { - range: 11..14, - value: UnaryOp( - ExprUnaryOp { - range: 11..14, - op: UAdd, - operand: NumberLiteral( - ExprNumberLiteral { - range: 13..14, - value: Int( - 1, - ), - }, - ), - }, - ), - }, - ), ], }, ) @@ -47,6 +69,7 @@ Module( ## Errors | -1 | nonlocal x + 1 - | ^ Syntax Error: Simple statements must be separated by newlines or semicolons +1 | def _(): +2 | nonlocal x + 1 + | ^ Syntax Error: Simple statements must be separated by newlines or semicolons | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap index 4a901178b0a958..ee76a9c939bb7e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nonlocal_stmt_trailing_comma.py.snap @@ -1,44 +1,66 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/nonlocal_stmt_trailing_comma.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..38, + range: 0..59, body: [ - Nonlocal( - StmtNonlocal { - range: 0..10, - names: [], - }, - ), - Nonlocal( - StmtNonlocal { - range: 11..22, - names: [ - Identifier { - id: Name("x"), - range: 20..21, - }, - ], - }, - ), - Nonlocal( - StmtNonlocal { - range: 23..37, - names: [ - Identifier { - id: Name("x"), - range: 32..33, - }, - Identifier { - id: Name("y"), - range: 35..36, - }, + FunctionDef( + StmtFunctionDef { + range: 0..58, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + }, + type_params: None, + parameters: Parameters { + range: 5..7, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + range: 13..23, + names: [], + }, + ), + Nonlocal( + StmtNonlocal { + range: 28..39, + names: [ + Identifier { + id: Name("x"), + range: 37..38, + }, + ], + }, + ), + Nonlocal( + StmtNonlocal { + range: 44..58, + names: [ + Identifier { + id: Name("x"), + range: 53..54, + }, + Identifier { + id: Name("y"), + range: 56..57, + }, + ], + }, + ), ], }, ), @@ -49,32 +71,35 @@ Module( ## Errors | -1 | nonlocal , - | ^ Syntax Error: Expected an identifier -2 | nonlocal x, -3 | nonlocal x, y, +1 | def _(): +2 | nonlocal , + | ^ Syntax Error: Expected an identifier +3 | nonlocal x, +4 | nonlocal x, y, | | -1 | nonlocal , - | ^ Syntax Error: Nonlocal statement must have at least one name -2 | nonlocal x, -3 | nonlocal x, y, +1 | def _(): +2 | nonlocal , + | ^ Syntax Error: Nonlocal statement must have at least one name +3 | nonlocal x, +4 | nonlocal x, y, | | -1 | nonlocal , -2 | nonlocal x, - | ^ Syntax Error: Trailing comma not allowed -3 | nonlocal x, y, +1 | def _(): +2 | nonlocal , +3 | nonlocal x, + | ^ Syntax Error: Trailing comma not allowed +4 | nonlocal x, y, | | -1 | nonlocal , -2 | nonlocal x, -3 | nonlocal x, y, - | ^ Syntax Error: Trailing comma not allowed +2 | nonlocal , +3 | nonlocal x, +4 | nonlocal x, y, + | ^ Syntax Error: Trailing comma not allowed | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap new file mode 100644 index 00000000000000..b1646e4b6cc09f --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_declaration_at_module_level.py.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/nonlocal_declaration_at_module_level.py +--- +## AST + +``` +Module( + ModModule { + range: 0..24, + body: [ + FunctionDef( + StmtFunctionDef { + range: 0..23, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + }, + type_params: None, + parameters: Parameters { + range: 5..7, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + range: 13..23, + names: [ + Identifier { + id: Name("x"), + range: 22..23, + }, + ], + }, + ), + ], + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap index 33287486f8ae0e..f7b7d0a1c8cbf7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@nonlocal_stmt.py.snap @@ -1,42 +1,64 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/nonlocal_stmt.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..28, + range: 0..45, body: [ - Nonlocal( - StmtNonlocal { - range: 0..10, - names: [ - Identifier { - id: Name("x"), - range: 9..10, - }, - ], - }, - ), - Nonlocal( - StmtNonlocal { - range: 11..27, - names: [ - Identifier { - id: Name("x"), - range: 20..21, - }, - Identifier { - id: Name("y"), - range: 23..24, - }, - Identifier { - id: Name("z"), - range: 26..27, - }, + FunctionDef( + StmtFunctionDef { + range: 0..44, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("_"), + range: 4..5, + }, + type_params: None, + parameters: Parameters { + range: 5..7, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Nonlocal( + StmtNonlocal { + range: 13..23, + names: [ + Identifier { + id: Name("x"), + range: 22..23, + }, + ], + }, + ), + Nonlocal( + StmtNonlocal { + range: 28..44, + names: [ + Identifier { + id: Name("x"), + range: 37..38, + }, + Identifier { + id: Name("y"), + range: 40..41, + }, + Identifier { + id: Name("z"), + range: 43..44, + }, + ], + }, + ), ], }, ), From 4eecc40110512682f22c3cf1c5256887b70b129a Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:14:33 +0200 Subject: [PATCH 0113/1161] [`semantic-syntax-errors`] test for `LoadBeforeGlobalDeclaration` - ruff linter (#17592) Hey @ntBre just one easy case to see if I understood the issue #17526 Let me know if is this what you had in mind. --- .../load_before_global_declaration.py | 23 +++++++++++++++++++ crates/ruff_linter/src/linter.rs | 4 ++++ ...ts__load_before_global_declaration.py.snap | 20 ++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/syntax_errors/load_before_global_declaration.py create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__load_before_global_declaration.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/syntax_errors/load_before_global_declaration.py b/crates/ruff_linter/resources/test/fixtures/syntax_errors/load_before_global_declaration.py new file mode 100644 index 00000000000000..70183b0891cd3a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/syntax_errors/load_before_global_declaration.py @@ -0,0 +1,23 @@ +x = 10 +def test_1(): + global x # ok + x += 1 + + +x = 10 +def test_2(): + x += 1 # error + global x + +def test_3(): + print(x) # error + global x + x = 5 + +def test_4(): + global x + print(x) + x = 1 + +x = 0 +test_4() \ No newline at end of file diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 4af0010c3879c3..4da66e9d08da61 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1064,6 +1064,10 @@ mod tests { #[test_case(Rule::YieldOutsideFunction, Path::new("yield_scope.py"))] #[test_case(Rule::ReturnOutsideFunction, Path::new("return_outside_function.py"))] + #[test_case( + Rule::LoadBeforeGlobalDeclaration, + Path::new("load_before_global_declaration.py") + )] fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); let path = Path::new("resources/test/fixtures/syntax_errors").join(path); diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__load_before_global_declaration.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__load_before_global_declaration.py.snap new file mode 100644 index 00000000000000..a917491f4125ab --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__load_before_global_declaration.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +resources/test/fixtures/syntax_errors/load_before_global_declaration.py:9:5: PLE0118 Name `x` is used prior to global declaration on line 10 + | + 7 | x = 10 + 8 | def test_2(): + 9 | x += 1 # error + | ^ PLE0118 +10 | global x + | + +resources/test/fixtures/syntax_errors/load_before_global_declaration.py:13:11: PLE0118 Name `x` is used prior to global declaration on line 14 + | +12 | def test_3(): +13 | print(x) # error + | ^ PLE0118 +14 | global x +15 | x = 5 + | From ef0343189c185f95892f3d4294bcf51243ebafca Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 24 Apr 2025 14:41:19 -0700 Subject: [PATCH 0114/1161] [red-knot] add TODO comment in specialization code (#17615) ## Summary As promised, this just adds a TODO comment to document something we discussed today that should probably be improved at some point, but isn't a priority right now (since it's an issue that in practice would only affect generic classes with both `__init__` and `__new__` methods, where some typevar is bound to `Unknown` in one and to some other type in another.) --- crates/red_knot_python_semantic/src/types/generics.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 66cd67f263e300..b1c0355ebf240b 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -154,6 +154,10 @@ impl<'db> Specialization<'db> { pub(crate) fn combine(self, db: &'db dyn Db, other: Self) -> Self { let generic_context = self.generic_context(db); assert!(other.generic_context(db) == generic_context); + // TODO special-casing Unknown to mean "no mapping" is not right here, and can give + // confusing/wrong results in cases where there was a mapping found for a typevar, and it + // was of type Unknown. We should probably add a bitset or similar to Specialization that + // explicitly tells us which typevars are mapped. let types: Box<[_]> = self .types(db) .into_iter() From f1a539dac6a203504702e799dfc6a0c83ad7e6db Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 25 Apr 2025 03:15:23 +0530 Subject: [PATCH 0115/1161] [red-knot] Special case `@final`, `@override` (#17608) ## Summary This PR adds special-casing for `@final` and `@override` decorator for a similar reason as https://github.com/astral-sh/ruff/pull/17591 to support the invalid overload check. Both `final` and `override` are identity functions which can be removed once `TypeVar` support is added. --- .../resources/primer/bad.txt | 1 + .../resources/primer/good.txt | 1 - crates/red_knot_python_semantic/src/types.rs | 8 ++++++++ .../src/types/call/bind.rs | 16 ++++++++++++++++ .../red_knot_python_semantic/src/types/infer.rs | 8 ++++++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/resources/primer/bad.txt b/crates/red_knot_python_semantic/resources/primer/bad.txt index d263c922c91865..24a74e3ac94778 100644 --- a/crates/red_knot_python_semantic/resources/primer/bad.txt +++ b/crates/red_knot_python_semantic/resources/primer/bad.txt @@ -1,3 +1,4 @@ +Expression # cycle panic (signature_) Tanjun # cycle panic (signature_) aiohttp # missing expression ID alerta # missing expression ID diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/red_knot_python_semantic/resources/primer/good.txt index 86cec204df8f50..f76755770702a7 100644 --- a/crates/red_knot_python_semantic/resources/primer/good.txt +++ b/crates/red_knot_python_semantic/resources/primer/good.txt @@ -1,5 +1,4 @@ AutoSplit -Expression PyGithub PyWinCtl SinbadCogs diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 44ce5f1561f08e..dfc55c73ffe3b0 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -6027,6 +6027,10 @@ bitflags! { const OVERLOAD = 1 << 2; /// `@abc.abstractmethod` const ABSTRACT_METHOD = 1 << 3; + /// `@typing.final` + const FINAL = 1 << 4; + /// `@typing.override` + const OVERRIDE = 1 << 6; } } @@ -6400,6 +6404,8 @@ pub enum KnownFunction { Cast, /// `typing(_extensions).overload` Overload, + /// `typing(_extensions).override` + Override, /// `typing(_extensions).is_protocol` IsProtocol, /// `typing(_extensions).get_protocol_members` @@ -6467,6 +6473,7 @@ impl KnownFunction { | Self::AssertNever | Self::Cast | Self::Overload + | Self::Override | Self::RevealType | Self::Final | Self::IsProtocol @@ -7844,6 +7851,7 @@ pub(crate) mod tests { KnownFunction::Cast | KnownFunction::Final | KnownFunction::Overload + | KnownFunction::Override | KnownFunction::RevealType | KnownFunction::AssertType | KnownFunction::AssertNever diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 3e519a6a835e6f..397c2aaa118e23 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -584,6 +584,14 @@ impl<'db> Bindings<'db> { } } + Some(KnownFunction::Override) => { + // TODO: This can be removed once we understand legacy generics because the + // typeshed definition for `typing.overload` is an identity function. + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(*ty); + } + } + Some(KnownFunction::AbstractMethod) => { // TODO: This can be removed once we understand legacy generics because the // typeshed definition for `abc.abstractmethod` is an identity function. @@ -592,6 +600,14 @@ impl<'db> Bindings<'db> { } } + Some(KnownFunction::Final) => { + // TODO: This can be removed once we understand legacy generics because the + // typeshed definition for `abc.abstractmethod` is an identity function. + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(*ty); + } + } + Some(KnownFunction::GetattrStatic) => { let [Some(instance_ty), Some(attr_name), default] = overload.parameter_types() diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 8bda7530d7f2b1..bfdddc49827fbf 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1500,6 +1500,14 @@ impl<'db> TypeInferenceBuilder<'db> { function_decorators |= FunctionDecorators::ABSTRACT_METHOD; continue; } + Some(KnownFunction::Final) => { + function_decorators |= FunctionDecorators::FINAL; + continue; + } + Some(KnownFunction::Override) => { + function_decorators |= FunctionDecorators::OVERRIDE; + continue; + } _ => {} } } From afc18ff1a12b38cf4b8430059732499a2f2d907a Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 24 Apr 2025 14:52:25 -0700 Subject: [PATCH 0116/1161] [red-knot] change TypeVarInstance to be interned, not tracked (#17616) ## Summary Tracked structs have some issues with fixpoint iteration in Salsa, and there's not actually any need for this to be tracked, it should be interned like most of our type structs. The removed comment was probably never correct (in that we could have disambiguated sufficiently), and is definitely not relevant now that `TypeVarInstance` also holds its `Definition`. ## Test Plan Existing tests. --- crates/red_knot_python_semantic/src/types.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index dfc55c73ffe3b0..b230c6bab84723 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -5230,11 +5230,7 @@ impl<'db> InvalidTypeExpression<'db> { /// typevar represents as an annotation: that is, an unknown set of objects, constrained by the /// upper-bound/constraints on this type var, defaulting to the default type of this type var when /// not otherwise bound to a type. -/// -/// This must be a tracked struct, not an interned one, because typevar equivalence is by identity, -/// not by value. Two typevars that have the same name, bound/constraints, and default, are still -/// different typevars: if used in the same scope, they may be bound to different types. -#[salsa::tracked(debug)] +#[salsa::interned(debug)] pub struct TypeVarInstance<'db> { /// The name of this TypeVar (e.g. `T`) #[return_ref] From 3f84e75e20a9f5342d030b6d547056e94dcb4494 Mon Sep 17 00:00:00 2001 From: Max Mynter <32773644+maxmynter@users.noreply.github.com> Date: Fri, 25 Apr 2025 14:32:57 +0200 Subject: [PATCH 0117/1161] Add Semantic Error Test for LateFutureImport (#17612) Adresses a question in #17526. ## Summary Adds a syntax error test for `__future__` import not at top of file. ## Question: Is this a redundant with https://github.com/astral-sh/ruff/blob/8d2c79276d167fcfcf9143a2bc1b328bb9d0f876/crates/ruff_linter/resources/test/fixtures/pyflakes/F404_0.py#L1-L8 and https://github.com/astral-sh/ruff/blob/8d2c79276d167fcfcf9143a2bc1b328bb9d0f876/crates/ruff_linter/resources/test/fixtures/pyflakes/F404_1.py#L1-L5 which test pyflake `F404`? ## Test Plan This is a test --- .../test/fixtures/syntax_errors/late_future_import.py | 2 ++ crates/ruff_linter/src/linter.rs | 1 + ...uff_linter__linter__tests__late_future_import.py.snap | 9 +++++++++ 3 files changed, 12 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/syntax_errors/late_future_import.py create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__late_future_import.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/syntax_errors/late_future_import.py b/crates/ruff_linter/resources/test/fixtures/syntax_errors/late_future_import.py new file mode 100644 index 00000000000000..1dd40c823c60d2 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/syntax_errors/late_future_import.py @@ -0,0 +1,2 @@ +import random +from __future__ import annotations # Error; not at top of file diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 4da66e9d08da61..d4d9daef16b5b5 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1062,6 +1062,7 @@ mod tests { Ok(()) } + #[test_case(Rule::LateFutureImport, Path::new("late_future_import.py"))] #[test_case(Rule::YieldOutsideFunction, Path::new("yield_scope.py"))] #[test_case(Rule::ReturnOutsideFunction, Path::new("return_outside_function.py"))] #[test_case( diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__late_future_import.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__late_future_import.py.snap new file mode 100644 index 00000000000000..6140d4cf408cae --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__late_future_import.py.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +resources/test/fixtures/syntax_errors/late_future_import.py:2:1: F404 `from __future__` imports must occur at the beginning of the file + | +1 | import random +2 | from __future__ import annotations # Error; not at top of file + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F404 + | From 6d3b1d13d6c7f5e88b164505b77a81136322d926 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 25 Apr 2025 08:37:16 -0400 Subject: [PATCH 0118/1161] [`pylint`] Detect `global` declarations in module scope (`PLE0118`) (#17411) Summary -- While going through the syntax errors in [this comment], I was surprised to see the error `name 'x' is assigned to before global declaration`, which corresponds to [load-before-global-declaration (PLE0118)] and has also been reimplemented as a syntax error (#17135). However, it looks like neither of the implementations consider `global` declarations in the top-level module scope, which is a syntax error in CPython: ```python # try.py x = None global x ``` ```shell > python -m compileall -f try.py Compiling 'try.py'... *** File "try.py", line 2 global x ^^^^^^^^ SyntaxError: name 'x' is assigned to before global declaration ``` I'm not sure this is the best or most elegant solution, but it was a quick fix that passed all of our tests. Test Plan -- New PLE0118 test case. [this comment]: https://github.com/astral-sh/ruff/issues/7633#issuecomment-1740424031 [load-before-global-declaration (PLE0118)]: https://docs.astral.sh/ruff/rules/load-before-global-declaration/#load-before-global-declaration-ple0118 --- .../pylint/load_before_global_declaration.py | 5 ++ crates/ruff_linter/src/checkers/ast/mod.rs | 6 +- ...118_load_before_global_declaration.py.snap | 8 +++ crates/ruff_python_semantic/src/model.rs | 60 +++++++++++++------ 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/load_before_global_declaration.py b/crates/ruff_linter/resources/test/fixtures/pylint/load_before_global_declaration.py index 5ca672e1a1a436..fdbe9cc9b87605 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/load_before_global_declaration.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/load_before_global_declaration.py @@ -156,3 +156,8 @@ def f(): def f(): global x print(f"{x=}") + + +# surprisingly still an error, global in module scope +x = None +global x diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 6d0c519dbf49de..73e875bf208705 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -2067,8 +2067,12 @@ impl<'a> Visitor<'a> for Checker<'a> { } impl<'a> Checker<'a> { - /// Visit a [`Module`]. Returns `true` if the module contains a module-level docstring. + /// Visit a [`Module`]. fn visit_module(&mut self, python_ast: &'a Suite) { + // Extract any global bindings from the module body. + if let Some(globals) = Globals::from_body(python_ast) { + self.semantic.set_globals(globals); + } analyze::module(python_ast, self); } diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0118_load_before_global_declaration.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0118_load_before_global_declaration.py.snap index 5d1994780df2a7..adabada987380c 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0118_load_before_global_declaration.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0118_load_before_global_declaration.py.snap @@ -123,3 +123,11 @@ load_before_global_declaration.py:113:14: PLE0118 Name `x` is used prior to glob | ^ PLE0118 114 | global x | + +load_before_global_declaration.py:162:1: PLE0118 Name `x` is used prior to global declaration on line 163 + | +161 | # surprisingly still an error, global in module scope +162 | x = None + | ^ PLE0118 +163 | global x + | diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index d24060b7c41ca8..70c26cd615898f 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -1503,24 +1503,48 @@ impl<'a> SemanticModel<'a> { /// Set the [`Globals`] for the current [`Scope`]. pub fn set_globals(&mut self, globals: Globals<'a>) { - // If any global bindings don't already exist in the global scope, add them. - for (name, range) in globals.iter() { - if self - .global_scope() - .get(name) - .is_none_or(|binding_id| self.bindings[binding_id].is_unbound()) - { - let id = self.bindings.push(Binding { - kind: BindingKind::Assignment, - range: *range, - references: Vec::new(), - scope: ScopeId::global(), - source: self.node_id, - context: self.execution_context(), - exceptions: self.exceptions(), - flags: BindingFlags::empty(), - }); - self.global_scope_mut().add(name, id); + // If any global bindings don't already exist in the global scope, add them, unless we are + // also in the global scope, where we don't want these to count as definitions for rules + // like `undefined-name` (F821). For example, adding bindings in the top-level scope causes + // a false negative in cases like this: + // + // ```python + // global x + // + // def f(): + // print(x) # F821 false negative + // ``` + // + // On the other hand, failing to add bindings in non-top-level scopes causes false + // positives: + // + // ```python + // def f(): + // global foo + // import foo + // + // def g(): + // foo.is_used() # F821 false positive + // ``` + if !self.at_top_level() { + for (name, range) in globals.iter() { + if self + .global_scope() + .get(name) + .is_none_or(|binding_id| self.bindings[binding_id].is_unbound()) + { + let id = self.bindings.push(Binding { + kind: BindingKind::Assignment, + range: *range, + references: Vec::new(), + scope: ScopeId::global(), + source: self.node_id, + context: self.execution_context(), + exceptions: self.exceptions(), + flags: BindingFlags::empty(), + }); + self.global_scope_mut().add(name, id); + } } } From 4c3f389598f2ae13715df0b3b082f372545d024d Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 25 Apr 2025 06:55:00 -0700 Subject: [PATCH 0119/1161] [red-knot] fix inheritance-cycle detection for generic classes (#17620) ## Summary The `ClassLiteralType::inheritance_cycle` method is intended to detect inheritance cycles that would result in cyclic MROs, emit a diagnostic, and skip actually trying to create the cyclic MRO, falling back to an "error" MRO instead with just `Unknown` and `object`. This method didn't work properly for generic classes. It used `fully_static_explicit_bases`, which filter-maps `explicit_bases` over `Type::into_class_type`, which returns `None` for an unspecialized generic class literal. So in a case like `class C[T](C): ...`, because the explicit base is an unspecialized generic, we just skipped it, and failed to detect the class as cyclically defined. Instead, iterate directly over all `explicit_bases`, and explicitly handle both the specialized (`GenericAlias`) and unspecialized (`ClassLiteral`) cases, so that we check all bases and correctly detect cyclic inheritance. ## Test Plan Added mdtests. --- .../resources/mdtest/generics/classes.md | 30 ++++++++++++++----- .../src/types/class.rs | 13 ++++++-- 2 files changed, 32 insertions(+), 11 deletions(-) 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 11d1d36b353dfd..82b2b8f7eb5b21 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -275,14 +275,16 @@ c: C[int] = C[int]() reveal_type(c.method("string")) # revealed: Literal["string"] ``` -## Cyclic class definition +## Cyclic class definitions + +### F-bounded quantification A class can use itself as the type parameter of one of its superclasses. (This is also known as the [curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].) -Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). +#### In a stub file -`stub.pyi`: +Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). ```pyi class Base[T]: ... @@ -291,9 +293,9 @@ class Sub(Base[Sub]): ... reveal_type(Sub) # revealed: Literal[Sub] ``` -A similar case can work in a non-stub file, if forward references are stringified: +#### With string forward references -`string_annotation.py`: +A similar case can work in a non-stub file, if forward references are stringified: ```py class Base[T]: ... @@ -302,9 +304,9 @@ class Sub(Base["Sub"]): ... reveal_type(Sub) # revealed: Literal[Sub] ``` -In a non-stub file, without stringified forward references, this raises a `NameError`: +#### Without string forward references -`bare_annotation.py`: +In a non-stub file, without stringified forward references, this raises a `NameError`: ```py class Base[T]: ... @@ -313,11 +315,23 @@ class Base[T]: ... class Sub(Base[Sub]): ... ``` -## Another cyclic case +### Cyclic inheritance as a generic parameter ```pyi class Derived[T](list[Derived[T]]): ... ``` +### Direct cyclic inheritance + +Inheritance that would result in a cyclic MRO is detected as an error. + +```py +# error: [cyclic-class-definition] +class C[T](C): ... + +# error: [cyclic-class-definition] +class D[T](D[int]): ... +``` + [crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern [f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 9f7e2f37388d74..6f8465bbe8ee36 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -174,6 +174,10 @@ impl<'db> GenericAlias<'db> { pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { self.origin(db).class(db).definition(db) } + + pub(crate) fn class_literal(self, db: &'db dyn Db) -> ClassLiteralType<'db> { + ClassLiteralType::Generic(self.origin(db)) + } } impl<'db> From> for Type<'db> { @@ -1690,8 +1694,12 @@ impl<'db> ClassLiteralType<'db> { visited_classes: &mut IndexSet>, ) -> bool { let mut result = false; - for explicit_base_class in class.fully_static_explicit_bases(db) { - let (explicit_base_class_literal, _) = explicit_base_class.class_literal(db); + for explicit_base in class.explicit_bases(db) { + let explicit_base_class_literal = match explicit_base { + Type::ClassLiteral(class_literal) => *class_literal, + Type::GenericAlias(generic_alias) => generic_alias.class_literal(db), + _ => continue, + }; if !classes_on_stack.insert(explicit_base_class_literal) { return true; } @@ -1705,7 +1713,6 @@ impl<'db> ClassLiteralType<'db> { visited_classes, ); } - classes_on_stack.pop(); } result From fa88989ef0085610dd8ddecbd289a5fcc1d38892 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 25 Apr 2025 06:55:54 -0700 Subject: [PATCH 0120/1161] [red-knot] fix detecting a metaclass on a not-explicitly-specialized generic base (#17621) ## Summary After https://github.com/astral-sh/ruff/pull/17620 (which this PR is based on), I was looking at other call sites of `Type::into_class_type`, and I began to feel that _all_ of them were currently buggy due to silently skipping unspecialized generic class literal types (though in some cases the bug hadn't shown up yet because we don't understand legacy generic classes from typeshed), and in every case they would be better off if an unspecialized generic class literal were implicitly specialized with the default specialization (which is the usual Python typing semantics for an unspecialized reference to a generic class), instead of silently skipped. So I changed the method to implicitly apply the default specialization, and added a test that previously failed for detecting metaclasses on an unspecialized generic base. I also renamed the method to `to_class_type`, because I feel we have a strong naming convention where `Type::into_foo` is always a trivial `const fn` that simply returns `Some()` if the type is of variant `Foo` and `None` otherwise. Even the existing method (with it handling both `GenericAlias` and `ClassLiteral`, and distinguishing kinds of `ClassLiteral`) was stretching this convention, and the new version definitely breaks that envelope. ## Test Plan Added a test that failed before this PR. --- .../resources/mdtest/metaclass.md | 19 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 15 ++++++++------- .../src/types/class.rs | 13 +++++++------ .../src/types/class_base.rs | 2 +- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md index fc66cc12207fe1..84499c766ae9d9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md +++ b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md @@ -53,6 +53,25 @@ class B(A): ... reveal_type(B.__class__) # revealed: Literal[M] ``` +## Linear inheritance with PEP 695 generic class + +The same is true if the base with the metaclass is a generic class. + +```toml +[environment] +python-version = "3.13" +``` + +```py +class M(type): ... +class A[T](metaclass=M): ... +class B(A): ... +class C(A[int]): ... + +reveal_type(B.__class__) # revealed: Literal[M] +reveal_type(C.__class__) # revealed: Literal[M] +``` + ## Conflict (1) The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b230c6bab84723..ab190dd4485688 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -688,20 +688,21 @@ impl<'db> Type<'db> { matches!(self, Type::ClassLiteral(..)) } - pub const fn into_class_type(self) -> Option> { + /// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`. + /// Since a `ClassType` must be specialized, apply the default specialization to any + /// unspecialized generic class literal. + pub fn to_class_type(self, db: &'db dyn Db) -> Option> { match self { - Type::ClassLiteral(ClassLiteralType::NonGeneric(non_generic)) => { - Some(ClassType::NonGeneric(non_generic)) - } + Type::ClassLiteral(class_literal) => Some(class_literal.default_specialization(db)), Type::GenericAlias(alias) => Some(ClassType::Generic(alias)), _ => None, } } #[track_caller] - pub fn expect_class_type(self) -> ClassType<'db> { - self.into_class_type() - .expect("Expected a Type::GenericAlias or non-generic Type::ClassLiteral variant") + pub fn expect_class_type(self, db: &'db dyn Db) -> ClassType<'db> { + self.to_class_type(db) + .expect("Expected a Type::GenericAlias or Type::ClassLiteral variant") } pub const fn is_class_type(&self) -> bool { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 6f8465bbe8ee36..f74e7d276f0712 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -577,12 +577,13 @@ impl<'db> ClassLiteralType<'db> { self.explicit_bases_query(db) } - /// Iterate over this class's explicit bases, filtering out any bases that are not class objects. + /// Iterate over this class's explicit bases, filtering out any bases that are not class + /// objects, and applying default specialization to any unspecialized generic class literals. fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { self.explicit_bases(db) .iter() .copied() - .filter_map(Type::into_class_type) + .filter_map(|ty| ty.to_class_type(db)) } #[salsa::tracked(return_ref, cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial)] @@ -767,7 +768,7 @@ impl<'db> ClassLiteralType<'db> { (KnownClass::Type.to_class_literal(db), self) }; - let mut candidate = if let Some(metaclass_ty) = metaclass.into_class_type() { + let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) { MetaclassCandidate { metaclass: metaclass_ty, explicit_metaclass_of: class_metaclass_was_from, @@ -809,7 +810,7 @@ impl<'db> ClassLiteralType<'db> { // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 for base_class in base_classes { let metaclass = base_class.metaclass(db); - let Some(metaclass) = metaclass.into_class_type() else { + let Some(metaclass) = metaclass.to_class_type(db) else { continue; }; if metaclass.is_subclass_of(db, candidate.metaclass) { @@ -2164,7 +2165,7 @@ impl<'db> KnownClass { /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { self.to_class_literal(db) - .into_class_type() + .to_class_type(db) .map(Type::instance) .unwrap_or_else(Type::unknown) } @@ -2231,7 +2232,7 @@ impl<'db> KnownClass { /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> { self.to_class_literal(db) - .into_class_type() + .to_class_type(db) .map(|class| SubclassOfType::from(db, class)) .unwrap_or_else(SubclassOfType::subclass_of_unknown) } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index fa5175a0ab84fe..395ff5268da160 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -62,7 +62,7 @@ impl<'db> ClassBase<'db> { pub(super) fn object(db: &'db dyn Db) -> Self { KnownClass::Object .to_class_literal(db) - .into_class_type() + .to_class_type(db) .map_or(Self::unknown(), Self::Class) } From 049280a3bc9a5c13f2f61716eb1792af08a57244 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 25 Apr 2025 10:11:25 -0400 Subject: [PATCH 0121/1161] red_knot_project: sort diagnostics from checking files Previously, we could iterate over files in an unspecified order (via `HashSet` iteration) and we could accumulate diagnostics from files in an unspecified order (via parallelism). Here, we change the status quo so that diagnostics collected from files are sorted after checking is complete. For now, we sort by severity (with higher severity diagnostics appearing first) and then by diagnostic ID to give a stable ordering. I'm not sure if this is the best ordering. --- crates/red_knot/tests/cli.rs | 28 +++++------ crates/red_knot_project/src/lib.rs | 80 ++++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index df8e21defbe269..4509882e505866 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -935,13 +935,6 @@ fn check_specific_paths() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - error: lint:unresolved-import: Cannot resolve import `does_not_exist` - --> /project/tests/test_main.py:2:8 - | - 2 | import does_not_exist # error: unresolved-import - | ^^^^^^^^^^^^^^ - | - error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero --> /project/main.py:2:5 | @@ -958,6 +951,13 @@ fn check_specific_paths() -> anyhow::Result<()> { 4 | print(z) | + error: lint:unresolved-import: Cannot resolve import `does_not_exist` + --> /project/tests/test_main.py:2:8 + | + 2 | import does_not_exist # error: unresolved-import + | ^^^^^^^^^^^^^^ + | + Found 3 diagnostics ----- stderr ----- @@ -972,13 +972,6 @@ fn check_specific_paths() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - error: lint:unresolved-import: Cannot resolve import `does_not_exist` - --> /project/tests/test_main.py:2:8 - | - 2 | import does_not_exist # error: unresolved-import - | ^^^^^^^^^^^^^^ - | - error: lint:unresolved-import: Cannot resolve import `main2` --> /project/other.py:2:6 | @@ -988,6 +981,13 @@ fn check_specific_paths() -> anyhow::Result<()> { 4 | print(z) | + error: lint:unresolved-import: Cannot resolve import `does_not_exist` + --> /project/tests/test_main.py:2:8 + | + 2 | import does_not_exist # error: unresolved-import + | ^^^^^^^^^^^^^^ + | + Found 2 diagnostics ----- stderr ----- diff --git a/crates/red_knot_project/src/lib.rs b/crates/red_knot_project/src/lib.rs index 8393aec6637ceb..de3207216fd352 100644 --- a/crates/red_knot_project/src/lib.rs +++ b/crates/red_knot_project/src/lib.rs @@ -187,30 +187,66 @@ impl Project { .map(IOErrorDiagnostic::to_diagnostic), ); - let result = Arc::new(std::sync::Mutex::new(diagnostics)); - let inner_result = Arc::clone(&result); - - let db = db.clone(); - let project_span = project_span.clone(); - - rayon::scope(move |scope| { - for file in &files { - let result = inner_result.clone(); - let db = db.clone(); - let project_span = project_span.clone(); - - scope.spawn(move |_| { - let check_file_span = - tracing::debug_span!(parent: &project_span, "check_file", ?file); - let _entered = check_file_span.entered(); - - let file_diagnostics = check_file_impl(&db, file); - result.lock().unwrap().extend(file_diagnostics); - }); + let file_diagnostics = Arc::new(std::sync::Mutex::new(vec![])); + + { + let file_diagnostics = Arc::clone(&file_diagnostics); + let db = db.clone(); + let project_span = project_span.clone(); + + rayon::scope(move |scope| { + for file in &files { + let result = Arc::clone(&file_diagnostics); + let db = db.clone(); + let project_span = project_span.clone(); + + scope.spawn(move |_| { + let check_file_span = + tracing::debug_span!(parent: &project_span, "check_file", ?file); + let _entered = check_file_span.entered(); + + let file_diagnostics = check_file_impl(&db, file); + result.lock().unwrap().extend(file_diagnostics); + }); + } + }); + } + + let mut file_diagnostics = Arc::into_inner(file_diagnostics) + .unwrap() + .into_inner() + .unwrap(); + // We sort diagnostics in a way that keeps them in source order + // and grouped by file. After that, we fall back to severity + // (with fatal messages sorting before info messages) and then + // finally the diagnostic ID. + file_diagnostics.sort_by(|d1, d2| { + if let (Some(span1), Some(span2)) = (d1.primary_span(), d2.primary_span()) { + let order = span1 + .file() + .path(db) + .as_str() + .cmp(span2.file().path(db).as_str()); + if order.is_ne() { + return order; + } + + if let (Some(range1), Some(range2)) = (span1.range(), span2.range()) { + let order = range1.start().cmp(&range2.start()); + if order.is_ne() { + return order; + } + } } + // Reverse so that, e.g., Fatal sorts before Info. + let order = d1.severity().cmp(&d2.severity()).reverse(); + if order.is_ne() { + return order; + } + d1.id().cmp(&d2.id()) }); - - Arc::into_inner(result).unwrap().into_inner().unwrap() + diagnostics.extend(file_diagnostics); + diagnostics } pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec { From b6281a88051fa77af6aef55f0bc67656b86c34a5 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Sat, 26 Apr 2025 01:44:28 +0900 Subject: [PATCH 0122/1161] [`airflow`] update existing `AIR302` rules with better suggestions (#17542) ## Summary Even though the original suggestion works, they've been removed in later version and is no longer the best practices. e.g., many sql realted operators have been removed and are now suggested to use SQLExecuteQueryOperator instead ## Test Plan The existing test fixtures have been updated --- .../resources/test/fixtures/airflow/AIR302.py | 12 +- .../ruff_linter/src/rules/airflow/helpers.rs | 1 + .../airflow/rules/moved_to_provider_in_3.rs | 225 ++++++++---------- .../suggested_to_move_to_provider_in_3.rs | 24 +- ...les__airflow__tests__AIR302_AIR302.py.snap | 77 +++--- 5 files changed, 158 insertions(+), 181 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py index df479f4d937d02..44d99bba80bd1a 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py @@ -139,18 +139,18 @@ from airflow.operators.pig_operator import PigOperator from airflow.operators.postgres_operator import Mapping, PostgresOperator from airflow.operators.presto_check_operator import ( - PrestoCheckOperator, - PrestoIntervalCheckOperator, - PrestoValueCheckOperator, + CheckOperator as SQLCheckOperator2, ) from airflow.operators.presto_check_operator import ( - SQLCheckOperator as SQLCheckOperator2, + IntervalCheckOperator as SQLIntervalCheckOperator2, ) from airflow.operators.presto_check_operator import ( - SQLIntervalCheckOperator as SQLIntervalCheckOperator2, + PrestoCheckOperator, + PrestoIntervalCheckOperator, + PrestoValueCheckOperator, ) from airflow.operators.presto_check_operator import ( - SQLValueCheckOperator as SQLValueCheckOperator2, + ValueCheckOperator as SQLValueCheckOperator2, ) from airflow.operators.presto_to_mysql import ( PrestoToMySqlOperator, diff --git a/crates/ruff_linter/src/rules/airflow/helpers.rs b/crates/ruff_linter/src/rules/airflow/helpers.rs index 23aaaa07d43f24..8c2845f9081046 100644 --- a/crates/ruff_linter/src/rules/airflow/helpers.rs +++ b/crates/ruff_linter/src/rules/airflow/helpers.rs @@ -21,6 +21,7 @@ pub(crate) enum Replacement { #[derive(Debug, Eq, PartialEq)] pub(crate) enum ProviderReplacement { + None, ProviderName { name: &'static str, provider: &'static str, diff --git a/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs index e5d97e8cba33d3..2840f9d691b19b 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs @@ -40,6 +40,9 @@ impl Violation for Airflow3MovedToProvider { replacement, } = self; match replacement { + ProviderReplacement::None => { + format!("`{deprecated}` is removed in Airflow 3.0") + } ProviderReplacement::ProviderName { name: _, provider, @@ -59,6 +62,7 @@ impl Violation for Airflow3MovedToProvider { fn fix_title(&self) -> Option { let Airflow3MovedToProvider { replacement, .. } = self; match replacement { + ProviderReplacement::None => {None} ProviderReplacement::ProviderName { name, provider, @@ -128,9 +132,9 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan version: "1.0.0" }, ["airflow", "operators", "s3_file_transform_operator", "S3FileTransformOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.operators.s3_file_transform.S3FileTransformOperator", + name: "airflow.providers.amazon.aws.operators.s3.S3FileTransformOperator", provider: "amazon", - version: "1.0.0" + version: "3.0.0" }, ["airflow", "operators", "s3_to_redshift_operator", "S3ToRedshiftOperator" | "S3ToRedshiftTransfer"] => ProviderReplacement::ProviderName { name: "airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator", @@ -183,63 +187,48 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan provider: "common-sql", version: "1.0.0" }, - ["airflow", "operators", "check_operator", rest] => match *rest { - "SQLCheckOperator" | "CheckOperator" => ProviderReplacement::ProviderName { + ["airflow", "operators", "check_operator" | "sql", "SQLCheckOperator"] + | ["airflow", "operators", "check_operator" | "druid_check_operator" | "presto_check_operator", "CheckOperator"] + | ["airflow", "operators", "druid_check_operator", "DruidCheckOperator"] + | ["airflow", "operators", "presto_check_operator", "PrestoCheckOperator"] => { + ProviderReplacement::ProviderName { name: "airflow.providers.common.sql.operators.sql.SQLCheckOperator", provider: "common-sql", version: "1.1.0" - }, - "SQLIntervalCheckOperator" | "IntervalCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - "SQLThresholdCheckOperator" | "ThresholdCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - "SQLValueCheckOperator" | "ValueCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLValueCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - _ => return + } + }, + ["airflow", "operators", "check_operator" | "presto_check_operator", "IntervalCheckOperator"] + | ["airflow", "operators", "check_operator" | "sql", "SQLIntervalCheckOperator"] + | ["airflow", "operators", "presto_check_operator", "PrestoIntervalCheckOperator"] => ProviderReplacement::ProviderName { + name: "airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator", + provider: "common-sql", + version: "1.1.0" + }, + ["airflow", "operators", "check_operator" | "sql" , "SQLThresholdCheckOperator"] + | ["airflow", "operators", "check_operator", "ThresholdCheckOperator"] => ProviderReplacement::ProviderName { + name: "airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator", + provider: "common-sql", + version: "1.1.0" + }, + ["airflow", "operators", "check_operator" | "presto_check_operator", "ValueCheckOperator"] + | ["airflow", "operators", "presto_check_operator", "PrestoValueCheckOperator"] + | ["airflow", "operators", "check_operator" | "sql", "SQLValueCheckOperator"] => ProviderReplacement::ProviderName { + name: "airflow.providers.common.sql.operators.sql.SQLValueCheckOperator", + provider: "common-sql", + version: "1.1.0" }, - ["airflow", "operators", "presto_check_operator", rest] => match *rest { - "SQLCheckOperator" | "PrestoCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - "SQLIntervalCheckOperator" | "PrestoIntervalCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - "SQLValueCheckOperator" | "PrestoValueCheckOperator" => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLValueCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - _ => return - } ["airflow", "operators", "sql", rest] => match *rest { - "BaseSQLOperator" | - "BranchSQLOperator" | - "SQLCheckOperator" | - "SQLIntervalCheckOperator" | - "SQLTablecheckOperator" | - "SQLThresholdCheckOperator" => ProviderReplacement::SourceModuleMovedToProvider { + "BaseSQLOperator" + | "BranchSQLOperator" + | "SQLTablecheckOperator" => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.common.sql.operators.sql", provider: "common-sql", version: "1.1.0" }, - "SQLColumnCheckOperator" | - "SQLValueCheckOperator" | - "_convert_to_float_if_possible" | - "parse_boolean" => ProviderReplacement::SourceModuleMovedToProvider { + "SQLColumnCheckOperator" + | "_convert_to_float_if_possible" + | "parse_boolean" => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.common.sql.operators.sql", provider: "common-sql", @@ -252,6 +241,16 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan provider: "common-sql", version: "1.0.0" }, + ["airflow", "operators", "jdbc_operator", "JdbcOperator"] + | ["airflow", "operators", "mssql_operator", "MsSqlOperator"] + | ["airflow", "operators", "mysql_operator", "MySqlOperator"] + | ["airflow", "operators", "oracle_operator", "OracleOperator"] + | ["airflow", "operators", "postgres_operator", "PostgresOperator"] + | ["airflow", "operators", "sqlite_operator", "SqliteOperator"] => ProviderReplacement::ProviderName { + name: "airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator", + provider: "common-sql", + version: "1.3.0" + }, // apache-airflow-providers-daskexecutor ["airflow", "executors", "dask_executor", "DaskExecutor"] => ProviderReplacement::ProviderName { @@ -282,11 +281,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan provider: "apache-druid", version: "1.0.0" }, - ["airflow", "operators", "druid_check_operator", "DruidCheckOperator"] => ProviderReplacement::ProviderName { - name: "DruidCheckOperator", - provider: "apache-druid", - version: "1.0.0" - }, ["airflow", "operators", "hive_to_druid", "HiveToDruidOperator" | "HiveToDruidTransfer"] => ProviderReplacement::ProviderName { name: "airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator", provider: "apache-druid", @@ -330,7 +324,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan version: "1.0.0" }, ["airflow", "auth", "managers", "fab", "fab_auth_manager", "FabAuthManager"] => ProviderReplacement::ProviderName { - name: "airflow.providers.fab.auth_manager.security_manager.FabAuthManager", + name: "airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager", provider: "fab", version: "1.0.0" }, @@ -398,7 +392,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan version: "1.0.0" }, ["airflow", "operators", "hive_to_samba_operator", "HiveToSambaOperator"] => ProviderReplacement::ProviderName { - name: "HiveToSambaOperator", + name: "airflow.providers.apache.hive.transfers.hive_to_samba.HiveToSambaOperator", provider: "apache-hive", version: "1.0.0" }, @@ -440,9 +434,9 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan version: "1.0.0" }, ["airflow", "operators", "http_operator", "SimpleHttpOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.http.operators.http.SimpleHttpOperator", + name: "airflow.providers.http.operators.http.HttpOperator", provider: "http", - version: "1.0.0" + version: "5.0.0" }, ["airflow", "sensors", "http_sensor", "HttpSensor"] => ProviderReplacement::ProviderName { name: "airflow.providers.http.sensors.http.HttpSensor", @@ -460,11 +454,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan provider: "jdbc", version: "1.0.0" }, - ["airflow", "operators", "jdbc_operator", "JdbcOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.jdbc.operators.jdbc.JdbcOperator", - provider: "jdbc", - version: "1.0.0" - }, // apache-airflow-providers-cncf-kubernetes ["airflow", "executors", "kubernetes_executor_types", rest @ ( @@ -491,15 +480,23 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan | "get_kube_client" )] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), - module: "airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client", + module: "airflow.providers.cncf.kubernetes.kube_client", provider: "cncf-kubernetes", version: "7.4.0" }, + ["airflow", "kubernetes", "kubernetes_helper_functions", "add_pod_suffix"] => ProviderReplacement::ProviderName { + name: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_unique_suffix", + provider: "cncf-kubernetes", + version: "10.0.0" + }, + ["airflow", "kubernetes", "kubernetes_helper_functions", "create_pod_id"] => ProviderReplacement::ProviderName { + name: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions.create_unique_id", + provider: "cncf-kubernetes", + version: "10.0.0" + }, ["airflow", "kubernetes", "kubernetes_helper_functions", rest @ ( - "add_pod_suffix" | "annotations_for_logging_task_metadata" | "annotations_to_key" - | "create_pod_id" | "get_logs_task_metadata" | "rand_str" )] => ProviderReplacement::SourceModuleMovedToProvider { @@ -522,27 +519,35 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan _ => return }, ["airflow", "kubernetes", "pod_generator", rest ] => match *rest { - "datetime_to_label_safe_datestring" | - "extend_object_field" | - "label_safe_datestring_to_datetime" | - "make_safe_label_value" | - "merge_objects" | - "PodGenerator" | - "PodDefaults" => ProviderReplacement::SourceModuleMovedToProvider { + "datetime_to_label_safe_datestring" + | "extend_object_field" + | "label_safe_datestring_to_datetime" + | "make_safe_label_value" + | "merge_objects" + | "PodGenerator" => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.cncf.kubernetes.pod_generator", provider: "cncf-kubernetes", version: "7.4.0" }, + "PodDefaults" => ProviderReplacement::ProviderName { + name: "airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults", + provider: "cncf-kubernetes", + version: "7.4.0" + }, "PodGeneratorDeprecated" => ProviderReplacement::ProviderName { name: "airflow.providers.cncf.kubernetes.pod_generator.PodGenerator", provider: "cncf-kubernetes", version: "7.4.0" }, - "add_pod_suffix" | + "add_pod_suffix" => ProviderReplacement::ProviderName { + name: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_unique_suffix", + provider: "cncf-kubernetes", + version: "10.0.0" + }, "rand_str" => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", + name: "rand_str".to_string(), provider: "cncf-kubernetes", version: "7.4.0" }, @@ -550,37 +555,33 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan }, ["airflow", "kubernetes", "pod_generator_deprecated", rest @ ( "make_safe_label_value" - | "PodDefaults" | "PodGenerator" )] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), - module: "airflow.providers.cncf.kubernetes.pod_generator_deprecated", + module: "airflow.providers.cncf.kubernetes.pod_generator", provider: "cncf-kubernetes", version: "7.4.0" }, - ["airflow", "kubernetes", "pod_launcher", rest @( - "PodLauncher" - | "PodStatus" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.cncf.kubernetes.pod_launcher_deprecated", + ["airflow", "kubernetes", "pod_generator_deprecated" | "pod_launcher_deprecated", "PodDefaults"] => ProviderReplacement::ProviderName { + name: "airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults", provider: "cncf-kubernetes", version: "7.4.0" }, - ["airflow", "kubernetes", "pod_launcher_deprecated", rest] => match *rest { - "PodLauncher" | "PodStatus" | "PodDefaults" => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.cncf.kubernetes.pod_launcher_deprecated", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - "get_kube_client" => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.kube_client.get_kube_client", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - _ => return, - } + ["airflow", "kubernetes", "pod_launcher_deprecated", "get_kube_client"] => ProviderReplacement::ProviderName { + name: "airflow.providers.cncf.kubernetes.kube_client.get_kube_client", + provider: "cncf-kubernetes", + version: "7.4.0" + }, + ["airflow", "kubernetes", "pod_launcher" | "pod_launcher_deprecated", "PodLauncher"] => ProviderReplacement::ProviderName { + name: "airflow.providers.cncf.kubernetes.utils.pod_manager.PodManager", + provider: "cncf-kubernetes", + version: "3.0.0" + }, + ["airflow", "kubernetes", "pod_launcher" | "pod_launcher_deprecated", "PodStatus"] => ProviderReplacement::ProviderName { + name: " airflow.providers.cncf.kubernetes.utils.pod_manager.PodPhase", + provider: "cncf-kubernetes", + version: "3.0.0" + }, ["airflow", "kubernetes", "pod_runtime_info_env", "PodRuntimeInfoEnv"] => ProviderReplacement::ProviderName { name: "kubernetes.client.models.V1EnvVar", provider: "cncf-kubernetes", @@ -616,11 +617,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan provider: "microsoft-mssql", version: "1.0.0" }, - ["airflow", "operators", "mssql_operator", "MsSqlOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.microsoft.mssql.operators.mssql.MsSqlOperator", - provider: "microsoft-mssql", - version: "1.0.0" - }, // apache-airflow-providers-mysql ["airflow", "hooks", "mysql_hook", "MySqlHook"] => ProviderReplacement::ProviderName { @@ -628,11 +624,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan provider: "mysql", version: "1.0.0" }, - ["airflow", "operators", "mysql_operator", "MySqlOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.mysql.operators.mysql.MySqlOperator", - provider: "mysql", - version: "1.0.0" - }, ["airflow", "operators", "presto_to_mysql", "PrestoToMySqlOperator" | "PrestoToMySqlTransfer"] => ProviderReplacement::ProviderName { name: "airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator", provider: "mysql", @@ -645,11 +636,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan provider: "oracle", version: "1.0.0" }, - ["airflow", "operators", "oracle_operator", "OracleOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.oracle.operators.oracle.OracleOperator", - provider: "oracle", - version: "1.0.0" - }, // apache-airflow-providers-papermill ["airflow", "operators", "papermill_operator", "PapermillOperator"] => ProviderReplacement::ProviderName { @@ -676,15 +662,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan provider: "postgres", version: "1.0.0" }, - ["airflow", "operators", "postgres_operator", rest @ ( - "Mapping" - | "PostgresOperator" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.postgres.operators.postgres", - provider: "postgres", - version: "1.0.0" - }, + ["airflow", "operators", "postgres_operator", "Mapping"] => ProviderReplacement::None, // apache-airflow-providers-presto ["airflow", "hooks", "presto_hook", "PrestoHook"] => ProviderReplacement::ProviderName { @@ -729,11 +707,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan provider: "sqlite", version: "1.0.0" }, - ["airflow", "operators", "sqlite_operator", "SqliteOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.sqlite.operators.sqlite.SqliteOperator", - provider: "sqlite", - version: "1.0.0" - }, // apache-airflow-providers-zendesk ["airflow", "hooks", "zendesk_hook", "ZendeskHook"] => diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs index 45b9648083cd6b..32219eff4d57c5 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs @@ -41,6 +41,9 @@ impl Violation for Airflow3SuggestedToMoveToProvider { replacement, } = self; match replacement { + ProviderReplacement::None => { + format!("`{deprecated}` is removed in Airflow 3.0") + } ProviderReplacement::ProviderName { name: _, provider, @@ -62,18 +65,19 @@ impl Violation for Airflow3SuggestedToMoveToProvider { fn fix_title(&self) -> Option { let Airflow3SuggestedToMoveToProvider { replacement, .. } = self; match replacement { - ProviderReplacement::ProviderName { - name, - provider, - version, - } => { - Some(format!( - "Install `apache-airflow-providers-{provider}>={version}` and use `{name}` instead." - )) - }, - ProviderReplacement::SourceModuleMovedToProvider { + ProviderReplacement::None => None, + ProviderReplacement::ProviderName { name, + provider, + version, + } => { + Some(format!( + "Install `apache-airflow-providers-{provider}>={version}` and use `{name}` instead." + )) + }, + ProviderReplacement::SourceModuleMovedToProvider { module, + name, provider, version, } => { diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap index 6f5e159a46a9fc..6d8ce502c7b59c 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap @@ -75,7 +75,7 @@ AIR302.py:232:1: AIR302 `airflow.operators.s3_file_transform_operator.S3FileTran 233 | S3Hook() 234 | SSQLTableCheckOperator3KeySensor() | - = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.operators.s3_file_transform.S3FileTransformOperator` instead. + = help: Install `apache-airflow-providers-amazon>=3.0.0` and use `airflow.providers.amazon.aws.operators.s3.S3FileTransformOperator` instead. AIR302.py:233:1: AIR302 `airflow.hooks.S3_hook.S3Hook` is moved into `amazon` provider in Airflow 3.0; | @@ -304,7 +304,7 @@ AIR302.py:259:1: AIR302 `airflow.operators.check_operator.SQLCheckOperator` is m | = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. -AIR302.py:260:1: AIR302 `airflow.operators.presto_check_operator.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; +AIR302.py:260:1: AIR302 `airflow.operators.presto_check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; | 258 | PrestoValueCheckOperator() 259 | SQLCheckOperator() @@ -348,7 +348,7 @@ AIR302.py:263:1: AIR302 `airflow.operators.check_operator.SQLIntervalCheckOperat | = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. -AIR302.py:264:1: AIR302 `airflow.operators.presto_check_operator.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; +AIR302.py:264:1: AIR302 `airflow.operators.presto_check_operator.IntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | 262 | SQLColumnCheckOperator2() 263 | SQLIntervalCheckOperator() @@ -403,7 +403,7 @@ AIR302.py:269:1: AIR302 `airflow.operators.check_operator.SQLValueCheckOperator` | = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. -AIR302.py:270:1: AIR302 `airflow.operators.presto_check_operator.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; +AIR302.py:270:1: AIR302 `airflow.operators.presto_check_operator.ValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | 268 | SQLThresholdCheckOperator2() 269 | SQLValueCheckOperator() @@ -423,7 +423,7 @@ AIR302.py:271:1: AIR302 `airflow.operators.sql.SQLValueCheckOperator` is moved i 272 | SqlSensor() 273 | SqlSensor2() | - = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. AIR302.py:272:1: AIR302 `airflow.sensors.sql.SqlSensor` is moved into `common-sql` provider in Airflow 3.0; | @@ -507,7 +507,7 @@ AIR302.py:286:1: AIR302 `airflow.hooks.druid_hook.DruidHook` is moved into `apac | = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidHook` instead. -AIR302.py:287:1: AIR302 `airflow.operators.druid_check_operator.DruidCheckOperator` is moved into `apache-druid` provider in Airflow 3.0; +AIR302.py:287:1: AIR302 `airflow.operators.druid_check_operator.DruidCheckOperator` is moved into `common-sql` provider in Airflow 3.0; | 285 | DruidDbApiHook() 286 | DruidHook() @@ -516,7 +516,7 @@ AIR302.py:287:1: AIR302 `airflow.operators.druid_check_operator.DruidCheckOperat 288 | 289 | # apache-airflow-providers-apache-hdfs | - = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `DruidCheckOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. AIR302.py:290:1: AIR302 `airflow.hooks.webhdfs_hook.WebHDFSHook` is moved into `apache-hdfs` provider in Airflow 3.0; | @@ -667,7 +667,7 @@ AIR302.py:305:1: AIR302 `airflow.operators.hive_to_samba_operator.HiveToSambaOpe 306 | S3ToHiveOperator() 307 | S3ToHiveTransfer() | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `HiveToSambaOperator` instead. + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_samba.HiveToSambaOperator` instead. AIR302.py:306:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; | @@ -741,7 +741,7 @@ AIR302.py:314:1: AIR302 `airflow.operators.http_operator.SimpleHttpOperator` is 315 | 316 | # apache-airflow-providers-jdbc | - = help: Install `apache-airflow-providers-http>=1.0.0` and use `airflow.providers.http.operators.http.SimpleHttpOperator` instead. + = help: Install `apache-airflow-providers-http>=5.0.0` and use `airflow.providers.http.operators.http.HttpOperator` instead. AIR302.py:317:1: AIR302 `airflow.hooks.jdbc_hook.jaydebeapi` is moved into `jdbc` provider in Airflow 3.0; | @@ -763,7 +763,7 @@ AIR302.py:318:1: AIR302 `airflow.hooks.jdbc_hook.JdbcHook` is moved into `jdbc` | = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.JdbcHook` instead. -AIR302.py:319:1: AIR302 `airflow.operators.jdbc_operator.JdbcOperator` is moved into `jdbc` provider in Airflow 3.0; +AIR302.py:319:1: AIR302 `airflow.operators.jdbc_operator.JdbcOperator` is moved into `common-sql` provider in Airflow 3.0; | 317 | jaydebeapi 318 | JdbcHook() @@ -772,7 +772,7 @@ AIR302.py:319:1: AIR302 `airflow.operators.jdbc_operator.JdbcOperator` is moved 320 | 321 | # apache-airflow-providers-fab | - = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `airflow.providers.jdbc.operators.jdbc.JdbcOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. AIR302.py:322:12: AIR302 `airflow.api.auth.backend.basic_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; | @@ -889,7 +889,7 @@ AIR302.py:335:1: AIR302 `airflow.auth.managers.fab.fab_auth_manager.FabAuthManag | ^^^^^^^^^^^^^^ AIR302 336 | FabAirflowSecurityManagerOverride() | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.FabAuthManager` instead. + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager` instead. AIR302.py:336:1: AIR302 `airflow.www.security.FabAirflowSecurityManagerOverride` is moved into `fab` provider in Airflow 3.0; | @@ -942,7 +942,7 @@ AIR302.py:344:1: AIR302 `airflow.kubernetes.kube_client._disable_verify_ssl` is 345 | _enable_tcp_keepalive() 346 | append_to_pod() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client._disable_verify_ssl` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client._disable_verify_ssl` instead. AIR302.py:345:1: AIR302 `airflow.kubernetes.kube_client._enable_tcp_keepalive` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -953,7 +953,7 @@ AIR302.py:345:1: AIR302 `airflow.kubernetes.kube_client._enable_tcp_keepalive` i 346 | append_to_pod() 347 | annotations_for_logging_task_metadata() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client._enable_tcp_keepalive` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client._enable_tcp_keepalive` instead. AIR302.py:346:1: AIR302 `airflow.kubernetes.k8s_model.append_to_pod` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -997,7 +997,7 @@ AIR302.py:349:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.create_p 350 | datetime_to_label_safe_datestring() 351 | extend_object_field() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.create_pod_id` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.create_unique_id` instead. AIR302.py:350:1: AIR302 `airflow.kubernetes.pod_generator.datetime_to_label_safe_datestring` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1139,7 +1139,7 @@ AIR302.py:363:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.add_pod_ 364 | add_pod_suffix2() 365 | get_kube_client() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_pod_suffix` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_unique_suffix` instead. AIR302.py:364:1: AIR302 `airflow.kubernetes.pod_generator.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1149,7 +1149,7 @@ AIR302.py:364:1: AIR302 `airflow.kubernetes.pod_generator.add_pod_suffix` is mov 365 | get_kube_client() 366 | get_kube_client2() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_pod_suffix` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_unique_suffix` instead. AIR302.py:365:1: AIR302 `airflow.kubernetes.kube_client.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1160,7 +1160,7 @@ AIR302.py:365:1: AIR302 `airflow.kubernetes.kube_client.get_kube_client` is move 366 | get_kube_client2() 367 | make_safe_label_value() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. AIR302.py:366:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1193,7 +1193,7 @@ AIR302.py:368:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.make_safe_l 369 | rand_str() 370 | rand_str2() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.make_safe_label_value` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.make_safe_label_value` instead. AIR302.py:369:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1237,7 +1237,7 @@ AIR302.py:373:1: AIR302 `airflow.kubernetes.pod_launcher.PodLauncher` is moved i 374 | PodLauncher2() 375 | PodStatus() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodLauncher` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use `airflow.providers.cncf.kubernetes.utils.pod_manager.PodManager` instead. AIR302.py:374:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1248,7 +1248,7 @@ AIR302.py:374:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodLauncher` 375 | PodStatus() 376 | PodStatus2() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodLauncher` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use `airflow.providers.cncf.kubernetes.utils.pod_manager.PodManager` instead. AIR302.py:375:1: AIR302 `airflow.kubernetes.pod_launcher.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1259,7 +1259,7 @@ AIR302.py:375:1: AIR302 `airflow.kubernetes.pod_launcher.PodStatus` is moved int 376 | PodStatus2() 377 | PodDefaults() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodStatus` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use ` airflow.providers.cncf.kubernetes.utils.pod_manager.PodPhase` instead. AIR302.py:376:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1270,7 +1270,7 @@ AIR302.py:376:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodStatus` i 377 | PodDefaults() 378 | PodDefaults2() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodStatus` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use ` airflow.providers.cncf.kubernetes.utils.pod_manager.PodPhase` instead. AIR302.py:377:1: AIR302 `airflow.kubernetes.pod_generator.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1281,7 +1281,7 @@ AIR302.py:377:1: AIR302 `airflow.kubernetes.pod_generator.PodDefaults` is moved 378 | PodDefaults2() 379 | PodDefaults3() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodDefaults` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults` instead. AIR302.py:378:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1292,7 +1292,7 @@ AIR302.py:378:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodDefaults` 379 | PodDefaults3() 380 | PodGenerator() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodDefaults` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults` instead. AIR302.py:379:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1303,7 +1303,7 @@ AIR302.py:379:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodDefaults 380 | PodGenerator() 381 | PodGenerator2() | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodDefaults` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults` instead. AIR302.py:380:1: AIR302 `airflow.kubernetes.pod_generator.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; | @@ -1322,7 +1322,7 @@ AIR302.py:381:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodGenerato 381 | PodGenerator2() | ^^^^^^^^^^^^^ AIR302 | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodGenerator` instead. + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. AIR302.py:385:1: AIR302 `airflow.hooks.mssql_hook.MsSqlHook` is moved into `microsoft-mssql` provider in Airflow 3.0; | @@ -1334,7 +1334,7 @@ AIR302.py:385:1: AIR302 `airflow.hooks.mssql_hook.MsSqlHook` is moved into `micr | = help: Install `apache-airflow-providers-microsoft-mssql>=1.0.0` and use `airflow.providers.microsoft.mssql.hooks.mssql.MsSqlHook` instead. -AIR302.py:386:1: AIR302 `airflow.operators.mssql_operator.MsSqlOperator` is moved into `microsoft-mssql` provider in Airflow 3.0; +AIR302.py:386:1: AIR302 `airflow.operators.mssql_operator.MsSqlOperator` is moved into `common-sql` provider in Airflow 3.0; | 384 | # apache-airflow-providers-microsoft-mssql 385 | MsSqlHook() @@ -1343,7 +1343,7 @@ AIR302.py:386:1: AIR302 `airflow.operators.mssql_operator.MsSqlOperator` is move 387 | MsSqlToHiveOperator() 388 | MsSqlToHiveTransfer() | - = help: Install `apache-airflow-providers-microsoft-mssql>=1.0.0` and use `airflow.providers.microsoft.mssql.operators.mssql.MsSqlOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. AIR302.py:387:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; | @@ -1398,7 +1398,7 @@ AIR302.py:393:1: AIR302 `airflow.hooks.mysql_hook.MySqlHook` is moved into `mysq | = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.hooks.mysql.MySqlHook` instead. -AIR302.py:394:1: AIR302 `airflow.operators.mysql_operator.MySqlOperator` is moved into `mysql` provider in Airflow 3.0; +AIR302.py:394:1: AIR302 `airflow.operators.mysql_operator.MySqlOperator` is moved into `common-sql` provider in Airflow 3.0; | 392 | HiveToMySqlTransfer() 393 | MySqlHook() @@ -1407,7 +1407,7 @@ AIR302.py:394:1: AIR302 `airflow.operators.mysql_operator.MySqlOperator` is move 395 | MySqlToHiveOperator() 396 | MySqlToHiveTransfer() | - = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.operators.mysql.MySqlOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. AIR302.py:395:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; | @@ -1461,7 +1461,7 @@ AIR302.py:401:1: AIR302 `airflow.hooks.oracle_hook.OracleHook` is moved into `or | = help: Install `apache-airflow-providers-oracle>=1.0.0` and use `airflow.providers.oracle.hooks.oracle.OracleHook` instead. -AIR302.py:402:1: AIR302 `airflow.operators.oracle_operator.OracleOperator` is moved into `oracle` provider in Airflow 3.0; +AIR302.py:402:1: AIR302 `airflow.operators.oracle_operator.OracleOperator` is moved into `common-sql` provider in Airflow 3.0; | 400 | # apache-airflow-providers-oracle 401 | OracleHook() @@ -1470,7 +1470,7 @@ AIR302.py:402:1: AIR302 `airflow.operators.oracle_operator.OracleOperator` is mo 403 | 404 | # apache-airflow-providers-papermill | - = help: Install `apache-airflow-providers-oracle>=1.0.0` and use `airflow.providers.oracle.operators.oracle.OracleOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. AIR302.py:405:1: AIR302 `airflow.operators.papermill_operator.PapermillOperator` is moved into `papermill` provider in Airflow 3.0; | @@ -1502,7 +1502,7 @@ AIR302.py:409:1: AIR302 `airflow.operators.pig_operator.PigOperator` is moved in | = help: Install `apache-airflow-providers-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.operators.pig.PigOperator` instead. -AIR302.py:412:1: AIR302 `airflow.operators.postgres_operator.Mapping` is moved into `postgres` provider in Airflow 3.0; +AIR302.py:412:1: AIR302 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0 | 411 | # apache-airflow-providers-postgres 412 | Mapping @@ -1510,7 +1510,6 @@ AIR302.py:412:1: AIR302 `airflow.operators.postgres_operator.Mapping` is moved i 413 | PostgresHook() 414 | PostgresOperator() | - = help: Install `apache-airflow-providers-postgres>=1.0.0` and use `airflow.providers.postgres.operators.postgres.Mapping` instead. AIR302.py:413:1: AIR302 `airflow.hooks.postgres_hook.PostgresHook` is moved into `postgres` provider in Airflow 3.0; | @@ -1522,7 +1521,7 @@ AIR302.py:413:1: AIR302 `airflow.hooks.postgres_hook.PostgresHook` is moved into | = help: Install `apache-airflow-providers-postgres>=1.0.0` and use `airflow.providers.postgres.hooks.postgres.PostgresHook` instead. -AIR302.py:414:1: AIR302 `airflow.operators.postgres_operator.PostgresOperator` is moved into `postgres` provider in Airflow 3.0; +AIR302.py:414:1: AIR302 `airflow.operators.postgres_operator.PostgresOperator` is moved into `common-sql` provider in Airflow 3.0; | 412 | Mapping 413 | PostgresHook() @@ -1531,7 +1530,7 @@ AIR302.py:414:1: AIR302 `airflow.operators.postgres_operator.PostgresOperator` i 415 | 416 | # apache-airflow-providers-presto | - = help: Install `apache-airflow-providers-postgres>=1.0.0` and use `airflow.providers.postgres.operators.postgres.PostgresOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. AIR302.py:417:1: AIR302 `airflow.hooks.presto_hook.PrestoHook` is moved into `presto` provider in Airflow 3.0; | @@ -1593,7 +1592,7 @@ AIR302.py:428:1: AIR302 `airflow.hooks.sqlite_hook.SqliteHook` is moved into `sq | = help: Install `apache-airflow-providers-sqlite>=1.0.0` and use `airflow.providers.sqlite.hooks.sqlite.SqliteHook` instead. -AIR302.py:429:1: AIR302 `airflow.operators.sqlite_operator.SqliteOperator` is moved into `sqlite` provider in Airflow 3.0; +AIR302.py:429:1: AIR302 `airflow.operators.sqlite_operator.SqliteOperator` is moved into `common-sql` provider in Airflow 3.0; | 427 | # apache-airflow-providers-sqlite 428 | SqliteHook() @@ -1602,7 +1601,7 @@ AIR302.py:429:1: AIR302 `airflow.operators.sqlite_operator.SqliteOperator` is mo 430 | 431 | # apache-airflow-providers-zendesk | - = help: Install `apache-airflow-providers-sqlite>=1.0.0` and use `airflow.providers.sqlite.operators.sqlite.SqliteOperator` instead. + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. AIR302.py:432:1: AIR302 `airflow.hooks.zendesk_hook.ZendeskHook` is moved into `zendesk` provider in Airflow 3.0; | From aba21a5d474a98a8ce4c88873e218bb20d46917a Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Sat, 26 Apr 2025 01:49:32 +0900 Subject: [PATCH 0123/1161] [`airflow`] Extend `AIR301` rule (#17598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add "airflow.operators.python.get_current_context" → "airflow.sdk.get_current_context" rule ## Test Plan the test fixture has been updated accordingly --- .../resources/test/fixtures/airflow/AIR301_names.py | 5 +++++ .../src/rules/airflow/rules/removal_in_3.rs | 4 ++++ ...rules__airflow__tests__AIR301_AIR301_names.py.snap | 11 +++++++++++ 3 files changed, 20 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py index ccab278a0fc18b..3d5018719cc998 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names.py @@ -150,3 +150,8 @@ # airflow.www.utils get_sensitive_variables_fields should_hide_value_for_key + +# airflow.operators.python +from airflow.operators.python import get_current_context + +get_current_context() diff --git a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs index a70403510323d8..7db3e1ebbd944b 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs @@ -672,6 +672,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { ["airflow", "operators", "subdag", ..] => { Replacement::Message("The whole `airflow.subdag` module has been removed.") } + ["airflow", "operators", "python", "get_current_context"] => Replacement::AutoImport { + module: "airflow.sdk", + name: "get_current_context", + }, // airflow.secrets ["airflow", "secrets", "local_filesystem", "load_connections"] => Replacement::AutoImport { diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap index 8c58bcb351eff8..a45ba0bce2e168 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR301_AIR301_names.py.snap @@ -433,5 +433,16 @@ AIR301_names.py:152:1: AIR301 `airflow.www.utils.should_hide_value_for_key` is r 151 | get_sensitive_variables_fields 152 | should_hide_value_for_key | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301 +153 | +154 | # airflow.operators.python | = help: Use `airflow.utils.log.secrets_masker.should_hide_value_for_key` instead + +AIR301_names.py:157:1: AIR301 `airflow.operators.python.get_current_context` is removed in Airflow 3.0 + | +155 | from airflow.operators.python import get_current_context +156 | +157 | get_current_context() + | ^^^^^^^^^^^^^^^^^^^ AIR301 + | + = help: Use `airflow.sdk.get_current_context` instead From bc0a5aa4098a5b4d03365a22384b0307a124391c Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 25 Apr 2025 13:05:58 -0400 Subject: [PATCH 0124/1161] ruff_db: add tests for annotations with no ranges ... and fix the case where an annotation with a `Span` but no `TextRange` or message gets completely dropped. --- crates/ruff_db/src/diagnostic/render.rs | 61 ++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 111af97b6d214c..ece51ae381d4ea 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -286,8 +286,7 @@ impl<'a> ResolvedAnnotation<'a> { let source = input.as_source_code(); let (range, line_start, line_end) = match (ann.span.range(), ann.message.is_some()) { // An annotation with no range AND no message is probably(?) - // meaningless, so just ignore it. - (None, false) => return None, + // meaningless, but we should try to render it anyway. (None, _) => ( TextRange::empty(TextSize::new(0)), OneIndexed::MIN, @@ -815,6 +814,50 @@ watermelon ); } + #[test] + fn no_range() { + let mut env = TestEnvironment::new(); + env.add("animals", ANIMALS); + + let mut builder = env.err(); + builder + .diag + .annotate(Annotation::primary(builder.env.path("animals"))); + let diag = builder.build(); + insta::assert_snapshot!( + env.render(&diag), + @r" + error: lint:test-diagnostic: main diagnostic message + --> /animals:1:1 + | + 1 | aardvark + | ^ + 2 | beetle + 3 | canary + | + ", + ); + + let mut builder = env.err(); + builder.diag.annotate( + Annotation::primary(builder.env.path("animals")).message("primary annotation message"), + ); + let diag = builder.build(); + insta::assert_snapshot!( + env.render(&diag), + @r" + error: lint:test-diagnostic: main diagnostic message + --> /animals:1:1 + | + 1 | aardvark + | ^ primary annotation message + 2 | beetle + 3 | canary + | + ", + ); + } + #[test] fn non_ascii() { let mut env = TestEnvironment::new(); @@ -2082,10 +2125,10 @@ watermelon /// otherwise, the span will end where the next line begins, and this /// confuses `ruff_annotate_snippets` as of 2025-03-13.) fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span { - let file = system_path_to_file(&self.db, path).unwrap(); + let span = self.path(path); - let text = source_text(&self.db, file); - let line_index = line_index(&self.db, file); + let text = source_text(&self.db, span.file()); + let line_index = line_index(&self.db, span.file()); let source = SourceCode::new(text.as_str(), &line_index); let (line_start, offset_start) = parse_line_offset(line_offset_start); @@ -2099,7 +2142,13 @@ watermelon None => source.line_end(line_end) - TextSize::from(1), Some(offset) => source.line_start(line_end) + offset, }; - Span::from(file).with_range(TextRange::new(start, end)) + span.with_range(TextRange::new(start, end)) + } + + /// Like `span`, but only attaches a file path. + fn path(&self, path: &str) -> Span { + let file = system_path_to_file(&self.db, path).unwrap(); + Span::from(file) } /// A convenience function for returning a builder for a diagnostic From 6ab32a77467152411d0deab57ff426991fc6a12e Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 25 Apr 2025 14:10:03 -0400 Subject: [PATCH 0125/1161] [red-knot] Create generic context for generic classes lazily (#17617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As discussed today, this is needed to handle legacy generic classes without having to infer the types of the class's explicit bases eagerly at class construction time. Pulling this out into a separate PR so there's a smaller diff to review. This also makes our representation of generic classes and functions more consistent — before, we had separate Rust types and enum variants for generic/non-generic classes, but a single type for generic functions. Now we each a single (respective) type for each. There were very few places we were differentiation between generic and non-generic _class literals_, and these are handled now by calling the (salsa cached) `generic_context` _accessor function_. Note that _`ClassType`_ is still an enum with distinct variants for non-generic classes and specialized generic classes. --- .../resources/mdtest/mro.md | 4 +- crates/red_knot_python_semantic/src/types.rs | 48 +-- .../src/types/class.rs | 294 +++++++----------- .../src/types/diagnostic.rs | 4 +- .../src/types/display.rs | 8 +- .../src/types/infer.rs | 84 ++--- .../red_knot_python_semantic/src/types/mro.rs | 20 +- .../src/types/narrow.rs | 8 +- .../src/types/slots.rs | 6 +- 9 files changed, 200 insertions(+), 276 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/mro.md b/crates/red_knot_python_semantic/resources/mdtest/mro.md index 7803e45e562883..9d0a9264699b0a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/mro.md +++ b/crates/red_knot_python_semantic/resources/mdtest/mro.md @@ -191,8 +191,8 @@ reveal_type(AA.__mro__) # revealed: tuple[Literal[AA], Literal[Z], Unknown, Lit ## `__bases__` includes a `Union` -We don't support union types in a class's bases; a base must resolve to a single `ClassLiteralType`. -If we find a union type in a class's bases, we infer the class's `__mro__` as being +We don't support union types in a class's bases; a base must resolve to a single `ClassType`. If we +find a union type in a class's bases, we infer the class's `__mro__` as being `[, Unknown, object]`, the same as for MROs that cause errors at runtime. ```py diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ab190dd4485688..5b4c37d39cff50 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -49,9 +49,7 @@ use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; use crate::types::signatures::{Parameter, ParameterForm, Parameters}; use crate::{Db, FxOrderSet, Module, Program}; -pub(crate) use class::{ - Class, ClassLiteralType, ClassType, GenericAlias, GenericClass, KnownClass, NonGenericClass, -}; +pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass}; pub(crate) use instance::InstanceType; pub(crate) use known_instance::KnownInstanceType; @@ -470,7 +468,7 @@ pub enum Type<'db> { /// A specific module object ModuleLiteral(ModuleLiteralType<'db>), /// A specific class object - ClassLiteral(ClassLiteralType<'db>), + ClassLiteral(ClassLiteral<'db>), /// A specialization of a generic class GenericAlias(GenericAlias<'db>), /// The set of all class objects that are subclasses of the given class (C), spelled `type[C]`. @@ -667,7 +665,7 @@ impl<'db> Type<'db> { } } - pub const fn into_class_literal(self) -> Option> { + pub const fn into_class_literal(self) -> Option> { match self { Type::ClassLiteral(class_type) => Some(class_type), _ => None, @@ -675,7 +673,7 @@ impl<'db> Type<'db> { } #[track_caller] - pub fn expect_class_literal(self) -> ClassLiteralType<'db> { + pub fn expect_class_literal(self) -> ClassLiteral<'db> { self.into_class_literal() .expect("Expected a Type::ClassLiteral variant") } @@ -705,11 +703,12 @@ impl<'db> Type<'db> { .expect("Expected a Type::GenericAlias or Type::ClassLiteral variant") } - pub const fn is_class_type(&self) -> bool { - matches!( - self, - Type::ClassLiteral(ClassLiteralType::NonGeneric(_)) | Type::GenericAlias(_) - ) + pub fn is_class_type(&self, db: &'db dyn Db) -> bool { + match self { + Type::ClassLiteral(class) if class.generic_context(db).is_none() => true, + Type::GenericAlias(_) => true, + _ => false, + } } pub const fn is_property_instance(&self) -> bool { @@ -4193,13 +4192,16 @@ impl<'db> Type<'db> { // do this, we instead use the _identity_ specialization, which maps each of the class's // generic typevars to itself. let (generic_origin, self_type) = match self { - Type::ClassLiteral(ClassLiteralType::Generic(generic)) => { - let specialization = generic.generic_context(db).identity_specialization(db); - ( - Some(generic), - Type::GenericAlias(GenericAlias::new(db, generic, specialization)), - ) - } + Type::ClassLiteral(class) => match class.generic_context(db) { + Some(generic_context) => { + let specialization = generic_context.identity_specialization(db); + ( + Some(class), + Type::GenericAlias(GenericAlias::new(db, class, specialization)), + ) + } + _ => (None, self), + }, _ => (None, self), }; @@ -6806,7 +6808,7 @@ impl<'db> TypeAliasType<'db> { #[derive(Debug, Clone, PartialEq, Eq, salsa::Update)] pub(super) struct MetaclassCandidate<'db> { metaclass: ClassType<'db>, - explicit_metaclass_of: ClassLiteralType<'db>, + explicit_metaclass_of: ClassLiteral<'db>, } #[salsa::interned(debug)] @@ -7655,11 +7657,9 @@ impl<'db> BoundSuperType<'db> { // super(B, b_int) // super(B[int], b_unknown) // ``` - match class_literal { - ClassLiteralType::Generic(_) => { - Symbol::bound(todo_type!("super in generic class")).into() - } - ClassLiteralType::NonGeneric(_) => class_literal.class_member_from_mro( + match class_literal.generic_context(db) { + Some(_) => Symbol::bound(todo_type!("super in generic class")).into(), + None => class_literal.class_member_from_mro( db, name, policy, diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index f74e7d276f0712..b3c28473fcde0f 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -50,14 +50,14 @@ fn explicit_bases_cycle_recover<'db>( _db: &'db dyn Db, _value: &[Type<'db>], _count: u32, - _self: ClassLiteralType<'db>, + _self: ClassLiteral<'db>, ) -> salsa::CycleRecoveryAction]>> { salsa::CycleRecoveryAction::Iterate } fn explicit_bases_cycle_initial<'db>( _db: &'db dyn Db, - _self: ClassLiteralType<'db>, + _self: ClassLiteral<'db>, ) -> Box<[Type<'db>]> { Box::default() } @@ -66,7 +66,7 @@ fn try_mro_cycle_recover<'db>( _db: &'db dyn Db, _value: &Result, MroError<'db>>, _count: u32, - _self: ClassLiteralType<'db>, + _self: ClassLiteral<'db>, _specialization: Option>, ) -> salsa::CycleRecoveryAction, MroError<'db>>> { salsa::CycleRecoveryAction::Iterate @@ -75,7 +75,7 @@ fn try_mro_cycle_recover<'db>( #[allow(clippy::unnecessary_wraps)] fn try_mro_cycle_initial<'db>( db: &'db dyn Db, - self_: ClassLiteralType<'db>, + self_: ClassLiteral<'db>, specialization: Option>, ) -> Result, MroError<'db>> { Ok(Mro::from_error( @@ -89,94 +89,28 @@ fn inheritance_cycle_recover<'db>( _db: &'db dyn Db, _value: &Option, _count: u32, - _self: ClassLiteralType<'db>, + _self: ClassLiteral<'db>, ) -> salsa::CycleRecoveryAction> { salsa::CycleRecoveryAction::Iterate } fn inheritance_cycle_initial<'db>( _db: &'db dyn Db, - _self: ClassLiteralType<'db>, + _self: ClassLiteral<'db>, ) -> Option { None } -/// Representation of a class definition statement in the AST. This does not in itself represent a -/// type, but is used as the inner data for several structs that *do* represent types. -#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)] -pub struct Class<'db> { - /// Name of the class at definition - pub(crate) name: ast::name::Name, - - pub(crate) body_scope: ScopeId<'db>, - - pub(crate) known: Option, - - pub(crate) dataclass_params: Option, - pub(crate) dataclass_transformer_params: Option, -} - -impl<'db> Class<'db> { - fn file(&self, db: &dyn Db) -> File { - self.body_scope.file(db) - } - - /// Return the original [`ast::StmtClassDef`] node associated with this class - /// - /// ## Note - /// Only call this function from queries in the same file or your - /// query depends on the AST of another file (bad!). - fn node(&self, db: &'db dyn Db) -> &'db ast::StmtClassDef { - self.body_scope.node(db).expect_class() - } - - fn definition(&self, db: &'db dyn Db) -> Definition<'db> { - let index = semantic_index(db, self.body_scope.file(db)); - index.expect_single_definition(self.body_scope.node(db).expect_class()) - } -} - -/// A [`Class`] that is not generic. -#[salsa::interned(debug)] -pub struct NonGenericClass<'db> { - #[return_ref] - pub(crate) class: Class<'db>, -} - -impl<'db> From> for Type<'db> { - fn from(class: NonGenericClass<'db>) -> Type<'db> { - Type::ClassLiteral(ClassLiteralType::NonGeneric(class)) - } -} - -/// A [`Class`] that is generic. -#[salsa::interned(debug)] -pub struct GenericClass<'db> { - #[return_ref] - pub(crate) class: Class<'db>, - pub(crate) generic_context: GenericContext<'db>, -} - -impl<'db> From> for Type<'db> { - fn from(class: GenericClass<'db>) -> Type<'db> { - Type::ClassLiteral(ClassLiteralType::Generic(class)) - } -} - /// A specialization of a generic class with a particular assignment of types to typevars. #[salsa::interned(debug)] pub struct GenericAlias<'db> { - pub(crate) origin: GenericClass<'db>, + pub(crate) origin: ClassLiteral<'db>, pub(crate) specialization: Specialization<'db>, } impl<'db> GenericAlias<'db> { pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - self.origin(db).class(db).definition(db) - } - - pub(crate) fn class_literal(self, db: &'db dyn Db) -> ClassLiteralType<'db> { - ClassLiteralType::Generic(self.origin(db)) + self.origin(db).definition(db) } } @@ -192,44 +126,37 @@ impl<'db> From> for Type<'db> { Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Supertype, salsa::Update, )] pub enum ClassType<'db> { - NonGeneric(NonGenericClass<'db>), + NonGeneric(ClassLiteral<'db>), Generic(GenericAlias<'db>), } #[salsa::tracked] impl<'db> ClassType<'db> { - fn class(self, db: &'db dyn Db) -> &'db Class<'db> { - match self { - Self::NonGeneric(non_generic) => non_generic.class(db), - Self::Generic(generic) => generic.origin(db).class(db), - } - } - /// Returns the class literal and specialization for this class. For a non-generic class, this /// is the class itself. For a generic alias, this is the alias's origin. pub(crate) fn class_literal( self, db: &'db dyn Db, - ) -> (ClassLiteralType<'db>, Option>) { + ) -> (ClassLiteral<'db>, Option>) { match self { - Self::NonGeneric(non_generic) => (ClassLiteralType::NonGeneric(non_generic), None), - Self::Generic(generic) => ( - ClassLiteralType::Generic(generic.origin(db)), - Some(generic.specialization(db)), - ), + Self::NonGeneric(non_generic) => (non_generic, None), + Self::Generic(generic) => (generic.origin(db), Some(generic.specialization(db))), } } pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { - &self.class(db).name + let (class_literal, _) = self.class_literal(db); + class_literal.name(db) } pub(crate) fn known(self, db: &'db dyn Db) -> Option { - self.class(db).known + let (class_literal, _) = self.class_literal(db); + class_literal.known(db) } pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - self.class(db).definition(db) + let (class_literal, _) = self.class_literal(db); + class_literal.definition(db) } fn specialize_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { @@ -241,7 +168,7 @@ impl<'db> ClassType<'db> { /// Return `true` if this class represents `known_class` pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { - self.class(db).known == Some(known_class) + self.known(db) == Some(known_class) } /// Return `true` if this class represents the builtin class `object` @@ -253,7 +180,7 @@ impl<'db> ClassType<'db> { /// /// If the MRO could not be accurately resolved, this method falls back to iterating /// over an MRO that has the class directly inheriting from `Unknown`. Use - /// [`ClassLiteralType::try_mro`] if you need to distinguish between the success and failure + /// [`ClassLiteral::try_mro`] if you need to distinguish between the success and failure /// cases rather than simply iterating over the inferred resolution order for the class. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order @@ -461,47 +388,40 @@ impl<'db> From> for Type<'db> { } } -/// Represents a single class object at runtime, which might be a non-generic class, or a generic -/// class that has not been specialized. -#[derive( - Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Supertype, salsa::Update, -)] -pub enum ClassLiteralType<'db> { - NonGeneric(NonGenericClass<'db>), - Generic(GenericClass<'db>), -} - -#[salsa::tracked] -impl<'db> ClassLiteralType<'db> { - fn class(self, db: &'db dyn Db) -> &'db Class<'db> { - match self { - Self::NonGeneric(non_generic) => non_generic.class(db), - Self::Generic(generic) => generic.class(db), - } - } +/// Representation of a class definition statement in the AST: either a non-generic class, or a +/// generic class that has not been specialized. +/// +/// This does not in itself represent a type, but can be transformed into a [`ClassType`] that +/// does. (For generic classes, this requires specializing its generic context.) +#[salsa::interned(debug)] +pub struct ClassLiteral<'db> { + /// Name of the class at definition + #[return_ref] + pub(crate) name: ast::name::Name, - pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { - &self.class(db).name - } + pub(crate) body_scope: ScopeId<'db>, - pub(crate) fn known(self, db: &'db dyn Db) -> Option { - self.class(db).known - } + pub(crate) known: Option, - pub(crate) fn dataclass_params(self, db: &'db dyn Db) -> Option { - self.class(db).dataclass_params - } + pub(crate) dataclass_params: Option, + pub(crate) dataclass_transformer_params: Option, +} +#[salsa::tracked] +impl<'db> ClassLiteral<'db> { /// Return `true` if this class represents `known_class` pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool { - self.class(db).known == Some(known_class) + self.known(db) == Some(known_class) } + #[salsa::tracked] pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { - match self { - Self::NonGeneric(_) => None, - Self::Generic(generic) => Some(generic.generic_context(db)), - } + let scope = self.body_scope(db); + let class_def_node = scope.node(db).expect_class(); + class_def_node.type_params.as_ref().map(|type_params| { + let index = semantic_index(db, scope.file(db)); + GenericContext::from_type_params(db, index, type_params) + }) } /// Return `true` if this class represents the builtin class `object` @@ -509,12 +429,23 @@ impl<'db> ClassLiteralType<'db> { self.is_known(db, KnownClass::Object) } - pub(crate) fn body_scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.class(db).body_scope + fn file(self, db: &dyn Db) -> File { + self.body_scope(db).file(db) + } + + /// Return the original [`ast::StmtClassDef`] node associated with this class + /// + /// ## Note + /// Only call this function from queries in the same file or your + /// query depends on the AST of another file (bad!). + fn node(self, db: &'db dyn Db) -> &'db ast::StmtClassDef { + self.body_scope(db).node(db).expect_class() } pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - self.class(db).definition(db) + let body_scope = self.body_scope(db); + let index = semantic_index(db, body_scope.file(db)); + index.expect_single_definition(body_scope.node(db).expect_class()) } pub(crate) fn apply_optional_specialization( @@ -522,14 +453,14 @@ impl<'db> ClassLiteralType<'db> { db: &'db dyn Db, specialization: Option>, ) -> ClassType<'db> { - match (self, specialization) { - (Self::NonGeneric(non_generic), _) => ClassType::NonGeneric(non_generic), - (Self::Generic(generic), None) => { - let specialization = generic.generic_context(db).default_specialization(db); - ClassType::Generic(GenericAlias::new(db, generic, specialization)) + match (self.generic_context(db), specialization) { + (None, _) => ClassType::NonGeneric(self), + (Some(generic_context), None) => { + let specialization = generic_context.default_specialization(db); + ClassType::Generic(GenericAlias::new(db, self, specialization)) } - (Self::Generic(generic), Some(specialization)) => { - ClassType::Generic(GenericAlias::new(db, generic, specialization)) + (Some(_), Some(specialization)) => { + ClassType::Generic(GenericAlias::new(db, self, specialization)) } } } @@ -538,11 +469,11 @@ impl<'db> ClassLiteralType<'db> { /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// applies the default specialization to the class's typevars. pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self { - Self::NonGeneric(non_generic) => ClassType::NonGeneric(non_generic), - Self::Generic(generic) => { - let specialization = generic.generic_context(db).default_specialization(db); - ClassType::Generic(GenericAlias::new(db, generic, specialization)) + match self.generic_context(db) { + None => ClassType::NonGeneric(self), + Some(generic_context) => { + let specialization = generic_context.default_specialization(db); + ClassType::Generic(GenericAlias::new(db, self, specialization)) } } } @@ -551,11 +482,11 @@ impl<'db> ClassLiteralType<'db> { /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// maps each of the class's typevars to `Unknown`. pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self { - Self::NonGeneric(non_generic) => ClassType::NonGeneric(non_generic), - Self::Generic(generic) => { - let specialization = generic.generic_context(db).unknown_specialization(db); - ClassType::Generic(GenericAlias::new(db, generic, specialization)) + match self.generic_context(db) { + None => ClassType::NonGeneric(self), + Some(generic_context) => { + let specialization = generic_context.unknown_specialization(db); + ClassType::Generic(GenericAlias::new(db, self, specialization)) } } } @@ -588,12 +519,11 @@ impl<'db> ClassLiteralType<'db> { #[salsa::tracked(return_ref, cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial)] fn explicit_bases_query(self, db: &'db dyn Db) -> Box<[Type<'db>]> { - let class = self.class(db); - tracing::trace!("ClassLiteralType::explicit_bases_query: {}", class.name); + tracing::trace!("ClassLiteral::explicit_bases_query: {}", self.name(db)); - let class_stmt = class.node(db); + let class_stmt = self.node(db); let class_definition = - semantic_index(db, class.file(db)).expect_single_definition(class_stmt); + semantic_index(db, self.file(db)).expect_single_definition(class_stmt); class_stmt .bases() @@ -616,16 +546,15 @@ impl<'db> ClassLiteralType<'db> { /// Return the types of the decorators on this class #[salsa::tracked(return_ref)] fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { - let class = self.class(db); - tracing::trace!("ClassLiteralType::decorators: {}", class.name); + tracing::trace!("ClassLiteral::decorators: {}", self.name(db)); - let class_stmt = class.node(db); + let class_stmt = self.node(db); if class_stmt.decorator_list.is_empty() { return Box::new([]); } let class_definition = - semantic_index(db, class.file(db)).expect_single_definition(class_stmt); + semantic_index(db, self.file(db)).expect_single_definition(class_stmt); class_stmt .decorator_list @@ -667,8 +596,7 @@ impl<'db> ClassLiteralType<'db> { db: &'db dyn Db, specialization: Option>, ) -> Result, MroError<'db>> { - let class = self.class(db); - tracing::trace!("ClassLiteralType::try_mro: {}", class.name); + tracing::trace!("ClassLiteral::try_mro: {}", self.name(db)); Mro::of_class(db, self, specialization) } @@ -676,7 +604,7 @@ impl<'db> ClassLiteralType<'db> { /// /// If the MRO could not be accurately resolved, this method falls back to iterating /// over an MRO that has the class directly inheriting from `Unknown`. Use - /// [`ClassLiteralType::try_mro`] if you need to distinguish between the success and failure + /// [`ClassLiteral::try_mro`] if you need to distinguish between the success and failure /// cases rather than simply iterating over the inferred resolution order for the class. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order @@ -707,15 +635,14 @@ impl<'db> ClassLiteralType<'db> { /// Only call this function from queries in the same file or your /// query depends on the AST of another file (bad!). fn explicit_metaclass(self, db: &'db dyn Db) -> Option> { - let class = self.class(db); - let class_stmt = class.node(db); + let class_stmt = self.node(db); let metaclass_node = &class_stmt .arguments .as_ref()? .find_keyword("metaclass")? .value; - let class_definition = class.definition(db); + let class_definition = self.definition(db); Some(definition_expression_type( db, @@ -745,8 +672,7 @@ impl<'db> ClassLiteralType<'db> { self, db: &'db dyn Db, ) -> Result<(Type<'db>, Option), MetaclassError<'db>> { - let class = self.class(db); - tracing::trace!("ClassLiteralType::try_metaclass: {}", class.name); + tracing::trace!("ClassLiteral::try_metaclass: {}", self.name(db)); // Identify the class's own metaclass (or take the first base class's metaclass). let mut base_classes = self.fully_static_explicit_bases(db).peekable(); @@ -774,7 +700,7 @@ impl<'db> ClassLiteralType<'db> { explicit_metaclass_of: class_metaclass_was_from, } } else { - let name = Type::string_literal(db, &class.name); + let name = Type::string_literal(db, self.name(db)); let bases = TupleType::from_elements(db, self.explicit_bases(db)); // TODO: Should be `dict[str, Any]` let namespace = KnownClass::Dict.to_instance(db); @@ -837,9 +763,10 @@ impl<'db> ClassLiteralType<'db> { }); } + let (metaclass_literal, _) = candidate.metaclass.class_literal(db); Ok(( candidate.metaclass.into(), - candidate.metaclass.class(db).dataclass_transformer_params, + metaclass_literal.dataclass_transformer_params(db), )) } @@ -1006,7 +933,7 @@ impl<'db> ClassLiteralType<'db> { /// or those marked as ClassVars are considered. /// /// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope - /// directly. Use [`ClassLiteralType::class_member`] if you require a method that will + /// directly. Use [`ClassLiteral::class_member`] if you require a method that will /// traverse through the MRO until it finds the member. pub(super) fn own_class_member( self, @@ -1027,14 +954,14 @@ impl<'db> ClassLiteralType<'db> { // to any method with a `@classmethod` decorator. (`__init__` would remain a special // case, since it's an _instance_ method where we don't yet know the generic class's // specialization.) - match (self, ty, specialization, name) { + match (self.generic_context(db), ty, specialization, name) { ( - ClassLiteralType::Generic(origin), + Some(generic_context), Type::FunctionLiteral(function), Some(_), "__new__" | "__init__", ) => Type::FunctionLiteral( - function.with_inherited_generic_context(db, origin.generic_context(db)), + function.with_inherited_generic_context(db, generic_context), ), _ => ty, } @@ -1169,7 +1096,7 @@ impl<'db> ClassLiteralType<'db> { /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. /// - /// See [`ClassLiteralType::own_dataclass_fields`] for more details. + /// See [`ClassLiteral::own_dataclass_fields`] for more details. fn dataclass_fields( self, db: &'db dyn Db, @@ -1690,15 +1617,15 @@ impl<'db> ClassLiteralType<'db> { /// Also, populates `visited_classes` with all base classes of `self`. fn is_cyclically_defined_recursive<'db>( db: &'db dyn Db, - class: ClassLiteralType<'db>, - classes_on_stack: &mut IndexSet>, - visited_classes: &mut IndexSet>, + class: ClassLiteral<'db>, + classes_on_stack: &mut IndexSet>, + visited_classes: &mut IndexSet>, ) -> bool { let mut result = false; for explicit_base in class.explicit_bases(db) { let explicit_base_class_literal = match explicit_base { Type::ClassLiteral(class_literal) => *class_literal, - Type::GenericAlias(generic_alias) => generic_alias.class_literal(db), + Type::GenericAlias(generic_alias) => generic_alias.origin(db), _ => continue, }; if !classes_on_stack.insert(explicit_base_class_literal) { @@ -1759,18 +1686,15 @@ impl<'db> ClassLiteralType<'db> { } } -impl<'db> From> for Type<'db> { - fn from(class: ClassLiteralType<'db>) -> Type<'db> { - match class { - ClassLiteralType::NonGeneric(non_generic) => non_generic.into(), - ClassLiteralType::Generic(generic) => generic.into(), - } +impl<'db> From> for Type<'db> { + fn from(class: ClassLiteral<'db>) -> Type<'db> { + Type::ClassLiteral(class) } } /// Representation of a single `Protocol` class definition. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(super) struct ProtocolClassLiteral<'db>(ClassLiteralType<'db>); +pub(super) struct ProtocolClassLiteral<'db>(ClassLiteral<'db>); impl<'db> ProtocolClassLiteral<'db> { /// Returns the protocol members of this class. @@ -1827,7 +1751,7 @@ impl<'db> ProtocolClassLiteral<'db> { #[salsa::tracked(return_ref)] fn cached_protocol_members<'db>( db: &'db dyn Db, - class: ClassLiteralType<'db>, + class: ClassLiteral<'db>, ) -> Box> { let mut members = FxOrderSet::default(); @@ -1885,7 +1809,7 @@ impl<'db> ProtocolClassLiteral<'db> { } impl<'db> Deref for ProtocolClassLiteral<'db> { - type Target = ClassLiteralType<'db>; + type Target = ClassLiteral<'db>; fn deref(&self) -> &Self::Target { &self.0 @@ -1916,7 +1840,7 @@ impl InheritanceCycle { /// Note: good candidates are any classes in `[crate::module_resolver::module::KnownModule]` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(test, derive(strum_macros::EnumIter))] -pub(crate) enum KnownClass { +pub enum KnownClass { // To figure out where an stdlib symbol is defined, you can go into `crates/red_knot_vendored` // and grep for the symbol name in any `.pyi` file. @@ -2177,7 +2101,7 @@ impl<'db> KnownClass { pub(crate) fn try_to_class_literal( self, db: &'db dyn Db, - ) -> Result, KnownClassLookupError<'db>> { + ) -> Result, KnownClassLookupError<'db>> { let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).symbol; match symbol { Symbol::Type(Type::ClassLiteral(class_literal), Boundness::Bound) => Ok(class_literal), @@ -2583,9 +2507,7 @@ pub(crate) enum KnownClassLookupError<'db> { SymbolNotAClass { found_type: Type<'db> }, /// There is a symbol by that name in the expected typeshed module, /// and it's a class definition, but it's possibly unbound. - ClassPossiblyUnbound { - class_literal: ClassLiteralType<'db>, - }, + ClassPossiblyUnbound { class_literal: ClassLiteral<'db> }, } impl<'db> KnownClassLookupError<'db> { diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 6cd6b3da483a1d..e051de715bd5bc 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -1,5 +1,5 @@ use super::context::InferContext; -use super::ClassLiteralType; +use super::ClassLiteral; use crate::declare_lint; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; use crate::suppression::FileSuppressionId; @@ -1321,7 +1321,7 @@ pub(crate) fn report_invalid_arguments_to_annotated( pub(crate) fn report_bad_argument_to_get_protocol_members( context: &InferContext, call: &ast::ExprCall, - class: ClassLiteralType, + class: ClassLiteral, ) { let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call) else { return; diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 70e8cd99cf42ca..4220b0cf41f138 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -6,7 +6,7 @@ use ruff_db::display::FormatterJoinExtension; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_literal::escape::AsciiEscape; -use crate::types::class::{ClassType, GenericAlias, GenericClass}; +use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ @@ -76,7 +76,7 @@ impl Display for DisplayRepresentation<'_> { Type::Instance(instance) => match (instance.class(), instance.class().known(self.db)) { (_, Some(KnownClass::NoneType)) => f.write_str("None"), (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), - (ClassType::NonGeneric(class), _) => f.write_str(&class.class(self.db).name), + (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)), (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)), }, Type::PropertyInstance(_) => f.write_str("property"), @@ -272,7 +272,7 @@ impl<'db> GenericAlias<'db> { } pub(crate) struct DisplayGenericAlias<'db> { - origin: GenericClass<'db>, + origin: ClassLiteral<'db>, specialization: Specialization<'db>, db: &'db dyn Db, } @@ -282,7 +282,7 @@ impl Display for DisplayGenericAlias<'_> { write!( f, "{origin}{specialization}", - origin = self.origin.class(self.db).name, + origin = self.origin.name(self.db), specialization = self.specialization.display_short(self.db), ) } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index bfdddc49827fbf..29d69e5099e8b1 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -81,14 +81,13 @@ use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ - binding_type, todo_type, CallDunderError, CallableSignature, CallableType, Class, - ClassLiteralType, ClassType, DataclassParams, DynamicType, FunctionDecorators, FunctionType, - GenericAlias, GenericClass, IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, - KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, NonGenericClass, Parameter, - ParameterForm, Parameters, Signature, Signatures, SliceLiteralType, StringLiteralType, - SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, - TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, - TypeVarInstance, UnionBuilder, UnionType, + binding_type, todo_type, CallDunderError, CallableSignature, CallableType, ClassLiteral, + ClassType, DataclassParams, DynamicType, FunctionDecorators, FunctionType, GenericAlias, + IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType, + MemberLookupPolicy, MetaclassCandidate, Parameter, ParameterForm, Parameters, Signature, + Signatures, SliceLiteralType, StringLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, + Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, + TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType, }; use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; @@ -1833,10 +1832,6 @@ impl<'db> TypeInferenceBuilder<'db> { } } - let generic_context = type_params.as_ref().map(|type_params| { - GenericContext::from_type_params(self.db(), self.index, type_params) - }); - let body_scope = self .index .node_scope(NodeWithScopeRef::Class(class_node)) @@ -1844,20 +1839,14 @@ impl<'db> TypeInferenceBuilder<'db> { let maybe_known_class = KnownClass::try_from_file_and_name(self.db(), self.file(), name); - let class = Class { - name: name.id.clone(), + let class_ty = Type::from(ClassLiteral::new( + self.db(), + name.id.clone(), body_scope, - known: maybe_known_class, + maybe_known_class, dataclass_params, dataclass_transformer_params, - }; - let class_literal = match generic_context { - Some(generic_context) => { - ClassLiteralType::Generic(GenericClass::new(self.db(), class, generic_context)) - } - None => ClassLiteralType::NonGeneric(NonGenericClass::new(self.db(), class)), - }; - let class_ty = Type::from(class_literal); + )); self.add_declaration_with_binding( class_node.into(), @@ -6218,8 +6207,15 @@ impl<'db> TypeInferenceBuilder<'db> { // updating all of the subscript logic below to use custom callables for all of the _other_ // special cases, too. let value_ty = self.infer_expression(value); - if let Type::ClassLiteral(ClassLiteralType::Generic(generic_class)) = value_ty { - return self.infer_explicit_class_specialization(subscript, value_ty, generic_class); + if let Type::ClassLiteral(class) = value_ty { + if let Some(generic_context) = class.generic_context(self.db()) { + return self.infer_explicit_class_specialization( + subscript, + value_ty, + class, + generic_context, + ); + } } let slice_ty = self.infer_expression(slice); @@ -6230,7 +6226,8 @@ impl<'db> TypeInferenceBuilder<'db> { &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, - generic_class: GenericClass<'db>, + generic_class: ClassLiteral<'db>, + generic_context: GenericContext<'db>, ) -> Type<'db> { let slice_node = subscript.slice.as_ref(); let mut call_argument_types = match slice_node { @@ -6239,7 +6236,6 @@ impl<'db> TypeInferenceBuilder<'db> { ), _ => CallArgumentTypes::positional([self.infer_type_expression(slice_node)]), }; - let generic_context = generic_class.generic_context(self.db()); let signatures = Signatures::single(CallableSignature::single( value_ty, generic_context.signature(self.db()), @@ -6511,7 +6507,7 @@ impl<'db> TypeInferenceBuilder<'db> { return KnownClass::GenericAlias.to_instance(self.db()); } - if let ClassLiteralType::Generic(_) = class { + if class.generic_context(self.db()).is_some() { // TODO: specialize the generic class using these explicit type // variable assignments return value_ty; @@ -7369,18 +7365,26 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_type_expression(slice); value_ty } - Type::ClassLiteral(ClassLiteralType::Generic(generic_class)) => { - let specialized_class = - self.infer_explicit_class_specialization(subscript, value_ty, generic_class); - specialized_class - .in_type_expression(self.db()) - .unwrap_or(Type::unknown()) - } - Type::ClassLiteral(ClassLiteralType::NonGeneric(_)) => { - // TODO: Once we know that e.g. `list` is generic, emit a diagnostic if you try to - // specialize a non-generic class. - self.infer_type_expression(slice); - todo_type!("specialized non-generic class") + Type::ClassLiteral(class) => { + match class.generic_context(self.db()) { + Some(generic_context) => { + let specialized_class = self.infer_explicit_class_specialization( + subscript, + value_ty, + class, + generic_context, + ); + specialized_class + .in_type_expression(self.db()) + .unwrap_or(Type::unknown()) + } + None => { + // TODO: Once we know that e.g. `list` is generic, emit a diagnostic if you try to + // specialize a non-generic class. + self.infer_type_expression(slice); + todo_type!("specialized non-generic class") + } + } } _ => { // TODO: Emit a diagnostic once we've implemented all valid subscript type diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index f1d945cae8a8e8..4cd27d6b31674e 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -5,13 +5,13 @@ use rustc_hash::FxHashSet; use crate::types::class_base::ClassBase; use crate::types::generics::Specialization; -use crate::types::{ClassLiteralType, ClassType, Type}; +use crate::types::{ClassLiteral, ClassType, Type}; use crate::Db; /// The inferred method resolution order of a given class. /// /// An MRO cannot contain non-specialized generic classes. (This is why [`ClassBase`] contains a -/// [`ClassType`], not a [`ClassLiteralType`].) Any generic classes in a base class list are always +/// [`ClassType`], not a [`ClassLiteral`].) Any generic classes in a base class list are always /// specialized — either because the class is explicitly specialized if there is a subscript /// expression, or because we create the default specialization if there isn't. /// @@ -33,7 +33,7 @@ pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>); impl<'db> Mro<'db> { /// Attempt to resolve the MRO of a given class. Because we derive the MRO from the list of /// base classes in the class definition, this operation is performed on a [class - /// literal][ClassLiteralType], not a [class type][ClassType]. (You can _also_ get the MRO of a + /// literal][ClassLiteral], not a [class type][ClassType]. (You can _also_ get the MRO of a /// class type, but this is done by first getting the MRO of the underlying class literal, and /// specializing each base class as needed if the class type is a generic alias.) /// @@ -47,7 +47,7 @@ impl<'db> Mro<'db> { /// [`super::infer::TypeInferenceBuilder::infer_region_scope`].) pub(super) fn of_class( db: &'db dyn Db, - class: ClassLiteralType<'db>, + class: ClassLiteral<'db>, specialization: Option>, ) -> Result> { Self::of_class_impl(db, class, specialization).map_err(|err| { @@ -65,7 +65,7 @@ impl<'db> Mro<'db> { fn of_class_impl( db: &'db dyn Db, - class: ClassLiteralType<'db>, + class: ClassLiteral<'db>, specialization: Option>, ) -> Result> { let class_bases = class.explicit_bases(db); @@ -220,12 +220,12 @@ impl<'db> FromIterator> for Mro<'db> { /// /// Even for first-party code, where we will have to resolve the MRO for every class we encounter, /// loading the cached MRO comes with a certain amount of overhead, so it's best to avoid calling the -/// Salsa-tracked [`ClassLiteralType::try_mro`] method unless it's absolutely necessary. +/// Salsa-tracked [`ClassLiteral::try_mro`] method unless it's absolutely necessary. pub(super) struct MroIterator<'db> { db: &'db dyn Db, /// The class whose MRO we're iterating over - class: ClassLiteralType<'db>, + class: ClassLiteral<'db>, /// The specialization to apply to each MRO element, if any specialization: Option>, @@ -244,7 +244,7 @@ pub(super) struct MroIterator<'db> { impl<'db> MroIterator<'db> { pub(super) fn new( db: &'db dyn Db, - class: ClassLiteralType<'db>, + class: ClassLiteral<'db>, specialization: Option>, ) -> Self { Self { @@ -326,11 +326,11 @@ pub(super) enum MroErrorKind<'db> { /// The class has one or more duplicate bases. /// - /// This variant records the indices and [`ClassLiteralType`]s + /// This variant records the indices and [`ClassLiteral`]s /// of the duplicate bases. The indices are the indices of nodes /// in the bases list of the class's [`StmtClassDef`](ruff_python_ast::StmtClassDef) node. /// Each index is the index of a node representing a duplicate base. - DuplicateBases(Box<[(usize, ClassLiteralType<'db>)]>), + DuplicateBases(Box<[(usize, ClassLiteral<'db>)]>), /// The MRO is otherwise unresolvable through the C3-merge algorithm. /// diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index 3c49626461faab..19cb5c2e5d7265 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -8,8 +8,8 @@ use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable}; use crate::semantic_index::symbol_table; use crate::types::infer::infer_same_file_expression_type; use crate::types::{ - infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass, SubclassOfType, - Truthiness, Type, UnionBuilder, + infer_expression_types, IntersectionBuilder, KnownClass, SubclassOfType, Truthiness, Type, + UnionBuilder, }; use crate::Db; use itertools::Itertools; @@ -648,9 +648,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> { }) if keywords.is_empty() => { let rhs_class = match rhs_ty { Type::ClassLiteral(class) => class, - Type::GenericAlias(alias) => { - ClassLiteralType::Generic(alias.origin(self.db)) - } + Type::GenericAlias(alias) => alias.origin(self.db), _ => { continue; } diff --git a/crates/red_knot_python_semantic/src/types/slots.rs b/crates/red_knot_python_semantic/src/types/slots.rs index b86f2e2fa3b128..760185db98a08c 100644 --- a/crates/red_knot_python_semantic/src/types/slots.rs +++ b/crates/red_knot_python_semantic/src/types/slots.rs @@ -4,7 +4,7 @@ use crate::db::Db; use crate::symbol::{Boundness, Symbol}; use crate::types::class_base::ClassBase; use crate::types::diagnostic::report_base_with_incompatible_slots; -use crate::types::{ClassLiteralType, Type}; +use crate::types::{ClassLiteral, Type}; use super::InferContext; @@ -23,7 +23,7 @@ enum SlotsKind { } impl SlotsKind { - fn from(db: &dyn Db, base: ClassLiteralType) -> Self { + fn from(db: &dyn Db, base: ClassLiteral) -> Self { let Symbol::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").symbol else { return Self::NotSpecified; @@ -53,7 +53,7 @@ impl SlotsKind { pub(super) fn check_class_slots( context: &InferContext, - class: ClassLiteralType, + class: ClassLiteral, node: &ast::StmtClassDef, ) { let db = context.db(); From 0251679f871a0888aa62169771d3a718874fd479 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Sat, 26 Apr 2025 03:58:13 +0530 Subject: [PATCH 0126/1161] [red-knot] Add new property tests for subtyping with "bottom" callable (#17635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary I remember we discussed about adding this as a property tests so here I am. ## Test Plan ```console ❯ QUICKCHECK_TESTS=10000000 cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable::bottom_callable_is_subtype_of_all_fully_static_callable Finished `release` profile [optimized] target(s) in 0.10s Running unittests src/lib.rs (target/release/deps/red_knot_python_semantic-e41596ca2dbd0e98) running 1 test test types::property_tests::stable::bottom_callable_is_subtype_of_all_fully_static_callable ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 233 filtered out; finished in 30.91s ``` --- crates/red_knot_python_semantic/src/types.rs | 17 +++++++++++++++++ .../src/types/property_tests.rs | 10 +++++++++- .../src/types/signatures.rs | 13 +++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 5b4c37d39cff50..070adc5aabae3e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -537,6 +537,11 @@ impl<'db> Type<'db> { matches!(self, Type::Never) } + /// Returns `true` if `self` is [`Type::Callable`]. + pub const fn is_callable_type(&self) -> bool { + matches!(self, Type::Callable(..)) + } + fn is_none(&self, db: &'db dyn Db) -> bool { self.into_instance() .is_some_and(|instance| instance.class().is_known(db, KnownClass::NoneType)) @@ -6571,6 +6576,18 @@ impl<'db> CallableType<'db> { ) } + /// Create a callable type which represents a fully-static "bottom" callable. + /// + /// Specifically, this represents a callable type with a single signature: + /// `(*args: object, **kwargs: object) -> Never`. + #[cfg(test)] + pub(crate) fn bottom(db: &'db dyn Db) -> Type<'db> { + Type::Callable(CallableType::single( + db, + Signature::new(Parameters::object(db), Some(Type::Never)), + )) + } + /// Return a "normalized" version of this `Callable` type. /// /// See [`Type::normalized`] for more details. diff --git a/crates/red_knot_python_semantic/src/types/property_tests.rs b/crates/red_knot_python_semantic/src/types/property_tests.rs index cbcfef74ac2db2..77c83e04e0f1b3 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests.rs +++ b/crates/red_knot_python_semantic/src/types/property_tests.rs @@ -58,7 +58,7 @@ macro_rules! type_property_test { mod stable { use super::union; - use crate::types::Type; + use crate::types::{CallableType, Type}; // Reflexivity: `T` is equivalent to itself. type_property_test!( @@ -169,6 +169,14 @@ mod stable { forall types t. t.is_fully_static(db) => Type::Never.is_subtype_of(db, t) ); + // Similar to `Never`, a fully-static "bottom" callable type should be a subtype of all + // fully-static callable types + type_property_test!( + bottom_callable_is_subtype_of_all_fully_static_callable, db, + forall types t. t.is_callable_type() && t.is_fully_static(db) + => CallableType::bottom(db).is_subtype_of(db, t) + ); + // For any two fully static types, each type in the pair must be a subtype of their union. type_property_test!( all_fully_static_type_pairs_are_subtype_of_their_union, db, diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 5e931c5d96570a..4fcd669be2ad61 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -934,6 +934,19 @@ impl<'db> Parameters<'db> { } } + /// Return parameters that represents `(*args: object, **kwargs: object)`. + #[cfg(test)] + pub(crate) fn object(db: &'db dyn Db) -> Self { + Self { + value: vec![ + Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object(db)), + Parameter::keyword_variadic(Name::new_static("kwargs")) + .with_annotated_type(Type::object(db)), + ], + is_gradual: false, + } + } + fn from_parameters( db: &'db dyn Db, definition: Definition<'db>, From cfa1505068c135785fb5d8a01baaba5823e1f6e9 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 26 Apr 2025 07:28:45 +0100 Subject: [PATCH 0127/1161] [red-knot] Fix CLI hang when a dependent query panics (#17631) --- crates/red_knot/src/main.rs | 15 +++++--- crates/red_knot_project/src/lib.rs | 54 ++++++++++++++++++++++++++-- crates/ruff_db/src/diagnostic/mod.rs | 3 ++ 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/crates/red_knot/src/main.rs b/crates/red_knot/src/main.rs index 021a777611c336..a0e47b2d71f86e 100644 --- a/crates/red_knot/src/main.rs +++ b/crates/red_knot/src/main.rs @@ -246,11 +246,16 @@ impl MainLoop { // Spawn a new task that checks the project. This needs to be done in a separate thread // to prevent blocking the main loop here. rayon::spawn(move || { - if let Ok(result) = db.check() { - // Send the result back to the main loop for printing. - sender - .send(MainLoopMessage::CheckCompleted { result, revision }) - .unwrap(); + match db.check() { + Ok(result) => { + // Send the result back to the main loop for printing. + sender + .send(MainLoopMessage::CheckCompleted { result, revision }) + .unwrap(); + } + Err(cancelled) => { + tracing::debug!("Check has been cancelled: {cancelled:?}"); + } } }); } diff --git a/crates/red_knot_project/src/lib.rs b/crates/red_knot_project/src/lib.rs index de3207216fd352..48a5de4d48662a 100644 --- a/crates/red_knot_project/src/lib.rs +++ b/crates/red_knot_project/src/lib.rs @@ -11,7 +11,7 @@ use red_knot_python_semantic::register_lints; use red_knot_python_semantic::types::check_types; use ruff_db::diagnostic::{ create_parse_diagnostic, create_unsupported_syntax_diagnostic, Annotation, Diagnostic, - DiagnosticId, Severity, Span, + DiagnosticId, Severity, Span, SubDiagnostic, }; use ruff_db::files::File; use ruff_db::parsed::parsed_module; @@ -20,8 +20,10 @@ use ruff_db::system::{SystemPath, SystemPathBuf}; use rustc_hash::FxHashSet; use salsa::Durability; use salsa::Setter; +use std::panic::{catch_unwind, AssertUnwindSafe, UnwindSafe}; use std::sync::Arc; use thiserror::Error; +use tracing::error; pub mod combine; @@ -471,7 +473,16 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec { .map(|error| create_unsupported_syntax_diagnostic(file, error)), ); - diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned()); + { + let db = AssertUnwindSafe(db); + match catch(&**db, file, || check_types(db.upcast(), file)) { + Ok(Some(type_check_diagnostics)) => { + diagnostics.extend(type_check_diagnostics.into_iter().cloned()); + } + Ok(None) => {} + Err(diagnostic) => diagnostics.push(diagnostic), + } + } diagnostics.sort_unstable_by_key(|diagnostic| { diagnostic @@ -562,6 +573,45 @@ enum IOErrorKind { SourceText(#[from] SourceTextError), } +fn catch(db: &dyn Db, file: File, f: F) -> Result, Diagnostic> +where + F: FnOnce() -> R + UnwindSafe, +{ + match catch_unwind(|| { + // Ignore salsa errors + salsa::Cancelled::catch(f).ok() + }) { + Ok(result) => Ok(result), + Err(error) => { + let payload = if let Some(s) = error.downcast_ref::<&str>() { + Some((*s).to_string()) + } else { + error.downcast_ref::().cloned() + }; + + let message = if let Some(payload) = payload { + format!( + "Panicked while checking `{file}`: `{payload}`", + file = file.path(db) + ) + } else { + format!("Panicked while checking `{file}`", file = { file.path(db) }) + }; + + let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message); + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "This indicates a bug in Red Knot.", + )); + + let report_message = "If you could open an issue at https://github.com/astral-sh/ruff/issues/new?title=%5Bred-knot%5D:%20panic we'd be very appreciative!"; + diagnostic.sub(SubDiagnostic::new(Severity::Info, report_message)); + + Err(diagnostic) + } + } +} + #[cfg(test)] mod tests { use crate::db::tests::TestDb; diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 02b298afbb0509..ffcdf2a026742b 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -461,6 +461,8 @@ impl PartialEq<&str> for LintName { /// Uniquely identifies the kind of a diagnostic. #[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] pub enum DiagnosticId { + Panic, + /// Some I/O operation failed Io, @@ -521,6 +523,7 @@ impl DiagnosticId { pub fn as_str(&self) -> Result<&str, DiagnosticAsStrError> { Ok(match self { + DiagnosticId::Panic => "panic", DiagnosticId::Io => "io", DiagnosticId::InvalidSyntax => "invalid-syntax", DiagnosticId::Lint(name) => { From 2e95475f577c101a1dbc0d79e0bfdc31b176c0a2 Mon Sep 17 00:00:00 2001 From: justin Date: Sat, 26 Apr 2025 06:02:03 -0400 Subject: [PATCH 0128/1161] [red-knot] Add --respect-ignore-files flag (#17569) Co-authored-by: Micha Reiser --- crates/red_knot/src/args.rs | 21 +++++ crates/red_knot/tests/cli.rs | 93 +++++++++++++++++++ .../red_knot_project/src/metadata/options.rs | 5 +- .../red_knot_project/src/metadata/settings.rs | 9 +- crates/red_knot_project/src/walk.rs | 5 +- knot.schema.json | 6 ++ 6 files changed, 136 insertions(+), 3 deletions(-) diff --git a/crates/red_knot/src/args.rs b/crates/red_knot/src/args.rs index 233a081445a42c..00b40dd5ff9991 100644 --- a/crates/red_knot/src/args.rs +++ b/crates/red_knot/src/args.rs @@ -105,6 +105,19 @@ pub(crate) struct CheckCommand { /// Watch files for changes and recheck files related to the changed files. #[arg(long, short = 'W')] pub(crate) watch: bool, + + /// Respect file exclusions via `.gitignore` and other standard ignore files. + /// Use `--no-respect-gitignore` to disable. + #[arg( + long, + overrides_with("no_respect_ignore_files"), + help_heading = "File selection", + default_missing_value = "true", + num_args = 0..1 + )] + respect_ignore_files: Option, + #[clap(long, overrides_with("respect_ignore_files"), hide = true)] + no_respect_ignore_files: bool, } impl CheckCommand { @@ -120,6 +133,13 @@ impl CheckCommand { ) }; + // --no-respect-gitignore defaults to false and is set true by CLI flag. If passed, override config file + // Otherwise, only pass this through if explicitly set (don't default to anything here to + // make sure that doesn't take precedence over an explicitly-set config file value) + let respect_ignore_files = self + .no_respect_ignore_files + .then_some(false) + .or(self.respect_ignore_files); Options { environment: Some(EnvironmentOptions { python_version: self @@ -144,6 +164,7 @@ impl CheckCommand { error_on_warning: self.error_on_warning, }), rules, + respect_ignore_files, ..Default::default() } } diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index 4509882e505866..fdee22358a2190 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -5,6 +5,99 @@ use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::TempDir; +#[test] +fn test_respect_ignore_files() -> anyhow::Result<()> { + // First test that the default option works correctly (the file is skipped) + let case = TestCase::with_files([(".ignore", "test.py"), ("test.py", "~")])?; + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN No python files found under the given path(s) + "); + + // Test that we can set to false via CLI + let case = TestCase::with_files([(".ignore", "test.py"), ("test.py", "~")])?; + assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error: invalid-syntax + --> /test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + "); + + // Test that we can set to false via config file + let case = TestCase::with_files([ + ("knot.toml", "respect-ignore-files = false"), + (".ignore", "test.py"), + ("test.py", "~"), + ])?; + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error: invalid-syntax + --> /test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + "); + + // Ensure CLI takes precedence + let case = TestCase::with_files([ + ("knot.toml", "respect-ignore-files = false"), + (".ignore", "test.py"), + ("test.py", "~"), + ])?; + assert_cmd_snapshot!(case.command().arg("--respect-ignore-files"), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN No python files found under the given path(s) + "); + + // Ensure --no-respect-ignore-files takes precedence over config file + let case = TestCase::with_files([ + ("knot.toml", "respect-ignore-files = true"), + (".ignore", "test.py"), + ("test.py", "~"), + ])?; + assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error: invalid-syntax + --> /test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + "); + Ok(()) +} /// Specifying an option on the CLI should take precedence over the same setting in the /// project's configuration. Here, this is tested for the Python version. #[test] diff --git a/crates/red_knot_project/src/metadata/options.rs b/crates/red_knot_project/src/metadata/options.rs index d035f4cf7be5db..eeff13808289c6 100644 --- a/crates/red_knot_project/src/metadata/options.rs +++ b/crates/red_knot_project/src/metadata/options.rs @@ -32,6 +32,9 @@ pub struct Options { #[serde(skip_serializing_if = "Option::is_none")] pub terminal: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub respect_ignore_files: Option, } impl Options { @@ -133,7 +136,7 @@ impl Options { pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec) { let (rules, diagnostics) = self.to_rule_selection(db); - let mut settings = Settings::new(rules); + let mut settings = Settings::new(rules, self.respect_ignore_files); if let Some(terminal) = self.terminal.as_ref() { settings.set_terminal(TerminalSettings { diff --git a/crates/red_knot_project/src/metadata/settings.rs b/crates/red_knot_project/src/metadata/settings.rs index e16a72d4a030e5..f5572f6657428a 100644 --- a/crates/red_knot_project/src/metadata/settings.rs +++ b/crates/red_knot_project/src/metadata/settings.rs @@ -21,13 +21,16 @@ pub struct Settings { rules: Arc, terminal: TerminalSettings, + + respect_ignore_files: bool, } impl Settings { - pub fn new(rules: RuleSelection) -> Self { + pub fn new(rules: RuleSelection, respect_ignore_files: Option) -> Self { Self { rules: Arc::new(rules), terminal: TerminalSettings::default(), + respect_ignore_files: respect_ignore_files.unwrap_or(true), } } @@ -35,6 +38,10 @@ impl Settings { &self.rules } + pub fn respect_ignore_files(&self) -> bool { + self.respect_ignore_files + } + pub fn to_rules(&self) -> Arc { self.rules.clone() } diff --git a/crates/red_knot_project/src/walk.rs b/crates/red_knot_project/src/walk.rs index 531c823a28afeb..025566db382712 100644 --- a/crates/red_knot_project/src/walk.rs +++ b/crates/red_knot_project/src/walk.rs @@ -129,7 +129,10 @@ impl<'a> ProjectFilesWalker<'a> { { let mut paths = paths.into_iter(); - let mut walker = db.system().walk_directory(paths.next()?.as_ref()); + let mut walker = db + .system() + .walk_directory(paths.next()?.as_ref()) + .standard_filters(db.project().settings(db).respect_ignore_files()); for path in paths { walker = walker.add(path); diff --git a/knot.schema.json b/knot.schema.json index 941eee5af93e72..7349373c0e467d 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -15,6 +15,12 @@ } ] }, + "respect-ignore-files": { + "type": [ + "boolean", + "null" + ] + }, "rules": { "description": "Configures the enabled lints and their severity.", "anyOf": [ From 6044f041374c54547b7af4d50692b30e3fac78e8 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 26 Apr 2025 11:30:50 +0100 Subject: [PATCH 0129/1161] Revert "[red-knot] Add --respect-ignore-files flag (#17569)" (#17642) --- crates/red_knot/src/args.rs | 21 ----- crates/red_knot/tests/cli.rs | 93 ------------------- .../red_knot_project/src/metadata/options.rs | 5 +- .../red_knot_project/src/metadata/settings.rs | 9 +- crates/red_knot_project/src/walk.rs | 5 +- knot.schema.json | 6 -- 6 files changed, 3 insertions(+), 136 deletions(-) diff --git a/crates/red_knot/src/args.rs b/crates/red_knot/src/args.rs index 00b40dd5ff9991..233a081445a42c 100644 --- a/crates/red_knot/src/args.rs +++ b/crates/red_knot/src/args.rs @@ -105,19 +105,6 @@ pub(crate) struct CheckCommand { /// Watch files for changes and recheck files related to the changed files. #[arg(long, short = 'W')] pub(crate) watch: bool, - - /// Respect file exclusions via `.gitignore` and other standard ignore files. - /// Use `--no-respect-gitignore` to disable. - #[arg( - long, - overrides_with("no_respect_ignore_files"), - help_heading = "File selection", - default_missing_value = "true", - num_args = 0..1 - )] - respect_ignore_files: Option, - #[clap(long, overrides_with("respect_ignore_files"), hide = true)] - no_respect_ignore_files: bool, } impl CheckCommand { @@ -133,13 +120,6 @@ impl CheckCommand { ) }; - // --no-respect-gitignore defaults to false and is set true by CLI flag. If passed, override config file - // Otherwise, only pass this through if explicitly set (don't default to anything here to - // make sure that doesn't take precedence over an explicitly-set config file value) - let respect_ignore_files = self - .no_respect_ignore_files - .then_some(false) - .or(self.respect_ignore_files); Options { environment: Some(EnvironmentOptions { python_version: self @@ -164,7 +144,6 @@ impl CheckCommand { error_on_warning: self.error_on_warning, }), rules, - respect_ignore_files, ..Default::default() } } diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index fdee22358a2190..4509882e505866 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -5,99 +5,6 @@ use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::TempDir; -#[test] -fn test_respect_ignore_files() -> anyhow::Result<()> { - // First test that the default option works correctly (the file is skipped) - let case = TestCase::with_files([(".ignore", "test.py"), ("test.py", "~")])?; - assert_cmd_snapshot!(case.command(), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN No python files found under the given path(s) - "); - - // Test that we can set to false via CLI - let case = TestCase::with_files([(".ignore", "test.py"), ("test.py", "~")])?; - assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" - success: false - exit_code: 1 - ----- stdout ----- - error: invalid-syntax - --> /test.py:1:2 - | - 1 | ~ - | ^ Expected an expression - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - // Test that we can set to false via config file - let case = TestCase::with_files([ - ("knot.toml", "respect-ignore-files = false"), - (".ignore", "test.py"), - ("test.py", "~"), - ])?; - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error: invalid-syntax - --> /test.py:1:2 - | - 1 | ~ - | ^ Expected an expression - | - - Found 1 diagnostic - - ----- stderr ----- - "); - - // Ensure CLI takes precedence - let case = TestCase::with_files([ - ("knot.toml", "respect-ignore-files = false"), - (".ignore", "test.py"), - ("test.py", "~"), - ])?; - assert_cmd_snapshot!(case.command().arg("--respect-ignore-files"), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN No python files found under the given path(s) - "); - - // Ensure --no-respect-ignore-files takes precedence over config file - let case = TestCase::with_files([ - ("knot.toml", "respect-ignore-files = true"), - (".ignore", "test.py"), - ("test.py", "~"), - ])?; - assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" - success: false - exit_code: 1 - ----- stdout ----- - error: invalid-syntax - --> /test.py:1:2 - | - 1 | ~ - | ^ Expected an expression - | - - Found 1 diagnostic - - ----- stderr ----- - "); - Ok(()) -} /// Specifying an option on the CLI should take precedence over the same setting in the /// project's configuration. Here, this is tested for the Python version. #[test] diff --git a/crates/red_knot_project/src/metadata/options.rs b/crates/red_knot_project/src/metadata/options.rs index eeff13808289c6..d035f4cf7be5db 100644 --- a/crates/red_knot_project/src/metadata/options.rs +++ b/crates/red_knot_project/src/metadata/options.rs @@ -32,9 +32,6 @@ pub struct Options { #[serde(skip_serializing_if = "Option::is_none")] pub terminal: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub respect_ignore_files: Option, } impl Options { @@ -136,7 +133,7 @@ impl Options { pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec) { let (rules, diagnostics) = self.to_rule_selection(db); - let mut settings = Settings::new(rules, self.respect_ignore_files); + let mut settings = Settings::new(rules); if let Some(terminal) = self.terminal.as_ref() { settings.set_terminal(TerminalSettings { diff --git a/crates/red_knot_project/src/metadata/settings.rs b/crates/red_knot_project/src/metadata/settings.rs index f5572f6657428a..e16a72d4a030e5 100644 --- a/crates/red_knot_project/src/metadata/settings.rs +++ b/crates/red_knot_project/src/metadata/settings.rs @@ -21,16 +21,13 @@ pub struct Settings { rules: Arc, terminal: TerminalSettings, - - respect_ignore_files: bool, } impl Settings { - pub fn new(rules: RuleSelection, respect_ignore_files: Option) -> Self { + pub fn new(rules: RuleSelection) -> Self { Self { rules: Arc::new(rules), terminal: TerminalSettings::default(), - respect_ignore_files: respect_ignore_files.unwrap_or(true), } } @@ -38,10 +35,6 @@ impl Settings { &self.rules } - pub fn respect_ignore_files(&self) -> bool { - self.respect_ignore_files - } - pub fn to_rules(&self) -> Arc { self.rules.clone() } diff --git a/crates/red_knot_project/src/walk.rs b/crates/red_knot_project/src/walk.rs index 025566db382712..531c823a28afeb 100644 --- a/crates/red_knot_project/src/walk.rs +++ b/crates/red_knot_project/src/walk.rs @@ -129,10 +129,7 @@ impl<'a> ProjectFilesWalker<'a> { { let mut paths = paths.into_iter(); - let mut walker = db - .system() - .walk_directory(paths.next()?.as_ref()) - .standard_filters(db.project().settings(db).respect_ignore_files()); + let mut walker = db.system().walk_directory(paths.next()?.as_ref()); for path in paths { walker = walker.add(path); diff --git a/knot.schema.json b/knot.schema.json index 7349373c0e467d..941eee5af93e72 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -15,12 +15,6 @@ } ] }, - "respect-ignore-files": { - "type": [ - "boolean", - "null" - ] - }, "rules": { "description": "Configures the enabled lints and their severity.", "anyOf": [ From 4bcf1778fa0a223653c75ef44695428ae309430f Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Sat, 26 Apr 2025 13:58:52 +0200 Subject: [PATCH 0130/1161] [`ruff`] add fix safety section (`RUF057`) (#17483) The PR add the `fix safety` section for rule `RUF057` (#15584 ) --- .../ruff_linter/src/rules/ruff/rules/unnecessary_round.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs index 6edf01cc1ba7ca..bd2eabea84fdf5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs @@ -27,6 +27,12 @@ use ruff_text_size::Ranged; /// ```python /// a = 1 /// ``` +/// +/// ## Fix safety +/// +/// The fix is marked unsafe if it is not possible to guarantee that the first argument of +/// `round()` is of type `int`, or if the fix deletes comments. +/// #[derive(ViolationMetadata)] pub(crate) struct UnnecessaryRound; From 45d0634b01a7044974743e9d36e639d2a44064c9 Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Sat, 26 Apr 2025 15:59:05 +0200 Subject: [PATCH 0131/1161] [`pydocstyle`] add fix safety section (`D200`) (#17502) The PR add the fix safety section for rule `D200` (#15584 ) --- crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs index 7c5ca77e19e381..6dc58411fed43d 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/one_liner.rs @@ -27,6 +27,11 @@ use crate::docstrings::Docstring; /// """Return the mean of the given values.""" /// ``` /// +/// ## Fix safety +/// The fix is marked as unsafe because it could affect tools that parse docstrings, +/// documentation generators, or custom introspection utilities that rely on +/// specific docstring formatting. +/// /// ## References /// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) /// From a4e225ee8ab59b1cbce4ab5b26c28142aaf32faf Mon Sep 17 00:00:00 2001 From: Hans Date: Sat, 26 Apr 2025 23:40:51 +0800 Subject: [PATCH 0132/1161] [`flake8-async`] Add fix safety section (`ASYNC116`) (#17497) ## Summary This PR add the `fix safety` section for rule `ASYNC116` in `long_sleep_not_forever.rs` for #15584 --------- Co-authored-by: dylwil3 --- .../src/rules/flake8_async/rules/long_sleep_not_forever.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs b/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs index ae623a4514867f..42dfdc30ba5f98 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs @@ -34,6 +34,10 @@ use crate::rules::flake8_async::helpers::AsyncModule; /// async def func(): /// await trio.sleep_forever() /// ``` +/// +/// ## Fix safety +/// +/// This fix is marked as unsafe as it changes program behavior. #[derive(ViolationMetadata)] pub(crate) struct LongSleepNotForever { module: AsyncModule, From 64ba39a385f13414141b7b7139a943d8f9613123 Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Sat, 26 Apr 2025 18:00:01 +0200 Subject: [PATCH 0133/1161] [`flynt`] add fix safety section (`FLY002`) (#17496) The PR add the fix safety section for rule `FLY002` (#15584 ) The motivation for the content of the fix safety section is given by the following example ```python foo = 1 bar = [2, 3] try: result_join = " ".join((foo, bar)) print(f"Join result: {result_join}") except TypeError as e: print(f"Join error: {e}") ``` which print `Join error: sequence item 0: expected str instance, int found` But after the fix is applied, we have ```python foo = 1 bar = [2, 3] try: result_join = f"{foo} {bar}" print(f"Join result: {result_join}") except TypeError as e: print(f"Join error: {e}") ``` which print `Join result: 1 [2, 3]` --------- Co-authored-by: dylwil3 --- .../src/rules/flynt/rules/static_join_to_fstring.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs index 309ec0ccc70a10..5aed9103bd69bb 100644 --- a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs @@ -28,6 +28,14 @@ use crate::rules::flynt::helpers; /// f"{foo} {bar}" /// ``` /// +/// # Fix safety +/// The fix is always marked unsafe because the evaluation of the f-string +/// expressions will default to calling the `__format__` method of each +/// object, whereas `str.join` expects each object to be an instance of +/// `str` and uses the corresponding string. Therefore it is possible for +/// the values of the resulting strings to differ, or for one expression +/// to raise an exception while the other does not. +/// /// ## References /// - [Python documentation: f-strings](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) #[derive(ViolationMetadata)] From b578a828ef711ec08808717c6aee9875ce9b779d Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Sat, 26 Apr 2025 23:43:02 +0200 Subject: [PATCH 0134/1161] [`ruff`] add fix safety section (`RUF005`) (#17484) The PR add the `fix safety` section for rule `RUF005` (#15584 ). --------- Co-authored-by: Dylan --- .../rules/ruff/rules/collection_literal_concatenation.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs b/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs index 6c9239ee47cc08..60dcd09b3435e1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -33,6 +33,12 @@ use crate::fix::snippet::SourceCodeSnippet; /// bar = [1, *foo, 5, 6] /// ``` /// +/// ## Fix safety +/// +/// The fix is always marked as unsafe because the `+` operator uses the `__add__` magic method and +/// `*`-unpacking uses the `__iter__` magic method. Both of these could have custom +/// implementations, causing the fix to change program behaviour. +/// /// ## References /// - [PEP 448 – Additional Unpacking Generalizations](https://peps.python.org/pep-0448/) /// - [Python documentation: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) From b0d475f3533b2ebbaa37ee8fc9c609c70ccd0fd8 Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Sat, 26 Apr 2025 23:43:53 +0200 Subject: [PATCH 0135/1161] [`ruff`] add fix safety section (`RUF027`) (#17485) The PR add the `fix safety` section for rule `RUF027` (#15584 ). Actually, I have an example of a false positive. Should I include it in the` fix safety` section? --------- Co-authored-by: Dylan --- .../src/rules/ruff/rules/missing_fstring_syntax.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs index c44bd2cb8a2145..f39b3c2b8f487e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs @@ -53,6 +53,11 @@ use crate::Locator; /// print(f"Hello {name}! It is {day_of_week} today!") /// ``` /// +/// ## Fix safety +/// +/// This fix will always change the behavior of the program and, despite the precautions detailed +/// above, this may be undesired. As such the fix is always marked as unsafe. +/// /// [logging]: https://docs.python.org/3/howto/logging-cookbook.html#using-particular-formatting-styles-throughout-your-application /// [gettext]: https://docs.python.org/3/library/gettext.html /// [FastAPI path]: https://fastapi.tiangolo.com/tutorial/path-params/ From 4443f6653c640fb2658209137f4523235cd4cd89 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 27 Apr 2025 05:55:41 -0400 Subject: [PATCH 0136/1161] [red-knot] Add --respect-ignore-files flag (#17645) Co-authored-by: Micha Reiser --- crates/red_knot/src/args.rs | 21 ++++++ crates/red_knot/tests/cli.rs | 68 +++++++++++++++++++ .../red_knot_project/src/metadata/options.rs | 5 +- .../red_knot_project/src/metadata/settings.rs | 9 ++- crates/red_knot_project/src/walk.rs | 5 +- knot.schema.json | 6 ++ 6 files changed, 111 insertions(+), 3 deletions(-) diff --git a/crates/red_knot/src/args.rs b/crates/red_knot/src/args.rs index 233a081445a42c..00b40dd5ff9991 100644 --- a/crates/red_knot/src/args.rs +++ b/crates/red_knot/src/args.rs @@ -105,6 +105,19 @@ pub(crate) struct CheckCommand { /// Watch files for changes and recheck files related to the changed files. #[arg(long, short = 'W')] pub(crate) watch: bool, + + /// Respect file exclusions via `.gitignore` and other standard ignore files. + /// Use `--no-respect-gitignore` to disable. + #[arg( + long, + overrides_with("no_respect_ignore_files"), + help_heading = "File selection", + default_missing_value = "true", + num_args = 0..1 + )] + respect_ignore_files: Option, + #[clap(long, overrides_with("respect_ignore_files"), hide = true)] + no_respect_ignore_files: bool, } impl CheckCommand { @@ -120,6 +133,13 @@ impl CheckCommand { ) }; + // --no-respect-gitignore defaults to false and is set true by CLI flag. If passed, override config file + // Otherwise, only pass this through if explicitly set (don't default to anything here to + // make sure that doesn't take precedence over an explicitly-set config file value) + let respect_ignore_files = self + .no_respect_ignore_files + .then_some(false) + .or(self.respect_ignore_files); Options { environment: Some(EnvironmentOptions { python_version: self @@ -144,6 +164,7 @@ impl CheckCommand { error_on_warning: self.error_on_warning, }), rules, + respect_ignore_files, ..Default::default() } } diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index 4509882e505866..c0ecb475fbbaa5 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -5,6 +5,74 @@ use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::TempDir; +#[test] +fn test_respect_ignore_files() -> anyhow::Result<()> { + // First test that the default option works correctly (the file is skipped) + let case = TestCase::with_files([(".ignore", "test.py"), ("test.py", "~")])?; + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN No python files found under the given path(s) + "); + + // Test that we can set to false via CLI + assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error: invalid-syntax + --> /test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + "); + + // Test that we can set to false via config file + case.write_file("knot.toml", "respect-ignore-files = false")?; + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error: invalid-syntax + --> /test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + "); + + // Ensure CLI takes precedence + case.write_file("knot.toml", "respect-ignore-files = true")?; + assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error: invalid-syntax + --> /test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + "); + Ok(()) +} /// Specifying an option on the CLI should take precedence over the same setting in the /// project's configuration. Here, this is tested for the Python version. #[test] diff --git a/crates/red_knot_project/src/metadata/options.rs b/crates/red_knot_project/src/metadata/options.rs index d035f4cf7be5db..eeff13808289c6 100644 --- a/crates/red_knot_project/src/metadata/options.rs +++ b/crates/red_knot_project/src/metadata/options.rs @@ -32,6 +32,9 @@ pub struct Options { #[serde(skip_serializing_if = "Option::is_none")] pub terminal: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub respect_ignore_files: Option, } impl Options { @@ -133,7 +136,7 @@ impl Options { pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec) { let (rules, diagnostics) = self.to_rule_selection(db); - let mut settings = Settings::new(rules); + let mut settings = Settings::new(rules, self.respect_ignore_files); if let Some(terminal) = self.terminal.as_ref() { settings.set_terminal(TerminalSettings { diff --git a/crates/red_knot_project/src/metadata/settings.rs b/crates/red_knot_project/src/metadata/settings.rs index e16a72d4a030e5..f5572f6657428a 100644 --- a/crates/red_knot_project/src/metadata/settings.rs +++ b/crates/red_knot_project/src/metadata/settings.rs @@ -21,13 +21,16 @@ pub struct Settings { rules: Arc, terminal: TerminalSettings, + + respect_ignore_files: bool, } impl Settings { - pub fn new(rules: RuleSelection) -> Self { + pub fn new(rules: RuleSelection, respect_ignore_files: Option) -> Self { Self { rules: Arc::new(rules), terminal: TerminalSettings::default(), + respect_ignore_files: respect_ignore_files.unwrap_or(true), } } @@ -35,6 +38,10 @@ impl Settings { &self.rules } + pub fn respect_ignore_files(&self) -> bool { + self.respect_ignore_files + } + pub fn to_rules(&self) -> Arc { self.rules.clone() } diff --git a/crates/red_knot_project/src/walk.rs b/crates/red_knot_project/src/walk.rs index 531c823a28afeb..025566db382712 100644 --- a/crates/red_knot_project/src/walk.rs +++ b/crates/red_knot_project/src/walk.rs @@ -129,7 +129,10 @@ impl<'a> ProjectFilesWalker<'a> { { let mut paths = paths.into_iter(); - let mut walker = db.system().walk_directory(paths.next()?.as_ref()); + let mut walker = db + .system() + .walk_directory(paths.next()?.as_ref()) + .standard_filters(db.project().settings(db).respect_ignore_files()); for path in paths { walker = walker.add(path); diff --git a/knot.schema.json b/knot.schema.json index 941eee5af93e72..7349373c0e467d 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -15,6 +15,12 @@ } ] }, + "respect-ignore-files": { + "type": [ + "boolean", + "null" + ] + }, "rules": { "description": "Configures the enabled lints and their severity.", "anyOf": [ From 1c65e0ad255fceeb71ef279a01b6e5d08bdd88dd Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sun, 27 Apr 2025 11:27:33 +0100 Subject: [PATCH 0137/1161] Split `SourceLocation` into `LineColumn` and `SourceLocation` (#17587) --- crates/red_knot_server/src/document.rs | 10 + crates/red_knot_server/src/document/range.rs | 128 +---- crates/red_knot_test/src/matcher.rs | 2 +- crates/red_knot_wasm/src/lib.rs | 35 +- crates/ruff/src/args.rs | 19 +- crates/ruff_db/src/diagnostic/render.rs | 4 +- .../src/checkers/ast/analyze/definitions.rs | 2 +- crates/ruff_linter/src/locator.rs | 6 +- crates/ruff_linter/src/logging.rs | 22 +- crates/ruff_linter/src/message/azure.rs | 6 +- crates/ruff_linter/src/message/github.rs | 12 +- crates/ruff_linter/src/message/gitlab.rs | 4 +- crates/ruff_linter/src/message/grouped.rs | 12 +- crates/ruff_linter/src/message/json.rs | 62 ++- crates/ruff_linter/src/message/junit.rs | 8 +- crates/ruff_linter/src/message/mod.rs | 14 +- crates/ruff_linter/src/message/pylint.rs | 2 +- crates/ruff_linter/src/message/rdjson.rs | 28 +- crates/ruff_linter/src/message/sarif.rs | 8 +- crates/ruff_linter/src/message/text.rs | 12 +- crates/ruff_notebook/src/index.rs | 25 +- .../ruff_python_formatter/tests/fixtures.rs | 4 +- crates/ruff_server/src/edit.rs | 10 + crates/ruff_server/src/edit/range.rs | 170 ++---- crates/ruff_server/src/server.rs | 3 + crates/ruff_source_file/src/lib.rs | 68 ++- crates/ruff_source_file/src/line_index.rs | 505 +++++++++++++----- crates/ruff_wasm/src/lib.rs | 37 +- crates/ruff_wasm/tests/api.rs | 16 +- 29 files changed, 696 insertions(+), 538 deletions(-) diff --git a/crates/red_knot_server/src/document.rs b/crates/red_knot_server/src/document.rs index 7dca10720fe2b3..741a7f74133eb5 100644 --- a/crates/red_knot_server/src/document.rs +++ b/crates/red_knot_server/src/document.rs @@ -27,6 +27,16 @@ pub enum PositionEncoding { UTF8, } +impl From for ruff_source_file::PositionEncoding { + fn from(value: PositionEncoding) -> Self { + match value { + PositionEncoding::UTF8 => Self::Utf8, + PositionEncoding::UTF16 => Self::Utf16, + PositionEncoding::UTF32 => Self::Utf32, + } + } +} + /// A unique document ID, derived from a URL passed as part of an LSP request. /// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook. #[derive(Clone, Debug)] diff --git a/crates/red_knot_server/src/document/range.rs b/crates/red_knot_server/src/document/range.rs index 0e268dcfb1cdb8..492f2f86ee308e 100644 --- a/crates/red_knot_server/src/document/range.rs +++ b/crates/red_knot_server/src/document/range.rs @@ -9,8 +9,8 @@ use red_knot_python_semantic::Db; use ruff_db::files::FileRange; use ruff_db::source::{line_index, source_text}; use ruff_notebook::NotebookIndex; -use ruff_source_file::OneIndexed; -use ruff_source_file::{LineIndex, SourceLocation}; +use ruff_source_file::LineIndex; +use ruff_source_file::{OneIndexed, SourceLocation}; use ruff_text_size::{Ranged, TextRange, TextSize}; #[expect(dead_code)] @@ -46,7 +46,7 @@ impl TextSizeExt for TextSize { index: &LineIndex, encoding: PositionEncoding, ) -> types::Position { - let source_location = offset_to_source_location(self, text, index, encoding); + let source_location = index.source_location(self, text, encoding.into()); source_location_to_position(&source_location) } } @@ -75,36 +75,14 @@ fn u32_index_to_usize(index: u32) -> usize { impl PositionExt for lsp_types::Position { fn to_text_size(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> TextSize { - let start_line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.line)), + index.offset( + SourceLocation { + line: OneIndexed::from_zero_indexed(u32_index_to_usize(self.line)), + character_offset: OneIndexed::from_zero_indexed(u32_index_to_usize(self.character)), + }, text, - ); - - let start_column_offset = match encoding { - PositionEncoding::UTF8 => TextSize::new(self.character), - - PositionEncoding::UTF16 => { - // Fast path for ASCII only documents - if index.is_ascii() { - TextSize::new(self.character) - } else { - // UTF16 encodes characters either as one or two 16 bit words. - // The position in `range` is the 16-bit word offset from the start of the line (and not the character offset) - // UTF-16 with a text that may use variable-length characters. - utf8_column_offset(self.character, &text[start_line]) - } - } - PositionEncoding::UTF32 => { - // UTF-32 uses 4 bytes for each character. Meaning, the position in range is a character offset. - return index.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(self.character)), - text, - ); - } - }; - - start_line.start() + start_column_offset.clamp(TextSize::new(0), start_line.end()) + encoding.into(), + ) } } @@ -142,26 +120,23 @@ impl ToRangeExt for TextRange { notebook_index: &NotebookIndex, encoding: PositionEncoding, ) -> NotebookRange { - let start = offset_to_source_location(self.start(), text, source_index, encoding); - let mut end = offset_to_source_location(self.end(), text, source_index, encoding); - let starting_cell = notebook_index.cell(start.row); + let start = source_index.source_location(self.start(), text, encoding.into()); + let mut end = source_index.source_location(self.end(), text, encoding.into()); + let starting_cell = notebook_index.cell(start.line); // weird edge case here - if the end of the range is where the newline after the cell got added (making it 'out of bounds') // we need to move it one character back (which should place it at the end of the last line). // we test this by checking if the ending offset is in a different (or nonexistent) cell compared to the cell of the starting offset. - if notebook_index.cell(end.row) != starting_cell { - end.row = end.row.saturating_sub(1); - end.column = offset_to_source_location( - self.end().checked_sub(1.into()).unwrap_or_default(), - text, - source_index, - encoding, - ) - .column; + if notebook_index.cell(end.line) != starting_cell { + end.line = end.line.saturating_sub(1); + let offset = self.end().checked_sub(1.into()).unwrap_or_default(); + end.character_offset = source_index + .source_location(offset, text, encoding.into()) + .character_offset; } - let start = source_location_to_position(¬ebook_index.translate_location(&start)); - let end = source_location_to_position(¬ebook_index.translate_location(&end)); + let start = source_location_to_position(¬ebook_index.translate_source_location(&start)); + let end = source_location_to_position(¬ebook_index.translate_source_location(&end)); NotebookRange { cell: starting_cell @@ -172,67 +147,10 @@ impl ToRangeExt for TextRange { } } -/// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number. -fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { - let mut utf8_code_unit_offset = TextSize::new(0); - - let mut i = 0u32; - - for c in line.chars() { - if i >= utf16_code_unit_offset { - break; - } - - // Count characters encoded as two 16 bit words as 2 characters. - { - utf8_code_unit_offset += - TextSize::new(u32::try_from(c.len_utf8()).expect("utf8 len always <=4")); - i += u32::try_from(c.len_utf16()).expect("utf16 len always <=2"); - } - } - - utf8_code_unit_offset -} - -fn offset_to_source_location( - offset: TextSize, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, -) -> SourceLocation { - match encoding { - PositionEncoding::UTF8 => { - let row = index.line_index(offset); - let column = offset - index.line_start(row, text); - - SourceLocation { - column: OneIndexed::from_zero_indexed(column.to_usize()), - row, - } - } - PositionEncoding::UTF16 => { - let row = index.line_index(offset); - - let column = if index.is_ascii() { - (offset - index.line_start(row, text)).to_usize() - } else { - let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)]; - up_to_line.encode_utf16().count() - }; - - SourceLocation { - column: OneIndexed::from_zero_indexed(column), - row, - } - } - PositionEncoding::UTF32 => index.source_location(offset, text), - } -} - fn source_location_to_position(location: &SourceLocation) -> types::Position { types::Position { - line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"), - character: u32::try_from(location.column.to_zero_indexed()) + line: u32::try_from(location.line.to_zero_indexed()).expect("line usize fits in u32"), + character: u32::try_from(location.character_offset.to_zero_indexed()) .expect("character usize fits in u32"), } } diff --git a/crates/red_knot_test/src/matcher.rs b/crates/red_knot_test/src/matcher.rs index 63ae8c44af7ab9..4be6658b00868d 100644 --- a/crates/red_knot_test/src/matcher.rs +++ b/crates/red_knot_test/src/matcher.rs @@ -263,7 +263,7 @@ impl Matcher { .and_then(|span| span.range()) .map(|range| { self.line_index - .source_location(range.start(), &self.source) + .line_column(range.start(), &self.source) .column }) .unwrap_or(OneIndexed::from_zero_indexed(0)) diff --git a/crates/red_knot_wasm/src/lib.rs b/crates/red_knot_wasm/src/lib.rs index ed96760409b847..5f9333d028bfa7 100644 --- a/crates/red_knot_wasm/src/lib.rs +++ b/crates/red_knot_wasm/src/lib.rs @@ -19,7 +19,7 @@ use ruff_db::system::{ use ruff_db::Upcast; use ruff_notebook::Notebook; use ruff_python_formatter::formatted_file; -use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; +use ruff_source_file::{LineColumn, LineIndex, OneIndexed, PositionEncoding, SourceLocation}; use ruff_text_size::{Ranged, TextSize}; use wasm_bindgen::prelude::*; @@ -408,8 +408,8 @@ impl Range { } } -impl From<(SourceLocation, SourceLocation)> for Range { - fn from((start, end): (SourceLocation, SourceLocation)) -> Self { +impl From<(LineColumn, LineColumn)> for Range { + fn from((start, end): (LineColumn, LineColumn)) -> Self { Self { start: start.into(), end: end.into(), @@ -438,29 +438,34 @@ impl Position { impl Position { fn to_text_size(self, text: &str, index: &LineIndex) -> Result { let text_size = index.offset( - OneIndexed::new(self.line).ok_or_else(|| { - Error::new("Invalid value `0` for `position.line`. The line index is 1-indexed.") - })?, - OneIndexed::new(self.column).ok_or_else(|| { - Error::new( - "Invalid value `0` for `position.column`. The column index is 1-indexed.", - ) - })?, + SourceLocation { + line: OneIndexed::new(self.line).ok_or_else(|| { + Error::new( + "Invalid value `0` for `position.line`. The line index is 1-indexed.", + ) + })?, + character_offset: OneIndexed::new(self.column).ok_or_else(|| { + Error::new( + "Invalid value `0` for `position.column`. The column index is 1-indexed.", + ) + })?, + }, text, + PositionEncoding::Utf32, ); Ok(text_size) } fn from_text_size(offset: TextSize, line_index: &LineIndex, source: &str) -> Self { - line_index.source_location(offset, source).into() + line_index.line_column(offset, source).into() } } -impl From for Position { - fn from(location: SourceLocation) -> Self { +impl From for Position { + fn from(location: LineColumn) -> Self { Self { - line: location.row.get(), + line: location.line.get(), column: location.column.get(), } } diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index fa4655de57a421..e28508b381837b 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; +use crate::commands::completions::config::{OptionString, OptionStringParser}; use anyhow::bail; use clap::builder::{TypedValueParser, ValueParserFactory}; use clap::{command, Parser, Subcommand}; @@ -22,7 +23,7 @@ use ruff_linter::settings::types::{ }; use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser}; use ruff_python_ast as ast; -use ruff_source_file::{LineIndex, OneIndexed}; +use ruff_source_file::{LineIndex, OneIndexed, PositionEncoding}; use ruff_text_size::TextRange; use ruff_workspace::configuration::{Configuration, RuleSelection}; use ruff_workspace::options::{Options, PycodestyleOptions}; @@ -31,8 +32,6 @@ use ruff_workspace::resolver::ConfigurationTransformer; use rustc_hash::FxHashMap; use toml; -use crate::commands::completions::config::{OptionString, OptionStringParser}; - /// All configuration options that can be passed "globally", /// i.e., can be passed to all subcommands #[derive(Debug, Default, Clone, clap::Args)] @@ -1070,8 +1069,9 @@ impl FormatRange { /// /// Returns an empty range if the start range is past the end of `source`. pub(super) fn to_text_range(self, source: &str, line_index: &LineIndex) -> TextRange { - let start_byte_offset = line_index.offset(self.start.line, self.start.column, source); - let end_byte_offset = line_index.offset(self.end.line, self.end.column, source); + let start_byte_offset = + line_index.offset(self.start.into(), source, PositionEncoding::Utf32); + let end_byte_offset = line_index.offset(self.end.into(), source, PositionEncoding::Utf32); TextRange::new(start_byte_offset, end_byte_offset) } @@ -1142,6 +1142,15 @@ pub struct LineColumn { pub column: OneIndexed, } +impl From for ruff_source_file::SourceLocation { + fn from(value: LineColumn) -> Self { + Self { + line: value.line, + character_offset: value.column, + } + } +} + impl std::fmt::Display for LineColumn { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{line}:{column}", line = self.line, column = self.column) diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index ece51ae381d4ea..f69980d0681f2e 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -71,8 +71,8 @@ impl std::fmt::Display for DisplayDiagnostic<'_> { write!(f, " {path}", path = self.resolver.path(span.file()))?; if let Some(range) = span.range() { let input = self.resolver.input(span.file()); - let start = input.as_source_code().source_location(range.start()); - write!(f, ":{line}:{col}", line = start.row, col = start.column)?; + let start = input.as_source_code().line_column(range.start()); + write!(f, ":{line}:{col}", line = start.line, col = start.column)?; } write!(f, ":")?; } diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index f0a4cd2f0a9ddc..392094ce56b2e5 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -191,7 +191,7 @@ pub(crate) fn definitions(checker: &mut Checker) { warn_user!( "Docstring at {}:{}:{} contains implicit string concatenation; ignoring...", relativize_path(checker.path), - location.row, + location.line, location.column ); continue; diff --git a/crates/ruff_linter/src/locator.rs b/crates/ruff_linter/src/locator.rs index 5aeaced1b31375..afc212e6a68ca0 100644 --- a/crates/ruff_linter/src/locator.rs +++ b/crates/ruff_linter/src/locator.rs @@ -2,7 +2,7 @@ use std::cell::OnceCell; -use ruff_source_file::{LineIndex, LineRanges, OneIndexed, SourceCode, SourceLocation}; +use ruff_source_file::{LineColumn, LineIndex, LineRanges, OneIndexed, SourceCode}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; #[derive(Debug)] @@ -36,8 +36,8 @@ impl<'a> Locator<'a> { #[deprecated( note = "This is expensive, avoid using outside of the diagnostic phase. Prefer the other `Locator` methods instead." )] - pub fn compute_source_location(&self, offset: TextSize) -> SourceLocation { - self.to_source_code().source_location(offset) + pub fn compute_source_location(&self, offset: TextSize) -> LineColumn { + self.to_source_code().line_column(offset) } pub fn to_index(&self) -> &LineIndex { diff --git a/crates/ruff_linter/src/logging.rs b/crates/ruff_linter/src/logging.rs index 8e07e0e84f7b2a..554c4e2c842442 100644 --- a/crates/ruff_linter/src/logging.rs +++ b/crates/ruff_linter/src/logging.rs @@ -9,7 +9,7 @@ use log::Level; use ruff_python_parser::{ParseError, ParseErrorType}; use rustc_hash::FxHashSet; -use ruff_source_file::{LineIndex, OneIndexed, SourceCode, SourceLocation}; +use ruff_source_file::{LineColumn, LineIndex, OneIndexed, SourceCode}; use crate::fs; use crate::source_kind::SourceKind; @@ -195,21 +195,21 @@ impl DisplayParseError { // Translate the byte offset to a location in the originating source. let location = if let Some(jupyter_index) = source_kind.as_ipy_notebook().map(Notebook::index) { - let source_location = source_code.source_location(error.location.start()); + let source_location = source_code.line_column(error.location.start()); ErrorLocation::Cell( jupyter_index - .cell(source_location.row) + .cell(source_location.line) .unwrap_or(OneIndexed::MIN), - SourceLocation { - row: jupyter_index - .cell_row(source_location.row) + LineColumn { + line: jupyter_index + .cell_row(source_location.line) .unwrap_or(OneIndexed::MIN), column: source_location.column, }, ) } else { - ErrorLocation::File(source_code.source_location(error.location.start())) + ErrorLocation::File(source_code.line_column(error.location.start())) }; Self { @@ -245,7 +245,7 @@ impl Display for DisplayParseError { write!( f, "{row}{colon}{column}{colon} {inner}", - row = location.row, + row = location.line, column = location.column, colon = ":".cyan(), inner = &DisplayParseErrorType(&self.error.error) @@ -256,7 +256,7 @@ impl Display for DisplayParseError { f, "{cell}{colon}{row}{colon}{column}{colon} {inner}", cell = cell, - row = location.row, + row = location.line, column = location.column, colon = ":".cyan(), inner = &DisplayParseErrorType(&self.error.error) @@ -283,9 +283,9 @@ impl Display for DisplayParseErrorType<'_> { #[derive(Debug)] enum ErrorLocation { /// The error occurred in a Python file. - File(SourceLocation), + File(LineColumn), /// The error occurred in a Jupyter cell. - Cell(OneIndexed, SourceLocation), + Cell(OneIndexed, LineColumn), } /// Truncates the display text before the first newline character to avoid line breaks. diff --git a/crates/ruff_linter/src/message/azure.rs b/crates/ruff_linter/src/message/azure.rs index c7d6049eac049f..727a427f9b15d0 100644 --- a/crates/ruff_linter/src/message/azure.rs +++ b/crates/ruff_linter/src/message/azure.rs @@ -1,6 +1,6 @@ use std::io::Write; -use ruff_source_file::SourceLocation; +use ruff_source_file::LineColumn; use crate::message::{Emitter, EmitterContext, Message}; @@ -20,7 +20,7 @@ impl Emitter for AzureEmitter { let location = if context.is_notebook(message.filename()) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback - SourceLocation::default() + LineColumn::default() } else { message.compute_start_location() }; @@ -30,7 +30,7 @@ impl Emitter for AzureEmitter { "##vso[task.logissue type=error\ ;sourcepath={filename};linenumber={line};columnnumber={col};{code}]{body}", filename = message.filename(), - line = location.row, + line = location.line, col = location.column, code = message .rule() diff --git a/crates/ruff_linter/src/message/github.rs b/crates/ruff_linter/src/message/github.rs index 9fd0a5ee6b912a..a038b177041428 100644 --- a/crates/ruff_linter/src/message/github.rs +++ b/crates/ruff_linter/src/message/github.rs @@ -1,6 +1,6 @@ use std::io::Write; -use ruff_source_file::SourceLocation; +use ruff_source_file::LineColumn; use crate::fs::relativize_path; use crate::message::{Emitter, EmitterContext, Message}; @@ -22,9 +22,9 @@ impl Emitter for GithubEmitter { let location = if context.is_notebook(message.filename()) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback - SourceLocation::default() + LineColumn::default() } else { - source_location.clone() + source_location }; let end_location = message.compute_end_location(); @@ -34,9 +34,9 @@ impl Emitter for GithubEmitter { "::error title=Ruff{code},file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::", code = message.rule().map_or_else(String::new, |rule| format!(" ({})", rule.noqa_code())), file = message.filename(), - row = source_location.row, + row = source_location.line, column = source_location.column, - end_row = end_location.row, + end_row = end_location.line, end_column = end_location.column, )?; @@ -44,7 +44,7 @@ impl Emitter for GithubEmitter { writer, "{path}:{row}:{column}:", path = relativize_path(message.filename()), - row = location.row, + row = location.line, column = location.column, )?; diff --git a/crates/ruff_linter/src/message/gitlab.rs b/crates/ruff_linter/src/message/gitlab.rs index 6433184ac52099..6d03a7c803f39e 100644 --- a/crates/ruff_linter/src/message/gitlab.rs +++ b/crates/ruff_linter/src/message/gitlab.rs @@ -71,8 +71,8 @@ impl Serialize for SerializedMessages<'_> { }) } else { json!({ - "begin": start_location.row, - "end": end_location.row + "begin": start_location.line, + "end": end_location.line }) }; diff --git a/crates/ruff_linter/src/message/grouped.rs b/crates/ruff_linter/src/message/grouped.rs index 1dfa5d15e6b2b9..b60830c961a4f3 100644 --- a/crates/ruff_linter/src/message/grouped.rs +++ b/crates/ruff_linter/src/message/grouped.rs @@ -57,7 +57,7 @@ impl Emitter for GroupedEmitter { let mut max_column_length = OneIndexed::MIN; for message in &messages { - max_row_length = max_row_length.max(message.start_location.row); + max_row_length = max_row_length.max(message.start_location.line); max_column_length = max_column_length.max(message.start_location.column); } @@ -115,8 +115,8 @@ impl Display for DisplayGroupedMessage<'_> { write!( f, " {row_padding}", - row_padding = - " ".repeat(self.row_length.get() - calculate_print_width(start_location.row).get()) + row_padding = " " + .repeat(self.row_length.get() - calculate_print_width(start_location.line).get()) )?; // Check if we're working on a jupyter notebook and translate positions with cell accordingly @@ -125,18 +125,18 @@ impl Display for DisplayGroupedMessage<'_> { f, "cell {cell}{sep}", cell = jupyter_index - .cell(start_location.row) + .cell(start_location.line) .unwrap_or(OneIndexed::MIN), sep = ":".cyan() )?; ( jupyter_index - .cell_row(start_location.row) + .cell_row(start_location.line) .unwrap_or(OneIndexed::MIN), start_location.column, ) } else { - (start_location.row, start_location.column) + (start_location.line, start_location.column) }; writeln!( diff --git a/crates/ruff_linter/src/message/json.rs b/crates/ruff_linter/src/message/json.rs index eaa968c167b5b7..b8d91c7db3bcf9 100644 --- a/crates/ruff_linter/src/message/json.rs +++ b/crates/ruff_linter/src/message/json.rs @@ -6,7 +6,7 @@ use serde_json::{json, Value}; use ruff_diagnostics::Edit; use ruff_notebook::NotebookIndex; -use ruff_source_file::{OneIndexed, SourceCode, SourceLocation}; +use ruff_source_file::{LineColumn, OneIndexed, SourceCode}; use ruff_text_size::Ranged; use crate::message::{Emitter, EmitterContext, Message}; @@ -60,22 +60,23 @@ pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) }) }); - let mut start_location = source_code.source_location(message.start()); - let mut end_location = source_code.source_location(message.end()); + let mut start_location = source_code.line_column(message.start()); + let mut end_location = source_code.line_column(message.end()); let mut noqa_location = message .noqa_offset() - .map(|offset| source_code.source_location(offset)); + .map(|offset| source_code.line_column(offset)); let mut notebook_cell_index = None; if let Some(notebook_index) = notebook_index { notebook_cell_index = Some( notebook_index - .cell(start_location.row) + .cell(start_location.line) .unwrap_or(OneIndexed::MIN), ); - start_location = notebook_index.translate_location(&start_location); - end_location = notebook_index.translate_location(&end_location); - noqa_location = noqa_location.map(|location| notebook_index.translate_location(&location)); + start_location = notebook_index.translate_line_column(&start_location); + end_location = notebook_index.translate_line_column(&end_location); + noqa_location = + noqa_location.map(|location| notebook_index.translate_line_column(&location)); } json!({ @@ -84,10 +85,17 @@ pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) "message": message.body(), "fix": fix, "cell": notebook_cell_index, - "location": start_location, - "end_location": end_location, + "location": location_to_json(start_location), + "end_location": location_to_json(end_location), "filename": message.filename(), - "noqa_row": noqa_location.map(|location| location.row) + "noqa_row": noqa_location.map(|location| location.line) + }) +} + +fn location_to_json(location: LineColumn) -> serde_json::Value { + json!({ + "row": location.line, + "column": location.column }) } @@ -105,8 +113,8 @@ impl Serialize for ExpandedEdits<'_> { let mut s = serializer.serialize_seq(Some(self.edits.len()))?; for edit in self.edits { - let mut location = self.source_code.source_location(edit.start()); - let mut end_location = self.source_code.source_location(edit.end()); + let mut location = self.source_code.line_column(edit.start()); + let mut end_location = self.source_code.line_column(edit.end()); if let Some(notebook_index) = self.notebook_index { // There exists a newline between each cell's source code in the @@ -118,44 +126,44 @@ impl Serialize for ExpandedEdits<'_> { // If it does, we need to translate the end location to the last // character of the previous cell. match ( - notebook_index.cell(location.row), - notebook_index.cell(end_location.row), + notebook_index.cell(location.line), + notebook_index.cell(end_location.line), ) { (Some(start_cell), Some(end_cell)) if start_cell != end_cell => { debug_assert_eq!(end_location.column.get(), 1); - let prev_row = end_location.row.saturating_sub(1); - end_location = SourceLocation { - row: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN), + let prev_row = end_location.line.saturating_sub(1); + end_location = LineColumn { + line: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN), column: self .source_code - .source_location(self.source_code.line_end_exclusive(prev_row)) + .line_column(self.source_code.line_end_exclusive(prev_row)) .column, }; } (Some(_), None) => { debug_assert_eq!(end_location.column.get(), 1); - let prev_row = end_location.row.saturating_sub(1); - end_location = SourceLocation { - row: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN), + let prev_row = end_location.line.saturating_sub(1); + end_location = LineColumn { + line: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN), column: self .source_code - .source_location(self.source_code.line_end_exclusive(prev_row)) + .line_column(self.source_code.line_end_exclusive(prev_row)) .column, }; } _ => { - end_location = notebook_index.translate_location(&end_location); + end_location = notebook_index.translate_line_column(&end_location); } } - location = notebook_index.translate_location(&location); + location = notebook_index.translate_line_column(&location); } let value = json!({ "content": edit.content().unwrap_or_default(), - "location": location, - "end_location": end_location + "location": location_to_json(location), + "end_location": location_to_json(end_location) }); s.serialize_element(&value)?; diff --git a/crates/ruff_linter/src/message/junit.rs b/crates/ruff_linter/src/message/junit.rs index 7c7e341809e9bf..38edfdfca82fcd 100644 --- a/crates/ruff_linter/src/message/junit.rs +++ b/crates/ruff_linter/src/message/junit.rs @@ -3,7 +3,7 @@ use std::path::Path; use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite, XmlString}; -use ruff_source_file::SourceLocation; +use ruff_source_file::LineColumn; use crate::message::{ group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation, @@ -47,14 +47,14 @@ impl Emitter for JunitEmitter { let location = if context.is_notebook(message.filename()) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback - SourceLocation::default() + LineColumn::default() } else { start_location }; status.set_description(format!( "line {row}, col {col}, {body}", - row = location.row, + row = location.line, col = location.column, body = message.body() )); @@ -72,7 +72,7 @@ impl Emitter for JunitEmitter { case.set_classname(classname.to_str().unwrap()); case.extra.insert( XmlString::new("line"), - XmlString::new(location.row.to_string()), + XmlString::new(location.line.to_string()), ); case.extra.insert( XmlString::new("column"), diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index 7f16eeac54df2d..a3146ed93d490d 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -18,7 +18,7 @@ pub use rdjson::RdjsonEmitter; use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix}; use ruff_notebook::NotebookIndex; use ruff_python_parser::{ParseError, UnsupportedSyntaxError}; -use ruff_source_file::{SourceFile, SourceLocation}; +use ruff_source_file::{LineColumn, SourceFile}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; pub use sarif::SarifEmitter; pub use text::TextEmitter; @@ -239,17 +239,15 @@ impl Message { } /// Computes the start source location for the message. - pub fn compute_start_location(&self) -> SourceLocation { + pub fn compute_start_location(&self) -> LineColumn { self.source_file() .to_source_code() - .source_location(self.start()) + .line_column(self.start()) } /// Computes the end source location for the message. - pub fn compute_end_location(&self) -> SourceLocation { - self.source_file() - .to_source_code() - .source_location(self.end()) + pub fn compute_end_location(&self) -> LineColumn { + self.source_file().to_source_code().line_column(self.end()) } /// Returns the [`SourceFile`] which the message belongs to. @@ -284,7 +282,7 @@ impl Ranged for Message { struct MessageWithLocation<'a> { message: &'a Message, - start_location: SourceLocation, + start_location: LineColumn, } impl Deref for MessageWithLocation<'_> { diff --git a/crates/ruff_linter/src/message/pylint.rs b/crates/ruff_linter/src/message/pylint.rs index 10b1f81f1076db..91d79326a71b35 100644 --- a/crates/ruff_linter/src/message/pylint.rs +++ b/crates/ruff_linter/src/message/pylint.rs @@ -23,7 +23,7 @@ impl Emitter for PylintEmitter { // so we show one that's clearly a fallback OneIndexed::from_zero_indexed(0) } else { - message.compute_start_location().row + message.compute_start_location().line }; let body = if let Some(rule) = message.rule() { diff --git a/crates/ruff_linter/src/message/rdjson.rs b/crates/ruff_linter/src/message/rdjson.rs index 99b3fc481e2578..26c3e1d09190fa 100644 --- a/crates/ruff_linter/src/message/rdjson.rs +++ b/crates/ruff_linter/src/message/rdjson.rs @@ -8,7 +8,7 @@ use ruff_diagnostics::Edit; use ruff_source_file::SourceCode; use ruff_text_size::Ranged; -use crate::message::{Emitter, EmitterContext, Message, SourceLocation}; +use crate::message::{Emitter, EmitterContext, LineColumn, Message}; #[derive(Default)] pub struct RdjsonEmitter; @@ -59,15 +59,15 @@ impl Serialize for ExpandedMessages<'_> { fn message_to_rdjson_value(message: &Message) -> Value { let source_code = message.source_file().to_source_code(); - let start_location = source_code.source_location(message.start()); - let end_location = source_code.source_location(message.end()); + let start_location = source_code.line_column(message.start()); + let end_location = source_code.line_column(message.end()); if let Some(fix) = message.fix() { json!({ "message": message.body(), "location": { "path": message.filename(), - "range": rdjson_range(&start_location, &end_location), + "range": rdjson_range(start_location, end_location), }, "code": { "value": message.rule().map(|rule| rule.noqa_code().to_string()), @@ -80,7 +80,7 @@ fn message_to_rdjson_value(message: &Message) -> Value { "message": message.body(), "location": { "path": message.filename(), - "range": rdjson_range(&start_location, &end_location), + "range": rdjson_range(start_location, end_location), }, "code": { "value": message.rule().map(|rule| rule.noqa_code().to_string()), @@ -95,11 +95,11 @@ fn rdjson_suggestions(edits: &[Edit], source_code: &SourceCode) -> Value { edits .iter() .map(|edit| { - let location = source_code.source_location(edit.start()); - let end_location = source_code.source_location(edit.end()); + let location = source_code.line_column(edit.start()); + let end_location = source_code.line_column(edit.end()); json!({ - "range": rdjson_range(&location, &end_location), + "range": rdjson_range(location, end_location), "text": edit.content().unwrap_or_default(), }) }) @@ -107,16 +107,10 @@ fn rdjson_suggestions(edits: &[Edit], source_code: &SourceCode) -> Value { ) } -fn rdjson_range(start: &SourceLocation, end: &SourceLocation) -> Value { +fn rdjson_range(start: LineColumn, end: LineColumn) -> Value { json!({ - "start": { - "line": start.row, - "column": start.column, - }, - "end": { - "line": end.row, - "column": end.column, - }, + "start": start, + "end": end, }) } diff --git a/crates/ruff_linter/src/message/sarif.rs b/crates/ruff_linter/src/message/sarif.rs index a04bb441488e9a..b74e293fd4cfd1 100644 --- a/crates/ruff_linter/src/message/sarif.rs +++ b/crates/ruff_linter/src/message/sarif.rs @@ -129,9 +129,9 @@ impl SarifResult { uri: url::Url::from_file_path(&path) .map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))? .to_string(), - start_line: start_location.row, + start_line: start_location.line, start_column: start_location.column, - end_line: end_location.row, + end_line: end_location.line, end_column: end_location.column, }) } @@ -147,9 +147,9 @@ impl SarifResult { level: "error".to_string(), message: message.body().to_string(), uri: path.display().to_string(), - start_line: start_location.row, + start_line: start_location.line, start_column: start_location.column, - end_line: end_location.row, + end_line: end_location.line, end_column: end_location.column, }) } diff --git a/crates/ruff_linter/src/message/text.rs b/crates/ruff_linter/src/message/text.rs index 914f12a0752683..f1ede8c6218895 100644 --- a/crates/ruff_linter/src/message/text.rs +++ b/crates/ruff_linter/src/message/text.rs @@ -7,7 +7,7 @@ use colored::Colorize; use ruff_annotate_snippets::{Level, Renderer, Snippet}; use ruff_notebook::NotebookIndex; -use ruff_source_file::{OneIndexed, SourceLocation}; +use ruff_source_file::{LineColumn, OneIndexed}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::fs::relativize_path; @@ -86,14 +86,14 @@ impl Emitter for TextEmitter { writer, "cell {cell}{sep}", cell = notebook_index - .cell(start_location.row) + .cell(start_location.line) .unwrap_or(OneIndexed::MIN), sep = ":".cyan(), )?; - SourceLocation { - row: notebook_index - .cell_row(start_location.row) + LineColumn { + line: notebook_index + .cell_row(start_location.line) .unwrap_or(OneIndexed::MIN), column: start_location.column, } @@ -104,7 +104,7 @@ impl Emitter for TextEmitter { writeln!( writer, "{row}{sep}{col}{sep} {code_and_body}", - row = diagnostic_location.row, + row = diagnostic_location.line, col = diagnostic_location.column, sep = ":".cyan(), code_and_body = RuleCodeAndBody { diff --git a/crates/ruff_notebook/src/index.rs b/crates/ruff_notebook/src/index.rs index 9912b94e6225ee..35e4e07fcbe528 100644 --- a/crates/ruff_notebook/src/index.rs +++ b/crates/ruff_notebook/src/index.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use ruff_source_file::{OneIndexed, SourceLocation}; +use ruff_source_file::{LineColumn, OneIndexed, SourceLocation}; /// Jupyter Notebook indexing table /// @@ -33,16 +33,29 @@ impl NotebookIndex { self.row_to_row_in_cell.get(row.to_zero_indexed()).copied() } - /// Translates the given source location based on the indexing table. + /// Translates the given [`LineColumn`] based on the indexing table. /// /// This will translate the row/column in the concatenated source code /// to the row/column in the Jupyter Notebook. - pub fn translate_location(&self, source_location: &SourceLocation) -> SourceLocation { - SourceLocation { - row: self - .cell_row(source_location.row) + pub fn translate_line_column(&self, source_location: &LineColumn) -> LineColumn { + LineColumn { + line: self + .cell_row(source_location.line) .unwrap_or(OneIndexed::MIN), column: source_location.column, } } + + /// Translates the given [`SourceLocation`] based on the indexing table. + /// + /// This will translate the line/character in the concatenated source code + /// to the line/character in the Jupyter Notebook. + pub fn translate_source_location(&self, source_location: &SourceLocation) -> SourceLocation { + SourceLocation { + line: self + .cell_row(source_location.line) + .unwrap_or(OneIndexed::MIN), + character_offset: source_location.character_offset, + } + } } diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index 7e83d969cb9c0b..c2489d2d75fd1f 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -430,10 +430,10 @@ fn ensure_unchanged_ast( formatted_unsupported_syntax_errors .into_values() .map(|error| { - let location = index.source_location(error.start(), formatted_code); + let location = index.line_column(error.start(), formatted_code); format!( "{row}:{col} {error}", - row = location.row, + row = location.line, col = location.column ) }) diff --git a/crates/ruff_server/src/edit.rs b/crates/ruff_server/src/edit.rs index 3a7ffb4e3eb731..d0dfb91ae3e255 100644 --- a/crates/ruff_server/src/edit.rs +++ b/crates/ruff_server/src/edit.rs @@ -31,6 +31,16 @@ pub enum PositionEncoding { UTF8, } +impl From for ruff_source_file::PositionEncoding { + fn from(value: PositionEncoding) -> Self { + match value { + PositionEncoding::UTF8 => Self::Utf8, + PositionEncoding::UTF16 => Self::Utf16, + PositionEncoding::UTF32 => Self::Utf32, + } + } +} + /// A unique document ID, derived from a URL passed as part of an LSP request. /// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook. #[derive(Clone, Debug)] diff --git a/crates/ruff_server/src/edit/range.rs b/crates/ruff_server/src/edit/range.rs index 9ccef9e67de03f..bde1e454dac8c2 100644 --- a/crates/ruff_server/src/edit/range.rs +++ b/crates/ruff_server/src/edit/range.rs @@ -2,9 +2,9 @@ use super::notebook; use super::PositionEncoding; use lsp_types as types; use ruff_notebook::NotebookIndex; -use ruff_source_file::OneIndexed; -use ruff_source_file::{LineIndex, SourceLocation}; -use ruff_text_size::{TextRange, TextSize}; +use ruff_source_file::LineIndex; +use ruff_source_file::{OneIndexed, SourceLocation}; +use ruff_text_size::TextRange; pub(crate) struct NotebookRange { pub(crate) cell: notebook::CellId, @@ -38,76 +38,43 @@ impl RangeExt for lsp_types::Range { index: &LineIndex, encoding: PositionEncoding, ) -> TextRange { - let start_line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.line)), + let start = index.offset( + SourceLocation { + line: OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.line)), + character_offset: OneIndexed::from_zero_indexed(u32_index_to_usize( + self.start.character, + )), + }, text, + encoding.into(), ); - let end_line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.line)), + let end = index.offset( + SourceLocation { + line: OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.line)), + character_offset: OneIndexed::from_zero_indexed(u32_index_to_usize( + self.end.character, + )), + }, text, + encoding.into(), ); - let (start_column_offset, end_column_offset) = match encoding { - PositionEncoding::UTF8 => ( - TextSize::new(self.start.character), - TextSize::new(self.end.character), - ), - - PositionEncoding::UTF16 => { - // Fast path for ASCII only documents - if index.is_ascii() { - ( - TextSize::new(self.start.character), - TextSize::new(self.end.character), - ) - } else { - // UTF16 encodes characters either as one or two 16 bit words. - // The position in `range` is the 16-bit word offset from the start of the line (and not the character offset) - // UTF-16 with a text that may use variable-length characters. - ( - utf8_column_offset(self.start.character, &text[start_line]), - utf8_column_offset(self.end.character, &text[end_line]), - ) - } - } - PositionEncoding::UTF32 => { - // UTF-32 uses 4 bytes for each character. Meaning, the position in range is a character offset. - return TextRange::new( - index.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.character)), - text, - ), - index.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.character)), - text, - ), - ); - } - }; - - TextRange::new( - start_line.start() + start_column_offset.clamp(TextSize::new(0), start_line.end()), - end_line.start() + end_column_offset.clamp(TextSize::new(0), end_line.end()), - ) + TextRange::new(start, end) } } impl ToRangeExt for TextRange { fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range { types::Range { - start: source_location_to_position(&offset_to_source_location( + start: source_location_to_position(&index.source_location( self.start(), text, - index, - encoding, + encoding.into(), )), - end: source_location_to_position(&offset_to_source_location( + end: source_location_to_position(&index.source_location( self.end(), text, - index, - encoding, + encoding.into(), )), } } @@ -119,26 +86,26 @@ impl ToRangeExt for TextRange { notebook_index: &NotebookIndex, encoding: PositionEncoding, ) -> NotebookRange { - let start = offset_to_source_location(self.start(), text, source_index, encoding); - let mut end = offset_to_source_location(self.end(), text, source_index, encoding); - let starting_cell = notebook_index.cell(start.row); + let start = source_index.source_location(self.start(), text, encoding.into()); + let mut end = source_index.source_location(self.end(), text, encoding.into()); + let starting_cell = notebook_index.cell(start.line); // weird edge case here - if the end of the range is where the newline after the cell got added (making it 'out of bounds') // we need to move it one character back (which should place it at the end of the last line). // we test this by checking if the ending offset is in a different (or nonexistent) cell compared to the cell of the starting offset. - if notebook_index.cell(end.row) != starting_cell { - end.row = end.row.saturating_sub(1); - end.column = offset_to_source_location( - self.end().checked_sub(1.into()).unwrap_or_default(), - text, - source_index, - encoding, - ) - .column; + if notebook_index.cell(end.line) != starting_cell { + end.line = end.line.saturating_sub(1); + end.character_offset = source_index + .source_location( + self.end().checked_sub(1.into()).unwrap_or_default(), + text, + encoding.into(), + ) + .character_offset; } - let start = source_location_to_position(¬ebook_index.translate_location(&start)); - let end = source_location_to_position(¬ebook_index.translate_location(&end)); + let start = source_location_to_position(¬ebook_index.translate_source_location(&start)); + let end = source_location_to_position(¬ebook_index.translate_source_location(&end)); NotebookRange { cell: starting_cell @@ -149,67 +116,10 @@ impl ToRangeExt for TextRange { } } -/// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number. -fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { - let mut utf8_code_unit_offset = TextSize::new(0); - - let mut i = 0u32; - - for c in line.chars() { - if i >= utf16_code_unit_offset { - break; - } - - // Count characters encoded as two 16 bit words as 2 characters. - { - utf8_code_unit_offset += - TextSize::new(u32::try_from(c.len_utf8()).expect("utf8 len always <=4")); - i += u32::try_from(c.len_utf16()).expect("utf16 len always <=2"); - } - } - - utf8_code_unit_offset -} - -fn offset_to_source_location( - offset: TextSize, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, -) -> SourceLocation { - match encoding { - PositionEncoding::UTF8 => { - let row = index.line_index(offset); - let column = offset - index.line_start(row, text); - - SourceLocation { - column: OneIndexed::from_zero_indexed(column.to_usize()), - row, - } - } - PositionEncoding::UTF16 => { - let row = index.line_index(offset); - - let column = if index.is_ascii() { - (offset - index.line_start(row, text)).to_usize() - } else { - let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)]; - up_to_line.encode_utf16().count() - }; - - SourceLocation { - column: OneIndexed::from_zero_indexed(column), - row, - } - } - PositionEncoding::UTF32 => index.source_location(offset, text), - } -} - fn source_location_to_position(location: &SourceLocation) -> types::Position { types::Position { - line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"), - character: u32::try_from(location.column.to_zero_indexed()) + line: u32::try_from(location.line.to_zero_indexed()).expect("row usize fits in u32"), + character: u32::try_from(location.character_offset.to_zero_indexed()) .expect("character usize fits in u32"), } } diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index 88c0273fb602ff..b515147ca23009 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -59,6 +59,7 @@ impl Server { let client_capabilities = init_params.capabilities; let position_encoding = Self::find_best_position_encoding(&client_capabilities); + let server_capabilities = Self::server_capabilities(position_encoding); let connection = connection.initialize_finish( @@ -98,6 +99,8 @@ impl Server { workspace_settings.unwrap_or_default(), )?; + tracing::debug!("Negotiated position encoding: {position_encoding:?}"); + Ok(Self { connection, worker_threads, diff --git a/crates/ruff_source_file/src/lib.rs b/crates/ruff_source_file/src/lib.rs index 5bf43e3a1d4937..51bd8852a17277 100644 --- a/crates/ruff_source_file/src/lib.rs +++ b/crates/ruff_source_file/src/lib.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use ruff_text_size::{Ranged, TextRange, TextSize}; -pub use crate::line_index::{LineIndex, OneIndexed}; +pub use crate::line_index::{LineIndex, OneIndexed, PositionEncoding}; pub use crate::line_ranges::LineRanges; pub use crate::newlines::{ find_newline, Line, LineEnding, NewlineWithTrailingNewline, UniversalNewlineIterator, @@ -18,7 +18,7 @@ mod line_index; mod line_ranges; mod newlines; -/// Gives access to the source code of a file and allows mapping between [`TextSize`] and [`SourceLocation`]. +/// Gives access to the source code of a file and allows mapping between [`TextSize`] and [`LineColumn`]. #[derive(Debug)] pub struct SourceCode<'src, 'index> { text: &'src str, @@ -33,10 +33,20 @@ impl<'src, 'index> SourceCode<'src, 'index> { } } - /// Computes the one indexed row and column numbers for `offset`. + /// Computes the one indexed line and column numbers for `offset`, skipping any potential BOM. #[inline] - pub fn source_location(&self, offset: TextSize) -> SourceLocation { - self.index.source_location(offset, self.text) + pub fn line_column(&self, offset: TextSize) -> LineColumn { + self.index.line_column(offset, self.text) + } + + #[inline] + pub fn source_location( + &self, + offset: TextSize, + position_encoding: PositionEncoding, + ) -> SourceLocation { + self.index + .source_location(offset, self.text, position_encoding) } #[inline] @@ -229,34 +239,62 @@ impl PartialEq for SourceFileInner { impl Eq for SourceFileInner {} -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +/// The line and column of an offset in a source file. +/// +/// See [`LineIndex::line_column`] for more information. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct SourceLocation { - pub row: OneIndexed, +pub struct LineColumn { + /// The line in the source text. + pub line: OneIndexed, + /// The column (UTF scalar values) relative to the start of the line except any + /// potential BOM on the first line. pub column: OneIndexed, } -impl Default for SourceLocation { +impl Default for LineColumn { fn default() -> Self { Self { - row: OneIndexed::MIN, + line: OneIndexed::MIN, column: OneIndexed::MIN, } } } -impl Debug for SourceLocation { +impl Debug for LineColumn { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SourceLocation") - .field("row", &self.row.get()) + f.debug_struct("LineColumn") + .field("line", &self.line.get()) .field("column", &self.column.get()) .finish() } } -impl std::fmt::Display for SourceLocation { +impl std::fmt::Display for LineColumn { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{row}:{column}", row = self.row, column = self.column) + write!(f, "{line}:{column}", line = self.line, column = self.column) + } +} + +/// A position into a source file represented by the line number and the offset to that character relative to the start of that line. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SourceLocation { + /// The line in the source text. + pub line: OneIndexed, + /// The offset from the start of the line to the character. + /// + /// This can be a byte offset, the number of UTF16 code points, or the UTF8 code units, depending on the + /// [`PositionEncoding`] used. + pub character_offset: OneIndexed, +} + +impl Default for SourceLocation { + fn default() -> Self { + Self { + line: OneIndexed::MIN, + character_offset: OneIndexed::MIN, + } } } diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/ruff_source_file/src/line_index.rs index e9d211ca5c1fff..893baa7ef41320 100644 --- a/crates/ruff_source_file/src/line_index.rs +++ b/crates/ruff_source_file/src/line_index.rs @@ -5,13 +5,12 @@ use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; +use crate::{LineColumn, SourceLocation}; use ruff_text_size::{TextLen, TextRange, TextSize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::SourceLocation; - -/// Index for fast [byte offset](TextSize) to [`SourceLocation`] conversions. +/// Index for fast [byte offset](TextSize) to [`LineColumn`] conversions. /// /// Cloning a [`LineIndex`] is cheap because it only requires bumping a reference count. #[derive(Clone, Eq, PartialEq)] @@ -66,60 +65,146 @@ impl LineIndex { self.inner.kind } - /// Returns the row and column index for an offset. + /// Returns the line and column number for an UTF-8 byte offset. + /// + /// The `column` number is the nth-character of the line, except for the first line + /// where it doesn't include the UTF-8 BOM marker at the start of the file. + /// + /// ### BOM handling + /// + /// For files starting with a UTF-8 BOM marker, the byte offsets + /// in the range `0...3` are all mapped to line 0 and column 0. + /// Because of this, the conversion isn't losless. /// /// ## Examples /// /// ``` /// # use ruff_text_size::TextSize; - /// # use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; - /// let source = "def a():\n pass"; - /// let index = LineIndex::from_source_text(source); + /// # use ruff_source_file::{LineIndex, OneIndexed, LineColumn}; + /// let source = format!("\u{FEFF}{}", "def a():\n pass"); + /// let index = LineIndex::from_source_text(&source); /// + /// // Before BOM, maps to after BOM /// assert_eq!( - /// index.source_location(TextSize::from(0), source), - /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } + /// index.line_column(TextSize::from(0), &source), + /// LineColumn { line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } /// ); /// + /// // After BOM, maps to after BOM /// assert_eq!( - /// index.source_location(TextSize::from(4), source), - /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } + /// index.line_column(TextSize::from(3), &source), + /// LineColumn { line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } + /// ); + /// + /// assert_eq!( + /// index.line_column(TextSize::from(7), &source), + /// LineColumn { line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } /// ); /// assert_eq!( - /// index.source_location(TextSize::from(13), source), - /// SourceLocation { row: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(4) } + /// index.line_column(TextSize::from(16), &source), + /// LineColumn { line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(4) } /// ); /// ``` /// /// ## Panics /// - /// If the offset is out of bounds. - pub fn source_location(&self, offset: TextSize, content: &str) -> SourceLocation { - match self.line_starts().binary_search(&offset) { - // Offset is at the start of a line - Ok(row) => SourceLocation { - row: OneIndexed::from_zero_indexed(row), - column: OneIndexed::from_zero_indexed(0), - }, - Err(next_row) => { - // SAFETY: Safe because the index always contains an entry for the offset 0 - let row = next_row - 1; - let mut line_start = self.line_starts()[row]; - - let column = if self.kind().is_ascii() { - usize::from(offset) - usize::from(line_start) - } else { - // Don't count the BOM character as a column. - if line_start == TextSize::from(0) && content.starts_with('\u{feff}') { - line_start = '\u{feff}'.text_len(); - } + /// If the byte offset isn't within the bounds of `content`. + pub fn line_column(&self, offset: TextSize, content: &str) -> LineColumn { + let location = self.source_location(offset, content, PositionEncoding::Utf32); + + // Don't count the BOM character as a column, but only on the first line. + let column = if location.line.to_zero_indexed() == 0 && content.starts_with('\u{feff}') { + location.character_offset.saturating_sub(1) + } else { + location.character_offset + }; - content[TextRange::new(line_start, offset)].chars().count() - }; + LineColumn { + line: location.line, + column, + } + } + + /// Given a UTF-8 byte offset, returns the line and character offset according to the given encoding. + /// + /// ### BOM handling + /// + /// Unlike [`Self::line_column`], this method does not skip the BOM character at the start of the file. + /// This allows for bidirectional mapping between [`SourceLocation`] and [`TextSize`] (see [`Self::offset`]). + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::TextSize; + /// # use ruff_source_file::{LineIndex, OneIndexed, LineColumn, SourceLocation, PositionEncoding, Line}; + /// let source = format!("\u{FEFF}{}", "def a():\n pass"); + /// let index = LineIndex::from_source_text(&source); + /// + /// // Before BOM, maps to character 0 + /// assert_eq!( + /// index.source_location(TextSize::from(0), &source, PositionEncoding::Utf32), + /// SourceLocation { line: OneIndexed::from_zero_indexed(0), character_offset: OneIndexed::from_zero_indexed(0) } + /// ); + /// + /// // After BOM, maps to after BOM + /// assert_eq!( + /// index.source_location(TextSize::from(3), &source, PositionEncoding::Utf32), + /// SourceLocation { line: OneIndexed::from_zero_indexed(0), character_offset: OneIndexed::from_zero_indexed(1) } + /// ); + /// + /// assert_eq!( + /// index.source_location(TextSize::from(7), &source, PositionEncoding::Utf32), + /// SourceLocation { line: OneIndexed::from_zero_indexed(0), character_offset: OneIndexed::from_zero_indexed(5) } + /// ); + /// assert_eq!( + /// index.source_location(TextSize::from(16), &source, PositionEncoding::Utf32), + /// SourceLocation { line: OneIndexed::from_zero_indexed(1), character_offset: OneIndexed::from_zero_indexed(4) } + /// ); + /// ``` + /// + /// ## Panics + /// + /// If the UTF-8 byte offset is out of bounds of `text`. + pub fn source_location( + &self, + offset: TextSize, + text: &str, + encoding: PositionEncoding, + ) -> SourceLocation { + let line = self.line_index(offset); + let line_start = self.line_start(line, text); + + if self.is_ascii() { + return SourceLocation { + line, + character_offset: OneIndexed::from_zero_indexed((offset - line_start).to_usize()), + }; + } + match encoding { + PositionEncoding::Utf8 => { + let character_offset = offset - line_start; SourceLocation { - row: OneIndexed::from_zero_indexed(row), - column: OneIndexed::from_zero_indexed(column), + line, + character_offset: OneIndexed::from_zero_indexed(character_offset.to_usize()), + } + } + PositionEncoding::Utf16 => { + let up_to_character = &text[TextRange::new(line_start, offset)]; + let character = up_to_character.encode_utf16().count(); + + SourceLocation { + line, + character_offset: OneIndexed::from_zero_indexed(character), + } + } + PositionEncoding::Utf32 => { + let up_to_character = &text[TextRange::new(line_start, offset)]; + let character = up_to_character.chars().count(); + + SourceLocation { + line, + character_offset: OneIndexed::from_zero_indexed(character), } } } @@ -141,7 +226,7 @@ impl LineIndex { /// /// ``` /// # use ruff_text_size::TextSize; - /// # use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; + /// # use ruff_source_file::{LineIndex, OneIndexed, LineColumn}; /// let source = "def a():\n pass"; /// let index = LineIndex::from_source_text(source); /// @@ -221,83 +306,211 @@ impl LineIndex { } } - /// Returns the [byte offset](TextSize) at `line` and `column`. + /// Returns the [UTF-8 byte offset](TextSize) at `line` and `character` where character is counted using the given encoding. /// /// ## Examples /// - /// ### ASCII + /// ### ASCII only source text /// /// ``` - /// use ruff_source_file::{LineIndex, OneIndexed}; - /// use ruff_text_size::TextSize; + /// # use ruff_source_file::{SourceLocation, LineIndex, OneIndexed, PositionEncoding}; + /// # use ruff_text_size::TextSize; /// let source = r#"a = 4 /// c = "some string" /// x = b"#; /// /// let index = LineIndex::from_source_text(source); /// - /// // First line, first column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::new(0)); + /// // First line, first character + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(0), + /// character_offset: OneIndexed::from_zero_indexed(0) + /// }, + /// source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(0) + /// ); /// - /// // Second line, 4th column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(4), source), TextSize::new(10)); + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(1), + /// character_offset: OneIndexed::from_zero_indexed(4) + /// }, + /// source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(10) + /// ); /// /// // Offset past the end of the first line - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(10), source), TextSize::new(6)); + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(0), + /// character_offset: OneIndexed::from_zero_indexed(10) + /// }, + /// source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(6) + /// ); /// /// // Offset past the end of the file - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::new(29)); + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(3), + /// character_offset: OneIndexed::from_zero_indexed(0) + /// }, + /// source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(29) + /// ); /// ``` /// - /// ### UTF8 + /// ### Non-ASCII source text /// /// ``` - /// use ruff_source_file::{LineIndex, OneIndexed}; + /// use ruff_source_file::{LineIndex, OneIndexed, SourceLocation, PositionEncoding}; /// use ruff_text_size::TextSize; - /// let source = r#"a = 4 + /// let source = format!("\u{FEFF}{}", r#"a = 4 /// c = "❤️" - /// x = b"#; + /// x = b"#); /// - /// let index = LineIndex::from_source_text(source); + /// let index = LineIndex::from_source_text(&source); + /// + /// // First line, first character, points at the BOM + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(0), + /// character_offset: OneIndexed::from_zero_indexed(0) + /// }, + /// &source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(0) + /// ); + /// + /// // First line, after the BOM + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(0), + /// character_offset: OneIndexed::from_zero_indexed(1) + /// }, + /// &source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(3) + /// ); /// - /// // First line, first column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::new(0)); + /// // second line, 7th character, after emoji, UTF32 + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(1), + /// character_offset: OneIndexed::from_zero_indexed(7) + /// }, + /// &source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(20) + /// ); + /// + /// // Second line, 7th character, after emoji, UTF 16 + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(1), + /// character_offset: OneIndexed::from_zero_indexed(7) + /// }, + /// &source, + /// PositionEncoding::Utf16, + /// ), + /// TextSize::new(20) + /// ); /// - /// // Third line, 2nd column, after emoji - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(2), OneIndexed::from_zero_indexed(1), source), TextSize::new(20)); /// /// // Offset past the end of the second line - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(10), source), TextSize::new(19)); + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(1), + /// character_offset: OneIndexed::from_zero_indexed(10) + /// }, + /// &source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(22) + /// ); /// /// // Offset past the end of the file - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::new(24)); + /// assert_eq!( + /// index.offset( + /// SourceLocation { + /// line: OneIndexed::from_zero_indexed(3), + /// character_offset: OneIndexed::from_zero_indexed(0) + /// }, + /// &source, + /// PositionEncoding::Utf32, + /// ), + /// TextSize::new(27) + /// ); /// ``` - /// - pub fn offset(&self, line: OneIndexed, column: OneIndexed, contents: &str) -> TextSize { + pub fn offset( + &self, + position: SourceLocation, + text: &str, + position_encoding: PositionEncoding, + ) -> TextSize { // If start-of-line position after last line - if line.to_zero_indexed() > self.line_starts().len() { - return contents.text_len(); + if position.line.to_zero_indexed() > self.line_starts().len() { + return text.text_len(); } - let line_range = self.line_range(line, contents); + let line_range = self.line_range(position.line, text); - match self.kind() { - IndexKind::Ascii => { - line_range.start() - + TextSize::try_from(column.to_zero_indexed()) - .unwrap_or(line_range.len()) - .clamp(TextSize::new(0), line_range.len()) - } - IndexKind::Utf8 => { - let rest = &contents[line_range]; - let column_offset: TextSize = rest + let character_offset = position.character_offset.to_zero_indexed(); + let character_byte_offset = if self.is_ascii() { + TextSize::try_from(character_offset).unwrap() + } else { + let line = &text[line_range]; + + match position_encoding { + PositionEncoding::Utf8 => { + TextSize::try_from(position.character_offset.to_zero_indexed()).unwrap() + } + PositionEncoding::Utf16 => { + let mut byte_offset = TextSize::new(0); + let mut utf16_code_unit_offset = 0; + + for c in line.chars() { + if utf16_code_unit_offset >= character_offset { + break; + } + + // Count characters encoded as two 16 bit words as 2 characters. + byte_offset += c.text_len(); + utf16_code_unit_offset += c.len_utf16(); + } + + byte_offset + } + PositionEncoding::Utf32 => line .chars() - .take(column.to_zero_indexed()) + .take(position.character_offset.to_zero_indexed()) .map(ruff_text_size::TextLen::text_len) - .sum(); - line_range.start() + column_offset + .sum(), } - } + }; + + line_range.start() + character_byte_offset.clamp(TextSize::new(0), line_range.len()) } /// Returns the [byte offsets](TextSize) for every line @@ -430,12 +643,26 @@ impl FromStr for OneIndexed { } } +#[derive(Default, Copy, Clone, Debug)] +pub enum PositionEncoding { + /// Character offsets count the number of bytes from the start of the line. + #[default] + Utf8, + + /// Character offsets count the number of UTF-16 code units from the start of the line. + Utf16, + + /// Character offsets count the number of UTF-32 code points units (the same as number of characters in Rust) + /// from the start of the line. + Utf32, +} + #[cfg(test)] mod tests { use ruff_text_size::TextSize; use crate::line_index::LineIndex; - use crate::{OneIndexed, SourceLocation}; + use crate::{LineColumn, OneIndexed}; #[test] fn ascii_index() { @@ -466,30 +693,30 @@ mod tests { let index = LineIndex::from_source_text(contents); // First row. - let loc = index.source_location(TextSize::from(2), contents); + let loc = index.line_column(TextSize::from(2), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(2) } ); // Second row. - let loc = index.source_location(TextSize::from(6), contents); + let loc = index.line_column(TextSize::from(6), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); - let loc = index.source_location(TextSize::from(11), contents); + let loc = index.line_column(TextSize::from(11), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(5) } ); @@ -502,23 +729,23 @@ mod tests { assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(6)]); assert_eq!( - index.source_location(TextSize::from(4), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + index.line_column(TextSize::from(4), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } ); assert_eq!( - index.source_location(TextSize::from(6), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(6), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); assert_eq!( - index.source_location(TextSize::from(7), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(7), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(1) } ); @@ -531,23 +758,23 @@ mod tests { assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(7)]); assert_eq!( - index.source_location(TextSize::from(4), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + index.line_column(TextSize::from(4), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } ); assert_eq!( - index.source_location(TextSize::from(7), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(7), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); assert_eq!( - index.source_location(TextSize::from(8), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(8), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(1) } ); @@ -598,23 +825,23 @@ mod tests { // Second ' assert_eq!( - index.source_location(TextSize::from(9), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + index.line_column(TextSize::from(9), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(6) } ); assert_eq!( - index.source_location(TextSize::from(11), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(11), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); assert_eq!( - index.source_location(TextSize::from(12), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(12), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(1) } ); @@ -632,23 +859,23 @@ mod tests { // Second ' assert_eq!( - index.source_location(TextSize::from(9), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + index.line_column(TextSize::from(9), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(6) } ); assert_eq!( - index.source_location(TextSize::from(12), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(12), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); assert_eq!( - index.source_location(TextSize::from(13), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + index.line_column(TextSize::from(13), contents), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(1) } ); @@ -664,49 +891,49 @@ mod tests { ); // First row. - let loc = index.source_location(TextSize::from(0), contents); + let loc = index.line_column(TextSize::from(0), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } ); - let loc = index.source_location(TextSize::from(5), contents); + let loc = index.line_column(TextSize::from(5), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(5) } ); - let loc = index.source_location(TextSize::from(8), contents); + let loc = index.line_column(TextSize::from(8), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), + LineColumn { + line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(6) } ); // Second row. - let loc = index.source_location(TextSize::from(10), contents); + let loc = index.line_column(TextSize::from(10), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) } ); // One-past-the-end. - let loc = index.source_location(TextSize::from(15), contents); + let loc = index.line_column(TextSize::from(15), contents); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), + LineColumn { + line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(5) } ); diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 06b0b6973c54a1..db007349bc8364 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -21,7 +21,7 @@ use ruff_python_formatter::{format_module_ast, pretty_comments, PyFormatContext, use ruff_python_index::Indexer; use ruff_python_parser::{parse, parse_unchecked, Mode, ParseOptions, Parsed}; use ruff_python_trivia::CommentRanges; -use ruff_source_file::SourceLocation; +use ruff_source_file::{LineColumn, OneIndexed}; use ruff_text_size::Ranged; use ruff_workspace::configuration::Configuration; use ruff_workspace::options::{FormatOptions, LintCommonOptions, LintOptions, Options}; @@ -61,8 +61,8 @@ export interface Diagnostic { pub struct ExpandedMessage { pub code: Option, pub message: String, - pub start_location: SourceLocation, - pub end_location: SourceLocation, + pub start_location: Location, + pub end_location: Location, pub fix: Option, } @@ -74,8 +74,8 @@ pub struct ExpandedFix { #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] struct ExpandedEdit { - location: SourceLocation, - end_location: SourceLocation, + location: Location, + end_location: Location, content: Option, } @@ -214,16 +214,16 @@ impl Workspace { }) => ExpandedMessage { code: Some(kind.rule().noqa_code().to_string()), message: kind.body, - start_location: source_code.source_location(range.start()), - end_location: source_code.source_location(range.end()), + start_location: source_code.line_column(range.start()).into(), + end_location: source_code.line_column(range.end()).into(), fix: fix.map(|fix| ExpandedFix { message: kind.suggestion, edits: fix .edits() .iter() .map(|edit| ExpandedEdit { - location: source_code.source_location(edit.start()), - end_location: source_code.source_location(edit.end()), + location: source_code.line_column(edit.start()).into(), + end_location: source_code.line_column(edit.end()).into(), content: edit.content().map(ToString::to_string), }) .collect(), @@ -233,8 +233,8 @@ impl Workspace { ExpandedMessage { code: None, message, - start_location: source_code.source_location(range.start()), - end_location: source_code.source_location(range.end()), + start_location: source_code.line_column(range.start()).into(), + end_location: source_code.line_column(range.end()).into(), fix: None, } } @@ -316,3 +316,18 @@ impl<'a> ParsedModule<'a> { ) } } + +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct Location { + pub row: OneIndexed, + pub column: OneIndexed, +} + +impl From for Location { + fn from(value: LineColumn) -> Self { + Self { + row: value.line, + column: value.column, + } + } +} diff --git a/crates/ruff_wasm/tests/api.rs b/crates/ruff_wasm/tests/api.rs index 49864125ff93ad..ee6166e14294a2 100644 --- a/crates/ruff_wasm/tests/api.rs +++ b/crates/ruff_wasm/tests/api.rs @@ -3,8 +3,8 @@ use wasm_bindgen_test::wasm_bindgen_test; use ruff_linter::registry::Rule; -use ruff_source_file::{OneIndexed, SourceLocation}; -use ruff_wasm::{ExpandedMessage, Workspace}; +use ruff_source_file::OneIndexed; +use ruff_wasm::{ExpandedMessage, Location, Workspace}; macro_rules! check { ($source:expr, $config:expr, $expected:expr) => {{ @@ -27,11 +27,11 @@ fn empty_config() { [ExpandedMessage { code: Some(Rule::IfTuple.noqa_code().to_string()), message: "If test is a tuple, which is always `True`".to_string(), - start_location: SourceLocation { + start_location: Location { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(3) }, - end_location: SourceLocation { + end_location: Location { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(9) }, @@ -48,11 +48,11 @@ fn syntax_error() { [ExpandedMessage { code: None, message: "SyntaxError: Expected an expression".to_string(), - start_location: SourceLocation { + start_location: Location { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(3) }, - end_location: SourceLocation { + end_location: Location { row: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(0) }, @@ -69,11 +69,11 @@ fn unsupported_syntax_error() { [ExpandedMessage { code: None, message: "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)".to_string(), - start_location: SourceLocation { + start_location: Location { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) }, - end_location: SourceLocation { + end_location: Location { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(5) }, From 1bdb22c13972b3a3dc9cb4ef31fbf37db051dd1c Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sun, 27 Apr 2025 11:44:55 +0100 Subject: [PATCH 0138/1161] [red-knot] Fix offset handling in playground for 2-code-point UTF16 characters (#17520) --- crates/red_knot_wasm/src/lib.rs | 125 ++++++++++++++++------ crates/red_knot_wasm/tests/api.rs | 10 +- crates/ruff_source_file/src/line_index.rs | 3 +- playground/knot/src/Playground.tsx | 4 +- 4 files changed, 101 insertions(+), 41 deletions(-) diff --git a/crates/red_knot_wasm/src/lib.rs b/crates/red_knot_wasm/src/lib.rs index 5f9333d028bfa7..918aaaf67acf25 100644 --- a/crates/red_knot_wasm/src/lib.rs +++ b/crates/red_knot_wasm/src/lib.rs @@ -19,7 +19,7 @@ use ruff_db::system::{ use ruff_db::Upcast; use ruff_notebook::Notebook; use ruff_python_formatter::formatted_file; -use ruff_source_file::{LineColumn, LineIndex, OneIndexed, PositionEncoding, SourceLocation}; +use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; use ruff_text_size::{Ranged, TextSize}; use wasm_bindgen::prelude::*; @@ -42,13 +42,18 @@ pub fn run() { #[wasm_bindgen] pub struct Workspace { db: ProjectDatabase, + position_encoding: PositionEncoding, system: WasmSystem, } #[wasm_bindgen] impl Workspace { #[wasm_bindgen(constructor)] - pub fn new(root: &str, options: JsValue) -> Result { + pub fn new( + root: &str, + position_encoding: PositionEncoding, + options: JsValue, + ) -> Result { let options = Options::deserialize_with( ValueSource::Cli, serde_wasm_bindgen::Deserializer::from(options), @@ -62,7 +67,11 @@ impl Workspace { let db = ProjectDatabase::new(project, system.clone()).map_err(into_error)?; - Ok(Self { db, system }) + Ok(Self { + db, + position_encoding, + system, + }) } #[wasm_bindgen(js_name = "updateOptions")] @@ -216,13 +225,18 @@ impl Workspace { let source = source_text(&self.db, file_id.file); let index = line_index(&self.db, file_id.file); - let offset = position.to_text_size(&source, &index)?; + let offset = position.to_text_size(&source, &index, self.position_encoding)?; let Some(targets) = goto_type_definition(&self.db, file_id.file, offset) else { return Ok(Vec::new()); }; - let source_range = Range::from_text_range(targets.file_range().range(), &index, &source); + let source_range = Range::from_text_range( + targets.file_range().range(), + &index, + &source, + self.position_encoding, + ); let links: Vec<_> = targets .into_iter() @@ -231,10 +245,12 @@ impl Workspace { full_range: Range::from_file_range( &self.db, FileRange::new(target.file(), target.full_range()), + self.position_encoding, ), selection_range: Some(Range::from_file_range( &self.db, FileRange::new(target.file(), target.focus_range()), + self.position_encoding, )), origin_selection_range: Some(source_range), }) @@ -248,13 +264,18 @@ impl Workspace { let source = source_text(&self.db, file_id.file); let index = line_index(&self.db, file_id.file); - let offset = position.to_text_size(&source, &index)?; + let offset = position.to_text_size(&source, &index, self.position_encoding)?; let Some(range_info) = hover(&self.db, file_id.file, offset) else { return Ok(None); }; - let source_range = Range::from_text_range(range_info.file_range().range(), &index, &source); + let source_range = Range::from_text_range( + range_info.file_range().range(), + &index, + &source, + self.position_encoding, + ); Ok(Some(Hover { markdown: range_info @@ -272,14 +293,19 @@ impl Workspace { let result = inlay_hints( &self.db, file_id.file, - range.to_text_range(&index, &source)?, + range.to_text_range(&index, &source, self.position_encoding)?, ); Ok(result .into_iter() .map(|hint| InlayHint { markdown: hint.display(&self.db).to_string(), - position: Position::from_text_size(hint.position, &index, &source), + position: Position::from_text_size( + hint.position, + &index, + &source, + self.position_encoding, + ), }) .collect()) } @@ -348,6 +374,7 @@ impl Diagnostic { Some(Range::from_file_range( &workspace.db, FileRange::new(span.file(), span.range()?), + workspace.position_encoding, )) }) } @@ -378,21 +405,31 @@ impl Range { } impl Range { - fn from_file_range(db: &dyn Db, file_range: FileRange) -> Self { + fn from_file_range( + db: &dyn Db, + file_range: FileRange, + position_encoding: PositionEncoding, + ) -> Self { let index = line_index(db.upcast(), file_range.file()); let source = source_text(db.upcast(), file_range.file()); - Self::from_text_range(file_range.range(), &index, &source) + Self::from_text_range(file_range.range(), &index, &source, position_encoding) } fn from_text_range( text_range: ruff_text_size::TextRange, line_index: &LineIndex, source: &str, + position_encoding: PositionEncoding, ) -> Self { Self { - start: Position::from_text_size(text_range.start(), line_index, source), - end: Position::from_text_size(text_range.end(), line_index, source), + start: Position::from_text_size( + text_range.start(), + line_index, + source, + position_encoding, + ), + end: Position::from_text_size(text_range.end(), line_index, source, position_encoding), } } @@ -400,23 +437,19 @@ impl Range { self, line_index: &LineIndex, source: &str, + position_encoding: PositionEncoding, ) -> Result { - let start = self.start.to_text_size(source, line_index)?; - let end = self.end.to_text_size(source, line_index)?; + let start = self + .start + .to_text_size(source, line_index, position_encoding)?; + let end = self + .end + .to_text_size(source, line_index, position_encoding)?; Ok(ruff_text_size::TextRange::new(start, end)) } } -impl From<(LineColumn, LineColumn)> for Range { - fn from((start, end): (LineColumn, LineColumn)) -> Self { - Self { - start: start.into(), - end: end.into(), - } - } -} - #[wasm_bindgen] #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct Position { @@ -436,7 +469,12 @@ impl Position { } impl Position { - fn to_text_size(self, text: &str, index: &LineIndex) -> Result { + fn to_text_size( + self, + text: &str, + index: &LineIndex, + position_encoding: PositionEncoding, + ) -> Result { let text_size = index.offset( SourceLocation { line: OneIndexed::new(self.line).ok_or_else(|| { @@ -451,22 +489,22 @@ impl Position { })?, }, text, - PositionEncoding::Utf32, + position_encoding.into(), ); Ok(text_size) } - fn from_text_size(offset: TextSize, line_index: &LineIndex, source: &str) -> Self { - line_index.line_column(offset, source).into() - } -} - -impl From for Position { - fn from(location: LineColumn) -> Self { + fn from_text_size( + offset: TextSize, + line_index: &LineIndex, + source: &str, + position_encoding: PositionEncoding, + ) -> Self { + let location = line_index.source_location(offset, source, position_encoding.into()); Self { line: location.line.get(), - column: location.column.get(), + column: location.character_offset.get(), } } } @@ -506,6 +544,25 @@ impl From for TextRange { } } +#[derive(Default, Copy, Clone)] +#[wasm_bindgen] +pub enum PositionEncoding { + #[default] + Utf8, + Utf16, + Utf32, +} + +impl From for ruff_source_file::PositionEncoding { + fn from(value: PositionEncoding) -> Self { + match value { + PositionEncoding::Utf8 => Self::Utf8, + PositionEncoding::Utf16 => Self::Utf16, + PositionEncoding::Utf32 => Self::Utf32, + } + } +} + #[wasm_bindgen] pub struct LocationLink { /// The target file path diff --git a/crates/red_knot_wasm/tests/api.rs b/crates/red_knot_wasm/tests/api.rs index 65ba79b28331c5..6b6802680baf9d 100644 --- a/crates/red_knot_wasm/tests/api.rs +++ b/crates/red_knot_wasm/tests/api.rs @@ -1,12 +1,16 @@ #![cfg(target_arch = "wasm32")] -use red_knot_wasm::{Position, Workspace}; +use red_knot_wasm::{Position, PositionEncoding, Workspace}; use wasm_bindgen_test::wasm_bindgen_test; #[wasm_bindgen_test] fn check() { - let mut workspace = - Workspace::new("/", js_sys::JSON::parse("{}").unwrap()).expect("Workspace to be created"); + let mut workspace = Workspace::new( + "/", + PositionEncoding::Utf32, + js_sys::JSON::parse("{}").unwrap(), + ) + .expect("Workspace to be created"); workspace .open_file("test.py", "import random22\n") diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/ruff_source_file/src/line_index.rs index 893baa7ef41320..73807034c1f5eb 100644 --- a/crates/ruff_source_file/src/line_index.rs +++ b/crates/ruff_source_file/src/line_index.rs @@ -643,10 +643,9 @@ impl FromStr for OneIndexed { } } -#[derive(Default, Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub enum PositionEncoding { /// Character offsets count the number of bytes from the start of the line. - #[default] Utf8, /// Character offsets count the number of UTF-16 code units from the start of the line. diff --git a/playground/knot/src/Playground.tsx b/playground/knot/src/Playground.tsx index c3440a55e66047..971d5f9eac7344 100644 --- a/playground/knot/src/Playground.tsx +++ b/playground/knot/src/Playground.tsx @@ -10,7 +10,7 @@ import { useState, } from "react"; import { ErrorMessage, Header, setupMonaco, useTheme } from "shared"; -import { FileHandle, Workspace } from "red_knot_wasm"; +import { FileHandle, PositionEncoding, Workspace } from "red_knot_wasm"; import { persist, persistLocal, restore } from "./Editor/persist"; import { loader } from "@monaco-editor/react"; import knotSchema from "../../../knot.schema.json"; @@ -30,7 +30,7 @@ export default function Playground() { workspacePromiseRef.current = workspacePromise = startPlayground().then( (fetched) => { setVersion(fetched.version); - const workspace = new Workspace("/", {}); + const workspace = new Workspace("/", PositionEncoding.Utf16, {}); restoreWorkspace(workspace, fetched.workspace, dispatchFiles, setError); setWorkspace(workspace); return workspace; From 4729ff2bc84c40cccf697e3972481ee703456057 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:08:41 +0200 Subject: [PATCH 0139/1161] Update Rust crate syn to v2.0.101 (#17672) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 74 +++++++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 941984daf80268..082802afbaf28d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -394,7 +394,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -724,7 +724,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -735,7 +735,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -805,7 +805,7 @@ dependencies = [ "glob", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -837,7 +837,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1323,7 +1323,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1488,7 +1488,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1574,7 +1574,7 @@ checksum = "526b834d727fd59d37b076b0c3236d9adde1b1729a4361e20b2026f738cc1dbe" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1671,7 +1671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa96ed35d0dccc67cf7ba49350cb86de3dcb1d072a7ab28f99117f19d874953" dependencies = [ "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2180,7 +2180,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2249,7 +2249,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3074,7 +3074,7 @@ dependencies = [ "proc-macro2", "quote", "ruff_python_trivia", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3491,7 +3491,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] @@ -3525,7 +3525,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3574,7 +3574,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3585,7 +3585,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3608,7 +3608,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3649,7 +3649,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3780,7 +3780,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3796,9 +3796,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -3813,7 +3813,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3884,7 +3884,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3895,7 +3895,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "test-case-core", ] @@ -3931,7 +3931,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3942,7 +3942,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4073,7 +4073,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4340,7 +4340,7 @@ checksum = "72dcd78c4f979627a754f5522cea6e6a25e55139056535fe6e69c506cd64a862" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4462,7 +4462,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "wasm-bindgen-shared", ] @@ -4497,7 +4497,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4532,7 +4532,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4647,7 +4647,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4658,7 +4658,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4896,7 +4896,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] @@ -4917,7 +4917,7 @@ checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4937,7 +4937,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] @@ -4960,7 +4960,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] From b45598389df172e42c7d77160ec9592a7a56dcb8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:09:10 +0200 Subject: [PATCH 0140/1161] Update Rust crate jiff to v0.2.10 (#17671) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 082802afbaf28d..ec52264dd9a8b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1553,9 +1553,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ec30f7142be6fe14e1b021f50b85db8df2d4324ea6e91ec3e5dcde092021d0" +checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1563,14 +1563,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "jiff-static" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "526b834d727fd59d37b076b0c3236d9adde1b1729a4361e20b2026f738cc1dbe" +checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" dependencies = [ "proc-macro2", "quote", From 03065c245c542c9ad7469f82b0328c3bcda865e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:09:33 +0200 Subject: [PATCH 0141/1161] Update dependency smol-toml to v1.3.4 (#17669) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- playground/package-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index 0f1af5d971b121..fbb54efc263c9d 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -44,7 +44,7 @@ "react-resizable-panels": "^2.1.7", "red_knot_wasm": "file:red_knot_wasm", "shared": "0.0.0", - "smol-toml": "^1.3.1" + "smol-toml": "^1.3.4" }, "devDependencies": { "vite-plugin-static-copy": "^2.3.0" @@ -5393,9 +5393,9 @@ } }, "node_modules/smol-toml": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.3.tgz", - "integrity": "sha512-KMVLNWu490KlNfD0lbfDBUktJIEaZRBj1eeK0SMfdpO/rfyARIzlnPVI1Ge4l0vtSJmQUAiGKxMyLGrCT38iyA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.4.tgz", + "integrity": "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -6090,7 +6090,7 @@ "react-resizable-panels": "^2.0.0", "ruff_wasm": "file:ruff_wasm", "shared": "0.0.0", - "smol-toml": "^1.3.0" + "smol-toml": "^1.3.4" } }, "ruff/ruff_wasm": { From b09f00a4ef96027f15ee0b11b7861e611761ab38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:09:50 +0200 Subject: [PATCH 0142/1161] Update dependency ruff to v0.11.7 (#17668) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/requirements-insiders.txt | 2 +- docs/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements-insiders.txt b/docs/requirements-insiders.txt index be4bb310d14b68..02123797b27e25 100644 --- a/docs/requirements-insiders.txt +++ b/docs/requirements-insiders.txt @@ -1,5 +1,5 @@ PyYAML==6.0.2 -ruff==0.11.6 +ruff==0.11.7 mkdocs==1.6.1 mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@39da7a5e761410349e9a1b8abf593b0cdd5453ff mkdocs-redirects==1.2.2 diff --git a/docs/requirements.txt b/docs/requirements.txt index 4c241d1be332ee..8b818105f1f329 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.2 -ruff==0.11.6 +ruff==0.11.7 mkdocs==1.6.1 mkdocs-material==9.5.38 mkdocs-redirects==1.2.2 From 516291b693f12545c6693d11336371e7ad53f9aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:10:24 +0200 Subject: [PATCH 0143/1161] Update dependency react-resizable-panels to v2.1.9 (#17667) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- playground/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index fbb54efc263c9d..7effd1c8304ab0 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -41,7 +41,7 @@ "pyodide": "^0.27.4", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable-panels": "^2.1.7", + "react-resizable-panels": "^2.1.9", "red_knot_wasm": "file:red_knot_wasm", "shared": "0.0.0", "smol-toml": "^1.3.4" @@ -4969,9 +4969,9 @@ "license": "MIT" }, "node_modules/react-resizable-panels": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.8.tgz", - "integrity": "sha512-oDvD0sw34Ecx00cQFLiRJpAE2fCgNLBr8DMrBzkrsaUiLpAycIQoY3eAWfMblDql3pTIMZ60wJ/P89RO1htM2w==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz", + "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==", "license": "MIT", "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", @@ -6087,7 +6087,7 @@ "monaco-editor": "^0.52.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable-panels": "^2.0.0", + "react-resizable-panels": "^2.1.9", "ruff_wasm": "file:ruff_wasm", "shared": "0.0.0", "smol-toml": "^1.3.4" @@ -6103,7 +6103,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.3.2", "react": "^19.0.0", - "react-resizable-panels": "^2.1.7" + "react-resizable-panels": "^2.1.9" } } } From 53a9448fb52a4339d4e0d80113b174cf44ec57b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:15:01 +0200 Subject: [PATCH 0144/1161] Update taiki-e/install-action digest to ab3728c (#17666) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7d05ad336b9a75..14ada97cac4238 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -239,11 +239,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 + uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 + uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 with: tool: cargo-insta - name: Red-knot mdtests (GitHub annotations) @@ -293,11 +293,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 + uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 + uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 with: tool: cargo-insta - name: "Run tests" @@ -320,7 +320,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 + uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 with: tool: cargo-nextest - name: "Run tests" @@ -403,11 +403,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 + uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 + uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 with: tool: cargo-insta - name: "Run tests" @@ -857,7 +857,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2 + uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 with: tool: cargo-codspeed From 97dc58fc7768f0b0fe2aefafa0e0e9db0d8bf2c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:15:54 +0200 Subject: [PATCH 0145/1161] Update docker/build-push-action digest to 14487ce (#17665) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 212d12425f0353..7c70d3360e0fab 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -79,7 +79,7 @@ jobs: # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/ - name: Build and push by digest id: build - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6 + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6 with: context: . platforms: ${{ matrix.platform }} @@ -231,7 +231,7 @@ jobs: ${{ env.TAG_PATTERNS }} - name: Build and push - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6 + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6 with: context: . platforms: linux/amd64,linux/arm64 From 6bd1863bf02b5c7124bb0610a823b003cb2956ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:16:10 +0200 Subject: [PATCH 0146/1161] Update pre-commit hook astral-sh/ruff-pre-commit to v0.11.7 (#17670) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b4572a5d891495..68f126eeb9a881 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -79,7 +79,7 @@ repos: pass_filenames: false # This makes it a lot faster - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.6 + rev: v0.11.7 hooks: - id: ruff-format - id: ruff From d2246278e670484892fb2eee95594301a6672c90 Mon Sep 17 00:00:00 2001 From: justin Date: Mon, 28 Apr 2025 02:21:11 -0400 Subject: [PATCH 0147/1161] [red-knot] Don't ignore hidden files by default (#17655) --- crates/red_knot/tests/cli.rs | 20 ++++++++++++++++++++ crates/red_knot_project/src/walk.rs | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index c0ecb475fbbaa5..7a9283e4fc7359 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -5,6 +5,26 @@ use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::TempDir; +#[test] +fn test_include_hidden_files_by_default() -> anyhow::Result<()> { + let case = TestCase::with_files([(".test.py", "~")])?; + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error: invalid-syntax + --> /.test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + "); + Ok(()) +} #[test] fn test_respect_ignore_files() -> anyhow::Result<()> { // First test that the default option works correctly (the file is skipped) diff --git a/crates/red_knot_project/src/walk.rs b/crates/red_knot_project/src/walk.rs index 025566db382712..582b1ac0324e98 100644 --- a/crates/red_knot_project/src/walk.rs +++ b/crates/red_knot_project/src/walk.rs @@ -132,7 +132,8 @@ impl<'a> ProjectFilesWalker<'a> { let mut walker = db .system() .walk_directory(paths.next()?.as_ref()) - .standard_filters(db.project().settings(db).respect_ignore_files()); + .standard_filters(db.project().settings(db).respect_ignore_files()) + .ignore_hidden(false); for path in paths { walker = walker.add(path); From a3e55cfd8fa56043b7f7dd8962b03a4ba9c75837 Mon Sep 17 00:00:00 2001 From: jie211 Date: Mon, 28 Apr 2025 15:31:41 +0900 Subject: [PATCH 0148/1161] [airflow] fix typos `AIR312` (#17673) --- .../suggested_to_move_to_provider_in_3.rs | 12 +++++----- ...les__airflow__tests__AIR312_AIR312.py.snap | 22 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs index 32219eff4d57c5..8b990d0b15680e 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs @@ -137,7 +137,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan ["airflow", "operators", "datetime", rest @ ("BranchDateTimeOperator" | "target_times_as_dates")] => { ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), - module: "airflow.providers.standard.time.operators.datetime", + module: "airflow.providers.standard.operators.datetime", provider: "standard", version: "0.0.1", } @@ -173,7 +173,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan }, ["airflow", "operators", "weekday", "BranchDayOfWeekOperator"] => { ProviderReplacement::ProviderName { - name: "airflow.providers.standard.time.operators.weekday.BranchDayOfWeekOperator", + name: "airflow.providers.standard.operators.weekday.BranchDayOfWeekOperator", provider: "standard", version: "0.0.1", } @@ -181,7 +181,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan ["airflow", "sensors", "date_time", rest @ ("DateTimeSensor" | "DateTimeSensorAsync")] => { ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), - module: "airflow.providers.standard.time.sensors.date_time", + module: "airflow.providers.standard.sensors.date_time", provider: "standard", version: "0.0.1", } @@ -202,7 +202,7 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan ["airflow", "sensors", "time_sensor", rest @ ("TimeSensor" | "TimeSensorAsync")] => { ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), - module: "airflow.providers.standard.time.sensors.time", + module: "airflow.providers.standard.sensors.time", provider: "standard", version: "0.0.1", } @@ -210,13 +210,13 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan ["airflow", "sensors", "time_delta", rest @ ("TimeDeltaSensor" | "TimeDeltaSensorAsync" | "WaitSensor")] => { ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), - module: "airflow.providers.standard.time.sensors.time_delta", + module: "airflow.providers.standard.sensors.time_delta", provider: "standard", version: "0.0.1", } } ["airflow", "sensors", "weekday", "DayOfWeekSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.time.sensors.weekday.DayOfWeekSensor", + name: "airflow.providers.standard.sensors.weekday.DayOfWeekSensor", provider: "standard", version: "0.0.1", }, diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap index ce7b397db792be..ea0495c0f0f735 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR312_AIR312.py.snap @@ -75,7 +75,7 @@ AIR312.py:36:1: AIR312 `airflow.operators.datetime.BranchDateTimeOperator` is de 37 | TriggerDagRunLink(), TriggerDagRunOperator() 38 | EmptyOperator() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.operators.datetime.BranchDateTimeOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.datetime.BranchDateTimeOperator` instead. AIR312.py:36:27: AIR312 `airflow.operators.datetime.target_times_as_dates` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -86,7 +86,7 @@ AIR312.py:36:27: AIR312 `airflow.operators.datetime.target_times_as_dates` is de 37 | TriggerDagRunLink(), TriggerDagRunOperator() 38 | EmptyOperator() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.operators.datetime.target_times_as_dates` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.datetime.target_times_as_dates` instead. AIR312.py:37:1: AIR312 `airflow.operators.trigger_dagrun.TriggerDagRunLink` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -185,7 +185,7 @@ AIR312.py:46:1: AIR312 `airflow.operators.weekday.BranchDayOfWeekOperator` is de 47 | DateTimeSensor(), DateTimeSensorAsync() 48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.operators.weekday.BranchDayOfWeekOperator` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.weekday.BranchDayOfWeekOperator` instead. AIR312.py:47:1: AIR312 `airflow.sensors.date_time.DateTimeSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -196,7 +196,7 @@ AIR312.py:47:1: AIR312 `airflow.sensors.date_time.DateTimeSensor` is deprecated 48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() 49 | FileSensor() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.date_time.DateTimeSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.sensors.date_time.DateTimeSensor` instead. AIR312.py:47:19: AIR312 `airflow.sensors.date_time.DateTimeSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -207,7 +207,7 @@ AIR312.py:47:19: AIR312 `airflow.sensors.date_time.DateTimeSensorAsync` is depre 48 | ExternalTaskMarker(), ExternalTaskSensor(), ExternalTaskSensorLink() 49 | FileSensor() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.date_time.DateTimeSensorAsync` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.sensors.date_time.DateTimeSensorAsync` instead. AIR312.py:48:1: AIR312 `airflow.sensors.external_task.ExternalTaskMarker` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -262,7 +262,7 @@ AIR312.py:50:1: AIR312 `airflow.sensors.time_sensor.TimeSensor` is deprecated an 51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() 52 | DayOfWeekSensor() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time.TimeSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.sensors.time.TimeSensor` instead. AIR312.py:50:15: AIR312 `airflow.sensors.time_sensor.TimeSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -273,7 +273,7 @@ AIR312.py:50:15: AIR312 `airflow.sensors.time_sensor.TimeSensorAsync` is depreca 51 | TimeDeltaSensor(), TimeDeltaSensorAsync(), WaitSensor() 52 | DayOfWeekSensor() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time.TimeSensorAsync` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.sensors.time.TimeSensorAsync` instead. AIR312.py:51:1: AIR312 `airflow.sensors.time_delta.TimeDeltaSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -284,7 +284,7 @@ AIR312.py:51:1: AIR312 `airflow.sensors.time_delta.TimeDeltaSensor` is deprecate 52 | DayOfWeekSensor() 53 | DagStateTrigger(), WorkflowTrigger() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.TimeDeltaSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.sensors.time_delta.TimeDeltaSensor` instead. AIR312.py:51:20: AIR312 `airflow.sensors.time_delta.TimeDeltaSensorAsync` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -295,7 +295,7 @@ AIR312.py:51:20: AIR312 `airflow.sensors.time_delta.TimeDeltaSensorAsync` is dep 52 | DayOfWeekSensor() 53 | DagStateTrigger(), WorkflowTrigger() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.TimeDeltaSensorAsync` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.sensors.time_delta.TimeDeltaSensorAsync` instead. AIR312.py:51:44: AIR312 `airflow.sensors.time_delta.WaitSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -306,7 +306,7 @@ AIR312.py:51:44: AIR312 `airflow.sensors.time_delta.WaitSensor` is deprecated an 52 | DayOfWeekSensor() 53 | DagStateTrigger(), WorkflowTrigger() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.time_delta.WaitSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.sensors.time_delta.WaitSensor` instead. AIR312.py:52:1: AIR312 `airflow.sensors.weekday.DayOfWeekSensor` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | @@ -317,7 +317,7 @@ AIR312.py:52:1: AIR312 `airflow.sensors.weekday.DayOfWeekSensor` is deprecated a 53 | DagStateTrigger(), WorkflowTrigger() 54 | FileTrigger() | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.time.sensors.weekday.DayOfWeekSensor` instead. + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.sensors.weekday.DayOfWeekSensor` instead. AIR312.py:53:1: AIR312 `airflow.triggers.external_task.DagStateTrigger` is deprecated and moved into `standard` provider in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version. | From 826b2c9ff3e7a4011c46516a6a06d5f877268d12 Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Mon, 28 Apr 2025 04:31:16 -0300 Subject: [PATCH 0149/1161] [`pycodestyle`] Fix duplicated diagnostic in `E712` (#17651) --- .../resources/test/fixtures/pycodestyle/E712.py | 4 ++++ .../pycodestyle/rules/literal_comparisons.rs | 10 ++++++++++ ...rules__pycodestyle__tests__E712_E712.py.snap | 17 +++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E712.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E712.py index c0be4d7aa1c473..adbb8d578fb3ec 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E712.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E712.py @@ -53,3 +53,7 @@ assert (not foo) in bar assert {"x": not foo} in bar assert [42, not foo] in bar + +# https://github.com/astral-sh/ruff/issues/17582 +if True == True: # No duplicated diagnostic + pass diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs index cffcde998fddf2..c12e316ab00e0e 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -311,6 +311,16 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare) if let Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) = next { match op { EqCmpOp::Eq => { + if let Expr::BooleanLiteral(ast::ExprBooleanLiteral { + value: comparator_value, + .. + }) = comparator + { + if value == comparator_value { + continue; + } + } + let cond = if compare.ops.len() == 1 { Some(SourceCodeSnippet::from_str( checker.locator().slice(comparator), diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap index 76d290a4b5cea7..70d56f46d7cea3 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E712_E712.py.snap @@ -265,3 +265,20 @@ E712.py:31:4: E712 [*] Avoid equality comparisons to `True`; use `if yield i:` f 32 32 | print("even") 33 33 | 34 34 | #: Okay + +E712.py:58:4: E712 [*] Avoid equality comparisons to `True`; use `if True:` for truth checks + | +57 | # https://github.com/astral-sh/ruff/issues/17582 +58 | if True == True: # No duplicated diagnostic + | ^^^^^^^^^^^^ E712 +59 | pass + | + = help: Replace with `True` + +ℹ Unsafe fix +55 55 | assert [42, not foo] in bar +56 56 | +57 57 | # https://github.com/astral-sh/ruff/issues/17582 +58 |-if True == True: # No duplicated diagnostic + 58 |+if True: # No duplicated diagnostic +59 59 | pass From dbc137c9516808baa292da7a928e50ba36a14d39 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 28 Apr 2025 09:03:14 +0100 Subject: [PATCH 0150/1161] [red-knot] Use 101 exit code when there's at least one diagnostic with severity 'fatal' (#17640) --- crates/red_knot/src/main.rs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/red_knot/src/main.rs b/crates/red_knot/src/main.rs index a0e47b2d71f86e..4ed4065c0c8731 100644 --- a/crates/red_knot/src/main.rs +++ b/crates/red_knot/src/main.rs @@ -169,8 +169,12 @@ pub enum ExitStatus { /// Checking was successful but there were errors. Failure = 1, - /// Checking failed. + /// Checking failed due to an invocation error (e.g. the current directory no longer exists, incorrect CLI arguments, ...) Error = 2, + + /// Internal Red Knot error (panic, or any other error that isn't due to the user using the + /// program incorrectly or transient environment errors). + InternalError = 101, } impl Termination for ExitStatus { @@ -269,12 +273,6 @@ impl MainLoop { .format(terminal_settings.output_format) .color(colored::control::SHOULD_COLORIZE.should_colorize()); - let min_error_severity = if terminal_settings.error_on_warning { - Severity::Warning - } else { - Severity::Error - }; - if check_revision == revision { if db.project().files(db).is_empty() { tracing::warn!("No python files found under the given path(s)"); @@ -289,13 +287,13 @@ impl MainLoop { return Ok(ExitStatus::Success); } } else { - let mut failed = false; + let mut max_severity = Severity::Info; let diagnostics_count = result.len(); for diagnostic in result { write!(stdout, "{}", diagnostic.display(db, &display_config))?; - failed |= diagnostic.severity() >= min_error_severity; + max_severity = max_severity.max(diagnostic.severity()); } writeln!( @@ -306,10 +304,17 @@ impl MainLoop { )?; if self.watcher.is_none() { - return Ok(if failed { - ExitStatus::Failure - } else { - ExitStatus::Success + return Ok(match max_severity { + Severity::Info => ExitStatus::Success, + Severity::Warning => { + if terminal_settings.error_on_warning { + ExitStatus::Failure + } else { + ExitStatus::Success + } + } + Severity::Error => ExitStatus::Failure, + Severity::Fatal => ExitStatus::InternalError, }); } } From 74081032d928b836b472cc3eea66d42fa9011a80 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:51:59 +0000 Subject: [PATCH 0151/1161] Update actions/download-artifact digest to d3f86a1 (#17664) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Waygood --- .github/workflows/release.yml | 10 +++++----- Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79bc54e7b9d76c..dad8faf3193eae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -129,14 +129,14 @@ jobs: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: target/distrib/ @@ -180,14 +180,14 @@ jobs: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: target/distrib/ @@ -257,7 +257,7 @@ jobs: submodules: recursive # Create a GitHub Release while uploading all files to it - name: "Download GitHub Artifacts" - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: artifacts diff --git a/Cargo.toml b/Cargo.toml index 6f89d108158d28..a5fbde7274aec1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -342,5 +342,5 @@ global = "depot-ubuntu-latest-4" [workspace.metadata.dist.github-action-commits] "actions/checkout" = "85e6279cec87321a52edac9c87bce653a07cf6c2" # v4 "actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2 -"actions/download-artifact" = "95815c38cf2ff2164869cbab79da8d1f422bc89e" # v4.2.1 +"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0 "actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3 From f521358033aa38fcd33b8a00cd1082bd836e4844 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 28 Apr 2025 13:13:28 +0200 Subject: [PATCH 0152/1161] [red-knot] No errors for definitions of `TypedDict`s (#17674) ## Summary Do not emit errors when defining `TypedDict`s: ```py from typing_extensions import TypedDict # No error here class Person(TypedDict): name: str age: int | None # No error for this alternative syntax Message = TypedDict("Message", {"id": int, "content": str}) ``` ## Ecosystem analysis * Removes ~ 450 false positives for `TypedDict` definitions. * Changes a few diagnostic messages. * Adds a few (< 10) false positives, for example: ```diff + error[lint:unresolved-attribute] /tmp/mypy_primer/projects/hydra-zen/src/hydra_zen/structured_configs/_utils.py:262:5: Type `Literal[DataclassOptions]` has no attribute `__required_keys__` + error[lint:unresolved-attribute] /tmp/mypy_primer/projects/hydra-zen/src/hydra_zen/structured_configs/_utils.py:262:42: Type `Literal[DataclassOptions]` has no attribute `__optional_keys__` ``` * New true positive https://github.com/zulip/zulip/blob/4f8263cd7f4d00fc9b9e7d687ab98b0cc8737308/corporate/lib/remote_billing_util.py#L155-L157 ```diff + error[lint:invalid-assignment] /tmp/mypy_primer/projects/zulip/corporate/lib/remote_billing_util.py:155:5: Object of type `RemoteBillingIdentityDict | LegacyServerIdentityDict | None` is not assignable to `LegacyServerIdentityDict | None` ``` ## Test Plan New Markdown tests --- .../mdtest/annotations/literal_string.md | 8 +++++-- .../annotations/unsupported_special_forms.md | 7 ++++-- .../unsupported_type_qualifiers.md | 2 -- .../resources/mdtest/typed_dict.md | 24 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 23 ++++++++++++++++++ .../src/types/call/bind.rs | 10 +++++--- .../src/types/class_base.rs | 3 +++ .../src/types/infer.rs | 7 ++++++ .../src/types/known_instance.rs | 6 +++++ .../src/types/type_ordering.rs | 3 +++ 10 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/typed_dict.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md index 445c7a05da3aac..0496dbb4ce4021 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md @@ -38,8 +38,12 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form] ```py from typing_extensions import LiteralString -a: LiteralString[str] # error: [invalid-type-form] -b: LiteralString["foo"] # error: [invalid-type-form] +# error: [invalid-type-form] +a: LiteralString[str] + +# error: [invalid-type-form] +# error: [unresolved-reference] "Name `foo` used when not defined" +b: LiteralString["foo"] ``` ### As a base class 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 3d76ece35d281f..1c7665d84d197f 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 @@ -89,9 +89,12 @@ python-version = "3.12" Some of these are not subscriptable: ```py -from typing_extensions import Self, TypeAlias +from typing_extensions import Self, TypeAlias, TypeVar -X: TypeAlias[T] = int # error: [invalid-type-form] +T = TypeVar("T") + +# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter" +X: TypeAlias[T] = int class Foo[T]: # error: [invalid-type-form] "Special form `typing.Self` expected no type parameter" diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md index 7826fbf92d1bd8..fcc61160f284d8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md @@ -11,8 +11,6 @@ from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict X: Final = 42 Y: Final[int] = 42 -# TODO: `TypedDict` is actually valid as a base -# error: [invalid-base] class Bar(TypedDict): x: Required[int] y: NotRequired[str] diff --git a/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md b/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md new file mode 100644 index 00000000000000..22d43260ba7587 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md @@ -0,0 +1,24 @@ +# `TypedDict` + +We do not support `TypedDict`s yet. This test mainly exists to make sure that we do not emit any +errors for the definition of a `TypedDict`. + +```py +from typing_extensions import TypedDict, Required + +class Person(TypedDict): + name: str + age: int | None + +# TODO: This should not be an error: +# error: [invalid-assignment] +alice: Person = {"name": "Alice", "age": 30} + +# Alternative syntax +Message = TypedDict("Message", {"id": Required[int], "content": str}, total=False) + +msg = Message(id=1, content="Hello") + +# No errors for yet-unsupported features (`closed`): +OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True) +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 070adc5aabae3e..9a86e1a6413d8e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3890,6 +3890,28 @@ impl<'db> Type<'db> { } }, + Type::KnownInstance(KnownInstanceType::TypedDict) => { + Signatures::single(CallableSignature::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("typename"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("fields"))) + .with_annotated_type(KnownClass::Dict.to_instance(db)) + .with_default_type(Type::any()), + Parameter::keyword_only(Name::new_static("total")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + // Future compatibility, in case new keyword arguments will be added: + Parameter::keyword_variadic(Name::new_static("kwargs")) + .with_annotated_type(Type::any()), + ]), + None, + ), + )) + } + Type::GenericAlias(_) => { // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` @@ -4471,6 +4493,7 @@ impl<'db> Type<'db> { KnownInstanceType::TypingSelf => Ok(todo_type!("Support for `typing.Self`")), KnownInstanceType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")), + KnownInstanceType::TypedDict => Ok(todo_type!("Support for `typing.TypedDict`")), KnownInstanceType::Protocol => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol], diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 397c2aaa118e23..8f5fb666589fd0 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -19,9 +19,9 @@ use crate::types::diagnostic::{ use crate::types::generics::{Specialization, SpecializationBuilder}; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ - BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, KnownClass, - KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, TupleType, - UnionType, WrapperDescriptorKind, + todo_type, BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, + KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, + TupleType, UnionType, WrapperDescriptorKind, }; use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic}; use ruff_python_ast as ast; @@ -772,6 +772,10 @@ impl<'db> Bindings<'db> { _ => {} }, + Type::KnownInstance(KnownInstanceType::TypedDict) => { + overload.set_return_type(todo_type!("TypedDict")); + } + // Not a special case _ => {} } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 395ff5268da160..91c132fc3d49e1 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -169,6 +169,9 @@ impl<'db> ClassBase<'db> { KnownInstanceType::OrderedDict => { Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db)) } + KnownInstanceType::TypedDict => { + Self::try_from_type(db, KnownClass::Dict.to_class_literal(db)) + } KnownInstanceType::Callable => { Self::try_from_type(db, todo_type!("Support for Callable as a base class")) } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 29d69e5099e8b1..e9448d83b09698 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -7710,6 +7710,8 @@ impl<'db> TypeInferenceBuilder<'db> { | KnownInstanceType::Any | KnownInstanceType::AlwaysTruthy | KnownInstanceType::AlwaysFalsy => { + self.infer_type_expression(arguments_slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", @@ -7720,7 +7722,10 @@ impl<'db> TypeInferenceBuilder<'db> { } KnownInstanceType::TypingSelf | KnownInstanceType::TypeAlias + | KnownInstanceType::TypedDict | KnownInstanceType::Unknown => { + self.infer_type_expression(arguments_slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected no type parameter", @@ -7730,6 +7735,8 @@ impl<'db> TypeInferenceBuilder<'db> { Type::unknown() } KnownInstanceType::LiteralString => { + self.infer_type_expression(arguments_slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let mut diag = builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", diff --git a/crates/red_knot_python_semantic/src/types/known_instance.rs b/crates/red_knot_python_semantic/src/types/known_instance.rs index 27ed7a533a1438..01316c1c5698bb 100644 --- a/crates/red_knot_python_semantic/src/types/known_instance.rs +++ b/crates/red_knot_python_semantic/src/types/known_instance.rs @@ -95,6 +95,7 @@ pub enum KnownInstanceType<'db> { NotRequired, TypeAlias, TypeGuard, + TypedDict, TypeIs, ReadOnly, // TODO: fill this enum out with more special forms, etc. @@ -125,6 +126,7 @@ impl<'db> KnownInstanceType<'db> { | Self::NotRequired | Self::TypeAlias | Self::TypeGuard + | Self::TypedDict | Self::TypeIs | Self::List | Self::Dict @@ -172,6 +174,7 @@ impl<'db> KnownInstanceType<'db> { Self::NotRequired => "typing.NotRequired", Self::TypeAlias => "typing.TypeAlias", Self::TypeGuard => "typing.TypeGuard", + Self::TypedDict => "typing.TypedDict", Self::TypeIs => "typing.TypeIs", Self::List => "typing.List", Self::Dict => "typing.Dict", @@ -220,6 +223,7 @@ impl<'db> KnownInstanceType<'db> { Self::NotRequired => KnownClass::SpecialForm, Self::TypeAlias => KnownClass::SpecialForm, Self::TypeGuard => KnownClass::SpecialForm, + Self::TypedDict => KnownClass::SpecialForm, Self::TypeIs => KnownClass::SpecialForm, Self::ReadOnly => KnownClass::SpecialForm, Self::List => KnownClass::StdlibAlias, @@ -293,6 +297,7 @@ impl<'db> KnownInstanceType<'db> { "Required" => Self::Required, "TypeAlias" => Self::TypeAlias, "TypeGuard" => Self::TypeGuard, + "TypedDict" => Self::TypedDict, "TypeIs" => Self::TypeIs, "ReadOnly" => Self::ReadOnly, "Concatenate" => Self::Concatenate, @@ -350,6 +355,7 @@ impl<'db> KnownInstanceType<'db> { | Self::NotRequired | Self::TypeAlias | Self::TypeGuard + | Self::TypedDict | Self::TypeIs | Self::ReadOnly | Self::TypeAliasType(_) diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 8037611f5109fc..64d9bb726a78a1 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -221,6 +221,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (KnownInstanceType::TypeGuard, _) => Ordering::Less, (_, KnownInstanceType::TypeGuard) => Ordering::Greater, + (KnownInstanceType::TypedDict, _) => Ordering::Less, + (_, KnownInstanceType::TypedDict) => Ordering::Greater, + (KnownInstanceType::List, _) => Ordering::Less, (_, KnownInstanceType::List) => Ordering::Greater, From ceb2bf116841fab63c02229e24d1bd5f42738fea Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Mon, 28 Apr 2025 08:23:29 -0300 Subject: [PATCH 0153/1161] [`flake8-pyi`] Ensure `Literal[None,] | Literal[None,]` is not autofixed to `None | None` (`PYI061`) (#17659) Co-authored-by: Alex Waygood --- crates/ruff/tests/lint.rs | 49 ++++++++++++++++++- .../test/fixtures/flake8_pyi/PYI061.py | 1 - .../test/fixtures/flake8_pyi/PYI061.pyi | 1 - .../rules/redundant_none_literal.rs | 19 +++---- ...__flake8_pyi__tests__PYI061_PYI061.py.snap | 20 -------- ..._flake8_pyi__tests__PYI061_PYI061.pyi.snap | 20 -------- ...ke8_pyi__tests__py38_PYI061_PYI061.py.snap | 20 -------- ...e8_pyi__tests__py38_PYI061_PYI061.pyi.snap | 20 -------- 8 files changed, 56 insertions(+), 94 deletions(-) diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index d9b18fcfb78a27..d58e85a4686126 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -3475,7 +3475,7 @@ requires-python = ">= 3.11" &inner_pyproject, r#" [tool.ruff] -target-version = "py310" +target-version = "py310" "#, )?; @@ -4980,6 +4980,53 @@ fn flake8_import_convention_unused_aliased_import_no_conflict() { .pass_stdin("1")); } +// See: https://github.com/astral-sh/ruff/issues/16177 +#[test] +fn flake8_pyi_redundant_none_literal() { + let snippet = r#" +from typing import Literal + +# For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements +# but not both, as if both were autofixed it would result in `None | None`, +# which leads to a `TypeError` at runtime. +a: Literal[None,] | Literal[None,] +b: Literal[None] | Literal[None] +c: Literal[None] | Literal[None,] +d: Literal[None,] | Literal[None] +"#; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--select", "PYI061"]) + .args(["--stdin-filename", "test.py"]) + .arg("--preview") + .arg("--diff") + .arg("-") + .pass_stdin(snippet), @r" + success: false + exit_code: 1 + ----- stdout ----- + --- test.py + +++ test.py + @@ -4,7 +4,7 @@ + # For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements + # but not both, as if both were autofixed it would result in `None | None`, + # which leads to a `TypeError` at runtime. + -a: Literal[None,] | Literal[None,] + -b: Literal[None] | Literal[None] + -c: Literal[None] | Literal[None,] + -d: Literal[None,] | Literal[None] + +a: None | Literal[None,] + +b: None | Literal[None] + +c: None | Literal[None,] + +d: None | Literal[None] + + + ----- stderr ----- + Would fix 4 errors. + "); +} + /// Test that private, old-style `TypeVar` generics /// 1. Get replaced with PEP 695 type parameters (UP046, UP047) /// 2. Get renamed to remove leading underscores (UP049) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py index 51baabe8921070..4bdc3d98799b90 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py @@ -78,4 +78,3 @@ def good_func(arg1: Literal[int] | None): c: (None | Literal[None]) | None d: None | (Literal[None] | None) e: None | ((None | Literal[None]) | None) | None -f: Literal[None] | Literal[None] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi index ed879dd646e34d..404ac1157edb90 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi @@ -53,4 +53,3 @@ b: None | Literal[None] | None c: (None | Literal[None]) | None d: None | (Literal[None] | None) e: None | ((None | Literal[None]) | None) | None -f: Literal[None] | Literal[None] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 16a48bc5f57173..07ca4a96c2662e 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -5,7 +5,7 @@ use ruff_python_ast::{ self as ast, helpers::{pep_604_union, typing_optional}, name::Name, - Expr, ExprBinOp, ExprContext, ExprNoneLiteral, ExprSubscript, Operator, PythonVersion, + Expr, ExprBinOp, ExprContext, ExprNoneLiteral, Operator, PythonVersion, }; use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union}; use ruff_text_size::{Ranged, TextRange}; @@ -130,6 +130,12 @@ pub(crate) fn redundant_none_literal<'a>(checker: &Checker, literal_expr: &'a Ex literal_elements.clone(), union_kind, ) + // Isolate the fix to ensure multiple fixes on the same expression (like + // `Literal[None,] | Literal[None,]` -> `None | None`) happen across separate passes, + // preventing the production of invalid code. + .map(|fix| { + fix.map(|fix| fix.isolate(Checker::isolation(semantic.current_statement_id()))) + }) }); checker.report_diagnostic(diagnostic); } @@ -172,18 +178,9 @@ fn create_fix( traverse_union( &mut |expr, _| { - if matches!(expr, Expr::NoneLiteral(_)) { + if expr.is_none_literal_expr() { is_fixable = false; } - if expr != literal_expr { - if let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr { - if semantic.match_typing_expr(value, "Literal") - && matches!(**slice, Expr::NoneLiteral(_)) - { - is_fixable = false; - } - } - } }, semantic, enclosing_pep604_union, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap index 33bf166e8b796c..32cfb3f84c2e44 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap @@ -422,7 +422,6 @@ PYI061.py:79:20: PYI061 Use `None` rather than `Literal[None]` 79 | d: None | (Literal[None] | None) | ^^^^ PYI061 80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] | = help: Replace with `None` @@ -432,24 +431,5 @@ PYI061.py:80:28: PYI061 Use `None` rather than `Literal[None]` 79 | d: None | (Literal[None] | None) 80 | e: None | ((None | Literal[None]) | None) | None | ^^^^ PYI061 -81 | f: Literal[None] | Literal[None] - | - = help: Replace with `None` - -PYI061.py:81:12: PYI061 Use `None` rather than `Literal[None]` - | -79 | d: None | (Literal[None] | None) -80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 - | - = help: Replace with `None` - -PYI061.py:81:28: PYI061 Use `None` rather than `Literal[None]` - | -79 | d: None | (Literal[None] | None) -80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 | = help: Replace with `None` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap index a2c845c0649b85..6a48501c8dad8b 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap @@ -291,7 +291,6 @@ PYI061.pyi:54:20: PYI061 Use `None` rather than `Literal[None]` 54 | d: None | (Literal[None] | None) | ^^^^ PYI061 55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] | = help: Replace with `None` @@ -301,24 +300,5 @@ PYI061.pyi:55:28: PYI061 Use `None` rather than `Literal[None]` 54 | d: None | (Literal[None] | None) 55 | e: None | ((None | Literal[None]) | None) | None | ^^^^ PYI061 -56 | f: Literal[None] | Literal[None] - | - = help: Replace with `None` - -PYI061.pyi:56:12: PYI061 Use `None` rather than `Literal[None]` - | -54 | d: None | (Literal[None] | None) -55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 - | - = help: Replace with `None` - -PYI061.pyi:56:28: PYI061 Use `None` rather than `Literal[None]` - | -54 | d: None | (Literal[None] | None) -55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 | = help: Replace with `None` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap index 99f4969598fa95..bbee6e9da58cd9 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.py.snap @@ -464,7 +464,6 @@ PYI061.py:79:20: PYI061 Use `None` rather than `Literal[None]` 79 | d: None | (Literal[None] | None) | ^^^^ PYI061 80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] | = help: Replace with `None` @@ -474,24 +473,5 @@ PYI061.py:80:28: PYI061 Use `None` rather than `Literal[None]` 79 | d: None | (Literal[None] | None) 80 | e: None | ((None | Literal[None]) | None) | None | ^^^^ PYI061 -81 | f: Literal[None] | Literal[None] - | - = help: Replace with `None` - -PYI061.py:81:12: PYI061 Use `None` rather than `Literal[None]` - | -79 | d: None | (Literal[None] | None) -80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 - | - = help: Replace with `None` - -PYI061.py:81:28: PYI061 Use `None` rather than `Literal[None]` - | -79 | d: None | (Literal[None] | None) -80 | e: None | ((None | Literal[None]) | None) | None -81 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 | = help: Replace with `None` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap index a2c845c0649b85..6a48501c8dad8b 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__py38_PYI061_PYI061.pyi.snap @@ -291,7 +291,6 @@ PYI061.pyi:54:20: PYI061 Use `None` rather than `Literal[None]` 54 | d: None | (Literal[None] | None) | ^^^^ PYI061 55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] | = help: Replace with `None` @@ -301,24 +300,5 @@ PYI061.pyi:55:28: PYI061 Use `None` rather than `Literal[None]` 54 | d: None | (Literal[None] | None) 55 | e: None | ((None | Literal[None]) | None) | None | ^^^^ PYI061 -56 | f: Literal[None] | Literal[None] - | - = help: Replace with `None` - -PYI061.pyi:56:12: PYI061 Use `None` rather than `Literal[None]` - | -54 | d: None | (Literal[None] | None) -55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 - | - = help: Replace with `None` - -PYI061.pyi:56:28: PYI061 Use `None` rather than `Literal[None]` - | -54 | d: None | (Literal[None] | None) -55 | e: None | ((None | Literal[None]) | None) | None -56 | f: Literal[None] | Literal[None] - | ^^^^ PYI061 | = help: Replace with `None` From 92f95ff4947b1a7c06ad459a0e30f749bd72c1ab Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 28 Apr 2025 13:28:43 +0200 Subject: [PATCH 0154/1161] [red-knot] TypedDict: No errors for introspection dunder attributes (#17677) ## Summary Do not emit errors when accessing introspection dunder attributes such as `__required_keys__` on `TypedDict`s. --- .../red_knot_python_semantic/resources/mdtest/typed_dict.md | 3 +++ crates/red_knot_python_semantic/src/types/class_base.rs | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md b/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md index 22d43260ba7587..217bfc9e4d5b3f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md @@ -21,4 +21,7 @@ msg = Message(id=1, content="Hello") # No errors for yet-unsupported features (`closed`): OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True) + +reveal_type(Person.__required_keys__) # revealed: @Todo(TypedDict) +reveal_type(Message.__required_keys__) # revealed: @Todo(TypedDict) ``` diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 91c132fc3d49e1..9f480ed07eaf05 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -169,9 +169,7 @@ impl<'db> ClassBase<'db> { KnownInstanceType::OrderedDict => { Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db)) } - KnownInstanceType::TypedDict => { - Self::try_from_type(db, KnownClass::Dict.to_class_literal(db)) - } + KnownInstanceType::TypedDict => Self::try_from_type(db, todo_type!("TypedDict")), KnownInstanceType::Callable => { Self::try_from_type(db, todo_type!("Support for Callable as a base class")) } From 1ad5015e19fd653f1ce6bd9dc33253bab1689c9c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 28 Apr 2025 13:32:19 +0100 Subject: [PATCH 0155/1161] Upgrade Salsa to a more recent commit (#17678) --- Cargo.lock | 34 ++++++++++------------------------ Cargo.toml | 2 +- fuzz/Cargo.toml | 2 +- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec52264dd9a8b2..34302a319ba588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,20 +490,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "compact_str" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - [[package]] name = "compact_str" version = "0.9.0" @@ -2572,7 +2558,7 @@ dependencies = [ "anyhow", "bitflags 2.9.0", "camino", - "compact_str 0.9.0", + "compact_str", "countme", "dir-test", "drop_bomb", @@ -3101,7 +3087,7 @@ version = "0.0.0" dependencies = [ "aho-corasick", "bitflags 2.9.0", - "compact_str 0.9.0", + "compact_str", "is-macro", "itertools 0.14.0", "memchr", @@ -3199,7 +3185,7 @@ dependencies = [ "anyhow", "bitflags 2.9.0", "bstr", - "compact_str 0.9.0", + "compact_str", "insta", "memchr", "ruff_annotate_snippets", @@ -3457,11 +3443,11 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" -version = "0.19.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26" +version = "0.20.0" +source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1" dependencies = [ "boxcar", - "compact_str 0.8.1", + "compact_str", "crossbeam-queue", "dashmap 6.1.0", "hashbrown 0.15.2", @@ -3480,13 +3466,13 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.19.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26" +version = "0.20.0" +source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1" [[package]] name = "salsa-macros" -version = "0.19.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26" +version = "0.20.0" +source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index a5fbde7274aec1..7aa260874976ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ rayon = { version = "1.10.0" } regex = { version = "1.10.2" } rustc-hash = { version = "2.0.0" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "87bf6b6c2d5f6479741271da73bd9d30c2580c26" } +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "c75b0161aba55965ab6ad8cc9aaee7dc177967f1" } schemars = { version = "0.8.16" } seahash = { version = "4.1.0" } serde = { version = "1.0.197", features = ["derive"] } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 2027caaefb6b34..a0d617b0f56ca5 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -29,7 +29,7 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter" } ruff_text_size = { path = "../crates/ruff_text_size" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "87bf6b6c2d5f6479741271da73bd9d30c2580c26" } +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "c75b0161aba55965ab6ad8cc9aaee7dc177967f1" } similar = { version = "2.5.0" } tracing = { version = "0.1.40" } From 152a0b658518587891fa850f96dea3f659b8ae10 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 28 Apr 2025 09:12:24 -0500 Subject: [PATCH 0156/1161] Collect preview lint behaviors in separate module (#17646) This PR collects all behavior gated under preview into a new module `ruff_linter::preview` that exposes functions like `is_my_new_feature_enabled` - just as is done in the formatter crate. --- crates/ruff_linter/src/checkers/ast/mod.rs | 5 +- crates/ruff_linter/src/checkers/filesystem.rs | 4 +- crates/ruff_linter/src/checkers/noqa.rs | 3 +- crates/ruff_linter/src/lib.rs | 1 + crates/ruff_linter/src/linter.rs | 3 +- crates/ruff_linter/src/preview.rs | 118 ++++++++++++++++++ .../flake8_bandit/rules/shell_injection.rs | 5 +- .../rules/suspicious_function_call.rs | 5 +- .../boolean_type_hint_positional_argument.rs | 3 +- .../unnecessary_comprehension_in_call.rs | 3 +- .../unnecessary_literal_within_tuple_call.rs | 5 +- .../rules/implicit_namespace_package.rs | 5 +- .../rules/bad_version_info_comparison.rs | 3 +- .../rules/future_annotations_in_stub.rs | 4 +- .../src/rules/flake8_return/rules/function.rs | 3 +- .../rules/if_else_block_instead_of_if_exp.rs | 68 +++++----- .../rules/manual_dict_comprehension.rs | 3 +- .../rules/manual_list_comprehension.rs | 7 +- .../src/rules/pyflakes/rules/unused_import.rs | 3 +- .../pyupgrade/rules/use_pep604_annotation.rs | 17 +-- .../ruff/rules/ambiguous_unicode_character.rs | 13 +- .../rules/collection_literal_concatenation.rs | 4 +- 22 files changed, 215 insertions(+), 70 deletions(-) create mode 100644 crates/ruff_linter/src/preview.rs diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 73e875bf208705..6611d27fa23c57 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -65,6 +65,7 @@ use crate::docstrings::extraction::ExtractionTarget; use crate::importer::{ImportRequest, Importer, ResolutionError}; use crate::noqa::NoqaMapping; use crate::package::PackageRoot; +use crate::preview::{is_semantic_errors_enabled, is_undefined_export_in_dunder_init_enabled}; use crate::registry::Rule; use crate::rules::pyflakes::rules::{ LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction, @@ -618,7 +619,7 @@ impl SemanticSyntaxContext for Checker<'_> { | SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_) | SemanticSyntaxErrorKind::DuplicateParameter(_) | SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => { - if self.settings.preview.is_enabled() { + if is_semantic_errors_enabled(self.settings) { self.semantic_errors.borrow_mut().push(error); } } @@ -2827,7 +2828,7 @@ impl<'a> Checker<'a> { } } else { if self.enabled(Rule::UndefinedExport) { - if self.settings.preview.is_enabled() + if is_undefined_export_in_dunder_init_enabled(self.settings) || !self.path.ends_with("__init__.py") { self.diagnostics.get_mut().push( diff --git a/crates/ruff_linter/src/checkers/filesystem.rs b/crates/ruff_linter/src/checkers/filesystem.rs index f6d9c491c8cccd..daf197c7d75efe 100644 --- a/crates/ruff_linter/src/checkers/filesystem.rs +++ b/crates/ruff_linter/src/checkers/filesystem.rs @@ -5,6 +5,7 @@ use ruff_python_ast::PythonVersion; use ruff_python_trivia::CommentRanges; use crate::package::PackageRoot; +use crate::preview::is_allow_nested_roots_enabled; use crate::registry::Rule; use crate::rules::flake8_builtins::rules::stdlib_module_shadowing; use crate::rules::flake8_no_pep420::rules::implicit_namespace_package; @@ -24,6 +25,7 @@ pub(crate) fn check_file_path( // flake8-no-pep420 if settings.rules.enabled(Rule::ImplicitNamespacePackage) { + let allow_nested_roots = is_allow_nested_roots_enabled(settings); if let Some(diagnostic) = implicit_namespace_package( path, package, @@ -31,7 +33,7 @@ pub(crate) fn check_file_path( comment_ranges, &settings.project_root, &settings.src, - settings.preview, + allow_nested_roots, ) { diagnostics.push(diagnostic); } diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index cb01ac686b6872..da7c4b1e6ff881 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -13,6 +13,7 @@ use crate::fix::edits::delete_comment; use crate::noqa::{ Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping, }; +use crate::preview::is_check_file_level_directives_enabled; use crate::registry::{AsRule, Rule, RuleSet}; use crate::rule_redirects::get_redirect_target; use crate::rules::pygrep_hooks; @@ -110,7 +111,7 @@ pub(crate) fn check_noqa( && !exemption.includes(Rule::UnusedNOQA) && !per_file_ignores.contains(Rule::UnusedNOQA) { - let directives: Vec<_> = if settings.preview.is_enabled() { + let directives: Vec<_> = if is_check_file_level_directives_enabled(settings) { noqa_directives .lines() .iter() diff --git a/crates/ruff_linter/src/lib.rs b/crates/ruff_linter/src/lib.rs index 5c024cb5f87ea3..8cb57446bc4b37 100644 --- a/crates/ruff_linter/src/lib.rs +++ b/crates/ruff_linter/src/lib.rs @@ -34,6 +34,7 @@ pub mod message; mod noqa; pub mod package; pub mod packaging; +pub mod preview; pub mod pyproject_toml; pub mod registry; mod renamer; diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index d4d9daef16b5b5..7f03f72e1a6e7a 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -30,6 +30,7 @@ use crate::fix::{fix_file, FixResult}; use crate::message::Message; use crate::noqa::add_noqa; use crate::package::PackageRoot; +use crate::preview::is_unsupported_syntax_enabled; use crate::registry::{AsRule, Rule, RuleSet}; #[cfg(any(feature = "test-rules", test))] use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES}; @@ -360,7 +361,7 @@ pub fn check_path( } } - let syntax_errors = if settings.preview.is_enabled() { + let syntax_errors = if is_unsupported_syntax_enabled(settings) { parsed.unsupported_syntax_errors() } else { &[] diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs new file mode 100644 index 00000000000000..f64821f80dfde0 --- /dev/null +++ b/crates/ruff_linter/src/preview.rs @@ -0,0 +1,118 @@ +//! Helpers to test if a specific preview style is enabled or not. +//! +//! The motivation for these functions isn't to avoid code duplication but to ease promoting preview behavior +//! to stable. The challenge with directly checking the `preview` attribute of [`LinterSettings`] is that it is unclear +//! which specific feature this preview check is for. Having named functions simplifies the promotion: +//! Simply delete the function and let Rust tell you which checks you have to remove. + +use crate::settings::LinterSettings; + +// https://github.com/astral-sh/ruff/issues/17412 +// https://github.com/astral-sh/ruff/issues/11934 +pub(crate) const fn is_semantic_errors_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/16429 +pub(crate) const fn is_unsupported_syntax_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// Rule-specific behavior + +// https://github.com/astral-sh/ruff/pull/17136 +pub(crate) const fn is_shell_injection_only_trusted_input_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/15541 +pub(crate) const fn is_suspicious_function_reference_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/7501 +pub(crate) const fn is_bool_subtype_of_annotation_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/10759 +pub(crate) const fn is_comprehension_with_min_max_sum_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/12657 +pub(crate) const fn is_check_comprehensions_in_tuple_call_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/issues/15347 +pub(crate) const fn is_bad_version_info_in_non_stub_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/12676 +pub(crate) const fn is_fix_future_annotations_in_stub_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/11074 +pub(crate) const fn is_only_add_return_none_at_end_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/12796 +pub(crate) const fn is_simplify_ternary_to_binary_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/16719 +pub(crate) const fn is_fix_manual_dict_comprehension_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/13919 +pub(crate) const fn is_fix_manual_list_comprehension_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/11436 +// https://github.com/astral-sh/ruff/pull/11168 +pub(crate) const fn is_dunder_init_fix_unused_import_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/15313 +pub(crate) const fn is_defer_optional_to_up045_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/8473 +pub(crate) const fn is_unicode_to_unicode_confusables_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/17078 +pub(crate) const fn is_support_slices_in_literal_concatenation_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/11370 +pub(crate) const fn is_undefined_export_in_dunder_init_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/14236 +pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/17061 +pub(crate) const fn is_check_file_level_directives_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs index 539b03ad5a39f9..fef19910e597ee 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs @@ -1,5 +1,6 @@ //! Checks relating to shell injection. +use crate::preview::is_shell_injection_only_trusted_input_enabled; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::helpers::Truthiness; @@ -324,7 +325,9 @@ pub(crate) fn shell_injection(checker: &Checker, call: &ast::ExprCall) { } // S603 _ => { - if !is_trusted_input(arg) || checker.settings.preview.is_disabled() { + if !is_trusted_input(arg) + || !is_shell_injection_only_trusted_input_enabled(checker.settings) + { if checker.enabled(Rule::SubprocessWithoutShellEqualsTrue) { checker.report_diagnostic(Diagnostic::new( SubprocessWithoutShellEqualsTrue, diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs index f47ecc06f4d20c..dc1765f56e33d5 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -8,6 +8,7 @@ use ruff_python_ast::{self as ast, Arguments, Decorator, Expr, ExprCall, Operato use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::preview::is_suspicious_function_reference_enabled; use crate::registry::AsRule; /// ## What it does @@ -936,7 +937,7 @@ pub(crate) fn suspicious_function_call(checker: &Checker, call: &ExprCall) { } pub(crate) fn suspicious_function_reference(checker: &Checker, func: &Expr) { - if checker.settings.preview.is_disabled() { + if !is_suspicious_function_reference_enabled(checker.settings) { return; } @@ -1210,7 +1211,7 @@ fn suspicious_function( /// S308 pub(crate) fn suspicious_function_decorator(checker: &Checker, decorator: &Decorator) { // In preview mode, references are handled collectively by `suspicious_function_reference` - if checker.settings.preview.is_disabled() { + if !is_suspicious_function_reference_enabled(checker.settings) { suspicious_function(checker, &decorator.expression, None, decorator.range); } } diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs index d7de29798345f8..e54f4119c662fc 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs @@ -8,6 +8,7 @@ use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; +use crate::preview::is_bool_subtype_of_annotation_enabled; use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// ## What it does @@ -128,7 +129,7 @@ pub(crate) fn boolean_type_hint_positional_argument( let Some(annotation) = parameter.annotation() else { continue; }; - if checker.settings.preview.is_enabled() { + if is_bool_subtype_of_annotation_enabled(checker.settings) { if !match_annotation_to_complex_bool(annotation, checker.semantic()) { continue; } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs index cfa611e7deb61e..97d6af93ee0658 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs @@ -6,6 +6,7 @@ use ruff_python_ast::{self as ast, Expr, Keyword}; use ruff_text_size::{Ranged, TextSize}; use crate::checkers::ast::Checker; +use crate::preview::is_comprehension_with_min_max_sum_enabled; use crate::rules::flake8_comprehensions::fixes; /// ## What it does @@ -125,7 +126,7 @@ pub(crate) fn unnecessary_comprehension_in_call( if !(matches!( builtin_function, SupportedBuiltins::Any | SupportedBuiltins::All - ) || (checker.settings.preview.is_enabled() + ) || (is_comprehension_with_min_max_sum_enabled(checker.settings) && matches!( builtin_function, SupportedBuiltins::Sum | SupportedBuiltins::Min | SupportedBuiltins::Max diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs index 50a44a31f5d3a5..c4eb96b832f10b 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs @@ -6,6 +6,7 @@ use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; +use crate::preview::is_check_comprehensions_in_tuple_call_enabled; use crate::rules::flake8_comprehensions::fixes; use super::helpers; @@ -100,7 +101,9 @@ pub(crate) fn unnecessary_literal_within_tuple_call( let argument_kind = match argument { Expr::Tuple(_) => TupleLiteralKind::Tuple, Expr::List(_) => TupleLiteralKind::List, - Expr::ListComp(_) if checker.settings.preview.is_enabled() => TupleLiteralKind::ListComp, + Expr::ListComp(_) if is_check_comprehensions_in_tuple_call_enabled(checker.settings) => { + TupleLiteralKind::ListComp + } _ => return, }; if !checker.semantic().has_builtin_binding("tuple") { diff --git a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs index 4e91d2058263a0..eb1c30a41ac683 100644 --- a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs +++ b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs @@ -10,7 +10,6 @@ use ruff_text_size::{TextRange, TextSize}; use crate::comments::shebang::ShebangDirective; use crate::fs; use crate::package::PackageRoot; -use crate::settings::types::PreviewMode; use crate::Locator; /// ## What it does @@ -60,7 +59,7 @@ pub(crate) fn implicit_namespace_package( comment_ranges: &CommentRanges, project_root: &Path, src: &[PathBuf], - preview: PreviewMode, + allow_nested_roots: bool, ) -> Option { if package.is_none() // Ignore non-`.py` files, which don't require an `__init__.py`. @@ -93,7 +92,7 @@ pub(crate) fn implicit_namespace_package( )); } - if preview.is_enabled() { + if allow_nested_roots { if let Some(PackageRoot::Nested { path: root }) = package.as_ref() { if path.ends_with("__init__.py") { // Identify the intermediary package that's missing the `__init__.py` file. diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs index 81066c00273434..daa54b7d207184 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs @@ -5,6 +5,7 @@ use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::preview::is_bad_version_info_in_non_stub_enabled; use crate::registry::Rule; /// ## What it does @@ -140,7 +141,7 @@ pub(crate) fn bad_version_info_comparison(checker: &Checker, test: &Expr, has_el if matches!(op, CmpOp::Lt) { if checker.enabled(Rule::BadVersionInfoOrder) // See https://github.com/astral-sh/ruff/issues/15347 - && (checker.source_type.is_stub() || checker.settings.preview.is_enabled()) + && (checker.source_type.is_stub() || is_bad_version_info_in_non_stub_enabled(checker.settings)) { if has_else_clause { checker.report_diagnostic(Diagnostic::new(BadVersionInfoOrder, test.range())); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs index 8949ff40ad0f17..ed71e194401c97 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs @@ -3,7 +3,7 @@ use ruff_python_ast::StmtImportFrom; use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use crate::{checkers::ast::Checker, fix}; +use crate::{checkers::ast::Checker, fix, preview::is_fix_future_annotations_in_stub_enabled}; /// ## What it does /// Checks for the presence of the `from __future__ import annotations` import @@ -55,7 +55,7 @@ pub(crate) fn from_future_import(checker: &Checker, target: &StmtImportFrom) { let mut diagnostic = Diagnostic::new(FutureAnnotationsInStub, *range); - if checker.settings.preview.is_enabled() { + if is_fix_future_annotations_in_stub_enabled(checker.settings) { let stmt = checker.semantic().current_statement(); diagnostic.try_set_fix(|| { diff --git a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs index a28597949e85e5..aa91da5e7409d0 100644 --- a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs @@ -21,6 +21,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::checkers::ast::Checker; use crate::fix::edits; use crate::fix::edits::adjust_indentation; +use crate::preview::is_only_add_return_none_at_end_enabled; use crate::registry::{AsRule, Rule}; use crate::rules::flake8_return::helpers::end_of_last_statement; use crate::Locator; @@ -552,7 +553,7 @@ fn implicit_return(checker: &Checker, function_def: &ast::StmtFunctionDef, stmt: return; } - if checker.settings.preview.is_enabled() { + if is_only_add_return_none_at_end_enabled(checker.settings) { add_return_none(checker, stmt, function_def.range()); } else { for implicit_stmt in implicit_stmts { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs index d96d11df9da8db..9d9929e1457f58 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs @@ -8,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::fits; +use crate::preview::is_simplify_ternary_to_binary_enabled; /// ## What it does /// Check for `if`-`else`-blocks that can be replaced with a ternary operator. @@ -180,39 +181,40 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &Checker, stmt_if: &ast:: // - If `test == not body_value` and preview enabled, replace with `target_var = body_value and else_value` // - If `not test == body_value` and preview enabled, replace with `target_var = body_value and else_value` // - Otherwise, replace with `target_var = body_value if test else else_value` - let (contents, assignment_kind) = - match (checker.settings.preview.is_enabled(), test, body_value) { - (true, test_node, body_node) - if ComparableExpr::from(test_node) == ComparableExpr::from(body_node) - && !contains_effect(test_node, |id| { - checker.semantic().has_builtin_binding(id) - }) => - { - let target_var = &body_target; - let binary = assignment_binary_or(target_var, body_value, else_value); - (checker.generator().stmt(&binary), AssignmentKind::Binary) - } - (true, test_node, body_node) - if (test_node.as_unary_op_expr().is_some_and(|op_expr| { - op_expr.op.is_not() - && ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(body_node) - }) || body_node.as_unary_op_expr().is_some_and(|op_expr| { - op_expr.op.is_not() - && ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(test_node) - })) && !contains_effect(test_node, |id| { - checker.semantic().has_builtin_binding(id) - }) => - { - let target_var = &body_target; - let binary = assignment_binary_and(target_var, body_value, else_value); - (checker.generator().stmt(&binary), AssignmentKind::Binary) - } - _ => { - let target_var = &body_target; - let ternary = assignment_ternary(target_var, body_value, test, else_value); - (checker.generator().stmt(&ternary), AssignmentKind::Ternary) - } - }; + let (contents, assignment_kind) = match ( + is_simplify_ternary_to_binary_enabled(checker.settings), + test, + body_value, + ) { + (true, test_node, body_node) + if ComparableExpr::from(test_node) == ComparableExpr::from(body_node) + && !contains_effect(test_node, |id| checker.semantic().has_builtin_binding(id)) => + { + let target_var = &body_target; + let binary = assignment_binary_or(target_var, body_value, else_value); + (checker.generator().stmt(&binary), AssignmentKind::Binary) + } + (true, test_node, body_node) + if (test_node.as_unary_op_expr().is_some_and(|op_expr| { + op_expr.op.is_not() + && ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(body_node) + }) || body_node.as_unary_op_expr().is_some_and(|op_expr| { + op_expr.op.is_not() + && ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(test_node) + })) && !contains_effect(test_node, |id| { + checker.semantic().has_builtin_binding(id) + }) => + { + let target_var = &body_target; + let binary = assignment_binary_and(target_var, body_value, else_value); + (checker.generator().stmt(&binary), AssignmentKind::Binary) + } + _ => { + let target_var = &body_target; + let ternary = assignment_ternary(target_var, body_value, test, else_value); + (checker.generator().stmt(&ternary), AssignmentKind::Ternary) + } + }; // Don't flag if the resulting expression would exceed the maximum line length. if !fits( diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs index c2b87b6efe2a8f..f3f6ad728c7aa0 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_dict_comprehension.rs @@ -8,6 +8,7 @@ use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::preview::is_fix_manual_dict_comprehension_enabled; use crate::rules::perflint::helpers::{comment_strings_in_range, statement_deletion_range}; /// ## What it does @@ -229,7 +230,7 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtF return; } - if checker.settings.preview.is_enabled() { + if is_fix_manual_dict_comprehension_enabled(checker.settings) { let binding_stmt = binding.statement(checker.semantic()); let binding_value = binding_stmt.and_then(|binding_stmt| match binding_stmt { ast::Stmt::AnnAssign(assign) => assign.value.as_deref(), diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs index 9d3a5ac9160b27..24e99ec8b9f592 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_list_comprehension.rs @@ -1,6 +1,9 @@ use ruff_python_ast::{self as ast, Arguments, Expr}; -use crate::{checkers::ast::Checker, rules::perflint::helpers::statement_deletion_range}; +use crate::{ + checkers::ast::Checker, preview::is_fix_manual_list_comprehension_enabled, + rules::perflint::helpers::statement_deletion_range, +}; use anyhow::{anyhow, Result}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; @@ -335,7 +338,7 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF ); // TODO: once this fix is stabilized, change the rule to always fixable - if checker.settings.preview.is_enabled() { + if is_fix_manual_list_comprehension_enabled(checker.settings) { diagnostic.try_set_fix(|| { convert_to_list_extend( comprehension_type, diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs index 1a9af61beb3a91..91fcb878a2cd41 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs @@ -16,6 +16,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix; +use crate::preview::is_dunder_init_fix_unused_import_enabled; use crate::registry::Rule; use crate::rules::{isort, isort::ImportSection, isort::ImportType}; @@ -352,7 +353,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) { let in_init = checker.path().ends_with("__init__.py"); let fix_init = !checker.settings.ignore_init_module_imports; - let preview_mode = checker.settings.preview.is_enabled(); + let preview_mode = is_dunder_init_fix_unused_import_enabled(checker.settings); let dunder_all_exprs = find_dunder_all_exprs(checker.semantic()); // Generate a diagnostic for every import, but share fixes across all imports within the same diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 22d27551dfdeee..48b5737fc41c12 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -11,7 +11,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::codes::Rule; use crate::fix::edits::pad; -use crate::settings::types::PreviewMode; +use crate::preview::is_defer_optional_to_up045_enabled; /// ## What it does /// Check for type annotations that can be rewritten based on [PEP 604] syntax. @@ -150,15 +150,16 @@ pub(crate) fn non_pep604_annotation( match operator { Pep604Operator::Optional => { - let (rule, diagnostic_kind) = match checker.settings.preview { - PreviewMode::Disabled => ( - Rule::NonPEP604AnnotationUnion, - DiagnosticKind::from(NonPEP604AnnotationUnion), - ), - PreviewMode::Enabled => ( + let (rule, diagnostic_kind) = if is_defer_optional_to_up045_enabled(checker.settings) { + ( Rule::NonPEP604AnnotationOptional, DiagnosticKind::from(NonPEP604AnnotationOptional), - ), + ) + } else { + ( + Rule::NonPEP604AnnotationUnion, + DiagnosticKind::from(NonPEP604AnnotationUnion), + ) }; if !checker.enabled(rule) { diff --git a/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs b/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs index 3457a55d7e31e1..2b8f8d3e23b9d3 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs @@ -8,6 +8,7 @@ use ruff_python_ast::{self as ast, StringLike}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::checkers::ast::Checker; +use crate::preview::is_unicode_to_unicode_confusables_enabled; use crate::registry::AsRule; use crate::rules::ruff::rules::confusables::confusable; use crate::rules::ruff::rules::Context; @@ -264,9 +265,9 @@ fn ambiguous_unicode_character( // Check if the boundary character is itself an ambiguous unicode character, in which // case, it's always included as a diagnostic. if !current_char.is_ascii() { - if let Some(representant) = confusable(current_char as u32) - .filter(|representant| settings.preview.is_enabled() || representant.is_ascii()) - { + if let Some(representant) = confusable(current_char as u32).filter(|representant| { + is_unicode_to_unicode_confusables_enabled(settings) || representant.is_ascii() + }) { let candidate = Candidate::new( TextSize::try_from(relative_offset).unwrap() + range.start(), current_char, @@ -280,9 +281,9 @@ fn ambiguous_unicode_character( } else if current_char.is_ascii() { // The current word contains at least one ASCII character. word_flags |= WordFlags::ASCII; - } else if let Some(representant) = confusable(current_char as u32) - .filter(|representant| settings.preview.is_enabled() || representant.is_ascii()) - { + } else if let Some(representant) = confusable(current_char as u32).filter(|representant| { + is_unicode_to_unicode_confusables_enabled(settings) || representant.is_ascii() + }) { // The current word contains an ambiguous unicode character. word_candidates.push(Candidate::new( TextSize::try_from(relative_offset).unwrap() + range.start(), diff --git a/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs b/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs index 60dcd09b3435e1..7c9b4265051237 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -5,6 +5,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; +use crate::preview::is_support_slices_in_literal_concatenation_enabled; /// ## What it does /// Checks for uses of the `+` operator to concatenate collections. @@ -197,7 +198,8 @@ pub(crate) fn collection_literal_concatenation(checker: &Checker, expr: &Expr) { return; } - let should_support_slices = checker.settings.preview.is_enabled(); + let should_support_slices = + is_support_slices_in_literal_concatenation_enabled(checker.settings); let Some((new_expr, type_)) = concatenate_expressions(expr, should_support_slices) else { return; From 1e8881f9afe5daa7450e5e2a1f4b4a77497e9212 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 28 Apr 2025 09:39:55 -0500 Subject: [PATCH 0157/1161] [`refurb`] Mark fix as safe for `readlines-in-for` (`FURB129`) (#17644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR promotes the fix applicability of [readlines-in-for (FURB129)](https://docs.astral.sh/ruff/rules/readlines-in-for/#readlines-in-for-furb129) to always safe. In the original PR (https://github.com/astral-sh/ruff/pull/9880), the author marked the rule as unsafe because Ruff's type inference couldn't quite guarantee that we had an `IOBase` object in hand. Some false positives were recorded in the test fixture. However, before the PR was merged, Charlie added the necessary type inference and the false positives went away. According to the [Python documentation](https://docs.python.org/3/library/io.html#io.IOBase), I believe this fix is safe for any proper implementation of `IOBase`: >[IOBase](https://docs.python.org/3/library/io.html#io.IOBase) (and its subclasses) supports the iterator protocol, meaning that an [IOBase](https://docs.python.org/3/library/io.html#io.IOBase) object can be iterated over yielding the lines in a stream. Lines are defined slightly differently depending on whether the stream is a binary stream (yielding bytes), or a text stream (yielding character strings). See [readline()](https://docs.python.org/3/library/io.html#io.IOBase.readline) below. and then in the [documentation for `readlines`](https://docs.python.org/3/library/io.html#io.IOBase.readlines): >Read and return a list of lines from the stream. hint can be specified to control the number of lines read: no more lines will be read if the total size (in bytes/characters) of all lines so far exceeds hint. [...] >Note that it’s already possible to iterate on file objects using for line in file: ... without calling file.readlines(). I believe that a careful reading of our [versioning policy](https://docs.astral.sh/ruff/versioning/#version-changes) requires that this change be deferred to a minor release - but please correct me if I'm wrong! --- .../resources/test/fixtures/refurb/FURB129.py | 2 +- crates/ruff_linter/src/preview.rs | 5 + crates/ruff_linter/src/rules/refurb/mod.rs | 18 ++ .../rules/refurb/rules/readlines_in_for.rs | 13 +- ...b__tests__preview__FURB129_FURB129.py.snap | 243 ++++++++++++++++++ 5 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview__FURB129_FURB129.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB129.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB129.py index 51a3cf73a575eb..c5c0307098e7b3 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB129.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB129.py @@ -57,7 +57,7 @@ def func(): pass -# False positives +# Ok def func(f): for _line in f.readlines(): pass diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index f64821f80dfde0..9973eada40d909 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -116,3 +116,8 @@ pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) -> pub(crate) const fn is_check_file_level_directives_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/17644 +pub(crate) const fn is_readlines_in_for_fix_safe(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index 830581d49f74a3..1793e0e9649ebc 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -61,6 +61,24 @@ mod tests { Ok(()) } + #[test_case(Rule::ReadlinesInFor, Path::new("FURB129.py"))] + fn preview(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!( + "preview__{}_{}", + rule_code.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("refurb").join(path).as_path(), + &settings::LinterSettings { + preview: settings::types::PreviewMode::Enabled, + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test] fn write_whole_file_python_39() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs b/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs index e06626da9c438c..23b6e337dbfd01 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/readlines_in_for.rs @@ -1,3 +1,4 @@ +use crate::preview::is_readlines_in_for_fix_safe; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{Comprehension, Expr, StmtFor}; @@ -85,8 +86,14 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) { } let mut diagnostic = Diagnostic::new(ReadlinesInFor, expr_call.range()); - diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion( - expr_call.range().add_start(expr_attr.value.range().len()), - ))); + diagnostic.set_fix(if is_readlines_in_for_fix_safe(checker.settings) { + Fix::safe_edit(Edit::range_deletion( + expr_call.range().add_start(expr_attr.value.range().len()), + )) + } else { + Fix::unsafe_edit(Edit::range_deletion( + expr_call.range().add_start(expr_attr.value.range().len()), + )) + }); checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview__FURB129_FURB129.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview__FURB129_FURB129.py.snap new file mode 100644 index 00000000000000..f0fdde19a88354 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview__FURB129_FURB129.py.snap @@ -0,0 +1,243 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB129.py:7:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +5 | # Errors +6 | with open("FURB129.py") as f: +7 | for _line in f.readlines(): + | ^^^^^^^^^^^^^ FURB129 +8 | pass +9 | a = [line.lower() for line in f.readlines()] + | + = help: Remove `readlines()` + +ℹ Safe fix +4 4 | +5 5 | # Errors +6 6 | with open("FURB129.py") as f: +7 |- for _line in f.readlines(): + 7 |+ for _line in f: +8 8 | pass +9 9 | a = [line.lower() for line in f.readlines()] +10 10 | b = {line.upper() for line in f.readlines()} + +FURB129.py:9:35: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | + 7 | for _line in f.readlines(): + 8 | pass + 9 | a = [line.lower() for line in f.readlines()] + | ^^^^^^^^^^^^^ FURB129 +10 | b = {line.upper() for line in f.readlines()} +11 | c = {line.lower(): line.upper() for line in f.readlines()} + | + = help: Remove `readlines()` + +ℹ Safe fix +6 6 | with open("FURB129.py") as f: +7 7 | for _line in f.readlines(): +8 8 | pass +9 |- a = [line.lower() for line in f.readlines()] + 9 |+ a = [line.lower() for line in f] +10 10 | b = {line.upper() for line in f.readlines()} +11 11 | c = {line.lower(): line.upper() for line in f.readlines()} +12 12 | + +FURB129.py:10:35: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | + 8 | pass + 9 | a = [line.lower() for line in f.readlines()] +10 | b = {line.upper() for line in f.readlines()} + | ^^^^^^^^^^^^^ FURB129 +11 | c = {line.lower(): line.upper() for line in f.readlines()} + | + = help: Remove `readlines()` + +ℹ Safe fix +7 7 | for _line in f.readlines(): +8 8 | pass +9 9 | a = [line.lower() for line in f.readlines()] +10 |- b = {line.upper() for line in f.readlines()} + 10 |+ b = {line.upper() for line in f} +11 11 | c = {line.lower(): line.upper() for line in f.readlines()} +12 12 | +13 13 | with Path("FURB129.py").open() as f: + +FURB129.py:11:49: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | + 9 | a = [line.lower() for line in f.readlines()] +10 | b = {line.upper() for line in f.readlines()} +11 | c = {line.lower(): line.upper() for line in f.readlines()} + | ^^^^^^^^^^^^^ FURB129 +12 | +13 | with Path("FURB129.py").open() as f: + | + = help: Remove `readlines()` + +ℹ Safe fix +8 8 | pass +9 9 | a = [line.lower() for line in f.readlines()] +10 10 | b = {line.upper() for line in f.readlines()} +11 |- c = {line.lower(): line.upper() for line in f.readlines()} + 11 |+ c = {line.lower(): line.upper() for line in f} +12 12 | +13 13 | with Path("FURB129.py").open() as f: +14 14 | for _line in f.readlines(): + +FURB129.py:14:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +13 | with Path("FURB129.py").open() as f: +14 | for _line in f.readlines(): + | ^^^^^^^^^^^^^ FURB129 +15 | pass + | + = help: Remove `readlines()` + +ℹ Safe fix +11 11 | c = {line.lower(): line.upper() for line in f.readlines()} +12 12 | +13 13 | with Path("FURB129.py").open() as f: +14 |- for _line in f.readlines(): + 14 |+ for _line in f: +15 15 | pass +16 16 | +17 17 | for _line in open("FURB129.py").readlines(): + +FURB129.py:17:14: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +15 | pass +16 | +17 | for _line in open("FURB129.py").readlines(): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB129 +18 | pass + | + = help: Remove `readlines()` + +ℹ Safe fix +14 14 | for _line in f.readlines(): +15 15 | pass +16 16 | +17 |-for _line in open("FURB129.py").readlines(): + 17 |+for _line in open("FURB129.py"): +18 18 | pass +19 19 | +20 20 | for _line in Path("FURB129.py").open().readlines(): + +FURB129.py:20:14: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +18 | pass +19 | +20 | for _line in Path("FURB129.py").open().readlines(): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB129 +21 | pass + | + = help: Remove `readlines()` + +ℹ Safe fix +17 17 | for _line in open("FURB129.py").readlines(): +18 18 | pass +19 19 | +20 |-for _line in Path("FURB129.py").open().readlines(): + 20 |+for _line in Path("FURB129.py").open(): +21 21 | pass +22 22 | +23 23 | + +FURB129.py:26:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +24 | def func(): +25 | f = Path("FURB129.py").open() +26 | for _line in f.readlines(): + | ^^^^^^^^^^^^^ FURB129 +27 | pass +28 | f.close() + | + = help: Remove `readlines()` + +ℹ Safe fix +23 23 | +24 24 | def func(): +25 25 | f = Path("FURB129.py").open() +26 |- for _line in f.readlines(): + 26 |+ for _line in f: +27 27 | pass +28 28 | f.close() +29 29 | + +FURB129.py:32:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +31 | def func(f: io.BytesIO): +32 | for _line in f.readlines(): + | ^^^^^^^^^^^^^ FURB129 +33 | pass + | + = help: Remove `readlines()` + +ℹ Safe fix +29 29 | +30 30 | +31 31 | def func(f: io.BytesIO): +32 |- for _line in f.readlines(): + 32 |+ for _line in f: +33 33 | pass +34 34 | +35 35 | + +FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +36 | def func(): +37 | with (open("FURB129.py") as f, foo as bar): +38 | for _line in f.readlines(): + | ^^^^^^^^^^^^^ FURB129 +39 | pass +40 | for _line in bar.readlines(): + | + = help: Remove `readlines()` + +ℹ Safe fix +35 35 | +36 36 | def func(): +37 37 | with (open("FURB129.py") as f, foo as bar): +38 |- for _line in f.readlines(): + 38 |+ for _line in f: +39 39 | pass +40 40 | for _line in bar.readlines(): +41 41 | pass + +FURB129.py:48:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +47 | with builtins.open("FURB129.py") as f: +48 | for line in f.readlines(): + | ^^^^^^^^^^^^^ FURB129 +49 | pass + | + = help: Remove `readlines()` + +ℹ Safe fix +45 45 | +46 46 | +47 47 | with builtins.open("FURB129.py") as f: +48 |- for line in f.readlines(): + 48 |+ for line in f: +49 49 | pass +50 50 | +51 51 | + +FURB129.py:56:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly + | +55 | with o("FURB129.py") as f: +56 | for line in f.readlines(): + | ^^^^^^^^^^^^^ FURB129 +57 | pass + | + = help: Remove `readlines()` + +ℹ Safe fix +53 53 | +54 54 | +55 55 | with o("FURB129.py") as f: +56 |- for line in f.readlines(): + 56 |+ for line in f: +57 57 | pass +58 58 | +59 59 | From 07718f47885546f321dee4b82e361c580ed824a3 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 28 Apr 2025 16:40:35 +0200 Subject: [PATCH 0158/1161] [red-knot] Allow all callables to be assignable to @Todo-signatures (#17680) ## Summary Removes ~850 diagnostics related to assignability of callable types, where the callable-being-assigned-to has a "Todo signature", which should probably accept any left hand side callable/signature. --- .../resources/mdtest/annotations/callable.md | 12 ++++++++---- .../red_knot_python_semantic/src/types/signatures.rs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) 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 e906b5de39b2ae..29cef6cae348ca 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md @@ -225,14 +225,16 @@ Using `Concatenate` as the first argument to `Callable`: from typing_extensions import Callable, Concatenate def _(c: Callable[Concatenate[int, str, ...], int]): - reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int + # TODO: Should reveal the correct signature + reveal_type(c) # revealed: (...) -> int ``` And, as one of the parameter types: ```py def _(c: Callable[[Concatenate[int, str, ...], int], int]): - reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int + # TODO: Should reveal the correct signature + reveal_type(c) # revealed: (...) -> int ``` ## Using `typing.ParamSpec` @@ -276,7 +278,8 @@ from typing_extensions import Callable, TypeVarTuple Ts = TypeVarTuple("Ts") def _(c: Callable[[int, *Ts], int]): - reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int + # TODO: Should reveal the correct signature + reveal_type(c) # revealed: (...) -> int ``` And, using the legacy syntax using `Unpack`: @@ -285,7 +288,8 @@ And, using the legacy syntax using `Unpack`: from typing_extensions import Unpack def _(c: Callable[[int, Unpack[Ts]], int]): - reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int + # TODO: Should reveal the correct signature + reveal_type(c) # revealed: (...) -> int ``` ## Member lookup diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 4fcd669be2ad61..811965cde91041 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -895,7 +895,7 @@ impl<'db> Parameters<'db> { Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(todo_type!("todo signature **kwargs")), ], - is_gradual: false, + is_gradual: true, } } From 9a8f3cf247987ae70794a1a3ddd5c777c4cdbe20 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 25 Apr 2025 11:42:00 -0400 Subject: [PATCH 0159/1161] red_knot_python_semantic: improve `not-iterable` diagnostic This cleans up one particular TODO by splitting the "because" part of the `not-iterable` diagnostic out into an info sub-diagnostic. --- .../resources/mdtest/loops/for.md | 10 +- ..._For_loops_-_Bad_`__getitem__`_method.snap | 4 +- ...for.md_-_For_loops_-_Invalid_iterable.snap | 3 +- ...New_over_old_style_iteration_protocol.snap | 3 +- ...hod_and_`__getitem__`_is_not_callable.snap | 3 +- ...bly-not-callable_`__getitem__`_method.snap | 8 +- ...ossibly_invalid_`__getitem__`_methods.snap | 8 +- ...-_Possibly_invalid_`__iter__`_methods.snap | 8 +- ..._-_Possibly_invalid_`__next__`_method.snap | 7 +- ..._iter__`_and_bad_`__getitem__`_method.snap | 4 +- ..._`_and_possibly_invalid_`__getitem__`.snap | 7 +- ..._`_and_possibly_unbound_`__getitem__`.snap | 3 +- ...element_has_invalid_`__iter__`_method.snap | 3 +- ...nion_element_has_no_`__iter__`_method.snap | 3 +- ...or_loops_-_With_non-callable_iterator.snap | 3 +- ...__iter__`_does_not_return_an_iterator.snap | 3 +- ...__iter__`_method_with_a_bad_signature.snap | 4 +- ...tor_with_an_invalid_`__next__`_method.snap | 7 +- ...acking_-_Right_hand_side_not_iterable.snap | 3 +- crates/red_knot_python_semantic/src/types.rs | 423 ++++++++++-------- 20 files changed, 308 insertions(+), 209 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md b/crates/red_knot_python_semantic/resources/mdtest/loops/for.md index 8fec8a52eed2a0..85f2390aa8919e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/for.md @@ -286,7 +286,7 @@ class Test: def __iter__(self) -> TestIter | int: return TestIter() -# error: [not-iterable] "Object of type `Test` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method" +# error: [not-iterable] "Object of type `Test` may not be iterable" for x in Test(): reveal_type(x) # revealed: int ``` @@ -316,12 +316,12 @@ def _(flag: bool): else: __iter__: None = None - # error: [not-iterable] "Object of type `Iterable1` may not be iterable because its `__iter__` attribute (with type `CustomCallable`) may not be callable" + # error: [not-iterable] "Object of type `Iterable1` may not be iterable" for x in Iterable1(): # TODO... `int` might be ideal here? reveal_type(x) # revealed: int | Unknown - # error: [not-iterable] "Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable" + # error: [not-iterable] "Object of type `Iterable2` may not be iterable" for y in Iterable2(): # TODO... `int` might be ideal here? reveal_type(y) # revealed: int | Unknown @@ -376,7 +376,7 @@ def _(flag: bool): def __iter__(self) -> Iterator: return Iterator() - # error: [not-iterable] "Object of type `Iterable` may not be iterable because its `__iter__` method returns an object of type `Iterator`, which may not have a `__next__` method" + # error: [not-iterable] "Object of type `Iterable` may not be iterable" for x in Iterable(): reveal_type(x) # revealed: int ``` @@ -461,7 +461,7 @@ def _(flag: bool): return Iterator() __getitem__: None = None - # error: [not-iterable] "Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable" + # error: [not-iterable] "Object of type `Iterable` may not be iterable" for x in Iterable(): reveal_type(x) # revealed: int ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap index 7c5331746e0d16..f5e2c0ca98f386 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap @@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) +error: lint:not-iterable: Object of type `Iterable` is not iterable --> /src/mdtest_snippet.py:10:10 | 9 | # error: [not-iterable] @@ -36,6 +36,8 @@ error: lint:not-iterable: Object of type `Iterable` is not iterable because it h | ^^^^^^^^^^ 11 | reveal_type(x) # revealed: int | +info: It has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol +info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap index 29541bbdb2e429..967995693eeda7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap @@ -20,7 +20,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method +error: lint:not-iterable: Object of type `Literal[123]` is not iterable --> /src/mdtest_snippet.py:2:10 | 1 | nonsense = 123 @@ -28,5 +28,6 @@ error: lint:not-iterable: Object of type `Literal[123]` is not iterable because | ^^^^^^^^ 3 | pass | +info: It doesn't have an `__iter__` method or a `__getitem__` method ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap index 9a6a7075309489..c193a16c5744d0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap @@ -24,7 +24,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable +error: lint:not-iterable: Object of type `NotIterable` is not iterable --> /src/mdtest_snippet.py:6:10 | 4 | __iter__: None = None @@ -33,5 +33,6 @@ error: lint:not-iterable: Object of type `NotIterable` is not iterable because i | ^^^^^^^^^^^^^ 7 | pass | +info: Its `__iter__` attribute has type `None`, which is not callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap index 69aad44e938b56..8f06a8baf60cbf 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap @@ -25,7 +25,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable +error: lint:not-iterable: Object of type `Bad` is not iterable --> /src/mdtest_snippet.py:7:10 | 6 | # error: [not-iterable] @@ -33,6 +33,7 @@ error: lint:not-iterable: Object of type `Bad` is not iterable because it has no | ^^^^^ 8 | reveal_type(x) # revealed: Unknown | +info: It has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap index aef7309c4d5496..85fb92f9112845 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap @@ -46,7 +46,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable +error: lint:not-iterable: Object of type `Iterable1` may not be iterable --> /src/mdtest_snippet.py:22:14 | 21 | # error: [not-iterable] @@ -55,6 +55,8 @@ error: lint:not-iterable: Object of type `Iterable1` may not be iterable because 23 | # TODO... `int` might be ideal here? 24 | reveal_type(x) # revealed: int | Unknown | +info: It has no `__iter__` method and its `__getitem__` attribute is invalid +info: `__getitem__` has type `CustomCallable`, which is not callable ``` @@ -73,7 +75,7 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable +error: lint:not-iterable: Object of type `Iterable2` may not be iterable --> /src/mdtest_snippet.py:27:14 | 26 | # error: [not-iterable] @@ -82,6 +84,8 @@ error: lint:not-iterable: Object of type `Iterable2` may not be iterable because 28 | # TODO... `int` might be ideal here? 29 | reveal_type(y) # revealed: int | Unknown | +info: It has no `__iter__` method and its `__getitem__` attribute is invalid +info: `__getitem__` has type `(bound method Iterable2.__getitem__(key: int) -> int) | None`, which is not callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap index 173adfdc413cb8..4167d3bdfbcba5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap @@ -43,7 +43,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable +error: lint:not-iterable: Object of type `Iterable1` may not be iterable --> /src/mdtest_snippet.py:20:14 | 19 | # error: [not-iterable] @@ -52,6 +52,8 @@ error: lint:not-iterable: Object of type `Iterable1` may not be iterable because 21 | # TODO: `str` might be better 22 | reveal_type(x) # revealed: str | Unknown | +info: It has no `__iter__` method and its `__getitem__` attribute is invalid +info: `__getitem__` has type `(bound method Iterable1.__getitem__(item: int) -> str) | None`, which is not callable ``` @@ -70,7 +72,7 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) +error: lint:not-iterable: Object of type `Iterable2` may not be iterable --> /src/mdtest_snippet.py:25:14 | 24 | # error: [not-iterable] @@ -78,6 +80,8 @@ error: lint:not-iterable: Object of type `Iterable2` may not be iterable because | ^^^^^^^^^^^ 26 | reveal_type(y) # revealed: str | int | +info: It has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol +info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap index 67b34560f8d337..c8e35feca0c847 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap @@ -47,7 +47,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`) +error: lint:not-iterable: Object of type `Iterable1` may not be iterable --> /src/mdtest_snippet.py:17:14 | 16 | # error: [not-iterable] @@ -55,6 +55,9 @@ error: lint:not-iterable: Object of type `Iterable1` may not be iterable because | ^^^^^^^^^^^ 18 | reveal_type(x) # revealed: int | +info: Its `__iter__` method may have an invalid signature +info: Type of `__iter__` is `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)` +info: Expected signature for `__iter__` is `def __iter__(self): ...` ``` @@ -73,7 +76,7 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable: Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable +error: lint:not-iterable: Object of type `Iterable2` may not be iterable --> /src/mdtest_snippet.py:28:14 | 27 | # error: [not-iterable] @@ -82,6 +85,7 @@ error: lint:not-iterable: Object of type `Iterable2` may not be iterable because 29 | # TODO: `int` would probably be better here: 30 | reveal_type(x) # revealed: int | Unknown | +info: Its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap index 7e56bce301f480..9cdce7cafe8d34 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap @@ -51,7 +51,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`) +error: lint:not-iterable: Object of type `Iterable1` may not be iterable --> /src/mdtest_snippet.py:28:14 | 27 | # error: [not-iterable] @@ -59,6 +59,8 @@ error: lint:not-iterable: Object of type `Iterable1` may not be iterable because | ^^^^^^^^^^^ 29 | reveal_type(x) # revealed: int | str | +info: Its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method +info: Expected signature for `__next__` is `def __next__(self): ...`) ``` @@ -77,7 +79,7 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable: Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable +error: lint:not-iterable: Object of type `Iterable2` may not be iterable --> /src/mdtest_snippet.py:32:14 | 31 | # error: [not-iterable] @@ -86,6 +88,7 @@ error: lint:not-iterable: Object of type `Iterable2` may not be iterable because 33 | # TODO: `int` would probably be better here: 34 | reveal_type(y) # revealed: int | Unknown | +info: Its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap index 4aa08e1173eda5..2f655a4590c550 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap @@ -36,7 +36,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) +error: lint:not-iterable: Object of type `Iterable` may not be iterable --> /src/mdtest_snippet.py:18:14 | 17 | # error: [not-iterable] @@ -44,6 +44,8 @@ error: lint:not-iterable: Object of type `Iterable` may not be iterable because | ^^^^^^^^^^ 19 | reveal_type(x) # revealed: int | bytes | +info: It may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol +info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap index 0bb17a5837b200..ef3a31a2d3fa5b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap @@ -54,7 +54,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable +error: lint:not-iterable: Object of type `Iterable1` may not be iterable --> /src/mdtest_snippet.py:31:14 | 30 | # error: [not-iterable] @@ -63,6 +63,7 @@ error: lint:not-iterable: Object of type `Iterable1` may not be iterable because 32 | # TODO: `bytes | str` might be better 33 | reveal_type(x) # revealed: bytes | str | Unknown | +info: It may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable ``` @@ -81,7 +82,7 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`) +error: lint:not-iterable: Object of type `Iterable2` may not be iterable --> /src/mdtest_snippet.py:36:14 | 35 | # error: [not-iterable] @@ -89,6 +90,8 @@ error: lint:not-iterable: Object of type `Iterable2` may not be iterable because | ^^^^^^^^^^^ 37 | reveal_type(y) # revealed: bytes | str | int | +info: It may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol +info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap index d55e7203a2a935..213e15d36dd08f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap @@ -35,7 +35,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method +error: lint:not-iterable: Object of type `Iterable` may not be iterable --> /src/mdtest_snippet.py:17:14 | 16 | # error: [not-iterable] @@ -43,6 +43,7 @@ error: lint:not-iterable: Object of type `Iterable` may not be iterable because | ^^^^^^^^^^ 18 | reveal_type(x) # revealed: int | bytes | +info: It may not have an `__iter__` method or a `__getitem__` method ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap index 1e708f0e989524..fd5882fd0f77d7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap @@ -36,7 +36,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method +error: lint:not-iterable: Object of type `Test | Test2` may not be iterable --> /src/mdtest_snippet.py:18:14 | 16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989) @@ -45,6 +45,7 @@ error: lint:not-iterable: Object of type `Test | Test2` may not be iterable beca | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 19 | reveal_type(x) # revealed: int | +info: Its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap index 62fb5bc8e8edf7..36c902d70e630d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap @@ -31,7 +31,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method +error: lint:not-iterable: Object of type `Test | Literal[42]` may not be iterable --> /src/mdtest_snippet.py:13:14 | 11 | def _(flag: bool): @@ -40,6 +40,7 @@ error: lint:not-iterable: Object of type `Test | Literal[42]` may not be iterabl | ^^^^^^^^^^^^^^^^^^^^^^ 14 | reveal_type(x) # revealed: int | +info: It may not have an `__iter__` method and it doesn't have a `__getitem__` method ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap index bd2bfcd156f256..35de9761e3279d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap @@ -33,7 +33,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable +error: lint:not-iterable: Object of type `NotIterable` is not iterable --> /src/mdtest_snippet.py:11:14 | 10 | # error: [not-iterable] @@ -41,6 +41,7 @@ error: lint:not-iterable: Object of type `NotIterable` is not iterable because i | ^^^^^^^^^^^^^ 12 | pass | +info: Its `__iter__` attribute has type `int | None`, which is not callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap index 5f66eb75e7daba..6b6c0fb0fccf69 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap @@ -26,7 +26,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method +error: lint:not-iterable: Object of type `Bad` is not iterable --> /src/mdtest_snippet.py:8:10 | 7 | # error: [not-iterable] @@ -34,6 +34,7 @@ error: lint:not-iterable: Object of type `Bad` is not iterable because its `__it | ^^^^^ 9 | reveal_type(x) # revealed: Unknown | +info: Its `__iter__` method returns an object of type `int`, which has no `__next__` method ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap index 62bcf1e730c481..534878678adf9b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap @@ -30,7 +30,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`) +error: lint:not-iterable: Object of type `Iterable` is not iterable --> /src/mdtest_snippet.py:12:10 | 11 | # error: [not-iterable] @@ -38,6 +38,8 @@ error: lint:not-iterable: Object of type `Iterable` is not iterable because its | ^^^^^^^^^^ 13 | reveal_type(x) # revealed: int | +info: Its `__iter__` method has an invalid signature +info: Expected signature `def __iter__(self): ...` ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap index 1d4e8e2eeda1e0..166d7b3ec87996 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap @@ -41,7 +41,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md # Diagnostics ``` -error: lint:not-iterable: Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`) +error: lint:not-iterable: Object of type `Iterable1` is not iterable --> /src/mdtest_snippet.py:19:10 | 18 | # error: [not-iterable] @@ -49,6 +49,8 @@ error: lint:not-iterable: Object of type `Iterable1` is not iterable because its | ^^^^^^^^^^^ 20 | reveal_type(x) # revealed: int | +info: Its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method +info: Expected signature for `__next__` is `def __next__(self): ...` ``` @@ -67,7 +69,7 @@ info: revealed-type: Revealed type ``` ``` -error: lint:not-iterable: Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable +error: lint:not-iterable: Object of type `Iterable2` is not iterable --> /src/mdtest_snippet.py:23:10 | 22 | # error: [not-iterable] @@ -75,6 +77,7 @@ error: lint:not-iterable: Object of type `Iterable2` is not iterable because its | ^^^^^^^^^^^ 24 | reveal_type(y) # revealed: Unknown | +info: Its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap index 026dd73c54b3e1..68a533a0b324d7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap @@ -18,11 +18,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack # Diagnostics ``` -error: lint:not-iterable: Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method +error: lint:not-iterable: Object of type `Literal[1]` is not iterable --> /src/mdtest_snippet.py:1:8 | 1 | a, b = 1 # error: [not-iterable] | ^ | +info: It doesn't have an `__iter__` method or a `__getitem__` method ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 9a86e1a6413d8e..1c2e15a3cd9aef 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -42,6 +42,7 @@ use crate::symbol::{ }; use crate::types::call::{Bindings, CallArgumentTypes, CallableBinding}; pub(crate) use crate::types::class_base::ClassBase; +use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; use crate::types::generics::{GenericContext, Specialization}; use crate::types::infer::infer_unpack_types; @@ -5505,219 +5506,281 @@ impl<'db> IterationError<'db> { iterable_type: Type<'db>, iterable_node: ast::AnyNodeRef, ) { + /// A little helper type for emitting a diagnostic + /// based on the variant of iteration error. + struct Reporter<'a> { + db: &'a dyn Db, + builder: LintDiagnosticGuardBuilder<'a, 'a>, + iterable_type: Type<'a>, + } + + impl<'a> Reporter<'a> { + /// Emit a diagnostic that is certain that `iterable_type` is not iterable. + /// + /// `because` should explain why `iterable_type` is not iterable. + #[allow(clippy::wrong_self_convention)] + fn is_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { + let mut diag = self.builder.into_diagnostic(format_args!( + "Object of type `{iterable_type}` is not iterable", + iterable_type = self.iterable_type.display(self.db), + )); + diag.info(because); + diag + } + + /// Emit a diagnostic that is uncertain that `iterable_type` is not iterable. + /// + /// `because` should explain why `iterable_type` is likely not iterable. + fn may_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { + let mut diag = self.builder.into_diagnostic(format_args!( + "Object of type `{iterable_type}` may not be iterable", + iterable_type = self.iterable_type.display(self.db), + )); + diag.info(because); + diag + } + } + let Some(builder) = context.report_lint(&NOT_ITERABLE, iterable_node) else { return; }; let db = context.db(); - - let report_not_iterable = |arguments: std::fmt::Arguments| { - builder.into_diagnostic(arguments); + let reporter = Reporter { + db, + builder, + iterable_type, }; // TODO: for all of these error variants, the "explanation" for the diagnostic // (everything after the "because") should really be presented as a "help:", "note", // or similar, rather than as part of the same sentence as the error message. match self { - Self::IterCallError(CallErrorKind::NotCallable, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because its `__iter__` attribute has type `{dunder_iter_type}`, \ - which is not callable", - iterable_type = iterable_type.display(db), - dunder_iter_type = bindings.callable_type().display(db), - )), - Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings) if bindings.is_single() => { - report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` attribute (with type `{dunder_iter_type}`) \ - may not be callable", - iterable_type = iterable_type.display(db), + Self::IterCallError(CallErrorKind::NotCallable, bindings) => { + reporter.is_not(format_args!( + "Its `__iter__` attribute has type `{dunder_iter_type}`, which is not callable", + dunder_iter_type = bindings.callable_type().display(db), + )); + } + Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings) + if bindings.is_single() => + { + reporter.may_not(format_args!( + "Its `__iter__` attribute (with type `{dunder_iter_type}`) \ + may not be callable", dunder_iter_type = bindings.callable_type().display(db), )); } Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings) => { - report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` attribute (with type `{dunder_iter_type}`) \ - may not be callable", - iterable_type = iterable_type.display(db), + reporter.may_not(format_args!( + "Its `__iter__` attribute (with type `{dunder_iter_type}`) \ + may not be callable", dunder_iter_type = bindings.callable_type().display(db), )); } - Self::IterCallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because its `__iter__` method has an invalid signature \ - (expected `def __iter__(self): ...`)", - iterable_type = iterable_type.display(db), - )), - Self::IterCallError(CallErrorKind::BindingError, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` method (with type `{dunder_iter_type}`) \ - may have an invalid signature (expected `def __iter__(self): ...`)", - iterable_type = iterable_type.display(db), - dunder_iter_type = bindings.callable_type().display(db), - )), + Self::IterCallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => { + reporter + .is_not("Its `__iter__` method has an invalid signature") + .info("Expected signature `def __iter__(self): ...`"); + } + Self::IterCallError(CallErrorKind::BindingError, bindings) => { + let mut diag = + reporter.may_not("Its `__iter__` method may have an invalid signature"); + diag.info(format_args!( + "Type of `__iter__` is `{dunder_iter_type}`", + dunder_iter_type = bindings.callable_type().display(db), + )); + diag.info("Expected signature for `__iter__` is `def __iter__(self): ...`"); + } Self::IterReturnsInvalidIterator { iterator, - dunder_next_error + dunder_next_error, } => match dunder_next_error { - CallDunderError::MethodNotAvailable => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which has no `__next__` method", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - CallDunderError::PossiblyUnbound(_) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which may not have a `__next__` method", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - CallDunderError::CallError(CallErrorKind::NotCallable, _) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which has a `__next__` attribute that is not callable", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which has a `__next__` attribute that may not be callable", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - CallDunderError::CallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which has an invalid `__next__` method (expected `def __next__(self): ...`)", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - CallDunderError::CallError(CallErrorKind::BindingError, _) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because its `__iter__` method returns an object of type `{iterator_type}`, \ - which may have an invalid `__next__` method (expected `def __next__(self): ...`)", - iterable_type = iterable_type.display(db), - iterator_type = iterator.display(db), - )), - } + CallDunderError::MethodNotAvailable => { + reporter.is_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which has no `__next__` method", + iterator_type = iterator.display(db), + )); + } + CallDunderError::PossiblyUnbound(_) => { + reporter.may_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which may not have a `__next__` method", + iterator_type = iterator.display(db), + )); + } + CallDunderError::CallError(CallErrorKind::NotCallable, _) => { + reporter.is_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which has a `__next__` attribute that is not callable", + iterator_type = iterator.display(db), + )); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => { + reporter.may_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which has a `__next__` attribute that may not be callable", + iterator_type = iterator.display(db), + )); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) + if bindings.is_single() => + { + reporter + .is_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which has an invalid `__next__` method", + iterator_type = iterator.display(db), + )) + .info("Expected signature for `__next__` is `def __next__(self): ...`"); + } + CallDunderError::CallError(CallErrorKind::BindingError, _) => { + reporter + .may_not(format_args!( + "Its `__iter__` method returns an object of type `{iterator_type}`, \ + which may have an invalid `__next__` method", + iterator_type = iterator.display(db), + )) + .info("Expected signature for `__next__` is `def __next__(self): ...`)"); + } + }, Self::PossiblyUnboundIterAndGetitemError { - dunder_getitem_error, .. + dunder_getitem_error, + .. } => match dunder_getitem_error { - CallDunderError::MethodNotAvailable => report_not_iterable(format_args!( - "Object of type `{}` may not be iterable \ - because it may not have an `__iter__` method \ - and it doesn't have a `__getitem__` method", - iterable_type.display(db) - )), - CallDunderError::PossiblyUnbound(_) => report_not_iterable(format_args!( - "Object of type `{}` may not be iterable \ - because it may not have an `__iter__` method or a `__getitem__` method", - iterable_type.display(db) - )), - CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it may not have an `__iter__` method \ - and its `__getitem__` attribute has type `{dunder_getitem_type}`, \ - which is not callable", - iterable_type = iterable_type.display(db), - dunder_getitem_type = bindings.callable_type().display(db), - )), - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it may not have an `__iter__` method \ - and its `__getitem__` attribute may not be callable", - iterable_type = iterable_type.display(db), - )), + CallDunderError::MethodNotAvailable => { + reporter.may_not( + "It may not have an `__iter__` method \ + and it doesn't have a `__getitem__` method", + ); + } + CallDunderError::PossiblyUnbound(_) => { + reporter + .may_not("It may not have an `__iter__` method or a `__getitem__` method"); + } + CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => { + reporter.may_not(format_args!( + "It may not have an `__iter__` method \ + and its `__getitem__` attribute has type `{dunder_getitem_type}`, \ + which is not callable", + dunder_getitem_type = bindings.callable_type().display(db), + )); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) + if bindings.is_single() => + { + reporter.may_not( + "It may not have an `__iter__` method \ + and its `__getitem__` attribute may not be callable", + ); + } CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) => { - report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it may not have an `__iter__` method \ - and its `__getitem__` attribute (with type `{dunder_getitem_type}`) \ - may not be callable", - iterable_type = iterable_type.display(db), + reporter.may_not(format_args!( + "It may not have an `__iter__` method \ + and its `__getitem__` attribute (with type `{dunder_getitem_type}`) \ + may not be callable", dunder_getitem_type = bindings.callable_type().display(db), )); } - CallDunderError::CallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it may not have an `__iter__` method \ - and its `__getitem__` method has an incorrect signature \ - for the old-style iteration protocol \ - (expected a signature at least as permissive as \ - `def __getitem__(self, key: int): ...`)", - iterable_type = iterable_type.display(db), - )), - CallDunderError::CallError(CallErrorKind::BindingError, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it may not have an `__iter__` method \ - and its `__getitem__` method (with type `{dunder_getitem_type}`) \ - may have an incorrect signature for the old-style iteration protocol \ - (expected a signature at least as permissive as \ - `def __getitem__(self, key: int): ...`)", - iterable_type = iterable_type.display(db), - dunder_getitem_type = bindings.callable_type().display(db), - )), - } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) + if bindings.is_single() => + { + reporter + .may_not( + "It may not have an `__iter__` method \ + and its `__getitem__` method has an incorrect signature \ + for the old-style iteration protocol", + ) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) => { + reporter + .may_not(format_args!( + "It may not have an `__iter__` method \ + and its `__getitem__` method (with type `{dunder_getitem_type}`) \ + may have an incorrect signature for the old-style iteration protocol", + dunder_getitem_type = bindings.callable_type().display(db), + )) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + }, - Self::UnboundIterAndGetitemError { dunder_getitem_error } => match dunder_getitem_error { - CallDunderError::MethodNotAvailable => report_not_iterable(format_args!( - "Object of type `{}` is not iterable because it doesn't have \ - an `__iter__` method or a `__getitem__` method", - iterable_type.display(db) - )), - CallDunderError::PossiblyUnbound(_) => report_not_iterable(format_args!( - "Object of type `{}` may not be iterable because it has no `__iter__` method \ - and it may not have a `__getitem__` method", - iterable_type.display(db) - )), - CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because it has no `__iter__` method and \ - its `__getitem__` attribute has type `{dunder_getitem_type}`, \ - which is not callable", - iterable_type = iterable_type.display(db), - dunder_getitem_type = bindings.callable_type().display(db), - )), - CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it has no `__iter__` method and its `__getitem__` attribute \ - may not be callable", - iterable_type = iterable_type.display(db), - )), + Self::UnboundIterAndGetitemError { + dunder_getitem_error, + } => match dunder_getitem_error { + CallDunderError::MethodNotAvailable => { + reporter + .is_not("It doesn't have an `__iter__` method or a `__getitem__` method"); + } + CallDunderError::PossiblyUnbound(_) => { + reporter.is_not( + "It has no `__iter__` method and it may not have a `__getitem__` method", + ); + } + CallDunderError::CallError(CallErrorKind::NotCallable, bindings) => { + reporter.is_not(format_args!( + "It has no `__iter__` method and \ + its `__getitem__` attribute has type `{dunder_getitem_type}`, \ + which is not callable", + dunder_getitem_type = bindings.callable_type().display(db), + )); + } + CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) + if bindings.is_single() => + { + reporter.may_not( + "It has no `__iter__` method and its `__getitem__` attribute \ + may not be callable", + ); + } CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings) => { - report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it has no `__iter__` method and its `__getitem__` attribute \ - (with type `{dunder_getitem_type}`) may not be callable", - iterable_type = iterable_type.display(db), + reporter.may_not( + "It has no `__iter__` method and its `__getitem__` attribute is invalid", + ).info(format_args!( + "`__getitem__` has type `{dunder_getitem_type}`, which is not callable", dunder_getitem_type = bindings.callable_type().display(db), )); } - CallDunderError::CallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => report_not_iterable(format_args!( - "Object of type `{iterable_type}` is not iterable \ - because it has no `__iter__` method and \ - its `__getitem__` method has an incorrect signature \ - for the old-style iteration protocol \ - (expected a signature at least as permissive as \ - `def __getitem__(self, key: int): ...`)", - iterable_type = iterable_type.display(db), - )), - CallDunderError::CallError(CallErrorKind::BindingError, bindings) => report_not_iterable(format_args!( - "Object of type `{iterable_type}` may not be iterable \ - because it has no `__iter__` method and \ - its `__getitem__` method (with type `{dunder_getitem_type}`) \ - may have an incorrect signature for the old-style iteration protocol \ - (expected a signature at least as permissive as \ - `def __getitem__(self, key: int): ...`)", - iterable_type = iterable_type.display(db), - dunder_getitem_type = bindings.callable_type().display(db), - )), - } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) + if bindings.is_single() => + { + reporter + .is_not( + "It has no `__iter__` method and \ + its `__getitem__` method has an incorrect signature \ + for the old-style iteration protocol", + ) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + CallDunderError::CallError(CallErrorKind::BindingError, bindings) => { + reporter + .may_not(format_args!( + "It has no `__iter__` method and \ + its `__getitem__` method (with type `{dunder_getitem_type}`) \ + may have an incorrect signature for the old-style iteration protocol", + dunder_getitem_type = bindings.callable_type().display(db), + )) + .info( + "`__getitem__` must be at least as permissive as \ + `def __getitem__(self, key: int): ...` \ + to satisfy the old-style iteration protocol", + ); + } + }, } } } From 80103a179d389ed1b64f8605c08f0ae5509ccf3a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 28 Apr 2025 17:13:07 +0100 Subject: [PATCH 0160/1161] Bump mypy_primer pin (#17685) --- .github/workflows/mypy_primer.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index acf975c30a8452..381de8e386c6e8 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -41,13 +41,10 @@ jobs: - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 with: workspaces: "ruff" + - name: Install Rust toolchain run: rustup show - - name: Install mypy_primer - run: | - uv tool install "git+https://github.com/hauntsaninja/mypy_primer@4c22d192a456e27badf85b3ea0f830707375d2b7" - - name: Run mypy_primer shell: bash run: | @@ -67,7 +64,9 @@ jobs: echo "Project selector: $PRIMER_SELECTOR" # Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs - uvx mypy_primer \ + uvx \ + --from="git+https://github.com/hauntsaninja/mypy_primer@b83b9eade0b7ed2f4b9b129b163acac1ecb48f71" \ + mypy_primer \ --repo ruff \ --type-checker knot \ --old base_commit \ From 405878a128b85ff88bb72228944a057194040193 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 28 Apr 2025 13:02:12 -0400 Subject: [PATCH 0161/1161] ruff_db: render file paths in diagnostics as relative paths if possible This is done in what appears to be the same way as Ruff: we get the CWD, strip the prefix from the path if possible, and use that. If stripping the prefix fails, then we print the full path as-is. Fixes #17233 --- crates/red_knot/tests/cli.rs | 108 ++++++++----- crates/red_knot_ide/src/goto.rs | 70 ++++---- crates/red_knot_ide/src/hover.rs | 32 ++-- ..._-_Invalid_`__set__`_method_signature.snap | 2 +- ...a_descriptors_-_Invalid_argument_type.snap | 2 +- ..._attributes_with_class-level_defaults.snap | 4 +- ...ignment_-_Possibly-unbound_attributes.snap | 4 +- ...assignment_-_Pure_instance_attributes.snap | 4 +- ...t_-_Setting_attributes_on_union_types.snap | 2 +- ...ibute_assignment_-_Unknown_attributes.snap | 4 +- ..._-_Attribute_assignment_-_`ClassVar`s.snap | 4 +- ...ructures_-_Unresolvable_module_import.snap | 2 +- ...ures_-_Unresolvable_submodule_imports.snap | 4 +- ..._For_loops_-_Bad_`__getitem__`_method.snap | 4 +- ...for.md_-_For_loops_-_Invalid_iterable.snap | 2 +- ...New_over_old_style_iteration_protocol.snap | 2 +- ...hod_and_`__getitem__`_is_not_callable.snap | 4 +- ...bly-not-callable_`__getitem__`_method.snap | 8 +- ...ossibly_invalid_`__getitem__`_methods.snap | 8 +- ...-_Possibly_invalid_`__iter__`_methods.snap | 8 +- ..._-_Possibly_invalid_`__next__`_method.snap | 8 +- ..._iter__`_and_bad_`__getitem__`_method.snap | 4 +- ..._`_and_possibly_invalid_`__getitem__`.snap | 8 +- ..._`_and_possibly_unbound_`__getitem__`.snap | 4 +- ...element_has_invalid_`__iter__`_method.snap | 4 +- ...nion_element_has_no_`__iter__`_method.snap | 4 +- ...or_loops_-_With_non-callable_iterator.snap | 6 +- ...__iter__`_does_not_return_an_iterator.snap | 4 +- ...__iter__`_method_with_a_bad_signature.snap | 4 +- ...tor_with_an_invalid_`__next__`_method.snap | 8 +- ...types_with_invalid_`__bool__`_methods.snap | 2 +- ...lid_argument_type_diagnostics_-_Basic.snap | 4 +- ...t_type_diagnostics_-_Calls_to_methods.snap | 4 +- ...nt_type_diagnostics_-_Different_files.snap | 4 +- ..._diagnostics_-_Different_source_order.snap | 4 +- ...nt_type_diagnostics_-_Many_parameters.snap | 4 +- ...Many_parameters_across_multiple_lines.snap | 4 +- ...eters_with_multiple_invalid_arguments.snap | 12 +- ...hose_type_is_vendored_from_`typeshed`.snap | 2 +- ...gument_types_-_Keyword_only_arguments.snap | 4 +- ..._of_argument_types_-_Mix_of_arguments.snap | 4 +- ...argument_types_-_One_keyword_argument.snap | 4 +- ...y_of_argument_types_-_Only_positional.snap | 4 +- ..._argument_types_-_Synthetic_arguments.snap | 4 +- ...f_argument_types_-_Variadic_arguments.snap | 4 +- ...nt_types_-_Variadic_keyword_arguments.snap | 4 +- ...oesn't_implement_`__bool__`_correctly.snap | 4 +- ...stics_-_Calls_to_overloaded_functions.snap | 2 +- ...hat_implements_`__bool__`_incorrectly.snap | 2 +- ...Protocols_-_Calls_to_protocol_classes.snap | 14 +- ...lid_calls_to_`get_protocol_members()`.snap | 8 +- ..._-_Protocols_-_Narrowing_of_protocols.snap | 28 ++-- ...ype_-_Invalid_conditional_return_type.snap | 8 +- ...n_type_-_Invalid_implicit_return_type.snap | 8 +- ...ion_return_type_-_Invalid_return_type.snap | 6 +- ...pe_-_Invalid_return_type_in_stub_file.snap | 6 +- ..._don't_implement_`__bool__`_correctly.snap | 4 +- ...chronous_comprehensions_-_Python_3.10.snap | 2 +- ..._Shadowing_-_Implicit_class_shadowing.snap | 2 +- ...adowing_-_Implicit_function_shadowing.snap | 2 +- ...that_incorrectly_implement_`__bool__`.snap | 2 +- ...that_incorrectly_implement_`__bool__`.snap | 2 +- ...ng_-_Exactly_too_few_values_to_unpack.snap | 2 +- ...g_-_Exactly_too_many_values_to_unpack.snap | 2 +- ...acking_-_Right_hand_side_not_iterable.snap | 2 +- ..._Unpacking_-_Too_few_values_to_unpack.snap | 2 +- ...vable_import_that_does_not_use_`from`.snap | 2 +- ...solvable_module_but_unresolvable_item.snap | 2 +- ...`from`_with_an_unknown_current_module.snap | 2 +- ..._`from`_with_an_unknown_nested_module.snap | 2 +- ...ng_`from`_with_an_unresolvable_module.snap | 2 +- ...ing_`from`_with_too_many_leading_dots.snap | 2 +- ...l__`_attribute,_but_it's_not_callable.snap | 2 +- ...hod,_but_has_an_incorrect_return_type.snap | 4 +- ..._method,_but_has_incorrect_parameters.snap | 4 +- ...ember_has_incorrect_`__bool__`_method.snap | 2 +- ...ics_-_`match`_statement_-_Before_3.10.snap | 2 +- crates/ruff_db/src/diagnostic/render.rs | 150 ++++++++++-------- 78 files changed, 359 insertions(+), 325 deletions(-) diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index 7a9283e4fc7359..4364b8f4973cdd 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -5,6 +5,27 @@ use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::TempDir; +#[test] +fn test_run_in_sub_directory() -> anyhow::Result<()> { + let case = TestCase::with_files([("test.py", "~"), ("subdir/nothing", "")])?; + assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r" + success: false + exit_code: 1 + ----- stdout ----- + error: invalid-syntax + --> /test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + "); + Ok(()) +} + #[test] fn test_include_hidden_files_by_default() -> anyhow::Result<()> { let case = TestCase::with_files([(".test.py", "~")])?; @@ -13,7 +34,7 @@ fn test_include_hidden_files_by_default() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: invalid-syntax - --> /.test.py:1:2 + --> .test.py:1:2 | 1 | ~ | ^ Expected an expression @@ -25,6 +46,7 @@ fn test_include_hidden_files_by_default() -> anyhow::Result<()> { "); Ok(()) } + #[test] fn test_respect_ignore_files() -> anyhow::Result<()> { // First test that the default option works correctly (the file is skipped) @@ -45,7 +67,7 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: invalid-syntax - --> /test.py:1:2 + --> test.py:1:2 | 1 | ~ | ^ Expected an expression @@ -63,7 +85,7 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: invalid-syntax - --> /test.py:1:2 + --> test.py:1:2 | 1 | ~ | ^ Expected an expression @@ -81,7 +103,7 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: invalid-syntax - --> /test.py:1:2 + --> test.py:1:2 | 1 | ~ | ^ Expected an expression @@ -121,7 +143,7 @@ fn config_override_python_version() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: lint:unresolved-attribute: Type `` has no attribute `last_exc` - --> /test.py:5:7 + --> test.py:5:7 | 4 | # Access `sys.last_exc` that was only added in Python 3.12 5 | print(sys.last_exc) @@ -172,7 +194,7 @@ fn config_override_python_platform() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- info: revealed-type: Revealed type - --> /test.py:5:1 + --> test.py:5:1 | 3 | from typing_extensions import reveal_type 4 | @@ -190,7 +212,7 @@ fn config_override_python_platform() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- info: revealed-type: Revealed type - --> /test.py:5:1 + --> test.py:5:1 | 3 | from typing_extensions import reveal_type 4 | @@ -254,7 +276,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: lint:unresolved-import: Cannot resolve import `utils` - --> /child/test.py:2:6 + --> test.py:2:6 | 2 | from utils import add | ^^^^^ @@ -354,7 +376,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero - --> /test.py:2:5 + --> test.py:2:5 | 2 | y = 4 / 0 | ^^^^^ @@ -363,7 +385,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> { | warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined - --> /test.py:7:7 + --> test.py:7:7 | 5 | x = a 6 | @@ -390,7 +412,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero - --> /test.py:2:5 + --> test.py:2:5 | 2 | y = 4 / 0 | ^^^^^ @@ -430,7 +452,7 @@ fn cli_rule_severity() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: lint:unresolved-import: Cannot resolve import `does_not_exit` - --> /test.py:2:8 + --> test.py:2:8 | 2 | import does_not_exit | ^^^^^^^^^^^^^ @@ -439,7 +461,7 @@ fn cli_rule_severity() -> anyhow::Result<()> { | error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero - --> /test.py:4:5 + --> test.py:4:5 | 2 | import does_not_exit 3 | @@ -450,7 +472,7 @@ fn cli_rule_severity() -> anyhow::Result<()> { | warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined - --> /test.py:9:7 + --> test.py:9:7 | 7 | x = a 8 | @@ -477,7 +499,7 @@ fn cli_rule_severity() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- warning: lint:unresolved-import: Cannot resolve import `does_not_exit` - --> /test.py:2:8 + --> test.py:2:8 | 2 | import does_not_exit | ^^^^^^^^^^^^^ @@ -486,7 +508,7 @@ fn cli_rule_severity() -> anyhow::Result<()> { | warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero - --> /test.py:4:5 + --> test.py:4:5 | 2 | import does_not_exit 3 | @@ -528,7 +550,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero - --> /test.py:2:5 + --> test.py:2:5 | 2 | y = 4 / 0 | ^^^^^ @@ -537,7 +559,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { | warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined - --> /test.py:7:7 + --> test.py:7:7 | 5 | x = a 6 | @@ -565,7 +587,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero - --> /test.py:2:5 + --> test.py:2:5 | 2 | y = 4 / 0 | ^^^^^ @@ -601,7 +623,7 @@ fn configuration_unknown_rules() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- warning: unknown-rule - --> /pyproject.toml:3:1 + --> pyproject.toml:3:1 | 2 | [tool.knot.rules] 3 | division-by-zer = "warn" # incorrect rule name @@ -644,7 +666,7 @@ fn exit_code_only_warnings() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- warning: lint:unresolved-reference: Name `x` used when not defined - --> /test.py:1:7 + --> test.py:1:7 | 1 | print(x) # [unresolved-reference] | ^ @@ -673,7 +695,7 @@ fn exit_code_only_info() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- info: revealed-type: Revealed type - --> /test.py:3:1 + --> test.py:3:1 | 2 | from typing_extensions import reveal_type 3 | reveal_type(1) @@ -703,7 +725,7 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- info: revealed-type: Revealed type - --> /test.py:3:1 + --> test.py:3:1 | 2 | from typing_extensions import reveal_type 3 | reveal_type(1) @@ -727,7 +749,7 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- warning: lint:unresolved-reference: Name `x` used when not defined - --> /test.py:1:7 + --> test.py:1:7 | 1 | print(x) # [unresolved-reference] | ^ @@ -759,7 +781,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any exit_code: 1 ----- stdout ----- warning: lint:unresolved-reference: Name `x` used when not defined - --> /test.py:1:7 + --> test.py:1:7 | 1 | print(x) # [unresolved-reference] | ^ @@ -788,7 +810,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- warning: lint:unresolved-reference: Name `x` used when not defined - --> /test.py:2:7 + --> test.py:2:7 | 2 | print(x) # [unresolved-reference] | ^ @@ -796,7 +818,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> { | error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method - --> /test.py:3:7 + --> test.py:3:7 | 2 | print(x) # [unresolved-reference] 3 | print(4[1]) # [non-subscriptable] @@ -826,7 +848,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow:: exit_code: 1 ----- stdout ----- warning: lint:unresolved-reference: Name `x` used when not defined - --> /test.py:2:7 + --> test.py:2:7 | 2 | print(x) # [unresolved-reference] | ^ @@ -834,7 +856,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow:: | error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method - --> /test.py:3:7 + --> test.py:3:7 | 2 | print(x) # [unresolved-reference] 3 | print(4[1]) # [non-subscriptable] @@ -864,7 +886,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- warning: lint:unresolved-reference: Name `x` used when not defined - --> /test.py:2:7 + --> test.py:2:7 | 2 | print(x) # [unresolved-reference] | ^ @@ -872,7 +894,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> { | error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method - --> /test.py:3:7 + --> test.py:3:7 | 2 | print(x) # [unresolved-reference] 3 | print(4[1]) # [non-subscriptable] @@ -924,7 +946,7 @@ fn user_configuration() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero - --> /project/main.py:2:5 + --> main.py:2:5 | 2 | y = 4 / 0 | ^^^^^ @@ -933,7 +955,7 @@ fn user_configuration() -> anyhow::Result<()> { | warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined - --> /project/main.py:7:7 + --> main.py:7:7 | 5 | x = a 6 | @@ -966,7 +988,7 @@ fn user_configuration() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero - --> /project/main.py:2:5 + --> main.py:2:5 | 2 | y = 4 / 0 | ^^^^^ @@ -975,7 +997,7 @@ fn user_configuration() -> anyhow::Result<()> { | error: lint:possibly-unresolved-reference: Name `x` used when possibly not defined - --> /project/main.py:7:7 + --> main.py:7:7 | 5 | x = a 6 | @@ -1024,14 +1046,14 @@ fn check_specific_paths() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero - --> /project/main.py:2:5 + --> project/main.py:2:5 | 2 | y = 4 / 0 # error: division-by-zero | ^^^^^ | error: lint:unresolved-import: Cannot resolve import `main2` - --> /project/other.py:2:6 + --> project/other.py:2:6 | 2 | from main2 import z # error: unresolved-import | ^^^^^ @@ -1040,7 +1062,7 @@ fn check_specific_paths() -> anyhow::Result<()> { | error: lint:unresolved-import: Cannot resolve import `does_not_exist` - --> /project/tests/test_main.py:2:8 + --> project/tests/test_main.py:2:8 | 2 | import does_not_exist # error: unresolved-import | ^^^^^^^^^^^^^^ @@ -1061,7 +1083,7 @@ fn check_specific_paths() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error: lint:unresolved-import: Cannot resolve import `main2` - --> /project/other.py:2:6 + --> project/other.py:2:6 | 2 | from main2 import z # error: unresolved-import | ^^^^^ @@ -1070,7 +1092,7 @@ fn check_specific_paths() -> anyhow::Result<()> { | error: lint:unresolved-import: Cannot resolve import `does_not_exist` - --> /project/tests/test_main.py:2:8 + --> project/tests/test_main.py:2:8 | 2 | import does_not_exist # error: unresolved-import | ^^^^^^^^^^^^^^ @@ -1130,8 +1152,8 @@ fn concise_diagnostics() -> anyhow::Result<()> { success: false exit_code: 1 ----- stdout ----- - warning[lint:unresolved-reference] /test.py:2:7: Name `x` used when not defined - error[lint:non-subscriptable] /test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + warning[lint:unresolved-reference] test.py:2:7: Name `x` used when not defined + error[lint:non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method Found 2 diagnostics ----- stderr ----- @@ -1164,7 +1186,7 @@ fn concise_revealed_type() -> anyhow::Result<()> { success: true exit_code: 0 ----- stdout ----- - info[revealed-type] /test.py:5:1: Revealed type: `Literal["hello"]` + info[revealed-type] test.py:5:1: Revealed type: `Literal["hello"]` Found 1 diagnostic ----- stderr ----- diff --git a/crates/red_knot_ide/src/goto.rs b/crates/red_knot_ide/src/goto.rs index ef8030141e9dce..a2804bac0b51bb 100644 --- a/crates/red_knot_ide/src/goto.rs +++ b/crates/red_knot_ide/src/goto.rs @@ -272,9 +272,9 @@ mod tests { "#, ); - assert_snapshot!(test.goto_type_definition(), @r###" + assert_snapshot!(test.goto_type_definition(), @r" info: lint:goto-type-definition: Type definition - --> /main.py:2:19 + --> main.py:2:19 | 2 | class Test: ... | ^^^^ @@ -282,14 +282,14 @@ mod tests { 4 | ab = Test() | info: Source - --> /main.py:4:13 + --> main.py:4:13 | 2 | class Test: ... 3 | 4 | ab = Test() | ^^ | - "###); + "); } #[test] @@ -304,9 +304,9 @@ mod tests { "#, ); - assert_snapshot!(test.goto_type_definition(), @r###" + assert_snapshot!(test.goto_type_definition(), @r" info: lint:goto-type-definition: Type definition - --> /main.py:2:17 + --> main.py:2:17 | 2 | def foo(a, b): ... | ^^^ @@ -314,14 +314,14 @@ mod tests { 4 | ab = foo | info: Source - --> /main.py:6:13 + --> main.py:6:13 | 4 | ab = foo 5 | 6 | ab | ^^ | - "###); + "); } #[test] @@ -344,7 +344,7 @@ mod tests { assert_snapshot!(test.goto_type_definition(), @r" info: lint:goto-type-definition: Type definition - --> /main.py:3:17 + --> main.py:3:17 | 3 | def foo(a, b): ... | ^^^ @@ -352,7 +352,7 @@ mod tests { 5 | def bar(a, b): ... | info: Source - --> /main.py:12:13 + --> main.py:12:13 | 10 | a = bar 11 | @@ -361,7 +361,7 @@ mod tests { | info: lint:goto-type-definition: Type definition - --> /main.py:5:17 + --> main.py:5:17 | 3 | def foo(a, b): ... 4 | @@ -371,7 +371,7 @@ mod tests { 7 | if random.choice(): | info: Source - --> /main.py:12:13 + --> main.py:12:13 | 10 | a = bar 11 | @@ -395,13 +395,13 @@ mod tests { assert_snapshot!(test.goto_type_definition(), @r" info: lint:goto-type-definition: Type definition - --> /lib.py:1:1 + --> lib.py:1:1 | 1 | a = 10 | ^^^^^^ | info: Source - --> /main.py:4:13 + --> main.py:4:13 | 2 | import lib 3 | @@ -433,7 +433,7 @@ mod tests { 440 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:4:13 + --> main.py:4:13 | 2 | a: str = "test" 3 | @@ -462,7 +462,7 @@ mod tests { 440 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:2:22 + --> main.py:2:22 | 2 | a: str = "test" | ^^^^^^ @@ -478,20 +478,20 @@ mod tests { "#, ); - assert_snapshot!(test.goto_type_definition(), @r###" + assert_snapshot!(test.goto_type_definition(), @r" info: lint:goto-type-definition: Type definition - --> /main.py:2:24 + --> main.py:2:24 | 2 | type Alias[T: int = bool] = list[T] | ^ | info: Source - --> /main.py:2:46 + --> main.py:2:46 | 2 | type Alias[T: int = bool] = list[T] | ^ | - "###); + "); } #[test] @@ -544,7 +544,7 @@ mod tests { 440 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:4:18 + --> main.py:4:18 | 2 | def test(a: str): ... 3 | @@ -579,7 +579,7 @@ mod tests { 233 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ... | info: Source - --> /main.py:4:18 + --> main.py:4:18 | 2 | def test(a: str): ... 3 | @@ -613,7 +613,7 @@ f(**kwargs) 1088 | # Also multiprocessing.managers.SyncManager.dict() | info: Source - --> /main.py:6:5 + --> main.py:6:5 | 4 | kwargs = { "name": "test"} 5 | @@ -644,7 +644,7 @@ f(**kwargs) 440 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:3:17 + --> main.py:3:17 | 2 | def foo(a: str): 3 | a @@ -666,23 +666,23 @@ f(**kwargs) "#, ); - assert_snapshot!(test.goto_type_definition(), @r###" + assert_snapshot!(test.goto_type_definition(), @r" info: lint:goto-type-definition: Type definition - --> /main.py:2:19 + --> main.py:2:19 | 2 | class X: | ^ 3 | def foo(a, b): ... | info: Source - --> /main.py:7:13 + --> main.py:7:13 | 5 | x = X() 6 | 7 | x.foo() | ^ | - "###); + "); } #[test] @@ -695,9 +695,9 @@ f(**kwargs) "#, ); - assert_snapshot!(test.goto_type_definition(), @r###" + assert_snapshot!(test.goto_type_definition(), @r" info: lint:goto-type-definition: Type definition - --> /main.py:2:17 + --> main.py:2:17 | 2 | def foo(a, b): ... | ^^^ @@ -705,14 +705,14 @@ f(**kwargs) 4 | foo() | info: Source - --> /main.py:4:13 + --> main.py:4:13 | 2 | def foo(a, b): ... 3 | 4 | foo() | ^^^ | - "###); + "); } #[test] @@ -737,7 +737,7 @@ f(**kwargs) 440 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:4:27 + --> main.py:4:27 | 2 | def foo(a: str | None, b): 3 | if a is not None: @@ -767,7 +767,7 @@ f(**kwargs) 672 | def __bool__(self) -> Literal[False]: ... | info: Source - --> /main.py:3:17 + --> main.py:3:17 | 2 | def foo(a: str | None, b): 3 | a @@ -785,7 +785,7 @@ f(**kwargs) 440 | def __new__(cls, object: object = ...) -> Self: ... | info: Source - --> /main.py:3:17 + --> main.py:3:17 | 2 | def foo(a: str | None, b): 3 | a diff --git a/crates/red_knot_ide/src/hover.rs b/crates/red_knot_ide/src/hover.rs index dfe3f780e1683b..11b1c78c0c1fd4 100644 --- a/crates/red_knot_ide/src/hover.rs +++ b/crates/red_knot_ide/src/hover.rs @@ -156,7 +156,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:4:9 + --> main.py:4:9 | 2 | a = 10 3 | @@ -192,7 +192,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:10:9 + --> main.py:10:9 | 9 | foo = Foo() 10 | foo.a @@ -214,7 +214,7 @@ mod tests { "#, ); - assert_snapshot!(test.hover(), @r###" + assert_snapshot!(test.hover(), @r" def foo(a, b) -> Unknown --------------------------------------------- ```text @@ -222,7 +222,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:4:13 + --> main.py:4:13 | 2 | def foo(a, b): ... 3 | @@ -231,7 +231,7 @@ mod tests { | | | source | - "###); + "); } #[test] @@ -251,7 +251,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:3:17 + --> main.py:3:17 | 2 | def foo(a: int, b: int, c: int): 3 | a + b == c @@ -282,7 +282,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:4:18 + --> main.py:4:18 | 2 | def test(a: int): ... 3 | @@ -312,7 +312,7 @@ mod tests { "#, ); - assert_snapshot!(test.hover(), @r###" + assert_snapshot!(test.hover(), @r" (def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown) --------------------------------------------- ```text @@ -320,7 +320,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:12:13 + --> main.py:12:13 | 10 | a = bar 11 | @@ -329,7 +329,7 @@ mod tests { | | | source | - "###); + "); } #[test] @@ -352,7 +352,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:4:13 + --> main.py:4:13 | 2 | import lib 3 | @@ -381,7 +381,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:2:46 + --> main.py:2:46 | 2 | type Alias[T: int = bool] = list[T] | ^- Cursor offset @@ -407,7 +407,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:2:53 + --> main.py:2:53 | 2 | type Alias[**P = [int, str]] = Callable[P, int] | ^- Cursor offset @@ -433,7 +433,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:2:43 + --> main.py:2:43 | 2 | type Alias[*Ts = ()] = tuple[*Ts] | ^^- Cursor offset @@ -461,7 +461,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:3:13 + --> main.py:3:13 | 2 | class Foo: 3 | a: int @@ -490,7 +490,7 @@ mod tests { ``` --------------------------------------------- info: lint:hover: Hovered content is - --> /main.py:4:27 + --> main.py:4:27 | 2 | def foo(a: str | None, b): 3 | if a is not None: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap index 8f46e05faca9c0..29d179dfcd5870 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap @@ -29,7 +29,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib ``` error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method - --> /src/mdtest_snippet.py:11:1 + --> src/mdtest_snippet.py:11:1 | 10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) 11 | instance.attr = 1 # error: [invalid-assignment] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap index 0b8d3b44347014..b56d17edf721ca 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap @@ -30,7 +30,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib ``` error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method - --> /src/mdtest_snippet.py:12:1 + --> src/mdtest_snippet.py:12:1 | 11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter) 12 | instance.attr = "wrong" # error: [invalid-assignment] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap index 8ea6c938749b4d..83bd2f0f12eef5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap @@ -27,7 +27,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib ``` error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` - --> /src/mdtest_snippet.py:6:1 + --> src/mdtest_snippet.py:6:1 | 4 | instance = C() 5 | instance.attr = 1 # fine @@ -41,7 +41,7 @@ error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assigna ``` error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` - --> /src/mdtest_snippet.py:9:1 + --> src/mdtest_snippet.py:9:1 | 8 | C.attr = 1 # fine 9 | C.attr = "wrong" # error: [invalid-assignment] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap index 9d98faf548b411..19d1547e5a0d47 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap @@ -27,7 +27,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib ``` warning: lint:possibly-unbound-attribute: Attribute `attr` on type `Literal[C]` is possibly unbound - --> /src/mdtest_snippet.py:6:5 + --> src/mdtest_snippet.py:6:5 | 4 | attr: int = 0 5 | @@ -41,7 +41,7 @@ warning: lint:possibly-unbound-attribute: Attribute `attr` on type `Literal[C]` ``` warning: lint:possibly-unbound-attribute: Attribute `attr` on type `C` is possibly unbound - --> /src/mdtest_snippet.py:9:5 + --> src/mdtest_snippet.py:9:5 | 8 | instance = C() 9 | instance.attr = 1 # error: [possibly-unbound-attribute] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap index 839dfd9fa0b698..560e04c18f4bd1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap @@ -27,7 +27,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib ``` error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` - --> /src/mdtest_snippet.py:7:1 + --> src/mdtest_snippet.py:7:1 | 5 | instance = C() 6 | instance.attr = 1 # fine @@ -41,7 +41,7 @@ error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assigna ``` error: lint:invalid-attribute-access: Cannot assign to instance attribute `attr` from the class object `Literal[C]` - --> /src/mdtest_snippet.py:9:1 + --> src/mdtest_snippet.py:9:1 | 7 | instance.attr = "wrong" # error: [invalid-assignment] 8 | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap index 92606ccb687c32..d1db352068e1cc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap @@ -38,7 +38,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib ``` error: lint:invalid-assignment: Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]` - --> /src/mdtest_snippet.py:11:5 + --> src/mdtest_snippet.py:11:5 | 10 | # TODO: The error message here could be improved to explain why the assignment fails. 11 | C1.attr = 1 # error: [invalid-assignment] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap index db88c02820195e..003bd123d3adfd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap @@ -24,7 +24,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib ``` error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `Literal[C]`. - --> /src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:3:1 | 1 | class C: ... 2 | @@ -38,7 +38,7 @@ error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `L ``` error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `C`. - --> /src/mdtest_snippet.py:6:1 + --> src/mdtest_snippet.py:6:1 | 5 | instance = C() 6 | instance.non_existent = 1 # error: [unresolved-attribute] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap index f5f48a660d75fe..61882d1faf8635 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap @@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib ``` error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int` - --> /src/mdtest_snippet.py:7:1 + --> src/mdtest_snippet.py:7:1 | 6 | C.attr = 1 # fine 7 | C.attr = "wrong" # error: [invalid-assignment] @@ -41,7 +41,7 @@ error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assigna ``` error: lint:invalid-attribute-access: Cannot assign to ClassVar `attr` from an instance of type `C` - --> /src/mdtest_snippet.py:10:1 + --> src/mdtest_snippet.py:10:1 | 9 | instance = C() 10 | instance.attr = 1 # error: [invalid-attribute-access] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap index b4d0d917d99e61..1301dfb6d0ae5e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap @@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md ``` error: lint:unresolved-import: Cannot resolve import `zqzqzqzqzqzqzq` - --> /src/mdtest_snippet.py:1:8 + --> src/mdtest_snippet.py:1:8 | 1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`" | ^^^^^^^^^^^^^^ diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap index fc8f6e2e6e85ce..3b1b2c43107fb6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap @@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md ``` error: lint:unresolved-import: Cannot resolve import `a.foo` - --> /src/mdtest_snippet.py:2:8 + --> src/mdtest_snippet.py:2:8 | 1 | # Topmost component resolvable, submodule not resolvable: 2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`" @@ -41,7 +41,7 @@ error: lint:unresolved-import: Cannot resolve import `a.foo` ``` error: lint:unresolved-import: Cannot resolve import `b.foo` - --> /src/mdtest_snippet.py:5:8 + --> src/mdtest_snippet.py:5:8 | 4 | # Topmost component unresolvable: 5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`" diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap index f5e2c0ca98f386..37f5cd5df05cc9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap @@ -29,7 +29,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Iterable` is not iterable - --> /src/mdtest_snippet.py:10:10 + --> src/mdtest_snippet.py:10:10 | 9 | # error: [not-iterable] 10 | for x in Iterable(): @@ -43,7 +43,7 @@ info: `__getitem__` must be at least as permissive as `def __getitem__(self, key ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:11:5 + --> src/mdtest_snippet.py:11:5 | 9 | # error: [not-iterable] 10 | for x in Iterable(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap index 967995693eeda7..01d7edcf57909e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap @@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Literal[123]` is not iterable - --> /src/mdtest_snippet.py:2:10 + --> src/mdtest_snippet.py:2:10 | 1 | nonsense = 123 2 | for x in nonsense: # error: [not-iterable] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap index c193a16c5744d0..adf5904e9bbe08 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap @@ -25,7 +25,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `NotIterable` is not iterable - --> /src/mdtest_snippet.py:6:10 + --> src/mdtest_snippet.py:6:10 | 4 | __iter__: None = None 5 | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap index 8f06a8baf60cbf..6ef81246c85283 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap @@ -26,7 +26,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Bad` is not iterable - --> /src/mdtest_snippet.py:7:10 + --> src/mdtest_snippet.py:7:10 | 6 | # error: [not-iterable] 7 | for x in Bad(): @@ -39,7 +39,7 @@ info: It has no `__iter__` method and its `__getitem__` attribute has type `None ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:8:5 + --> src/mdtest_snippet.py:8:5 | 6 | # error: [not-iterable] 7 | for x in Bad(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap index 85fb92f9112845..3d3362acf45837 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap @@ -47,7 +47,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Iterable1` may not be iterable - --> /src/mdtest_snippet.py:22:14 + --> src/mdtest_snippet.py:22:14 | 21 | # error: [not-iterable] 22 | for x in Iterable1(): @@ -62,7 +62,7 @@ info: `__getitem__` has type `CustomCallable`, which is not callable ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:24:9 + --> src/mdtest_snippet.py:24:9 | 22 | for x in Iterable1(): 23 | # TODO... `int` might be ideal here? @@ -76,7 +76,7 @@ info: revealed-type: Revealed type ``` error: lint:not-iterable: Object of type `Iterable2` may not be iterable - --> /src/mdtest_snippet.py:27:14 + --> src/mdtest_snippet.py:27:14 | 26 | # error: [not-iterable] 27 | for y in Iterable2(): @@ -91,7 +91,7 @@ info: `__getitem__` has type `(bound method Iterable2.__getitem__(key: int) -> i ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:29:9 + --> src/mdtest_snippet.py:29:9 | 27 | for y in Iterable2(): 28 | # TODO... `int` might be ideal here? diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap index 4167d3bdfbcba5..8b5ff406e47d08 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap @@ -44,7 +44,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Iterable1` may not be iterable - --> /src/mdtest_snippet.py:20:14 + --> src/mdtest_snippet.py:20:14 | 19 | # error: [not-iterable] 20 | for x in Iterable1(): @@ -59,7 +59,7 @@ info: `__getitem__` has type `(bound method Iterable1.__getitem__(item: int) -> ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:22:9 + --> src/mdtest_snippet.py:22:9 | 20 | for x in Iterable1(): 21 | # TODO: `str` might be better @@ -73,7 +73,7 @@ info: revealed-type: Revealed type ``` error: lint:not-iterable: Object of type `Iterable2` may not be iterable - --> /src/mdtest_snippet.py:25:14 + --> src/mdtest_snippet.py:25:14 | 24 | # error: [not-iterable] 25 | for y in Iterable2(): @@ -87,7 +87,7 @@ info: `__getitem__` must be at least as permissive as `def __getitem__(self, key ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:26:9 + --> src/mdtest_snippet.py:26:9 | 24 | # error: [not-iterable] 25 | for y in Iterable2(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap index c8e35feca0c847..758a101b4a5fa1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap @@ -48,7 +48,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Iterable1` may not be iterable - --> /src/mdtest_snippet.py:17:14 + --> src/mdtest_snippet.py:17:14 | 16 | # error: [not-iterable] 17 | for x in Iterable1(): @@ -63,7 +63,7 @@ info: Expected signature for `__iter__` is `def __iter__(self): ...` ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:18:9 + --> src/mdtest_snippet.py:18:9 | 16 | # error: [not-iterable] 17 | for x in Iterable1(): @@ -77,7 +77,7 @@ info: revealed-type: Revealed type ``` error: lint:not-iterable: Object of type `Iterable2` may not be iterable - --> /src/mdtest_snippet.py:28:14 + --> src/mdtest_snippet.py:28:14 | 27 | # error: [not-iterable] 28 | for x in Iterable2(): @@ -91,7 +91,7 @@ info: Its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:30:9 + --> src/mdtest_snippet.py:30:9 | 28 | for x in Iterable2(): 29 | # TODO: `int` would probably be better here: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap index 9cdce7cafe8d34..7b5200bc243473 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap @@ -52,7 +52,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Iterable1` may not be iterable - --> /src/mdtest_snippet.py:28:14 + --> src/mdtest_snippet.py:28:14 | 27 | # error: [not-iterable] 28 | for x in Iterable1(): @@ -66,7 +66,7 @@ info: Expected signature for `__next__` is `def __next__(self): ...`) ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:29:9 + --> src/mdtest_snippet.py:29:9 | 27 | # error: [not-iterable] 28 | for x in Iterable1(): @@ -80,7 +80,7 @@ info: revealed-type: Revealed type ``` error: lint:not-iterable: Object of type `Iterable2` may not be iterable - --> /src/mdtest_snippet.py:32:14 + --> src/mdtest_snippet.py:32:14 | 31 | # error: [not-iterable] 32 | for y in Iterable2(): @@ -94,7 +94,7 @@ info: Its `__iter__` method returns an object of type `Iterator2`, which has a ` ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:34:9 + --> src/mdtest_snippet.py:34:9 | 32 | for y in Iterable2(): 33 | # TODO: `int` would probably be better here: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap index 2f655a4590c550..ca7f8bd055b3b9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap @@ -37,7 +37,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Iterable` may not be iterable - --> /src/mdtest_snippet.py:18:14 + --> src/mdtest_snippet.py:18:14 | 17 | # error: [not-iterable] 18 | for x in Iterable(): @@ -51,7 +51,7 @@ info: `__getitem__` must be at least as permissive as `def __getitem__(self, key ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:19:9 + --> src/mdtest_snippet.py:19:9 | 17 | # error: [not-iterable] 18 | for x in Iterable(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap index ef3a31a2d3fa5b..8157542d95a4b8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap @@ -55,7 +55,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Iterable1` may not be iterable - --> /src/mdtest_snippet.py:31:14 + --> src/mdtest_snippet.py:31:14 | 30 | # error: [not-iterable] 31 | for x in Iterable1(): @@ -69,7 +69,7 @@ info: It may not have an `__iter__` method and its `__getitem__` attribute (with ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:33:9 + --> src/mdtest_snippet.py:33:9 | 31 | for x in Iterable1(): 32 | # TODO: `bytes | str` might be better @@ -83,7 +83,7 @@ info: revealed-type: Revealed type ``` error: lint:not-iterable: Object of type `Iterable2` may not be iterable - --> /src/mdtest_snippet.py:36:14 + --> src/mdtest_snippet.py:36:14 | 35 | # error: [not-iterable] 36 | for y in Iterable2(): @@ -97,7 +97,7 @@ info: `__getitem__` must be at least as permissive as `def __getitem__(self, key ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:37:9 + --> src/mdtest_snippet.py:37:9 | 35 | # error: [not-iterable] 36 | for y in Iterable2(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap index 213e15d36dd08f..58fc572d7476b5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap @@ -36,7 +36,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Iterable` may not be iterable - --> /src/mdtest_snippet.py:17:14 + --> src/mdtest_snippet.py:17:14 | 16 | # error: [not-iterable] 17 | for x in Iterable(): @@ -49,7 +49,7 @@ info: It may not have an `__iter__` method or a `__getitem__` method ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:18:9 + --> src/mdtest_snippet.py:18:9 | 16 | # error: [not-iterable] 17 | for x in Iterable(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap index fd5882fd0f77d7..1d6cf8f415becc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap @@ -37,7 +37,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Test | Test2` may not be iterable - --> /src/mdtest_snippet.py:18:14 + --> src/mdtest_snippet.py:18:14 | 16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989) 17 | # error: [not-iterable] @@ -51,7 +51,7 @@ info: Its `__iter__` method returns an object of type `TestIter | int`, which ma ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:19:9 + --> src/mdtest_snippet.py:19:9 | 17 | # error: [not-iterable] 18 | for x in Test() if flag else Test2(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap index 36c902d70e630d..bf2af5ac7554d9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap @@ -32,7 +32,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Test | Literal[42]` may not be iterable - --> /src/mdtest_snippet.py:13:14 + --> src/mdtest_snippet.py:13:14 | 11 | def _(flag: bool): 12 | # error: [not-iterable] @@ -46,7 +46,7 @@ info: It may not have an `__iter__` method and it doesn't have a `__getitem__` m ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:14:9 + --> src/mdtest_snippet.py:14:9 | 12 | # error: [not-iterable] 13 | for x in Test() if flag else 42: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap index 35de9761e3279d..4e4d5807b5872f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap @@ -34,7 +34,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `NotIterable` is not iterable - --> /src/mdtest_snippet.py:11:14 + --> src/mdtest_snippet.py:11:14 | 10 | # error: [not-iterable] 11 | for x in NotIterable(): @@ -47,7 +47,7 @@ info: Its `__iter__` attribute has type `int | None`, which is not callable ``` warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined - --> /src/mdtest_snippet.py:16:17 + --> src/mdtest_snippet.py:16:17 | 14 | # revealed: Unknown 15 | # error: [possibly-unresolved-reference] @@ -59,7 +59,7 @@ warning: lint:possibly-unresolved-reference: Name `x` used when possibly not def ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:16:5 + --> src/mdtest_snippet.py:16:5 | 14 | # revealed: Unknown 15 | # error: [possibly-unresolved-reference] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap index 6b6c0fb0fccf69..1360f85797f57f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap @@ -27,7 +27,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Bad` is not iterable - --> /src/mdtest_snippet.py:8:10 + --> src/mdtest_snippet.py:8:10 | 7 | # error: [not-iterable] 8 | for x in Bad(): @@ -40,7 +40,7 @@ info: Its `__iter__` method returns an object of type `int`, which has no `__nex ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:9:5 + --> src/mdtest_snippet.py:9:5 | 7 | # error: [not-iterable] 8 | for x in Bad(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap index 534878678adf9b..515921e9b8d05e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap @@ -31,7 +31,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Iterable` is not iterable - --> /src/mdtest_snippet.py:12:10 + --> src/mdtest_snippet.py:12:10 | 11 | # error: [not-iterable] 12 | for x in Iterable(): @@ -45,7 +45,7 @@ info: Expected signature `def __iter__(self): ...` ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:13:5 + --> src/mdtest_snippet.py:13:5 | 11 | # error: [not-iterable] 12 | for x in Iterable(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap index 166d7b3ec87996..f5d3a90199d2c6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap @@ -42,7 +42,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md ``` error: lint:not-iterable: Object of type `Iterable1` is not iterable - --> /src/mdtest_snippet.py:19:10 + --> src/mdtest_snippet.py:19:10 | 18 | # error: [not-iterable] 19 | for x in Iterable1(): @@ -56,7 +56,7 @@ info: Expected signature for `__next__` is `def __next__(self): ...` ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:20:5 + --> src/mdtest_snippet.py:20:5 | 18 | # error: [not-iterable] 19 | for x in Iterable1(): @@ -70,7 +70,7 @@ info: revealed-type: Revealed type ``` error: lint:not-iterable: Object of type `Iterable2` is not iterable - --> /src/mdtest_snippet.py:23:10 + --> src/mdtest_snippet.py:23:10 | 22 | # error: [not-iterable] 23 | for y in Iterable2(): @@ -83,7 +83,7 @@ info: Its `__iter__` method returns an object of type `Iterator2`, which has a ` ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:24:5 + --> src/mdtest_snippet.py:24:5 | 22 | # error: [not-iterable] 23 | for y in Iterable2(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap index 9366971e470be1..ab24c67fdbf3ad 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap @@ -25,7 +25,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.m ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` - --> /src/mdtest_snippet.py:7:8 + --> src/mdtest_snippet.py:7:8 | 6 | # error: [unsupported-bool-conversion] 7 | 10 and a and True diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap index a95ae257f481a3..d7c380f6f10ab4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap @@ -22,7 +22,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:5 + --> src/mdtest_snippet.py:4:5 | 2 | return x * x 3 | @@ -30,7 +30,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int) -> int: | ^^^ ------ Parameter declared here diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap index b6ef2802033d85..e4e379e49963c1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap @@ -24,14 +24,14 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:6:10 + --> src/mdtest_snippet.py:6:10 | 5 | c = C() 6 | c.square("hello") # error: [invalid-argument-type] | ^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/mdtest_snippet.py:2:9 + --> src/mdtest_snippet.py:2:9 | 1 | class C: 2 | def square(self, x: int) -> int: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap index 25d65a1940435a..3a0857fb507daa 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap @@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:3:13 + --> src/mdtest_snippet.py:3:13 | 1 | import package 2 | @@ -36,7 +36,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/package.py:1:5 + --> src/package.py:1:5 | 1 | def foo(x: int) -> int: | ^^^ ------ Parameter declared here diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap index 8910a6e1bf6367..ca4201e6241678 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap @@ -23,7 +23,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:2:9 + --> src/mdtest_snippet.py:2:9 | 1 | def bar(): 2 | foo("hello") # error: [invalid-argument-type] @@ -32,7 +32,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect 4 | def foo(x: int) -> int: | info: Function defined here - --> /src/mdtest_snippet.py:4:5 + --> src/mdtest_snippet.py:4:5 | 2 | foo("hello") # error: [invalid-argument-type] 3 | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap index b23b61c9ba2168..c96af22564d9ff 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap @@ -22,7 +22,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:8 + --> src/mdtest_snippet.py:4:8 | 2 | return x * y * z 3 | @@ -30,7 +30,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int) -> int: | ^^^ ------ Parameter declared here diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap index 8d08b8b9dec2a4..872613113e6775 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap @@ -26,7 +26,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:8:8 + --> src/mdtest_snippet.py:8:8 | 6 | return x * y * z 7 | @@ -34,7 +34,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo( | ^^^ diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap index c4b5dd367734dc..7fa5b1c59d21e4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap @@ -25,7 +25,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:7:5 + --> src/mdtest_snippet.py:7:5 | 5 | # error: [invalid-argument-type] 6 | # error: [invalid-argument-type] @@ -33,7 +33,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^ Expected `int`, found `Literal["a"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int) -> int: | ^^^ ------ Parameter declared here @@ -44,7 +44,7 @@ info: Function defined here ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:7:10 + --> src/mdtest_snippet.py:7:10 | 5 | # error: [invalid-argument-type] 6 | # error: [invalid-argument-type] @@ -52,7 +52,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^ Expected `int`, found `Literal["b"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int) -> int: | ^^^ ------ Parameter declared here @@ -63,7 +63,7 @@ info: Function defined here ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:7:15 + --> src/mdtest_snippet.py:7:15 | 5 | # error: [invalid-argument-type] 6 | # error: [invalid-argument-type] @@ -71,7 +71,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^ Expected `int`, found `Literal["c"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int) -> int: | ^^^ ------ Parameter declared here diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap index 8ff31730b76756..4dab8917ec9d60 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap @@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:3:12 + --> src/mdtest_snippet.py:3:12 | 1 | import json 2 | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap index 6bb4b90f2280ac..e0c157292b3028 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap @@ -22,7 +22,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:11 + --> src/mdtest_snippet.py:4:11 | 2 | return x * y * z 3 | @@ -30,7 +30,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, *, z: int = 0) -> int: | ^^^ ---------- Parameter declared here diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap index e9fc47e2bcacf9..591bea50baec04 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap @@ -22,7 +22,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:11 + --> src/mdtest_snippet.py:4:11 | 2 | return x * y * z 3 | @@ -30,7 +30,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, /, y: int, *, z: int = 0) -> int: | ^^^ ---------- Parameter declared here diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap index 6add6e72c6d7f5..0ca5219152f5a1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap @@ -22,7 +22,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:11 + --> src/mdtest_snippet.py:4:11 | 2 | return x * y * z 3 | @@ -30,7 +30,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int = 0) -> int: | ^^^ ---------- Parameter declared here diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap index 7834637de11517..912c60f2e14436 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap @@ -22,7 +22,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:8 + --> src/mdtest_snippet.py:4:8 | 2 | return x * y * z 3 | @@ -30,7 +30,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int, /) -> int: | ^^^ ------ Parameter declared here diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap index 6dfa130e9bc6dd..7cbf4908a80bb3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap @@ -24,14 +24,14 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:6:3 + --> src/mdtest_snippet.py:6:3 | 5 | c = C() 6 | c("wrong") # error: [invalid-argument-type] | ^^^^^^^ Expected `int`, found `Literal["wrong"]` | info: Function defined here - --> /src/mdtest_snippet.py:2:9 + --> src/mdtest_snippet.py:2:9 | 1 | class C: 2 | def __call__(self, x: int) -> int: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap index b9ef2e2e33ed7a..b729a73e2cc9b6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap @@ -22,7 +22,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:14 + --> src/mdtest_snippet.py:4:14 | 2 | return len(numbers) 3 | @@ -30,7 +30,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(*numbers: int) -> int: | ^^^ ------------- Parameter declared here diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap index d61b5503490e14..a3a8c87e8f19e1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap @@ -22,7 +22,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali ``` error: lint:invalid-argument-type: Argument to this function is incorrect - --> /src/mdtest_snippet.py:4:20 + --> src/mdtest_snippet.py:4:20 | 2 | return len(numbers) 3 | @@ -30,7 +30,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` | info: Function defined here - --> /src/mdtest_snippet.py:1:5 + --> src/mdtest_snippet.py:1:5 | 1 | def foo(**numbers: int) -> int: | ^^^ -------------- Parameter declared here diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap index 6dc5ab6afd4349..831d23cda8b1ab 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap @@ -29,7 +29,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` - --> /src/mdtest_snippet.py:9:1 + --> src/mdtest_snippet.py:9:1 | 8 | # error: [unsupported-bool-conversion] 9 | 10 in WithContains() @@ -43,7 +43,7 @@ info: `__bool__` on `NotBoolable` must be callable ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` - --> /src/mdtest_snippet.py:11:1 + --> src/mdtest_snippet.py:11:1 | 9 | 10 in WithContains() 10 | # error: [unsupported-bool-conversion] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap index 27eba8849f757c..73b3e60e6bf119 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap @@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_mat ``` error: lint:no-matching-overload: No overload of class `type` matches arguments - --> /src/mdtest_snippet.py:1:1 + --> src/mdtest_snippet.py:1:1 | 1 | type("Foo", ()) # error: [no-matching-overload] | ^^^^^^^^^^^^^^^ diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap index 21d48d1a7a0aaa..f3b7734eb9cdb4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap @@ -23,7 +23,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` - --> /src/mdtest_snippet.py:5:1 + --> src/mdtest_snippet.py:5:1 | 4 | # error: [unsupported-bool-conversion] 5 | not NotBoolable() diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap index 92abc59799e3ef..9f5d4e2781e4d3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap @@ -33,7 +33,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md ``` error: lint:call-non-callable: Object of type `typing.Protocol` is not callable - --> /src/mdtest_snippet.py:4:13 + --> src/mdtest_snippet.py:4:13 | 3 | # error: [call-non-callable] 4 | reveal_type(Protocol()) # revealed: Unknown @@ -46,7 +46,7 @@ error: lint:call-non-callable: Object of type `typing.Protocol` is not callable ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:4:1 | 3 | # error: [call-non-callable] 4 | reveal_type(Protocol()) # revealed: Unknown @@ -59,7 +59,7 @@ info: revealed-type: Revealed type ``` error: lint:call-non-callable: Cannot instantiate class `MyProtocol` - --> /src/mdtest_snippet.py:10:13 + --> src/mdtest_snippet.py:10:13 | 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" 10 | reveal_type(MyProtocol()) # revealed: MyProtocol @@ -67,7 +67,7 @@ error: lint:call-non-callable: Cannot instantiate class `MyProtocol` 11 | class SubclassOfMyProtocol(MyProtocol): ... | info: Protocol classes cannot be instantiated - --> /src/mdtest_snippet.py:6:7 + --> src/mdtest_snippet.py:6:7 | 4 | reveal_type(Protocol()) # revealed: Unknown 5 | @@ -80,7 +80,7 @@ info: Protocol classes cannot be instantiated ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:10:1 + --> src/mdtest_snippet.py:10:1 | 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" 10 | reveal_type(MyProtocol()) # revealed: MyProtocol @@ -92,7 +92,7 @@ info: revealed-type: Revealed type ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:13:1 + --> src/mdtest_snippet.py:13:1 | 11 | class SubclassOfMyProtocol(MyProtocol): ... 12 | @@ -106,7 +106,7 @@ info: revealed-type: Revealed type ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:15:5 + --> src/mdtest_snippet.py:15:5 | 13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol 14 | def f(x: type[MyProtocol]): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap index 59d90c9e36fe59..ca38907009374e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap @@ -30,7 +30,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md ``` error: lint:invalid-argument-type: Invalid argument to `get_protocol_members` - --> /src/mdtest_snippet.py:5:1 + --> src/mdtest_snippet.py:5:1 | 3 | class NotAProtocol: ... 4 | @@ -41,7 +41,7 @@ error: lint:invalid-argument-type: Invalid argument to `get_protocol_members` | info: Only protocol classes can be passed to `get_protocol_members` info: `NotAProtocol` is declared here, but it is not a protocol class: - --> /src/mdtest_snippet.py:3:7 + --> src/mdtest_snippet.py:3:7 | 1 | from typing_extensions import Protocol, get_protocol_members 2 | @@ -57,7 +57,7 @@ info: See https://typing.python.org/en/latest/spec/protocol.html# ``` error: lint:invalid-argument-type: Invalid argument to `get_protocol_members` - --> /src/mdtest_snippet.py:9:1 + --> src/mdtest_snippet.py:9:1 | 7 | class AlsoNotAProtocol(NotAProtocol, object): ... 8 | @@ -67,7 +67,7 @@ error: lint:invalid-argument-type: Invalid argument to `get_protocol_members` | info: Only protocol classes can be passed to `get_protocol_members` info: `AlsoNotAProtocol` is declared here, but it is not a protocol class: - --> /src/mdtest_snippet.py:7:7 + --> src/mdtest_snippet.py:7:7 | 5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] 6 | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap index 5767ef3dbf4f8a..4d6894f835786b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap @@ -58,7 +58,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md ``` error: lint:invalid-argument-type: Class `HasX` cannot be used as the second argument to `isinstance` - --> /src/mdtest_snippet.py:7:8 + --> src/mdtest_snippet.py:7:8 | 6 | def f(arg: object, arg2: type): 7 | if isinstance(arg, HasX): # error: [invalid-argument-type] @@ -67,7 +67,7 @@ error: lint:invalid-argument-type: Class `HasX` cannot be used as the second arg 9 | else: | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable - --> /src/mdtest_snippet.py:3:7 + --> src/mdtest_snippet.py:3:7 | 1 | from typing_extensions import Protocol, reveal_type 2 | @@ -82,7 +82,7 @@ info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:8:9 + --> src/mdtest_snippet.py:8:9 | 6 | def f(arg: object, arg2: type): 7 | if isinstance(arg, HasX): # error: [invalid-argument-type] @@ -96,7 +96,7 @@ info: revealed-type: Revealed type ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:10:9 + --> src/mdtest_snippet.py:10:9 | 8 | reveal_type(arg) # revealed: HasX 9 | else: @@ -110,7 +110,7 @@ info: revealed-type: Revealed type ``` error: lint:invalid-argument-type: Class `HasX` cannot be used as the second argument to `issubclass` - --> /src/mdtest_snippet.py:12:8 + --> src/mdtest_snippet.py:12:8 | 10 | reveal_type(arg) # revealed: ~HasX 11 | @@ -120,7 +120,7 @@ error: lint:invalid-argument-type: Class `HasX` cannot be used as the second arg 14 | else: | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable - --> /src/mdtest_snippet.py:3:7 + --> src/mdtest_snippet.py:3:7 | 1 | from typing_extensions import Protocol, reveal_type 2 | @@ -135,7 +135,7 @@ info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:13:9 + --> src/mdtest_snippet.py:13:9 | 12 | if issubclass(arg2, HasX): # error: [invalid-argument-type] 13 | reveal_type(arg2) # revealed: type[HasX] @@ -148,7 +148,7 @@ info: revealed-type: Revealed type ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:15:9 + --> src/mdtest_snippet.py:15:9 | 13 | reveal_type(arg2) # revealed: type[HasX] 14 | else: @@ -161,7 +161,7 @@ info: revealed-type: Revealed type ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:24:9 + --> src/mdtest_snippet.py:24:9 | 22 | def f(arg: object): 23 | if isinstance(arg, RuntimeCheckableHasX): # no error! @@ -175,7 +175,7 @@ info: revealed-type: Revealed type ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:26:9 + --> src/mdtest_snippet.py:26:9 | 24 | reveal_type(arg) # revealed: RuntimeCheckableHasX 25 | else: @@ -189,7 +189,7 @@ info: revealed-type: Revealed type ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:33:9 + --> src/mdtest_snippet.py:33:9 | 31 | def f(arg1: type, arg2: type): 32 | if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members) @@ -203,7 +203,7 @@ info: revealed-type: Revealed type ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:35:9 + --> src/mdtest_snippet.py:35:9 | 33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX] 34 | else: @@ -217,7 +217,7 @@ info: revealed-type: Revealed type ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:38:9 + --> src/mdtest_snippet.py:38:9 | 37 | if issubclass(arg2, OnlyMethodMembers): # no error! 38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers] @@ -230,7 +230,7 @@ info: revealed-type: Revealed type ``` info: revealed-type: Revealed type - --> /src/mdtest_snippet.py:40:9 + --> src/mdtest_snippet.py:40:9 | 38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers] 39 | else: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap index f21ec59ed7e0dd..aebad91a20cf65 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap @@ -32,7 +32,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty ``` error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:1:22 + --> src/mdtest_snippet.py:1:22 | 1 | def f(cond: bool) -> str: | --- Expected `str` because of return type @@ -50,7 +50,7 @@ error: lint:invalid-return-type: Return type does not match returned value ``` error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:8:22 + --> src/mdtest_snippet.py:8:22 | 6 | return 1 7 | @@ -68,14 +68,14 @@ error: lint:invalid-return-type: Return type does not match returned value ``` error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:14:16 + --> src/mdtest_snippet.py:14:16 | 12 | else: 13 | # error: [invalid-return-type] 14 | return 2 | ^ Expected `str`, found `Literal[2]` | - ::: /src/mdtest_snippet.py:8:22 + ::: src/mdtest_snippet.py:8:22 | 6 | return 1 7 | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap index 1b57002c775edd..f03d7011c0106a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap @@ -41,7 +41,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty ``` error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:1:12 + --> src/mdtest_snippet.py:1:12 | 1 | def f() -> None: | ---- Expected `None` because of return type @@ -57,7 +57,7 @@ error: lint:invalid-return-type: Return type does not match returned value ``` error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` - --> /src/mdtest_snippet.py:7:22 + --> src/mdtest_snippet.py:7:22 | 6 | # error: [invalid-return-type] 7 | def f(cond: bool) -> int: @@ -70,7 +70,7 @@ error: lint:invalid-return-type: Function can implicitly return `None`, which is ``` error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` - --> /src/mdtest_snippet.py:12:22 + --> src/mdtest_snippet.py:12:22 | 11 | # error: [invalid-return-type] 12 | def f(cond: bool) -> int: @@ -83,7 +83,7 @@ error: lint:invalid-return-type: Function can implicitly return `None`, which is ``` error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` - --> /src/mdtest_snippet.py:17:22 + --> src/mdtest_snippet.py:17:22 | 16 | # error: [invalid-return-type] 17 | def f(cond: bool) -> int: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap index c039791eae7e44..67a88e4dc8d335 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap @@ -36,7 +36,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty ``` error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` - --> /src/mdtest_snippet.py:2:12 + --> src/mdtest_snippet.py:2:12 | 1 | # error: [invalid-return-type] 2 | def f() -> int: @@ -48,7 +48,7 @@ error: lint:invalid-return-type: Function can implicitly return `None`, which is ``` error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:5:12 + --> src/mdtest_snippet.py:5:12 | 3 | 1 4 | @@ -65,7 +65,7 @@ error: lint:invalid-return-type: Return type does not match returned value ``` error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.py:9:12 + --> src/mdtest_snippet.py:9:12 | 7 | return 1 8 | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap index 3c02500ae3832d..5e2334447fff1d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap @@ -31,7 +31,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty ``` error: lint:invalid-return-type: Return type does not match returned value - --> /src/mdtest_snippet.pyi:1:12 + --> src/mdtest_snippet.pyi:1:12 | 1 | def f() -> int: | --- Expected `int` because of return type @@ -46,7 +46,7 @@ error: lint:invalid-return-type: Return type does not match returned value ``` error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` - --> /src/mdtest_snippet.pyi:6:14 + --> src/mdtest_snippet.pyi:6:14 | 5 | # error: [invalid-return-type] 6 | def foo() -> int: @@ -59,7 +59,7 @@ error: lint:invalid-return-type: Function can implicitly return `None`, which is ``` error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int` - --> /src/mdtest_snippet.pyi:11:14 + --> src/mdtest_snippet.pyi:11:14 | 10 | # error: [invalid-return-type] 11 | def foo() -> int: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap index baf8e5b0e07258..7da9f44a682c2a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap @@ -34,7 +34,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` - --> /src/mdtest_snippet.py:12:1 + --> src/mdtest_snippet.py:12:1 | 11 | # error: [unsupported-bool-conversion] 12 | 10 < Comparable() < 20 @@ -48,7 +48,7 @@ info: `__bool__` on `NotBoolable` must be callable ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` - --> /src/mdtest_snippet.py:14:1 + --> src/mdtest_snippet.py:14:1 | 12 | 10 < Comparable() < 20 13 | # error: [unsupported-bool-conversion] diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap index 974595ccfa42fa..ff4fd7a4fadc42 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap @@ -33,7 +33,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/semant ``` error: invalid-syntax - --> /src/mdtest_snippet.py:6:19 + --> src/mdtest_snippet.py:6:19 | 4 | async def f(): 5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (synt... diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap index 74d8fb95bb5673..19d5bb12d766e6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap @@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadow ``` error: lint:invalid-assignment: Implicit shadowing of class `C` - --> /src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:3:1 | 1 | class C: ... 2 | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap index 04de7c982bc49e..ec187843c5395e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap @@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadow ``` error: lint:invalid-assignment: Implicit shadowing of function `f` - --> /src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:3:1 | 1 | def f(): ... 2 | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap index db99489b41552b..09c17b82b71df6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap @@ -35,7 +35,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples. ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable | Literal[False]` - --> /src/mdtest_snippet.py:15:1 + --> src/mdtest_snippet.py:15:1 | 14 | # error: [unsupported-bool-conversion] 15 | a < b < b diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap index 972e7ed6d80c29..222b3e132459f2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap @@ -27,7 +27,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples. ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` - --> /src/mdtest_snippet.py:9:1 + --> src/mdtest_snippet.py:9:1 | 8 | # error: [unsupported-bool-conversion] 9 | (A(),) == (A(),) diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap index c3c5ebe6bec434..eed266117fd539 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap @@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack ``` error: lint:invalid-assignment: Not enough values to unpack - --> /src/mdtest_snippet.py:1:1 + --> src/mdtest_snippet.py:1:1 | 1 | a, b = (1,) # error: [invalid-assignment] | ^^^^ ---- Got 1 diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap index daf1af653e73a6..771602ec42d5ce 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap @@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack ``` error: lint:invalid-assignment: Too many values to unpack - --> /src/mdtest_snippet.py:1:1 + --> src/mdtest_snippet.py:1:1 | 1 | a, b = (1, 2, 3) # error: [invalid-assignment] | ^^^^ --------- Got 3 diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap index 68a533a0b324d7..ab2fe8d044615e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap @@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack ``` error: lint:not-iterable: Object of type `Literal[1]` is not iterable - --> /src/mdtest_snippet.py:1:8 + --> src/mdtest_snippet.py:1:8 | 1 | a, b = 1 # error: [not-iterable] | ^ diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap index dc312682555609..834d4080e3ffab 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap @@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack ``` error: lint:invalid-assignment: Not enough values to unpack - --> /src/mdtest_snippet.py:1:1 + --> src/mdtest_snippet.py:1:1 | 1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment] | ^^^^^^^^^^^^^ ------ Got 2 diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap index 259d63ea91203e..45d5aea73a5c0f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap @@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso ``` error: lint:unresolved-import: Cannot resolve import `does_not_exist` - --> /src/mdtest_snippet.py:1:8 + --> src/mdtest_snippet.py:1:8 | 1 | import does_not_exist # error: [unresolved-import] | ^^^^^^^^^^^^^^ diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap index 0aa7c7109ec56a..051ba77378612c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap @@ -26,7 +26,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso ``` error: lint:unresolved-import: Module `a` has no member `does_not_exist` - --> /src/mdtest_snippet.py:1:28 + --> src/mdtest_snippet.py:1:28 | 1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import] | ^^^^^^^^^^^^^^ diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap index 7105afa48a1c4e..e311847c4e9ca6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap @@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso ``` error: lint:unresolved-import: Cannot resolve import `.does_not_exist` - --> /src/mdtest_snippet.py:1:7 + --> src/mdtest_snippet.py:1:7 | 1 | from .does_not_exist import add # error: [unresolved-import] | ^^^^^^^^^^^^^^ diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap index 42cc96a5fab335..3cf055505f52d0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap @@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso ``` error: lint:unresolved-import: Cannot resolve import `.does_not_exist.foo.bar` - --> /src/mdtest_snippet.py:1:7 + --> src/mdtest_snippet.py:1:7 | 1 | from .does_not_exist.foo.bar import add # error: [unresolved-import] | ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap index 4dd7179de9b6f0..88a68630078fdd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap @@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso ``` error: lint:unresolved-import: Cannot resolve import `does_not_exist` - --> /src/mdtest_snippet.py:1:6 + --> src/mdtest_snippet.py:1:6 | 1 | from does_not_exist import add # error: [unresolved-import] | ^^^^^^^^^^^^^^ diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap index 2d20320108721f..b088fc704fee74 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap @@ -33,7 +33,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso ``` error: lint:unresolved-import: Cannot resolve import `....foo` - --> /src/package/subpackage/subsubpackage/__init__.py:1:10 + --> src/package/subpackage/subsubpackage/__init__.py:1:10 | 1 | from ....foo import add # error: [unresolved-import] | ^^^ diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap index ca93c74cd0449a..c2745e3ce3d0b2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap @@ -25,7 +25,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupp ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` - --> /src/mdtest_snippet.py:7:8 + --> src/mdtest_snippet.py:7:8 | 6 | # error: [unsupported-bool-conversion] 7 | 10 and a and True diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap index 5db16801026396..ac326c9bf7fed1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap @@ -26,14 +26,14 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupp ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` - --> /src/mdtest_snippet.py:8:8 + --> src/mdtest_snippet.py:8:8 | 7 | # error: [unsupported-bool-conversion] 8 | 10 and a and True | ^ | info: `str` is not assignable to `bool` - --> /src/mdtest_snippet.py:2:9 + --> src/mdtest_snippet.py:2:9 | 1 | class NotBoolable: 2 | def __bool__(self) -> str: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap index 89c05b6193a58f..2aec49f346aa53 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap @@ -26,14 +26,14 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupp ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable` - --> /src/mdtest_snippet.py:8:8 + --> src/mdtest_snippet.py:8:8 | 7 | # error: [unsupported-bool-conversion] 8 | 10 and a and True | ^ | info: `__bool__` methods must only have a `self` parameter - --> /src/mdtest_snippet.py:2:9 + --> src/mdtest_snippet.py:2:9 | 1 | class NotBoolable: 2 | def __bool__(self, foo): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap index 113522db4985a9..9624f725271e20 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap @@ -33,7 +33,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupp ``` error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for union `NotBoolable1 | NotBoolable2 | NotBoolable3` because `NotBoolable1` doesn't implement `__bool__` correctly - --> /src/mdtest_snippet.py:15:8 + --> src/mdtest_snippet.py:15:8 | 14 | # error: [unsupported-bool-conversion] 15 | 10 and get() and True 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 index 5cdf0e883866d6..ab4887c2cb6bf1 100644 --- 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 @@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/versio ``` error: invalid-syntax - --> /src/mdtest_snippet.py:1:1 + --> 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) diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index f69980d0681f2e..56e3ea4eaaf691 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -10,6 +10,7 @@ use ruff_text_size::{TextRange, TextSize}; use crate::{ files::File, source::{line_index, source_text, SourceText}, + system::SystemPath, Db, }; @@ -604,7 +605,10 @@ impl<'a> FileResolver<'a> { /// Returns the path associated with the file given. fn path(&self, file: File) -> &'a str { - file.path(self.db).as_str() + relativize_path( + self.db.system().current_directory(), + file.path(self.db).as_str(), + ) } /// Returns the input contents associated with the file given. @@ -676,6 +680,14 @@ fn context_after(source: &SourceCode<'_, '_>, len: usize, start: OneIndexed) -> line } +/// Convert an absolute path to be relative to the current working directory. +fn relativize_path<'p>(cwd: &SystemPath, path: &'p str) -> &'p str { + if let Ok(path) = SystemPath::new(path).strip_prefix(cwd) { + return path.as_str(); + } + path +} + #[cfg(test)] mod tests { @@ -757,7 +769,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 3 | canary 4 | dog @@ -781,7 +793,7 @@ watermelon env.render(&diag), @r" warning: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 3 | canary 4 | dog @@ -801,7 +813,7 @@ watermelon env.render(&diag), @r" info: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 3 | canary 4 | dog @@ -828,7 +840,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + --> animals:1:1 | 1 | aardvark | ^ @@ -847,7 +859,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + --> animals:1:1 | 1 | aardvark | ^ primary annotation message @@ -868,7 +880,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /non-ascii:5:1 + --> non-ascii:5:1 | 3 | ΔΔΔΔΔΔΔΔΔΔΔΔ 4 | ββββββββββββ @@ -887,7 +899,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /non-ascii:2:2 + --> non-ascii:2:2 | 1 | ☃☃☃☃☃☃☃☃☃☃☃☃ 2 | 💩💩💩💩💩💩💩💩💩💩💩💩 @@ -911,7 +923,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 4 | dog 5 | elephant @@ -928,7 +940,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 5 | elephant | ^^^^^^^^ @@ -943,7 +955,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -960,7 +972,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:11:1 + --> animals:11:1 | 9 | inchworm 10 | jackrabbit @@ -977,7 +989,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 1 | aardvark 2 | beetle @@ -1010,14 +1022,14 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ 2 | beetle 3 | canary | - ::: /animals:11:1 + ::: animals:11:1 | 9 | inchworm 10 | jackrabbit @@ -1054,7 +1066,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -1079,7 +1091,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -1107,13 +1119,13 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ 2 | beetle | - ::: /animals:5:1 + ::: animals:5:1 | 4 | dog 5 | elephant @@ -1135,7 +1147,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -1160,7 +1172,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -1191,7 +1203,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:1:1 + --> animals:1:1 | 1 | aardvark | ^^^^^^^^ @@ -1199,7 +1211,7 @@ watermelon 3 | canary 4 | dog | - ::: /animals:9:1 + ::: animals:9:1 | 6 | finch 7 | gorilla @@ -1229,7 +1241,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /spacey-animals:8:1 + --> spacey-animals:8:1 | 7 | dog 8 | elephant @@ -1246,7 +1258,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /spacey-animals:12:1 + --> spacey-animals:12:1 | 11 | gorilla 12 | hippopotamus @@ -1264,7 +1276,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /spacey-animals:13:1 + --> spacey-animals:13:1 | 11 | gorilla 12 | hippopotamus @@ -1304,12 +1316,12 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /spacey-animals:3:1 + --> spacey-animals:3:1 | 3 | beetle | ^^^^^^ | - ::: /spacey-animals:5:1 + ::: spacey-animals:5:1 | 5 | canary | ^^^^^^ @@ -1333,7 +1345,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1342,7 +1354,7 @@ watermelon 4 | dog 5 | elephant | - ::: /fruits:3:1 + ::: fruits:3:1 | 1 | apple 2 | banana @@ -1370,7 +1382,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1407,7 +1419,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1435,7 +1447,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1445,7 +1457,7 @@ watermelon 5 | elephant | warning: sub-diagnostic message - --> /fruits:3:1 + --> fruits:3:1 | 1 | apple 2 | banana @@ -1471,7 +1483,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1481,7 +1493,7 @@ watermelon 5 | elephant | warning: sub-diagnostic message - --> /fruits:3:1 + --> fruits:3:1 | 1 | apple 2 | banana @@ -1491,7 +1503,7 @@ watermelon 5 | orange | warning: sub-diagnostic message - --> /animals:11:1 + --> animals:11:1 | 9 | inchworm 10 | jackrabbit @@ -1510,7 +1522,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1520,7 +1532,7 @@ watermelon 5 | elephant | warning: sub-diagnostic message - --> /animals:11:1 + --> animals:11:1 | 9 | inchworm 10 | jackrabbit @@ -1528,7 +1540,7 @@ watermelon | ^^^^^^^^ | warning: sub-diagnostic message - --> /fruits:3:1 + --> fruits:3:1 | 1 | apple 2 | banana @@ -1558,7 +1570,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:3:1 + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1568,7 +1580,7 @@ watermelon 5 | elephant | warning: sub-diagnostic message - --> /animals:3:1 + --> animals:3:1 | 1 | aardvark 2 | beetle @@ -1594,7 +1606,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 3 | canary 4 | dog @@ -1617,7 +1629,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 3 | canary 4 | dog @@ -1637,7 +1649,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 3 | canary 4 | dog @@ -1657,7 +1669,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:4 + --> animals:5:4 | 3 | canary 4 | dog @@ -1679,7 +1691,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:4 + --> animals:5:4 | 3 | canary 4 | dog @@ -1711,7 +1723,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:4:1 + --> animals:4:1 | 2 | beetle 3 | canary @@ -1740,7 +1752,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:4:1 + --> animals:4:1 | 2 | beetle 3 | canary @@ -1771,7 +1783,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 3 | canary 4 | dog @@ -1806,7 +1818,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 3 | canary 4 | dog @@ -1834,7 +1846,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 3 | canary 4 | dog @@ -1866,7 +1878,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:3 + --> animals:5:3 | 3 | canary 4 | dog @@ -1888,7 +1900,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:3 + --> animals:5:3 | 3 | canary 4 | dog @@ -1921,7 +1933,7 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:8:1 + --> animals:8:1 | 6 | finch 7 | gorilla @@ -1930,7 +1942,7 @@ watermelon 9 | inchworm 10 | jackrabbit | - ::: /animals:1:1 + ::: animals:1:1 | 1 | aardvark | -------- secondary @@ -1961,27 +1973,27 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:5:1 + --> animals:5:1 | 5 | elephant | ^^^^^^^^ primary 5 | - ::: /animals:9:1 + ::: animals:9:1 | 9 | inchworm | ^^^^^^^^ primary 9 | - ::: /animals:1:1 + ::: animals:1:1 | 1 | aardvark | -------- secondary 1 | - ::: /animals:3:1 + ::: animals:3:1 | 3 | canary | ------ secondary 3 | - ::: /animals:7:1 + ::: animals:7:1 | 7 | gorilla | ------- secondary 7 @@ -2005,14 +2017,14 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /fruits:1:1 + --> fruits:1:1 | 1 | apple | ^^^^^ primary 2 | banana 3 | cantelope | - ::: /animals:1:1 + ::: animals:1:1 | 1 | aardvark | -------- secondary @@ -2040,32 +2052,32 @@ watermelon env.render(&diag), @r" error: lint:test-diagnostic: main diagnostic message - --> /animals:11:1 + --> animals:11:1 | 11 | kangaroo | ^^^^^^^^ primary animals 11 | - ::: /animals:1:1 + ::: animals:1:1 | 1 | aardvark | -------- secondary animals 1 | - ::: /animals:3:1 + ::: animals:3:1 | 3 | canary | ------ secondary animals 3 | - ::: /animals:7:1 + ::: animals:7:1 | 7 | gorilla | ------- secondary animals 7 | - ::: /fruits:10:1 + ::: fruits:10:1 | 10 | watermelon | ^^^^^^^^^^ primary fruits 10 | - ::: /fruits:2:1 + ::: fruits:2:1 | 2 | banana | ------ secondary fruits 2 From 01a31c08f5b18b1d4c3b9452c8ff17aecf005830 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:57:36 -0400 Subject: [PATCH 0162/1161] Add config option to disable `typing_extensions` imports (#17611) Summary -- This PR resolves https://github.com/astral-sh/ruff/issues/9761 by adding a linter configuration option to disable `typing_extensions` imports. As mentioned [here], it would be ideal if we could detect whether or not `typing_extensions` is available as a dependency automatically, but this seems like a much easier fix in the meantime. The default for the new option, `typing-extensions`, is `true`, preserving the current behavior. Setting it to `false` will bail out of the new `Checker::typing_importer` method, which has been refactored from the `Checker::import_from_typing` method in https://github.com/astral-sh/ruff/pull/17340), with `None`, which is then handled specially by each rule that calls it. I considered some alternatives to a config option, such as checking if `typing_extensions` has been imported or checking for a `TYPE_CHECKING` block we could use, but I think defaulting to allowing `typing_extensions` imports and allowing the user to disable this with an option is both simple to implement and pretty intuitive. [here]: https://github.com/astral-sh/ruff/issues/9761#issuecomment-2790492853 Test Plan -- New linter tests exercising several combinations of Python versions and the new config option for PYI019. I also added tests for the other affected rules, but only in the case where the new config option is enabled. The rules' existing tests also cover the default case. --- crates/ruff/tests/lint.rs | 41 +++--- ...ow_settings__display_default_settings.snap | 1 + crates/ruff_linter/src/checkers/ast/mod.rs | 44 ++++-- crates/ruff_linter/src/linter.rs | 130 +++++++++++++++--- .../rules/fastapi_non_annotated_dependency.rs | 20 ++- .../src/rules/flake8_annotations/helpers.rs | 9 +- .../flake8_annotations/rules/definition.rs | 20 +++ .../rules/custom_type_var_for_self.rs | 18 ++- .../rules/duplicate_union_member.rs | 60 +++----- .../src/rules/flake8_pyi/rules/mod.rs | 45 ++++++ .../flake8_pyi/rules/non_self_return_type.rs | 30 ++-- .../rules/redundant_none_literal.rs | 10 +- .../rules/redundant_numeric_union.rs | 59 ++------ .../rules/flake8_pyi/rules/simple_defaults.rs | 17 ++- .../src/rules/ruff/rules/implicit_optional.rs | 8 +- crates/ruff_linter/src/settings/mod.rs | 3 + ...ed_typing_extensions_fast002_disabled.snap | 4 + ...ensions_pyi019_adds_typing_extensions.snap | 23 ++++ ..._adds_typing_with_extensions_disabled.snap | 23 ++++ ...ds_typing_without_extensions_disabled.snap | 23 ++++ ...pyi019_does_not_add_typing_extensions.snap | 4 + ...led_typing_extensions_pyi034_disabled.snap | 4 + ...typing_extensions_pyi_pyi026_disabled.snap | 4 + crates/ruff_workspace/src/configuration.rs | 4 + crates/ruff_workspace/src/options.rs | 19 +++ ruff.schema.json | 7 + 26 files changed, 470 insertions(+), 160 deletions(-) create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_fast002_disabled.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_extensions.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_with_extensions_disabled.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_without_extensions_disabled.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_does_not_add_typing_extensions.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi034_disabled.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi_pyi026_disabled.snap diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index d58e85a4686126..77dcc222e7ed1c 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -994,6 +994,7 @@ fn value_given_to_table_key_is_not_inline_table_2() { - `lint.extend-per-file-ignores` - `lint.exclude` - `lint.preview` + - `lint.typing-extensions` For more information, try '--help'. "); @@ -2117,7 +2118,7 @@ requires-python = ">= 3.11" .arg("test.py") .arg("-") .current_dir(project_dir) - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2207,6 +2208,7 @@ requires-python = ">= 3.11" XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -2390,7 +2392,7 @@ requires-python = ">= 3.11" analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -2428,7 +2430,7 @@ requires-python = ">= 3.11" .arg("test.py") .arg("-") .current_dir(project_dir) - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2518,6 +2520,7 @@ requires-python = ">= 3.11" XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -2701,7 +2704,7 @@ requires-python = ">= 3.11" analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -2790,7 +2793,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("test.py") .arg("--show-settings") - .current_dir(project_dir), @r###" + .current_dir(project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -2881,6 +2884,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -3064,7 +3068,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -3170,7 +3174,7 @@ from typing import Union;foo: Union[int, str] = 1 .arg("--show-settings") .args(["--select","UP007"]) .arg("foo/test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -3260,6 +3264,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -3443,7 +3448,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -3497,7 +3502,7 @@ from typing import Union;foo: Union[int, str] = 1 .arg("--show-settings") .args(["--select","UP007"]) .arg("foo/test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -3587,6 +3592,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -3770,7 +3776,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -3823,7 +3829,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("--show-settings") .arg("foo/test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -3914,6 +3920,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -4097,7 +4104,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); insta::with_settings!({ @@ -4107,7 +4114,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("--show-settings") .arg("test.py") - .current_dir(project_dir.join("foo")), @r###" + .current_dir(project_dir.join("foo")), @r#" success: true exit_code: 0 ----- stdout ----- @@ -4198,6 +4205,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -4381,7 +4389,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -4444,7 +4452,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("--show-settings") .arg("test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -4535,6 +4543,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -4718,7 +4727,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index c404d07c8cd1f8..37e8eae6bcac73 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -213,6 +213,7 @@ linter.task_tags = [ XXX, ] linter.typing_modules = [] +linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 6611d27fa23c57..b647d9ac00bf91 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -534,26 +534,50 @@ impl<'a> Checker<'a> { self.semantic_checker = checker; } - /// Attempt to create an [`Edit`] that imports `member`. + /// Create a [`TypingImporter`] that will import `member` from either `typing` or + /// `typing_extensions`. /// /// On Python <`version_added_to_typing`, `member` is imported from `typing_extensions`, while /// on Python >=`version_added_to_typing`, it is imported from `typing`. /// - /// See [`Importer::get_or_import_symbol`] for more details on the returned values. - pub(crate) fn import_from_typing( - &self, - member: &str, - position: TextSize, + /// If the Python version is less than `version_added_to_typing` but + /// `LinterSettings::typing_extensions` is `false`, this method returns `None`. + pub(crate) fn typing_importer<'b>( + &'b self, + member: &'b str, version_added_to_typing: PythonVersion, - ) -> Result<(Edit, String), ResolutionError> { + ) -> Option> { let source_module = if self.target_version() >= version_added_to_typing { "typing" + } else if !self.settings.typing_extensions { + return None; } else { "typing_extensions" }; - let request = ImportRequest::import_from(source_module, member); - self.importer() - .get_or_import_symbol(&request, position, self.semantic()) + Some(TypingImporter { + checker: self, + source_module, + member, + }) + } +} + +pub(crate) struct TypingImporter<'a, 'b> { + checker: &'a Checker<'b>, + source_module: &'static str, + member: &'a str, +} + +impl TypingImporter<'_, '_> { + /// Create an [`Edit`] that makes the requested symbol available at `position`. + /// + /// See [`Importer::get_or_import_symbol`] for more details on the returned values and + /// [`Checker::typing_importer`] for a way to construct a [`TypingImporter`]. + pub(crate) fn import(&self, position: TextSize) -> Result<(Edit, String), ResolutionError> { + let request = ImportRequest::import_from(self.source_module, self.member); + self.checker + .importer + .get_or_import_symbol(&request, position, self.checker.semantic()) } } diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 7f03f72e1a6e7a..00d07892dc261d 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -791,8 +791,9 @@ mod tests { use crate::linter::check_path; use crate::message::Message; use crate::registry::Rule; + use crate::settings::LinterSettings; use crate::source_kind::SourceKind; - use crate::test::{assert_notebook_path, test_contents, TestedNotebook}; + use crate::test::{assert_notebook_path, test_contents, test_snippet, TestedNotebook}; use crate::{assert_messages, directives, settings, Locator}; /// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory. @@ -811,7 +812,7 @@ mod tests { } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnsortedImports), + &LinterSettings::for_rule(Rule::UnsortedImports), )?; assert_messages!(messages, actual, source_notebook); Ok(()) @@ -828,7 +829,7 @@ mod tests { } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), )?; assert_messages!(messages, actual, source_notebook); Ok(()) @@ -845,7 +846,7 @@ mod tests { } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnusedVariable), + &LinterSettings::for_rule(Rule::UnusedVariable), )?; assert_messages!(messages, actual, source_notebook); Ok(()) @@ -862,7 +863,7 @@ mod tests { } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UndefinedName), + &LinterSettings::for_rule(Rule::UndefinedName), )?; assert_messages!(messages, actual, source_notebook); Ok(()) @@ -879,7 +880,7 @@ mod tests { } = assert_notebook_path( actual_path, &expected_path, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), )?; let mut writer = Vec::new(); fixed_notebook.write(&mut writer)?; @@ -900,7 +901,7 @@ mod tests { } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), )?; assert_messages!(messages, actual, source_notebook); Ok(()) @@ -930,7 +931,7 @@ mod tests { let (_, transformed) = test_contents( &source_kind, path, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), ); let linted_notebook = transformed.into_owned().expect_ipy_notebook(); let mut writer = Vec::new(); @@ -946,10 +947,7 @@ mod tests { /// Wrapper around `test_contents_syntax_errors` for testing a snippet of code instead of a /// file. - fn test_snippet_syntax_errors( - contents: &str, - settings: &settings::LinterSettings, - ) -> Vec { + fn test_snippet_syntax_errors(contents: &str, settings: &LinterSettings) -> Vec { let contents = dedent(contents); test_contents_syntax_errors( &SourceKind::Python(contents.to_string()), @@ -963,7 +961,7 @@ mod tests { fn test_contents_syntax_errors( source_kind: &SourceKind, path: &Path, - settings: &settings::LinterSettings, + settings: &LinterSettings, ) -> Vec { let source_type = PySourceType::from(path); let options = @@ -1032,7 +1030,7 @@ mod tests { let snapshot = format!("async_comprehension_in_sync_comprehension_{name}_{python_version}"); let messages = test_snippet_syntax_errors( contents, - &settings::LinterSettings { + &LinterSettings { rules: settings::rule_table::RuleTable::empty(), unresolved_target_version: python_version, preview: settings::types::PreviewMode::Enabled, @@ -1051,7 +1049,7 @@ mod tests { let messages = test_contents_syntax_errors( &SourceKind::IpyNotebook(Notebook::from_path(path)?), path, - &settings::LinterSettings { + &LinterSettings { unresolved_target_version: python_version, rules: settings::rule_table::RuleTable::empty(), preview: settings::types::PreviewMode::Enabled, @@ -1076,7 +1074,7 @@ mod tests { let messages = test_contents_syntax_errors( &SourceKind::Python(std::fs::read_to_string(&path)?), &path, - &settings::LinterSettings::for_rule(rule), + &LinterSettings::for_rule(rule), ); insta::with_settings!({filters => vec![(r"\\", "/")]}, { assert_messages!(snapshot, messages); @@ -1095,10 +1093,108 @@ mod tests { } = assert_notebook_path( path, path, - &settings::LinterSettings::for_rule(Rule::YieldOutsideFunction), + &LinterSettings::for_rule(Rule::YieldOutsideFunction), )?; assert_messages!(messages, path, source_notebook); Ok(()) } + + const PYI019_EXAMPLE: &str = r#" + from typing import TypeVar + + T = TypeVar("T", bound="_NiceReprEnum") + + class C: + def __new__(cls: type[T]) -> T: + return cls + "#; + + #[test_case( + "pyi019_adds_typing_extensions", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY310, + typing_extensions: true, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi019_does_not_add_typing_extensions", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY310, + typing_extensions: false, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi019_adds_typing_without_extensions_disabled", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY311, + typing_extensions: true, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi019_adds_typing_with_extensions_disabled", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY311, + typing_extensions: false, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi034_disabled", + " + class C: + def __new__(cls) -> C: ... + ", + &LinterSettings { + unresolved_target_version: PythonVersion { major: 3, minor: 10 }, + typing_extensions: false, + ..LinterSettings::for_rule(Rule::NonSelfReturnType) + } + )] + #[test_case( + "fast002_disabled", + r#" + from fastapi import Depends, FastAPI + + app = FastAPI() + + @app.get("/items/") + async def read_items(commons: dict = Depends(common_parameters)): + return commons + "#, + &LinterSettings { + unresolved_target_version: PythonVersion { major: 3, minor: 8 }, + typing_extensions: false, + ..LinterSettings::for_rule(Rule::FastApiNonAnnotatedDependency) + } + )] + fn test_disabled_typing_extensions(name: &str, contents: &str, settings: &LinterSettings) { + let snapshot = format!("disabled_typing_extensions_{name}"); + let messages = test_snippet(contents, settings); + assert_messages!(snapshot, messages); + } + + #[test_case( + "pyi026_disabled", + "Vector = list[float]", + &LinterSettings { + unresolved_target_version: PythonVersion { major: 3, minor: 9 }, + typing_extensions: false, + ..LinterSettings::for_rule(Rule::TypeAliasWithoutAnnotation) + } + )] + fn test_disabled_typing_extensions_pyi(name: &str, contents: &str, settings: &LinterSettings) { + let snapshot = format!("disabled_typing_extensions_pyi_{name}"); + let path = Path::new(".pyi"); + let contents = dedent(contents); + let messages = test_contents(&SourceKind::Python(contents.into_owned()), path, settings).0; + assert_messages!(snapshot, messages); + } } diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs index 9e537d398c8c56..f9873e860b29ad 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs @@ -59,6 +59,16 @@ use ruff_python_ast::PythonVersion; /// return commons /// ``` /// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.9, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` +/// /// [FastAPI documentation]: https://fastapi.tiangolo.com/tutorial/query-params-str-validations/?h=annotated#advantages-of-annotated /// [typing-annotated]: https://docs.python.org/3/library/typing.html#typing.Annotated /// [typing-extensions]: https://typing-extensions.readthedocs.io/en/stable/ @@ -223,6 +233,10 @@ fn create_diagnostic( dependency_call: Option, mut seen_default: bool, ) -> bool { + let Some(importer) = checker.typing_importer("Annotated", PythonVersion::PY39) else { + return seen_default; + }; + let mut diagnostic = Diagnostic::new( FastApiNonAnnotatedDependency { py_version: checker.target_version(), @@ -231,11 +245,7 @@ fn create_diagnostic( ); let try_generate_fix = || { - let (import_edit, binding) = checker.import_from_typing( - "Annotated", - parameter.range.start(), - PythonVersion::PY39, - )?; + let (import_edit, binding) = importer.import(parameter.range.start())?; // Each of these classes takes a single, optional default // argument, followed by kw-only arguments diff --git a/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs b/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs index 1adf672cfe85ec..3fb45451e237e0 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs @@ -131,7 +131,8 @@ impl AutoPythonType { "NoReturn" }; let (no_return_edit, binding) = checker - .import_from_typing(member, at, PythonVersion::lowest()) + .typing_importer(member, PythonVersion::lowest())? + .import(at) .ok()?; let expr = Expr::Name(ast::ExprName { id: Name::from(binding), @@ -169,7 +170,8 @@ impl AutoPythonType { // Ex) `Optional[int]` let (optional_edit, binding) = checker - .import_from_typing("Optional", at, PythonVersion::lowest()) + .typing_importer("Optional", PythonVersion::lowest())? + .import(at) .ok()?; let expr = typing_optional(element, Name::from(binding)); Some((expr, vec![optional_edit])) @@ -182,7 +184,8 @@ impl AutoPythonType { // Ex) `Union[int, str]` let (union_edit, binding) = checker - .import_from_typing("Union", at, PythonVersion::lowest()) + .typing_importer("Union", PythonVersion::lowest())? + .import(at) .ok()?; let expr = typing_union(&elements, Name::from(binding)); Some((expr, vec![union_edit])) diff --git a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs index 786877146c6e6a..e9560cfd93d5ff 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs @@ -224,6 +224,16 @@ impl Violation for MissingTypeCls { /// def add(a: int, b: int) -> int: /// return a + b /// ``` +/// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for some Python versions, +/// its diagnostic will not be emitted, and no fix will be offered, if `typing_extensions` imports +/// have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` #[derive(ViolationMetadata)] pub(crate) struct MissingReturnTypeUndocumentedPublicFunction { name: String, @@ -267,6 +277,16 @@ impl Violation for MissingReturnTypeUndocumentedPublicFunction { /// def _add(a: int, b: int) -> int: /// return a + b /// ``` +/// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for some Python versions, +/// its diagnostic will not be emitted, and no fix will be offered, if `typing_extensions` imports +/// have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` #[derive(ViolationMetadata)] pub(crate) struct MissingReturnTypePrivateFunction { name: String, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index e3b84a605a4ce1..cfadfcc97c6c4f 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -10,7 +10,7 @@ use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; use ruff_python_semantic::{Binding, ResolvedReference, ScopeId, SemanticModel}; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::ast::Checker; +use crate::checkers::ast::{Checker, TypingImporter}; use ruff_python_ast::PythonVersion; /// ## What it does @@ -70,6 +70,16 @@ use ruff_python_ast::PythonVersion; /// The fix is only marked as unsafe if there is the possibility that it might delete a comment /// from your code. /// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.11, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` +/// /// [PEP 673]: https://peps.python.org/pep-0673/#motivation /// [PEP-695]: https://peps.python.org/pep-0695/ /// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/ @@ -109,6 +119,7 @@ pub(crate) fn custom_type_var_instead_of_self( let semantic = checker.semantic(); let current_scope = &semantic.scopes[binding.scope]; let function_def = binding.statement(semantic)?.as_function_def_stmt()?; + let importer = checker.typing_importer("Self", PythonVersion::PY311)?; let ast::StmtFunctionDef { name: function_name, @@ -178,6 +189,7 @@ pub(crate) fn custom_type_var_instead_of_self( diagnostic.try_set_fix(|| { replace_custom_typevar_with_self( checker, + &importer, function_def, custom_typevar, self_or_cls_parameter, @@ -310,14 +322,14 @@ fn custom_typevar<'a>( /// * If it was a PEP-695 type variable, removes that `TypeVar` from the PEP-695 type-parameter list fn replace_custom_typevar_with_self( checker: &Checker, + importer: &TypingImporter, function_def: &ast::StmtFunctionDef, custom_typevar: TypeVar, self_or_cls_parameter: &ast::ParameterWithDefault, self_or_cls_annotation: &ast::Expr, ) -> anyhow::Result { // (1) Import `Self` (if necessary) - let (import_edit, self_symbol_binding) = - checker.import_from_typing("Self", function_def.start(), PythonVersion::PY311)?; + let (import_edit, self_symbol_binding) = importer.import(function_def.start())?; // (2) Remove the first parameter's annotation let mut other_edits = vec![Edit::deletion( diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs index 2ddc53ba1e55cd..0c11167a91f8b2 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs @@ -1,21 +1,18 @@ use std::collections::HashSet; -use anyhow::Result; - use rustc_hash::FxHashSet; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::name::Name; -use ruff_python_ast::{ - Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, Operator, PythonVersion, -}; +use ruff_python_ast::{Expr, ExprBinOp, Operator, PythonVersion}; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use super::generate_union_fix; + /// ## What it does /// Checks for duplicate union members. /// @@ -118,7 +115,19 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) { applicability, )), UnionKind::TypingUnion => { - generate_union_fix(checker, unique_nodes, expr, applicability).ok() + // Request `typing.Union` + let Some(importer) = checker.typing_importer("Union", PythonVersion::lowest()) + else { + return; + }; + generate_union_fix( + checker.generator(), + &importer, + unique_nodes, + expr, + applicability, + ) + .ok() } } }; @@ -171,40 +180,3 @@ fn generate_pep604_fix( applicability, ) } - -/// Generate a [`Fix`] for two or more type expressions, e.g. `typing.Union[int, float, complex]`. -fn generate_union_fix( - checker: &Checker, - nodes: Vec<&Expr>, - annotation: &Expr, - applicability: Applicability, -) -> Result { - debug_assert!(nodes.len() >= 2, "At least two nodes required"); - - // Request `typing.Union` - let (import_edit, binding) = - checker.import_from_typing("Union", annotation.start(), PythonVersion::lowest())?; - - // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` - let new_expr = Expr::Subscript(ExprSubscript { - range: TextRange::default(), - value: Box::new(Expr::Name(ExprName { - id: Name::new(binding), - ctx: ExprContext::Store, - range: TextRange::default(), - })), - slice: Box::new(Expr::Tuple(ExprTuple { - elts: nodes.into_iter().cloned().collect(), - range: TextRange::default(), - ctx: ExprContext::Load, - parenthesized: false, - })), - ctx: ExprContext::Load, - }); - - Ok(Fix::applicable_edits( - Edit::range_replacement(checker.generator().expr(&new_expr), annotation.range()), - [import_edit], - applicability, - )) -} diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs index 25be942f197c65..ca7aadec7ffc15 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs @@ -1,5 +1,14 @@ use std::fmt; +use anyhow::Result; + +use ruff_diagnostics::{Applicability, Edit, Fix}; +use ruff_python_ast::{name::Name, Expr, ExprContext, ExprName, ExprSubscript, ExprTuple}; +use ruff_python_codegen::Generator; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::TypingImporter; + pub(crate) use any_eq_ne_annotation::*; pub(crate) use bad_generator_return_type::*; pub(crate) use bad_version_info_comparison::*; @@ -108,3 +117,39 @@ impl fmt::Display for TypingModule { fmt.write_str(self.as_str()) } } + +/// Generate a [`Fix`] for two or more type expressions, e.g. `typing.Union[int, float, complex]`. +fn generate_union_fix( + generator: Generator, + importer: &TypingImporter, + nodes: Vec<&Expr>, + annotation: &Expr, + applicability: Applicability, +) -> Result { + debug_assert!(nodes.len() >= 2, "At least two nodes required"); + + let (import_edit, binding) = importer.import(annotation.start())?; + + // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` + let new_expr = Expr::Subscript(ExprSubscript { + range: TextRange::default(), + value: Box::new(Expr::Name(ExprName { + id: Name::new(binding), + ctx: ExprContext::Store, + range: TextRange::default(), + })), + slice: Box::new(Expr::Tuple(ExprTuple { + elts: nodes.into_iter().cloned().collect(), + range: TextRange::default(), + ctx: ExprContext::Load, + parenthesized: false, + })), + ctx: ExprContext::Load, + }); + + Ok(Fix::applicable_edits( + Edit::range_replacement(generator.expr(&new_expr), annotation.range()), + [import_edit], + applicability, + )) +} diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index 1eda92b7ee43b2..838c0154544a28 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -1,4 +1,4 @@ -use crate::checkers::ast::Checker; +use crate::checkers::ast::{Checker, TypingImporter}; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast as ast; @@ -75,6 +75,16 @@ use ruff_text_size::Ranged; /// ## Fix safety /// This rule's fix is marked as unsafe as it changes the meaning of your type annotations. /// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.11, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` +/// /// ## References /// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) #[derive(ViolationMetadata)] @@ -192,6 +202,10 @@ fn add_diagnostic( class_def: &ast::StmtClassDef, method_name: &str, ) { + let Some(importer) = checker.typing_importer("Self", PythonVersion::PY311) else { + return; + }; + let mut diagnostic = Diagnostic::new( NonSelfReturnType { class_name: class_def.name.to_string(), @@ -200,21 +214,21 @@ fn add_diagnostic( stmt.identifier(), ); - diagnostic.try_set_fix(|| replace_with_self_fix(checker, stmt, returns, class_def)); + diagnostic.try_set_fix(|| { + replace_with_self_fix(checker.semantic(), &importer, stmt, returns, class_def) + }); checker.report_diagnostic(diagnostic); } fn replace_with_self_fix( - checker: &Checker, + semantic: &SemanticModel, + importer: &TypingImporter, stmt: &ast::Stmt, returns: &ast::Expr, class_def: &ast::StmtClassDef, ) -> anyhow::Result { - let semantic = checker.semantic(); - - let (self_import, self_binding) = - checker.import_from_typing("Self", returns.start(), PythonVersion::PY311)?; + let (self_import, self_binding) = importer.import(returns.start())?; let mut others = Vec::with_capacity(2); @@ -230,7 +244,7 @@ fn replace_with_self_fix( others.extend(remove_first_argument_type_hint()); others.push(Edit::range_replacement(self_binding, returns.range())); - let applicability = if might_be_generic(class_def, checker.semantic()) { + let applicability = if might_be_generic(class_def, semantic) { Applicability::DisplayOnly } else { Applicability::Unsafe diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 07ca4a96c2662e..5a918c41b692ab 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -222,11 +222,11 @@ fn create_fix( let fix = match union_kind { UnionKind::TypingOptional => { - let (import_edit, bound_name) = checker.import_from_typing( - "Optional", - literal_expr.start(), - PythonVersion::lowest(), - )?; + let Some(importer) = checker.typing_importer("Optional", PythonVersion::lowest()) + else { + return Ok(None); + }; + let (import_edit, bound_name) = importer.import(literal_expr.start())?; let optional_expr = typing_optional(new_literal_expr, Name::from(bound_name)); let content = checker.generator().expr(&optional_expr); let optional_edit = Edit::range_replacement(content, literal_expr.range()); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs index 2f196c2cafb370..d8454b405b5ce4 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs @@ -1,18 +1,15 @@ use bitflags::bitflags; -use anyhow::Result; - use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{ - name::Name, AnyParameterRef, Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, - Operator, Parameters, PythonVersion, -}; +use ruff_python_ast::{AnyParameterRef, Expr, ExprBinOp, Operator, Parameters, PythonVersion}; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use super::generate_union_fix; + /// ## What it does /// Checks for parameter annotations that contain redundant unions between /// builtin numeric types (e.g., `int | float`). @@ -157,7 +154,18 @@ fn check_annotation<'a>(checker: &Checker, annotation: &'a Expr) { applicability, )), UnionKind::TypingUnion => { - generate_union_fix(checker, necessary_nodes, annotation, applicability).ok() + let Some(importer) = checker.typing_importer("Union", PythonVersion::lowest()) + else { + return; + }; + generate_union_fix( + checker.generator(), + &importer, + necessary_nodes, + annotation, + applicability, + ) + .ok() } } }; @@ -257,40 +265,3 @@ fn generate_pep604_fix( applicability, ) } - -/// Generate a [`Fix`] for two or more type expressions, e.g. `typing.Union[int, float, complex]`. -fn generate_union_fix( - checker: &Checker, - nodes: Vec<&Expr>, - annotation: &Expr, - applicability: Applicability, -) -> Result { - debug_assert!(nodes.len() >= 2, "At least two nodes required"); - - // Request `typing.Union` - let (import_edit, binding) = - checker.import_from_typing("Optional", annotation.start(), PythonVersion::lowest())?; - - // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` - let new_expr = Expr::Subscript(ExprSubscript { - range: TextRange::default(), - value: Box::new(Expr::Name(ExprName { - id: Name::new(binding), - ctx: ExprContext::Store, - range: TextRange::default(), - })), - slice: Box::new(Expr::Tuple(ExprTuple { - elts: nodes.into_iter().cloned().collect(), - range: TextRange::default(), - ctx: ExprContext::Load, - parenthesized: false, - })), - ctx: ExprContext::Load, - }); - - Ok(Fix::applicable_edits( - Edit::range_replacement(checker.generator().expr(&new_expr), annotation.range()), - [import_edit], - applicability, - )) -} diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs index 3a2c05e4d16266..11719dedcb21e9 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -217,6 +217,16 @@ impl Violation for UnassignedSpecialVariableInStub { /// /// Vector: TypeAlias = list[float] /// ``` +/// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.10, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` #[derive(ViolationMetadata)] pub(crate) struct TypeAliasWithoutAnnotation { module: TypingModule, @@ -672,6 +682,10 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar TypingModule::TypingExtensions }; + let Some(importer) = checker.typing_importer("TypeAlias", PythonVersion::PY310) else { + return; + }; + let mut diagnostic = Diagnostic::new( TypeAliasWithoutAnnotation { module, @@ -681,8 +695,7 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar target.range(), ); diagnostic.try_set_fix(|| { - let (import_edit, binding) = - checker.import_from_typing("TypeAlias", target.start(), PythonVersion::PY310)?; + let (import_edit, binding) = importer.import(target.start())?; Ok(Fix::safe_edits( Edit::range_replacement(format!("{id}: {binding}"), target.range()), [import_edit], diff --git a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs index c59a94b1a198f5..e1a9d6f7bd9203 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs @@ -1,6 +1,6 @@ use std::fmt; -use anyhow::Result; +use anyhow::{Context, Result}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; @@ -136,8 +136,10 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) ))) } ConversionType::Optional => { - let (import_edit, binding) = - checker.import_from_typing("Optional", expr.start(), PythonVersion::lowest())?; + let importer = checker + .typing_importer("Optional", PythonVersion::lowest()) + .context("Optional should be available on all supported Python versions")?; + let (import_edit, binding) = importer.import(expr.start())?; let new_expr = Expr::Subscript(ast::ExprSubscript { range: TextRange::default(), value: Box::new(Expr::Name(ast::ExprName { diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index e4aff6e74908ff..6b9c19e8a918ab 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -250,6 +250,7 @@ pub struct LinterSettings { pub line_length: LineLength, pub task_tags: Vec, pub typing_modules: Vec, + pub typing_extensions: bool, // Plugins pub flake8_annotations: flake8_annotations::settings::Settings, @@ -313,6 +314,7 @@ impl Display for LinterSettings { self.line_length, self.task_tags | array, self.typing_modules | array, + self.typing_extensions, ] } writeln!(f, "\n# Linter Plugins")?; @@ -450,6 +452,7 @@ impl LinterSettings { preview: PreviewMode::default(), explicit_preview_rules: false, extension: ExtensionMapping::default(), + typing_extensions: true, } } diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_fast002_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_fast002_disabled.snap new file mode 100644 index 00000000000000..4ba33c756c4946 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_fast002_disabled.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_extensions.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_extensions.snap new file mode 100644 index 00000000000000..b2537598f04a1e --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_extensions.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:7:13: PYI019 [*] Use `Self` instead of custom TypeVar `T` + | +6 | class C: +7 | def __new__(cls: type[T]) -> T: + | ^^^^^^^^^^^^^^^^^^^ PYI019 +8 | return cls + | + = help: Replace TypeVar `T` with `Self` + +ℹ Safe fix +1 1 | +2 2 | from typing import TypeVar + 3 |+from typing_extensions import Self +3 4 | +4 5 | T = TypeVar("T", bound="_NiceReprEnum") +5 6 | +6 7 | class C: +7 |- def __new__(cls: type[T]) -> T: + 8 |+ def __new__(cls) -> Self: +8 9 | return cls diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_with_extensions_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_with_extensions_disabled.snap new file mode 100644 index 00000000000000..0dee5cdff1f70c --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_with_extensions_disabled.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:7:13: PYI019 [*] Use `Self` instead of custom TypeVar `T` + | +6 | class C: +7 | def __new__(cls: type[T]) -> T: + | ^^^^^^^^^^^^^^^^^^^ PYI019 +8 | return cls + | + = help: Replace TypeVar `T` with `Self` + +ℹ Safe fix +1 1 | +2 |-from typing import TypeVar + 2 |+from typing import TypeVar, Self +3 3 | +4 4 | T = TypeVar("T", bound="_NiceReprEnum") +5 5 | +6 6 | class C: +7 |- def __new__(cls: type[T]) -> T: + 7 |+ def __new__(cls) -> Self: +8 8 | return cls diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_without_extensions_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_without_extensions_disabled.snap new file mode 100644 index 00000000000000..0dee5cdff1f70c --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_without_extensions_disabled.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:7:13: PYI019 [*] Use `Self` instead of custom TypeVar `T` + | +6 | class C: +7 | def __new__(cls: type[T]) -> T: + | ^^^^^^^^^^^^^^^^^^^ PYI019 +8 | return cls + | + = help: Replace TypeVar `T` with `Self` + +ℹ Safe fix +1 1 | +2 |-from typing import TypeVar + 2 |+from typing import TypeVar, Self +3 3 | +4 4 | T = TypeVar("T", bound="_NiceReprEnum") +5 5 | +6 6 | class C: +7 |- def __new__(cls: type[T]) -> T: + 7 |+ def __new__(cls) -> Self: +8 8 | return cls diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_does_not_add_typing_extensions.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_does_not_add_typing_extensions.snap new file mode 100644 index 00000000000000..4ba33c756c4946 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_does_not_add_typing_extensions.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi034_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi034_disabled.snap new file mode 100644 index 00000000000000..4ba33c756c4946 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi034_disabled.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi_pyi026_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi_pyi026_disabled.snap new file mode 100644 index 00000000000000..4ba33c756c4946 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi_pyi026_disabled.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 260f4edc689869..e311a0bf85fc24 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -430,6 +430,7 @@ impl Configuration { .ruff .map(RuffOptions::into_settings) .unwrap_or_default(), + typing_extensions: lint.typing_extensions.unwrap_or(true), }, formatter, @@ -633,6 +634,7 @@ pub struct LintConfiguration { pub logger_objects: Option>, pub task_tags: Option>, pub typing_modules: Option>, + pub typing_extensions: Option, // Plugins pub flake8_annotations: Option, @@ -746,6 +748,7 @@ impl LintConfiguration { task_tags: options.common.task_tags, logger_objects: options.common.logger_objects, typing_modules: options.common.typing_modules, + typing_extensions: options.typing_extensions, // Plugins flake8_annotations: options.common.flake8_annotations, @@ -1170,6 +1173,7 @@ impl LintConfiguration { pylint: self.pylint.combine(config.pylint), pyupgrade: self.pyupgrade.combine(config.pyupgrade), ruff: self.ruff.combine(config.ruff), + typing_extensions: self.typing_extensions, } } } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index fa83cd684d9b31..6778c2682e24e9 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -513,6 +513,22 @@ pub struct LintOptions { "# )] pub preview: Option, + + /// Whether to allow imports from the third-party `typing_extensions` module for Python versions + /// before a symbol was added to the first-party `typing` module. + /// + /// Many rules try to import symbols from the `typing` module but fall back to + /// `typing_extensions` for earlier versions of Python. This option can be used to disable this + /// fallback behavior in cases where `typing_extensions` is not installed. + #[option( + default = "true", + value_type = "bool", + example = r#" + # Disable `typing_extensions` imports + typing-extensions = false + "# + )] + pub typing_extensions: Option, } /// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`]. @@ -3876,6 +3892,7 @@ pub struct LintOptionsWire { pydoclint: Option, ruff: Option, preview: Option, + typing_extensions: Option, } impl From for LintOptions { @@ -3930,6 +3947,7 @@ impl From for LintOptions { pydoclint, ruff, preview, + typing_extensions, } = value; LintOptions { @@ -3985,6 +4003,7 @@ impl From for LintOptions { pydoclint, ruff, preview, + typing_extensions, } } } diff --git a/ruff.schema.json b/ruff.schema.json index cbe8fa22cb282a..096df71618c5f1 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2459,6 +2459,13 @@ "type": "string" } }, + "typing-extensions": { + "description": "Whether to allow imports from the third-party `typing_extensions` module for Python versions before a symbol was added to the first-party `typing` module.\n\nMany rules try to import symbols from the `typing` module but fall back to `typing_extensions` for earlier versions of Python. This option can be used to disable this fallback behavior in cases where `typing_extensions` is not installed.", + "type": [ + "boolean", + "null" + ] + }, "typing-modules": { "description": "A list of modules whose exports should be treated equivalently to members of the `typing` module.\n\nThis is useful for ensuring proper type annotation inference for projects that re-export `typing` and `typing_extensions` members from a compatibility module. If omitted, any members imported from modules apart from `typing` and `typing_extensions` will be treated as ordinary Python objects.", "type": [ From f0868ac0c9e22b3aa103f439a1ed88ca68b1e71c Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 28 Apr 2025 21:21:53 +0200 Subject: [PATCH 0163/1161] [red-knot] Revert blanket `clippy::too_many_arguments` allow (#17688) ## Summary Now that https://github.com/salsa-rs/salsa/issues/808 has been fixed, we can revert this global change in `Cargo.toml`. --- Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7aa260874976ae..67b5bbc0ff2387 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -231,10 +231,6 @@ unused_peekable = "warn" # Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved. large_stack_arrays = "allow" -# Salsa generates functions with parameters for each field of a `salsa::interned` struct. -# If we don't allow this, we get warnings for structs with too many fields. -too_many_arguments = "allow" - [profile.release] # Note that we set these explicitly, and these values # were chosen based on a trade-off between compile times From 504fa20057da414de66446bd832c2cd850cb8d8d Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Tue, 29 Apr 2025 05:35:17 +0900 Subject: [PATCH 0164/1161] [`airflow`] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR302`) (#17553) ## Summary Apply auto fixes to cases where the names have changed in Airflow 3 in AIR302 and split the huge test cases into different test cases based on proivder ## Test Plan the test cases has been split into multiple for easier checking --- .../resources/test/fixtures/airflow/AIR302.py | 481 ----- .../test/fixtures/airflow/AIR302_amazon.py | 39 + .../test/fixtures/airflow/AIR302_celery.py | 12 + .../fixtures/airflow/AIR302_common_sql.py | 129 ++ .../fixtures/airflow/AIR302_daskexecutor.py | 5 + .../test/fixtures/airflow/AIR302_docker.py | 7 + .../test/fixtures/airflow/AIR302_druid.py | 16 + .../test/fixtures/airflow/AIR302_fab.py | 55 + .../test/fixtures/airflow/AIR302_hdfs.py | 7 + .../test/fixtures/airflow/AIR302_hive.py | 66 + .../test/fixtures/airflow/AIR302_http.py | 9 + .../test/fixtures/airflow/AIR302_jdbc.py | 9 + .../fixtures/airflow/AIR302_kubernetes.py | 117 ++ .../test/fixtures/airflow/AIR302_mysql.py | 11 + .../test/fixtures/airflow/AIR302_oracle.py | 5 + .../test/fixtures/airflow/AIR302_papermill.py | 5 + .../test/fixtures/airflow/AIR302_pig.py | 7 + .../test/fixtures/airflow/AIR302_postgres.py | 7 + .../test/fixtures/airflow/AIR302_presto.py | 5 + .../test/fixtures/airflow/AIR302_samba.py | 5 + .../test/fixtures/airflow/AIR302_slack.py | 8 + .../test/fixtures/airflow/AIR302_smtp.py | 9 + .../test/fixtures/airflow/AIR302_sqlite.py | 5 + .../test/fixtures/airflow/AIR302_standard.py | 49 + .../test/fixtures/airflow/AIR302_zendesk.py | 5 + .../ruff_linter/src/rules/airflow/helpers.rs | 12 +- crates/ruff_linter/src/rules/airflow/mod.rs | 24 +- .../airflow/rules/moved_to_provider_in_3.rs | 1194 +++++++----- .../suggested_to_move_to_provider_in_3.rs | 19 +- ...les__airflow__tests__AIR302_AIR302.py.snap | 1646 ----------------- ...rflow__tests__AIR302_AIR302_amazon.py.snap | 113 ++ ...rflow__tests__AIR302_AIR302_celery.py.snap | 31 + ...w__tests__AIR302_AIR302_common_sql.py.snap | 352 ++++ ..._tests__AIR302_AIR302_daskexecutor.py.snap | 11 + ...irflow__tests__AIR302_AIR302_druid.py.snap | 40 + ..._airflow__tests__AIR302_AIR302_fab.py.snap | 190 ++ ...airflow__tests__AIR302_AIR302_hdfs.py.snap | 20 + ...airflow__tests__AIR302_AIR302_hive.py.snap | 208 +++ ...airflow__tests__AIR302_AIR302_http.py.snap | 42 + ...airflow__tests__AIR302_AIR302_jdbc.py.snap | 20 + ...w__tests__AIR302_AIR302_kubernetes.py.snap | 599 ++++++ ...irflow__tests__AIR302_AIR302_mysql.py.snap | 31 + ...rflow__tests__AIR302_AIR302_oracle.py.snap | 11 + ...ow__tests__AIR302_AIR302_papermill.py.snap | 11 + ..._airflow__tests__AIR302_AIR302_pig.py.snap | 20 + ...low__tests__AIR302_AIR302_postgres.py.snap | 19 + ...rflow__tests__AIR302_AIR302_presto.py.snap | 11 + ...irflow__tests__AIR302_AIR302_samba.py.snap | 11 + ...irflow__tests__AIR302_AIR302_slack.py.snap | 31 + ...airflow__tests__AIR302_AIR302_smtp.py.snap | 22 + ...rflow__tests__AIR302_AIR302_sqlite.py.snap | 11 + ...low__tests__AIR302_AIR302_standard.py.snap | 157 ++ ...flow__tests__AIR302_AIR302_zendesk.py.snap | 11 + 53 files changed, 3302 insertions(+), 2638 deletions(-) delete mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_amazon.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_celery.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_common_sql.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_daskexecutor.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_docker.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_druid.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_fab.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hdfs.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hive.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_http.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_jdbc.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_kubernetes.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_mysql.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_oracle.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_papermill.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_pig.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_postgres.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_presto.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_samba.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_slack.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_smtp.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_sqlite.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_standard.py create mode 100644 crates/ruff_linter/resources/test/fixtures/airflow/AIR302_zendesk.py delete mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_amazon.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_celery.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_common_sql.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_daskexecutor.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_druid.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_fab.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hdfs.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hive.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_http.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_jdbc.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_kubernetes.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_mysql.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_oracle.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_papermill.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_pig.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_postgres.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_presto.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_samba.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_slack.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_smtp.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_sqlite.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_standard.py.snap create mode 100644 crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_zendesk.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py deleted file mode 100644 index 44d99bba80bd1a..00000000000000 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302.py +++ /dev/null @@ -1,481 +0,0 @@ -from __future__ import annotations - -from airflow.api.auth.backend import basic_auth, kerberos_auth -from airflow.api.auth.backend.basic_auth import auth_current_user -from airflow.auth.managers.fab.api.auth.backend import ( - kerberos_auth as backend_kerberos_auth, -) -from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager -from airflow.auth.managers.fab.security_manager import override as fab_override -from airflow.config_templates.default_celery import DEFAULT_CELERY_CONFIG -from airflow.executors.celery_executor import CeleryExecutor, app -from airflow.executors.celery_kubernetes_executor import CeleryKubernetesExecutor -from airflow.executors.dask_executor import DaskExecutor -from airflow.executors.kubernetes_executor_types import ( - ALL_NAMESPACES, - POD_EXECUTOR_DONE_KEY, -) -from airflow.hooks.dbapi import ConnectorProtocol, DbApiHook -from airflow.hooks.dbapi_hook import DbApiHook as DbApiHook2 -from airflow.hooks.docker_hook import DockerHook -from airflow.hooks.druid_hook import DruidDbApiHook, DruidHook -from airflow.hooks.filesystem import FSHook -from airflow.hooks.hive_hooks import ( - HIVE_QUEUE_PRIORITIES, - HiveCliHook, - HiveMetastoreHook, - HiveServer2Hook, -) -from airflow.hooks.http_hook import HttpHook -from airflow.hooks.jdbc_hook import JdbcHook, jaydebeapi -from airflow.hooks.mssql_hook import MsSqlHook -from airflow.hooks.mysql_hook import MySqlHook -from airflow.hooks.oracle_hook import OracleHook -from airflow.hooks.package_index import PackageIndexHook -from airflow.hooks.pig_hook import PigCliHook -from airflow.hooks.postgres_hook import PostgresHook -from airflow.hooks.presto_hook import PrestoHook -from airflow.hooks.S3_hook import S3Hook, provide_bucket_name -from airflow.hooks.samba_hook import SambaHook -from airflow.hooks.slack_hook import SlackHook -from airflow.hooks.sqlite_hook import SqliteHook -from airflow.hooks.subprocess import SubprocessHook, SubprocessResult, working_directory -from airflow.hooks.webhdfs_hook import WebHDFSHook -from airflow.hooks.zendesk_hook import ZendeskHook -from airflow.kubernetes.k8s_model import K8SModel, append_to_pod -from airflow.kubernetes.kube_client import ( - _disable_verify_ssl, - _enable_tcp_keepalive, - get_kube_client, -) -from airflow.kubernetes.kubernetes_helper_functions import ( - add_pod_suffix, - annotations_for_logging_task_metadata, - annotations_to_key, - create_pod_id, - get_logs_task_metadata, - rand_str, -) -from airflow.kubernetes.pod import Port, Resources -from airflow.kubernetes.pod_generator import ( - PodDefaults, - PodGenerator, - PodGeneratorDeprecated, - datetime_to_label_safe_datestring, - extend_object_field, - label_safe_datestring_to_datetime, - make_safe_label_value, - merge_objects, -) -from airflow.kubernetes.pod_generator import ( - add_pod_suffix as add_pod_suffix2, -) -from airflow.kubernetes.pod_generator import ( - rand_str as rand_str2, -) -from airflow.kubernetes.pod_generator_deprecated import ( - PodDefaults as PodDefaults3, -) -from airflow.kubernetes.pod_generator_deprecated import ( - PodGenerator as PodGenerator2, -) -from airflow.kubernetes.pod_generator_deprecated import ( - make_safe_label_value as make_safe_label_value2, -) -from airflow.kubernetes.pod_launcher import PodLauncher, PodStatus -from airflow.kubernetes.pod_launcher_deprecated import ( - PodDefaults as PodDefaults2, -) -from airflow.kubernetes.pod_launcher_deprecated import ( - PodLauncher as PodLauncher2, -) -from airflow.kubernetes.pod_launcher_deprecated import ( - PodStatus as PodStatus2, -) -from airflow.kubernetes.pod_launcher_deprecated import ( - get_kube_client as get_kube_client2, -) -from airflow.kubernetes.pod_runtime_info_env import PodRuntimeInfoEnv -from airflow.kubernetes.secret import K8SModel2, Secret -from airflow.kubernetes.volume import Volume -from airflow.kubernetes.volume_mount import VolumeMount -from airflow.macros.hive import closest_ds_partition, max_partition -from airflow.operators.bash import BashOperator -from airflow.operators.bash_operator import BashOperator as LegacyBashOperator -from airflow.operators.check_operator import ( - CheckOperator, - IntervalCheckOperator, - SQLCheckOperator, - SQLIntervalCheckOperator, - SQLThresholdCheckOperator, - SQLValueCheckOperator, - ThresholdCheckOperator, - ValueCheckOperator, -) -from airflow.operators.datetime import BranchDateTimeOperator, target_times_as_dates -from airflow.operators.docker_operator import DockerOperator -from airflow.operators.druid_check_operator import DruidCheckOperator -from airflow.operators.dummy import DummyOperator, EmptyOperator -from airflow.operators.email import EmailOperator -from airflow.operators.email_operator import EmailOperator -from airflow.operators.gcs_to_s3 import GCSToS3Operator -from airflow.operators.google_api_to_s3_transfer import ( - GoogleApiToS3Operator, - GoogleApiToS3Transfer, -) -from airflow.operators.hive_operator import HiveOperator -from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator -from airflow.operators.hive_to_druid import HiveToDruidOperator, HiveToDruidTransfer -from airflow.operators.hive_to_mysql import HiveToMySqlOperator, HiveToMySqlTransfer -from airflow.operators.hive_to_samba_operator import HiveToSambaOperator -from airflow.operators.http_operator import SimpleHttpOperator -from airflow.operators.jdbc_operator import JdbcOperator -from airflow.operators.mssql_operator import MsSqlOperator -from airflow.operators.mssql_to_hive import MsSqlToHiveOperator, MsSqlToHiveTransfer -from airflow.operators.mysql_operator import MySqlOperator -from airflow.operators.mysql_to_hive import MySqlToHiveOperator, MySqlToHiveTransfer -from airflow.operators.oracle_operator import OracleOperator -from airflow.operators.papermill_operator import PapermillOperator -from airflow.operators.pig_operator import PigOperator -from airflow.operators.postgres_operator import Mapping, PostgresOperator -from airflow.operators.presto_check_operator import ( - CheckOperator as SQLCheckOperator2, -) -from airflow.operators.presto_check_operator import ( - IntervalCheckOperator as SQLIntervalCheckOperator2, -) -from airflow.operators.presto_check_operator import ( - PrestoCheckOperator, - PrestoIntervalCheckOperator, - PrestoValueCheckOperator, -) -from airflow.operators.presto_check_operator import ( - ValueCheckOperator as SQLValueCheckOperator2, -) -from airflow.operators.presto_to_mysql import ( - PrestoToMySqlOperator, - PrestoToMySqlTransfer, -) -from airflow.operators.python import ( - BranchPythonOperator, - PythonOperator, - PythonVirtualenvOperator, - ShortCircuitOperator, -) -from airflow.operators.redshift_to_s3_operator import ( - RedshiftToS3Operator, - RedshiftToS3Transfer, -) -from airflow.operators.s3_file_transform_operator import S3FileTransformOperator -from airflow.operators.s3_to_hive_operator import S3ToHiveOperator, S3ToHiveTransfer -from airflow.operators.s3_to_redshift_operator import ( - S3ToRedshiftOperator, - S3ToRedshiftTransfer, -) -from airflow.operators.slack_operator import SlackAPIOperator, SlackAPIPostOperator -from airflow.operators.sql import ( - BaseSQLOperator, - BranchSQLOperator, - SQLTableCheckOperator, - _convert_to_float_if_possible, - parse_boolean, -) -from airflow.operators.sql import ( - SQLCheckOperator as SQLCheckOperator3, -) -from airflow.operators.sql import ( - SQLColumnCheckOperator as SQLColumnCheckOperator2, -) -from airflow.operators.sql import ( - SQLIntervalCheckOperator as SQLIntervalCheckOperator3, -) -from airflow.operators.sql import ( - SQLThresholdCheckOperator as SQLThresholdCheckOperator2, -) -from airflow.operators.sql import ( - SQLValueCheckOperator as SQLValueCheckOperator3, -) -from airflow.operators.sqlite_operator import SqliteOperator -from airflow.operators.trigger_dagrun import TriggerDagRunOperator -from airflow.operators.weekday import BranchDayOfWeekOperator -from airflow.sensors import filesystem -from airflow.sensors.date_time import DateTimeSensor, DateTimeSensorAsync -from airflow.sensors.date_time_sensor import DateTimeSensor -from airflow.sensors.external_task import ( - ExternalTaskMarker, - ExternalTaskSensor, - ExternalTaskSensorLink, -) -from airflow.sensors.filesystem import FileSensor -from airflow.sensors.hive_partition_sensor import HivePartitionSensor -from airflow.sensors.http_sensor import HttpSensor -from airflow.sensors.metastore_partition_sensor import MetastorePartitionSensor -from airflow.sensors.named_hive_partition_sensor import NamedHivePartitionSensor -from airflow.sensors.sql import SqlSensor -from airflow.sensors.sql_sensor import SqlSensor2 -from airflow.sensors.time_delta import TimeDeltaSensor, TimeDeltaSensorAsync, WaitSensor -from airflow.sensors.time_sensor import TimeSensor, TimeSensorAsync -from airflow.sensors.web_hdfs_sensor import WebHdfsSensor -from airflow.sensors.weekday import DayOfWeekSensor -from airflow.triggers.external_task import DagStateTrigger, WorkflowTrigger -from airflow.triggers.file import FileTrigger -from airflow.triggers.temporal import DateTimeTrigger, TimeDeltaTrigger -from airflow.www.security import FabAirflowSecurityManagerOverride - -# apache-airflow-providers-amazon -provide_bucket_name() -GCSToS3Operator() -GoogleApiToS3Operator() -GoogleApiToS3Transfer() -RedshiftToS3Operator() -RedshiftToS3Transfer() -S3FileTransformOperator() -S3Hook() -SSQLTableCheckOperator3KeySensor() -S3ToRedshiftOperator() -S3ToRedshiftTransfer() - -# apache-airflow-providers-celery -DEFAULT_CELERY_CONFIG -app -CeleryExecutor() -CeleryKubernetesExecutor() - -# apache-airflow-providers-common-sql -_convert_to_float_if_possible() -parse_boolean() -BaseSQLOperator() -BashOperator() -LegacyBashOperator() -BranchSQLOperator() -CheckOperator() -ConnectorProtocol() -DbApiHook() -DbApiHook2() -IntervalCheckOperator() -PrestoCheckOperator() -PrestoIntervalCheckOperator() -PrestoValueCheckOperator() -SQLCheckOperator() -SQLCheckOperator2() -SQLCheckOperator3() -SQLColumnCheckOperator2() -SQLIntervalCheckOperator() -SQLIntervalCheckOperator2() -SQLIntervalCheckOperator3() -SQLTableCheckOperator() -SQLThresholdCheckOperator() -SQLThresholdCheckOperator2() -SQLValueCheckOperator() -SQLValueCheckOperator2() -SQLValueCheckOperator3() -SqlSensor() -SqlSensor2() -ThresholdCheckOperator() -ValueCheckOperator() - -# apache-airflow-providers-daskexecutor -DaskExecutor() - -# apache-airflow-providers-docker -DockerHook() -DockerOperator() - -# apache-airflow-providers-apache-druid -DruidDbApiHook() -DruidHook() -DruidCheckOperator() - -# apache-airflow-providers-apache-hdfs -WebHDFSHook() -WebHdfsSensor() - -# apache-airflow-providers-apache-hive -HIVE_QUEUE_PRIORITIES -closest_ds_partition() -max_partition() -HiveCliHook() -HiveMetastoreHook() -HiveOperator() -HivePartitionSensor() -HiveServer2Hook() -HiveStatsCollectionOperator() -HiveToDruidOperator() -HiveToDruidTransfer() -HiveToSambaOperator() -S3ToHiveOperator() -S3ToHiveTransfer() -MetastorePartitionSensor() -NamedHivePartitionSensor() - -# apache-airflow-providers-http -HttpHook() -HttpSensor() -SimpleHttpOperator() - -# apache-airflow-providers-jdbc -jaydebeapi -JdbcHook() -JdbcOperator() - -# apache-airflow-providers-fab -basic_auth.CLIENT_AUTH -basic_auth.init_app -basic_auth.auth_current_user -basic_auth.requires_authentication - -kerberos_auth.log -kerberos_auth.CLIENT_AUTH -kerberos_auth.find_user -kerberos_auth.init_app -kerberos_auth.requires_authentication -auth_current_user -backend_kerberos_auth -fab_override -FabAuthManager() -FabAirflowSecurityManagerOverride() - -# check whether attribute access -basic_auth.auth_current_user - -# apache-airflow-providers-cncf-kubernetes -ALL_NAMESPACES -POD_EXECUTOR_DONE_KEY -_disable_verify_ssl() -_enable_tcp_keepalive() -append_to_pod() -annotations_for_logging_task_metadata() -annotations_to_key() -create_pod_id() -datetime_to_label_safe_datestring() -extend_object_field() -get_logs_task_metadata() -label_safe_datestring_to_datetime() -merge_objects() -Port() -Resources() -PodRuntimeInfoEnv() -PodGeneratorDeprecated() -Volume() -VolumeMount() -Secret() - -add_pod_suffix() -add_pod_suffix2() -get_kube_client() -get_kube_client2() -make_safe_label_value() -make_safe_label_value2() -rand_str() -rand_str2() -K8SModel() -K8SModel2() -PodLauncher() -PodLauncher2() -PodStatus() -PodStatus2() -PodDefaults() -PodDefaults2() -PodDefaults3() -PodGenerator() -PodGenerator2() - - -# apache-airflow-providers-microsoft-mssql -MsSqlHook() -MsSqlOperator() -MsSqlToHiveOperator() -MsSqlToHiveTransfer() - -# apache-airflow-providers-mysql -HiveToMySqlOperator() -HiveToMySqlTransfer() -MySqlHook() -MySqlOperator() -MySqlToHiveOperator() -MySqlToHiveTransfer() -PrestoToMySqlOperator() -PrestoToMySqlTransfer() - -# apache-airflow-providers-oracle -OracleHook() -OracleOperator() - -# apache-airflow-providers-papermill -PapermillOperator() - -# apache-airflow-providers-apache-pig -PigCliHook() -PigOperator() - -# apache-airflow-providers-postgres -Mapping -PostgresHook() -PostgresOperator() - -# apache-airflow-providers-presto -PrestoHook() - -# apache-airflow-providers-samba -SambaHook() - -# apache-airflow-providers-slack -SlackHook() -SlackAPIOperator() -SlackAPIPostOperator() - -# apache-airflow-providers-sqlite -SqliteHook() -SqliteOperator() - -# apache-airflow-providers-zendesk -ZendeskHook() - -# apache-airflow-providers-smtp -EmailOperator() - -# apache-airflow-providers-standard -filesystem.FileSensor() -FileSensor() -TriggerDagRunOperator() -ExternalTaskMarker() -ExternalTaskSensor() -BranchDateTimeOperator() -BranchDayOfWeekOperator() -BranchPythonOperator() -DateTimeSensor() -DateTimeSensorAsync() -TimeSensor() -TimeDeltaSensor() -DayOfWeekSensor() -DummyOperator() -EmptyOperator() -ExternalTaskMarker() -ExternalTaskSensor() -ExternalTaskSensorLink() -FileSensor() -FileTrigger() -FSHook() -PackageIndexHook() -SubprocessHook() -ShortCircuitOperator() -TimeDeltaSensor() -TimeSensor() -TriggerDagRunOperator() -WorkflowTrigger() -PythonOperator() -PythonVirtualenvOperator() -DagStateTrigger() -FileTrigger() -DateTimeTrigger() -TimeDeltaTrigger() -SubprocessResult() -SubprocessHook() -TimeDeltaSensor() -TimeDeltaSensorAsync() -WaitSensor() -TimeSensor() -TimeSensorAsync() -BranchDateTimeOperator() -working_directory() -target_times_as_dates() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_amazon.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_amazon.py new file mode 100644 index 00000000000000..b72ae553bfa491 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_amazon.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from airflow.hooks.S3_hook import ( + S3Hook, + provide_bucket_name, +) +from airflow.operators.gcs_to_s3 import GCSToS3Operator +from airflow.operators.google_api_to_s3_transfer import ( + GoogleApiToS3Operator, + GoogleApiToS3Transfer, +) +from airflow.operators.redshift_to_s3_operator import ( + RedshiftToS3Operator, + RedshiftToS3Transfer, +) +from airflow.operators.s3_file_transform_operator import S3FileTransformOperator +from airflow.operators.s3_to_redshift_operator import ( + S3ToRedshiftOperator, + S3ToRedshiftTransfer, +) +from airflow.sensors.s3_key_sensor import S3KeySensor + +S3Hook() +provide_bucket_name() + +GCSToS3Operator() + +GoogleApiToS3Operator() +GoogleApiToS3Transfer() + +RedshiftToS3Operator() +RedshiftToS3Transfer() + +S3FileTransformOperator() + +S3ToRedshiftOperator() +S3ToRedshiftTransfer() + +S3KeySensor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_celery.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_celery.py new file mode 100644 index 00000000000000..defe8fe915d00e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_celery.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from airflow.config_templates.default_celery import DEFAULT_CELERY_CONFIG +from airflow.executors.celery_executor import ( + CeleryExecutor, + app, +) + +DEFAULT_CELERY_CONFIG + +app +CeleryExecutor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_common_sql.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_common_sql.py new file mode 100644 index 00000000000000..9e17bebb162ebf --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_common_sql.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from airflow.hooks.dbapi import ( + ConnectorProtocol, + DbApiHook, +) +from airflow.hooks.dbapi_hook import DbApiHook +from airflow.operators.check_operator import SQLCheckOperator + +ConnectorProtocol() +DbApiHook() +SQLCheckOperator() + + +from airflow.operators.check_operator import CheckOperator +from airflow.operators.sql import SQLCheckOperator + +SQLCheckOperator() +CheckOperator() + + +from airflow.operators.druid_check_operator import CheckOperator + +CheckOperator() + + +from airflow.operators.presto_check_operator import CheckOperator + +CheckOperator() + + +from airflow.operators.check_operator import ( + IntervalCheckOperator, + SQLIntervalCheckOperator, +) +from airflow.operators.druid_check_operator import DruidCheckOperator +from airflow.operators.presto_check_operator import PrestoCheckOperator + +DruidCheckOperator() +PrestoCheckOperator() +IntervalCheckOperator() +SQLIntervalCheckOperator() + + +from airflow.operators.presto_check_operator import ( + IntervalCheckOperator, + PrestoIntervalCheckOperator, +) +from airflow.operators.sql import SQLIntervalCheckOperator + +IntervalCheckOperator() +SQLIntervalCheckOperator() +PrestoIntervalCheckOperator() + + +from airflow.operators.check_operator import ( + SQLThresholdCheckOperator, + ThresholdCheckOperator, +) + +SQLThresholdCheckOperator() +ThresholdCheckOperator() + + +from airflow.operators.sql import SQLThresholdCheckOperator + +SQLThresholdCheckOperator() + + +from airflow.operators.check_operator import ( + SQLValueCheckOperator, + ValueCheckOperator, +) + +SQLValueCheckOperator() +ValueCheckOperator() + + +from airflow.operators.presto_check_operator import ( + PrestoValueCheckOperator, + ValueCheckOperator, +) +from airflow.operators.sql import SQLValueCheckOperator + +SQLValueCheckOperator() +ValueCheckOperator() +PrestoValueCheckOperator() + + +from airflow.operators.sql import ( + BaseSQLOperator, + BranchSQLOperator, + SQLColumnCheckOperator, + SQLTablecheckOperator, + _convert_to_float_if_possible, + parse_boolean, +) + +BaseSQLOperator() +BranchSQLOperator() +SQLTablecheckOperator() +SQLColumnCheckOperator() +_convert_to_float_if_possible() +parse_boolean() + + +from airflow.sensors.sql import SqlSensor + +SqlSensor() + + +from airflow.sensors.sql_sensor import SqlSensor + +SqlSensor() + + +from airflow.operators.jdbc_operator import JdbcOperator +from airflow.operators.mssql_operator import MsSqlOperator +from airflow.operators.mysql_operator import MySqlOperator +from airflow.operators.oracle_operator import OracleOperator +from airflow.operators.postgres_operator import PostgresOperator +from airflow.operators.sqlite_operator import SqliteOperator + +JdbcOperator() +MsSqlOperator() +MySqlOperator() +OracleOperator() +PostgresOperator() +SqliteOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_daskexecutor.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_daskexecutor.py new file mode 100644 index 00000000000000..9b6c273b137120 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_daskexecutor.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.executors.dask_executor import DaskExecutor + +DaskExecutor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_docker.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_docker.py new file mode 100644 index 00000000000000..85b816a8be4234 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_docker.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from airflow.hooks.docker_hook import DockerHook +from airflow.operators.docker_operator import DockerOperator + +DockerHook() +DockerOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_druid.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_druid.py new file mode 100644 index 00000000000000..1de011de9f0691 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_druid.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from airflow.hooks.druid_hook import ( + DruidDbApiHook, + DruidHook, +) +from airflow.operators.hive_to_druid import ( + HiveToDruidOperator, + HiveToDruidTransfer, +) + +DruidDbApiHook() +DruidHook() + +HiveToDruidOperator() +HiveToDruidTransfer() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_fab.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_fab.py new file mode 100644 index 00000000000000..6a5a5ba7bd6a82 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_fab.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from airflow.api.auth.backend.basic_auth import ( + CLIENT_AUTH, + auth_current_user, + init_app, + requires_authentication, +) + +CLIENT_AUTH +init_app() +auth_current_user() +requires_authentication() + +from airflow.api.auth.backend.kerberos_auth import ( + CLIENT_AUTH, + find_user, + init_app, + log, + requires_authentication, +) + +log() +CLIENT_AUTH +find_user() +init_app() +requires_authentication() + +from airflow.auth.managers.fab.api.auth.backend.kerberos_auth import ( + CLIENT_AUTH, + find_user, + init_app, + log, + requires_authentication, +) + +log() +CLIENT_AUTH +find_user() +init_app() +requires_authentication() + +from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager +from airflow.auth.managers.fab.security_manager.override import ( + MAX_NUM_DATABASE_USER_SESSIONS, + FabAirflowSecurityManagerOverride, +) + +FabAuthManager() +MAX_NUM_DATABASE_USER_SESSIONS +FabAirflowSecurityManagerOverride() + +from airflow.www.security import FabAirflowSecurityManagerOverride + +FabAirflowSecurityManagerOverride() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hdfs.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hdfs.py new file mode 100644 index 00000000000000..a52239fe80bc5a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hdfs.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from airflow.hooks.webhdfs_hook import WebHDFSHook +from airflow.sensors.web_hdfs_sensor import WebHdfsSensor + +WebHDFSHook() +WebHdfsSensor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hive.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hive.py new file mode 100644 index 00000000000000..a018a9fc11a1d5 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_hive.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from airflow.hooks.hive_hooks import ( + HIVE_QUEUE_PRIORITIES, + HiveCliHook, + HiveMetastoreHook, + HiveServer2Hook, +) +from airflow.macros.hive import ( + closest_ds_partition, + max_partition, +) +from airflow.operators.hive_operator import HiveOperator +from airflow.operators.hive_stats_operator import HiveStatsCollectionOperator +from airflow.operators.hive_to_mysql import ( + HiveToMySqlOperator, + HiveToMySqlTransfer, +) +from airflow.operators.hive_to_samba_operator import HiveToSambaOperator +from airflow.operators.mssql_to_hive import ( + MsSqlToHiveOperator, + MsSqlToHiveTransfer, +) +from airflow.operators.mysql_to_hive import ( + MySqlToHiveOperator, + MySqlToHiveTransfer, +) +from airflow.operators.s3_to_hive_operator import ( + S3ToHiveOperator, + S3ToHiveTransfer, +) +from airflow.sensors.hive_partition_sensor import HivePartitionSensor +from airflow.sensors.metastore_partition_sensor import MetastorePartitionSensor +from airflow.sensors.named_hive_partition_sensor import NamedHivePartitionSensor + +closest_ds_partition() +max_partition() + +HiveCliHook() +HiveMetastoreHook() +HiveServer2Hook() +HIVE_QUEUE_PRIORITIES + +HiveOperator() + +HiveStatsCollectionOperator() + +HiveToMySqlOperator() +HiveToMySqlTransfer() + +HiveToSambaOperator() + +MsSqlToHiveOperator() +MsSqlToHiveTransfer() + +MySqlToHiveOperator() +MySqlToHiveTransfer() + +S3ToHiveOperator() +S3ToHiveTransfer() + +HivePartitionSensor() + +MetastorePartitionSensor() + +NamedHivePartitionSensor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_http.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_http.py new file mode 100644 index 00000000000000..d2ca3a42068596 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_http.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from airflow.hooks.http_hook import HttpHook +from airflow.operators.http_operator import SimpleHttpOperator +from airflow.sensors.http_sensor import HttpSensor + +HttpHook() +SimpleHttpOperator() +HttpSensor() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_jdbc.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_jdbc.py new file mode 100644 index 00000000000000..2a00135a9f3724 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_jdbc.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from airflow.hooks.jdbc_hook import ( + JdbcHook, + jaydebeapi, +) + +JdbcHook() +jaydebeapi() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_kubernetes.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_kubernetes.py new file mode 100644 index 00000000000000..2d943b8731ff0f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_kubernetes.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from airflow.executors.kubernetes_executor_types import ( + ALL_NAMESPACES, + POD_EXECUTOR_DONE_KEY, +) +from airflow.kubernetes.k8s_model import ( + K8SModel, + append_to_pod, +) +from airflow.kubernetes.kube_client import ( + _disable_verify_ssl, + _enable_tcp_keepalive, + get_kube_client, +) +from airflow.kubernetes.kubernetes_helper_functions import ( + add_pod_suffix, + annotations_for_logging_task_metadata, + annotations_to_key, + create_pod_id, + get_logs_task_metadata, + rand_str, +) +from airflow.kubernetes.pod import ( + Port, + Resources, +) + +ALL_NAMESPACES +POD_EXECUTOR_DONE_KEY + +K8SModel() +append_to_pod() + +_disable_verify_ssl() +_enable_tcp_keepalive() +get_kube_client() + +add_pod_suffix() +create_pod_id() + +annotations_for_logging_task_metadata() +annotations_to_key() +get_logs_task_metadata() +rand_str() + +Port() +Resources() + + +from airflow.kubernetes.pod_generator import ( + PodDefaults, + PodGenerator, + PodGeneratorDeprecated, + add_pod_suffix, + datetime_to_label_safe_datestring, + extend_object_field, + label_safe_datestring_to_datetime, + make_safe_label_value, + merge_objects, + rand_str, +) + +datetime_to_label_safe_datestring() +extend_object_field() +label_safe_datestring_to_datetime() +make_safe_label_value() +merge_objects() +PodGenerator() +PodDefaults() +PodGeneratorDeprecated() +add_pod_suffix() +rand_str() + + +from airflow.kubernetes.pod_generator_deprecated import ( + PodDefaults, + PodGenerator, + make_safe_label_value, +) +from airflow.kubernetes.pod_launcher import ( + PodLauncher, + PodStatus, +) + +PodDefaults() +PodGenerator() +make_safe_label_value() + +PodLauncher() +PodStatus() + + +from airflow.kubernetes.pod_launcher_deprecated import ( + PodDefaults, + PodLauncher, + PodStatus, + get_kube_client, +) +from airflow.kubernetes.pod_runtime_info_env import PodRuntimeInfoEnv +from airflow.kubernetes.secret import ( + K8SModel, + Secret, +) +from airflow.kubernetes.volume import Volume +from airflow.kubernetes.volume_mount import VolumeMount + +PodDefaults() +PodLauncher() +PodStatus() +get_kube_client() + +PodRuntimeInfoEnv() +K8SModel() +Secret() +Volume() +VolumeMount() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_mysql.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_mysql.py new file mode 100644 index 00000000000000..0bc3eab8dd04e3 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_mysql.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from airflow.hooks.mysql_hook import MySqlHook +from airflow.operators.presto_to_mysql import ( + PrestoToMySqlOperator, + PrestoToMySqlTransfer, +) + +MySqlHook() +PrestoToMySqlOperator() +PrestoToMySqlTransfer() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_oracle.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_oracle.py new file mode 100644 index 00000000000000..3c525e4eb705cf --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_oracle.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.hooks.oracle_hook import OracleHook + +OracleHook() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_papermill.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_papermill.py new file mode 100644 index 00000000000000..42be2959e36600 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_papermill.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.operators.papermill_operator import PapermillOperator + +PapermillOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_pig.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_pig.py new file mode 100644 index 00000000000000..e09bb35fa6b185 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_pig.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from airflow.hooks.pig_hook import PigCliHook +from airflow.operators.pig_operator import PigOperator + +PigCliHook() +PigOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_postgres.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_postgres.py new file mode 100644 index 00000000000000..bbd79299fa24e1 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_postgres.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from airflow.hooks.postgres_hook import PostgresHook +from airflow.operators.postgres_operator import Mapping + +PostgresHook() +Mapping() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_presto.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_presto.py new file mode 100644 index 00000000000000..f5117884a7b42d --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_presto.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.hooks.presto_hook import PrestoHook + +PrestoHook() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_samba.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_samba.py new file mode 100644 index 00000000000000..f18cd97a6e7e9f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_samba.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.hooks.samba_hook import SambaHook + +SambaHook() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_slack.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_slack.py new file mode 100644 index 00000000000000..644f7d06b90849 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_slack.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from airflow.hooks.slack_hook import SlackHook +from airflow.operators.slack_operator import SlackAPIOperator, SlackAPIPostOperator + +SlackHook() +SlackAPIOperator() +SlackAPIPostOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_smtp.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_smtp.py new file mode 100644 index 00000000000000..ea3795460e67be --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_smtp.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from airflow.operators.email_operator import EmailOperator + +EmailOperator() + +from airflow.operators.email import EmailOperator + +EmailOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_sqlite.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_sqlite.py new file mode 100644 index 00000000000000..a4b256ff03a616 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_sqlite.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.hooks.sqlite_hook import SqliteHook + +SqliteHook() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_standard.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_standard.py new file mode 100644 index 00000000000000..887a8da5579945 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_standard.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from airflow.operators.bash_operator import BashOperator +from airflow.operators.dagrun_operator import ( + TriggerDagRunLink, + TriggerDagRunOperator, +) +from airflow.operators.dummy import ( + DummyOperator, + EmptyOperator, +) +from airflow.operators.latest_only_operator import LatestOnlyOperator +from airflow.operators.python_operator import ( + BranchPythonOperator, + PythonOperator, + PythonVirtualenvOperator, + ShortCircuitOperator, +) +from airflow.sensors.external_task_sensor import ( + ExternalTaskMarker, + ExternalTaskSensor, + ExternalTaskSensorLink, +) + +BashOperator() + +TriggerDagRunLink() +TriggerDagRunOperator() +DummyOperator() +EmptyOperator() + +LatestOnlyOperator() + +BranchPythonOperator() +PythonOperator() +PythonVirtualenvOperator() +ShortCircuitOperator() + +ExternalTaskMarker() +ExternalTaskSensor() +ExternalTaskSensorLink() + +from airflow.operators.dummy_operator import ( + DummyOperator, + EmptyOperator, +) + +DummyOperator() +EmptyOperator() diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_zendesk.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_zendesk.py new file mode 100644 index 00000000000000..b64d4c45cf31f9 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR302_zendesk.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from airflow.hooks.zendesk_hook import ZendeskHook + +ZendeskHook() diff --git a/crates/ruff_linter/src/rules/airflow/helpers.rs b/crates/ruff_linter/src/rules/airflow/helpers.rs index 8c2845f9081046..9cd5818b8feb7d 100644 --- a/crates/ruff_linter/src/rules/airflow/helpers.rs +++ b/crates/ruff_linter/src/rules/airflow/helpers.rs @@ -4,7 +4,7 @@ use ruff_python_ast::{Expr, ExprName, StmtTry}; use ruff_python_semantic::Exceptions; use ruff_python_semantic::SemanticModel; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum Replacement { None, Name(&'static str), @@ -19,7 +19,7 @@ pub(crate) enum Replacement { }, } -#[derive(Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum ProviderReplacement { None, ProviderName { @@ -27,9 +27,15 @@ pub(crate) enum ProviderReplacement { provider: &'static str, version: &'static str, }, + AutoImport { + module: &'static str, + name: &'static str, + provider: &'static str, + version: &'static str, + }, SourceModuleMovedToProvider { - name: String, module: &'static str, + name: String, provider: &'static str, version: &'static str, }, diff --git a/crates/ruff_linter/src/rules/airflow/mod.rs b/crates/ruff_linter/src/rules/airflow/mod.rs index 744712b5d144e0..4b4864a97d27c4 100644 --- a/crates/ruff_linter/src/rules/airflow/mod.rs +++ b/crates/ruff_linter/src/rules/airflow/mod.rs @@ -23,7 +23,29 @@ mod tests { #[test_case(Rule::Airflow3Removal, Path::new("AIR301_class_attribute.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_airflow_plugin.py"))] #[test_case(Rule::Airflow3Removal, Path::new("AIR301_context.py"))] - #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_amazon.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_celery.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_common_sql.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_daskexecutor.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_druid.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_fab.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_hdfs.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_hive.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_http.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_jdbc.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_kubernetes.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_mysql.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_oracle.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_papermill.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_pig.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_postgres.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_presto.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_samba.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_slack.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_smtp.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_sqlite.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_zendesk.py"))] + #[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_standard.py"))] #[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_args.py"))] #[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_names.py"))] #[test_case(Rule::Airflow3SuggestedToMoveToProvider, Path::new("AIR312.py"))] diff --git a/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs index 2840f9d691b19b..6dc4ffaf26bf72 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/moved_to_provider_in_3.rs @@ -1,5 +1,6 @@ +use crate::importer::ImportRequest; use crate::rules::airflow::helpers::ProviderReplacement; -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{Expr, ExprAttribute}; use ruff_python_semantic::Modules; @@ -33,6 +34,8 @@ pub(crate) struct Airflow3MovedToProvider { } impl Violation for Airflow3MovedToProvider { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let Airflow3MovedToProvider { @@ -48,6 +51,12 @@ impl Violation for Airflow3MovedToProvider { provider, version: _, } + | ProviderReplacement::AutoImport { + name: _, + module: _, + provider, + version: _, + } | ProviderReplacement::SourceModuleMovedToProvider { name: _, module: _, @@ -72,6 +81,14 @@ impl Violation for Airflow3MovedToProvider { "Install `apache-airflow-providers-{provider}>={version}` and use `{name}` instead." )) }, + ProviderReplacement::AutoImport { + name, + module, + provider, + version, + } => { + Some(format!("Install `apache-airflow-providers-{provider}>={version}` and use `{module}.{name}` instead.")) + } , ProviderReplacement::SourceModuleMovedToProvider { name, module, @@ -107,418 +124,517 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan let replacement = match qualified_name.segments() { // ProviderName: for cases that only one name has been moved // apache-airflow-providers-amazon - ["airflow", "hooks", "S3_hook", rest @ ( - "S3Hook" - | "provide_bucket_name" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.amazon.aws.hooks.s3", - provider: "amazon", - version: "1.0.0" - }, - ["airflow", "operators", "gcs_to_s3", "GCSToS3Operator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.transfers.gcs_to_s3.GCSToS3Operator", - provider: "amazon", - version: "1.0.0" - }, - ["airflow", "operators", "google_api_to_s3_transfer", "GoogleApiToS3Operator" | "GoogleApiToS3Transfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator", - provider: "amazon", - version: "1.0.0" - }, - ["airflow", "operators", "redshift_to_s3_operator", "RedshiftToS3Operator" | "RedshiftToS3Transfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator", - provider: "amazon", - version: "1.0.0" - }, - ["airflow", "operators", "s3_file_transform_operator", "S3FileTransformOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.operators.s3.S3FileTransformOperator", - provider: "amazon", - version: "3.0.0" - }, - ["airflow", "operators", "s3_to_redshift_operator", "S3ToRedshiftOperator" | "S3ToRedshiftTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator", - provider: "amazon", - version: "1.0.0" - }, - ["airflow", "sensors", "s3_key_sensor", "S3KeySensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.amazon.aws.sensors.s3.S3KeySensor", + ["airflow", "hooks", "S3_hook", rest @ ("S3Hook" | "provide_bucket_name")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.amazon.aws.hooks.s3", + provider: "amazon", + version: "1.0.0", + } + } + ["airflow", "operators", "gcs_to_s3", "GCSToS3Operator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.transfers.gcs_to_s3", + name: "GCSToS3Operator", + provider: "amazon", + version: "1.0.0", + } + } + ["airflow", "operators", "google_api_to_s3_transfer", "GoogleApiToS3Operator" | "GoogleApiToS3Transfer"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.transfers.google_api_to_s3", + name: "GoogleApiToS3Operator", + provider: "amazon", + version: "1.0.0", + } + } + ["airflow", "operators", "redshift_to_s3_operator", "RedshiftToS3Operator" | "RedshiftToS3Transfer"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.transfers.redshift_to_s3", + name: "RedshiftToS3Operator", + provider: "amazon", + version: "1.0.0", + } + } + ["airflow", "operators", "s3_file_transform_operator", "S3FileTransformOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.operators.s3", + name: "S3FileTransformOperator", + provider: "amazon", + version: "3.0.0", + } + } + ["airflow", "operators", "s3_to_redshift_operator", "S3ToRedshiftOperator" | "S3ToRedshiftTransfer"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.transfers.s3_to_redshift", + name: "S3ToRedshiftOperator", + provider: "amazon", + version: "1.0.0", + } + } + ["airflow", "sensors", "s3_key_sensor", "S3KeySensor"] => ProviderReplacement::AutoImport { + module: "airflow.providers.amazon.aws.sensors.s3", + name: "S3KeySensor", provider: "amazon", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-celery - ["airflow", "config_templates", "default_celery", "DEFAULT_CELERY_CONFIG"] => ProviderReplacement::ProviderName { - name: "airflow.providers.celery.executors.default_celery.DEFAULT_CELERY_CONFIG", - provider: "celery", - version: "3.3.0" - }, - ["airflow", "executors", "celery_executor", rest ] => match *rest { - "app" => ProviderReplacement::ProviderName { - name: "airflow.providers.celery.executors.celery_executor_utils.app", + ["airflow", "config_templates", "default_celery", "DEFAULT_CELERY_CONFIG"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.celery.executors.default_celery", + name: "DEFAULT_CELERY_CONFIG", + provider: "celery", + version: "3.3.0", + } + } + ["airflow", "executors", "celery_executor", rest] => match *rest { + "app" => ProviderReplacement::AutoImport { + module: "airflow.providers.celery.executors.celery_executor_utils", + name: "app", provider: "celery", - version: "3.3.0" + version: "3.3.0", }, - "CeleryExecutor" => ProviderReplacement::ProviderName { - name: "airflow.providers.celery.executors.celery_executor.CeleryExecutor", + "CeleryExecutor" => ProviderReplacement::AutoImport { + module: "airflow.providers.celery.executors.celery_executor", + name: "CeleryExecutor", provider: "celery", - version: "3.3.0" + version: "3.3.0", }, _ => return, }, - ["airflow", "executors", "celery_kubernetes_executor", "CeleryKubernetesExecutor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.celery.executors.celery_kubernetes_executor.CeleryKubernetesExecutor", - provider: "celery", - version: "3.3.0" - }, + ["airflow", "executors", "celery_kubernetes_executor", "CeleryKubernetesExecutor"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.celery.executors.celery_kubernetes_executor", + name: "CeleryKubernetesExecutor", + provider: "celery", + version: "3.3.0", + } + } // apache-airflow-providers-common-sql - ["airflow", "hooks", "dbapi", rest @ ( - "ConnectorProtocol" - | "DbApiHook" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), + ["airflow", "hooks", "dbapi", rest @ ("ConnectorProtocol" | "DbApiHook")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.common.sql.hooks.sql", + provider: "common-sql", + version: "1.0.0", + } + } + ["airflow", "hooks", "dbapi_hook", "DbApiHook"] => ProviderReplacement::AutoImport { module: "airflow.providers.common.sql.hooks.sql", + name: "DbApiHook", provider: "common-sql", - version: "1.0.0" - }, - ["airflow", "hooks", "dbapi_hook", "DbApiHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.hooks.sql.DbApiHook", - provider: "common-sql", - version: "1.0.0" + version: "1.0.0", }, ["airflow", "operators", "check_operator" | "sql", "SQLCheckOperator"] - | ["airflow", "operators", "check_operator" | "druid_check_operator" | "presto_check_operator", "CheckOperator"] - | ["airflow", "operators", "druid_check_operator", "DruidCheckOperator"] + | ["airflow", "operators", "check_operator" | "druid_check_operator" | "presto_check_operator", "CheckOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.operators.sql", + name: "SQLCheckOperator", + provider: "common-sql", + version: "1.1.0", + } + } + ["airflow", "operators", "druid_check_operator", "DruidCheckOperator"] | ["airflow", "operators", "presto_check_operator", "PrestoCheckOperator"] => { ProviderReplacement::ProviderName { name: "airflow.providers.common.sql.operators.sql.SQLCheckOperator", provider: "common-sql", - version: "1.1.0" + version: "1.1.0", } - }, - ["airflow", "operators", "check_operator" | "presto_check_operator", "IntervalCheckOperator"] - | ["airflow", "operators", "check_operator" | "sql", "SQLIntervalCheckOperator"] - | ["airflow", "operators", "presto_check_operator", "PrestoIntervalCheckOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - ["airflow", "operators", "check_operator" | "sql" , "SQLThresholdCheckOperator"] - | ["airflow", "operators", "check_operator", "ThresholdCheckOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - ["airflow", "operators", "check_operator" | "presto_check_operator", "ValueCheckOperator"] - | ["airflow", "operators", "presto_check_operator", "PrestoValueCheckOperator"] - | ["airflow", "operators", "check_operator" | "sql", "SQLValueCheckOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLValueCheckOperator", - provider: "common-sql", - version: "1.1.0" - }, - ["airflow", "operators", "sql", rest] => match *rest { - "BaseSQLOperator" - | "BranchSQLOperator" - | "SQLTablecheckOperator" => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), + } + ["airflow", "operators", "check_operator", "IntervalCheckOperator" | "SQLIntervalCheckOperator"] + | ["airflow", "operators", "presto_check_operator", "IntervalCheckOperator"] + | ["airflow", "operators", "sql", "SQLIntervalCheckOperator"] => { + ProviderReplacement::AutoImport { module: "airflow.providers.common.sql.operators.sql", + name: "SQLIntervalCheckOperator", provider: "common-sql", - version: "1.1.0" - }, - "SQLColumnCheckOperator" - | "_convert_to_float_if_possible" - | "parse_boolean" => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), + version: "1.1.0", + } + } + ["airflow", "operators", "presto_check_operator", "PrestoIntervalCheckOperator"] => { + ProviderReplacement::ProviderName { + name: "airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator", + provider: "common-sql", + version: "1.1.0", + } + } + ["airflow", "operators", "check_operator", "SQLThresholdCheckOperator" | "ThresholdCheckOperator"] + | ["airflow", "operators", "sql", "SQLThresholdCheckOperator"] => { + ProviderReplacement::AutoImport { module: "airflow.providers.common.sql.operators.sql", + name: "SQLThresholdCheckOperator", provider: "common-sql", - version: "1.0.0" - }, - _ => return + version: "1.1.0", + } } - ["airflow", "sensors", "sql" | "sql_sensor", "SqlSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.sensors.sql.SqlSensor", - provider: "common-sql", - version: "1.0.0" + ["airflow", "operators", "check_operator", "SQLValueCheckOperator" | "ValueCheckOperator"] + | ["airflow", "operators", "presto_check_operator", "ValueCheckOperator"] + | ["airflow", "operators", "sql", "SQLValueCheckOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.operators.sql", + name: "SQLValueCheckOperator", + provider: "common-sql", + version: "1.1.0", + } + } + ["airflow", "operators", "presto_check_operator", "PrestoValueCheckOperator"] => { + ProviderReplacement::ProviderName { + name: "airflow.providers.common.sql.operators.sql.SQLValueCheckOperator", + provider: "common-sql", + version: "1.1.0", + } + } + ["airflow", "operators", "sql", rest] => match *rest { + "BaseSQLOperator" | "BranchSQLOperator" | "SQLTablecheckOperator" => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.common.sql.operators.sql", + provider: "common-sql", + version: "1.1.0", + } + } + "SQLColumnCheckOperator" | "_convert_to_float_if_possible" | "parse_boolean" => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.common.sql.operators.sql", + provider: "common-sql", + version: "1.0.0", + } + } + _ => return, }, + ["airflow", "sensors", "sql" | "sql_sensor", "SqlSensor"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.common.sql.sensors.sql", + name: "SqlSensor", + provider: "common-sql", + version: "1.0.0", + } + } ["airflow", "operators", "jdbc_operator", "JdbcOperator"] | ["airflow", "operators", "mssql_operator", "MsSqlOperator"] | ["airflow", "operators", "mysql_operator", "MySqlOperator"] | ["airflow", "operators", "oracle_operator", "OracleOperator"] | ["airflow", "operators", "postgres_operator", "PostgresOperator"] - | ["airflow", "operators", "sqlite_operator", "SqliteOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator", - provider: "common-sql", - version: "1.3.0" - }, + | ["airflow", "operators", "sqlite_operator", "SqliteOperator"] => { + ProviderReplacement::ProviderName { + name: "airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator", + provider: "common-sql", + version: "1.3.0", + } + } // apache-airflow-providers-daskexecutor - ["airflow", "executors", "dask_executor", "DaskExecutor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.daskexecutor.executors.dask_executor.DaskExecutor", - provider: "daskexecutor", - version: "1.0.0" - }, + ["airflow", "executors", "dask_executor", "DaskExecutor"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.daskexecutor.executors.dask_executor", + name: "DaskExecutor", + provider: "daskexecutor", + version: "1.0.0", + } + } // apache-airflow-providers-docker - ["airflow", "hooks", "docker_hook", "DockerHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.docker.hooks.docker.DockerHook", + ["airflow", "hooks", "docker_hook", "DockerHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.docker.hooks.docker", + name: "DockerHook", provider: "docker", - version: "1.0.0" - }, - ["airflow", "operators", "docker_operator", "DockerOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.docker.operators.docker.DockerOperator", - provider: "docker", - version: "1.0.0" + version: "1.0.0", }, + ["airflow", "operators", "docker_operator", "DockerOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.docker.operators.docker", + name: "DockerOperator", + provider: "docker", + version: "1.0.0", + } + } // apache-airflow-providers-apache-druid - ["airflow", "hooks", "druid_hook", rest @ ( - "DruidDbApiHook" - | "DruidHook" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.apache.druid.hooks.druid", - provider: "apache-druid", - version: "1.0.0" - }, - ["airflow", "operators", "hive_to_druid", "HiveToDruidOperator" | "HiveToDruidTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator", - provider: "apache-druid", - version: "1.0.0" - }, + ["airflow", "hooks", "druid_hook", rest @ ("DruidDbApiHook" | "DruidHook")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.apache.druid.hooks.druid", + provider: "apache-druid", + version: "1.0.0", + } + } + ["airflow", "operators", "hive_to_druid", "HiveToDruidOperator" | "HiveToDruidTransfer"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.druid.transfers.hive_to_druid", + name: "HiveToDruidOperator", + provider: "apache-druid", + version: "1.0.0", + } + } // apache-airflow-providers-fab - ["airflow", "api", "auth", "backend", "basic_auth", rest @ ( - "CLIENT_AUTH" - | "init_app" - | "auth_current_user" - | "requires_authentication" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.fab.auth_manager.api.auth.backend.basic_auth", - provider: "fab", - version: "1.0.0" - }, - ["airflow", "api", "auth", "backend", "kerberos_auth", rest @ ( - "log" - | "CLIENT_AUTH" - | "find_user" - | "init_app" - | "requires_authentication" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth", - provider: "fab", - version: "1.0.0" - }, - ["airflow", "auth", "managers", "fab", "api", "auth", "backend", "kerberos_auth", rest @ ( - "log" - | "CLIENT_AUTH" - | "find_user" - | "init_app" - | "requires_authentication" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth", - provider: "fab", - version: "1.0.0" - }, - ["airflow", "auth", "managers", "fab", "fab_auth_manager", "FabAuthManager"] => ProviderReplacement::ProviderName { - name: "airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager", - provider: "fab", - version: "1.0.0" - }, - ["airflow", "auth", "managers", "fab", "security_manager", "override", rest @ ( - "MAX_NUM_DATABASE_USER_SESSIONS" - | "FabAirflowSecurityManagerOverride" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.fab.auth_manager.security_manager.override", - provider: "fab", - version: "1.0.0" - }, - ["airflow", "www", "security", "FabAirflowSecurityManagerOverride"] => ProviderReplacement::ProviderName { - name: "airflow.providers.fab.auth_manager.security_manager.override.FabAirflowSecurityManagerOverride", - provider: "fab", - version: "1.0.0" - }, + ["airflow", "api", "auth", "backend", "basic_auth", rest @ ("CLIENT_AUTH" | "init_app" | "auth_current_user" | "requires_authentication")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.fab.auth_manager.api.auth.backend.basic_auth", + provider: "fab", + version: "1.0.0", + } + } + ["airflow", "api", "auth", "backend", "kerberos_auth", rest @ ("log" | "CLIENT_AUTH" | "find_user" | "init_app" | "requires_authentication")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth", + provider: "fab", + version: "1.0.0", + } + } + ["airflow", "auth", "managers", "fab", "api", "auth", "backend", "kerberos_auth", rest @ ("log" | "CLIENT_AUTH" | "find_user" | "init_app" | "requires_authentication")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth", + provider: "fab", + version: "1.0.0", + } + } + ["airflow", "auth", "managers", "fab", "fab_auth_manager", "FabAuthManager"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.fab.auth_manager.fab_auth_manager", + name: "FabAuthManager", + provider: "fab", + version: "1.0.0", + } + } + ["airflow", "auth", "managers", "fab", "security_manager", "override", "MAX_NUM_DATABASE_USER_SESSIONS"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.fab.auth_manager.security_manager.override", + name: "MAX_NUM_DATABASE_USER_SESSIONS", + provider: "fab", + version: "1.0.0", + } + } + ["airflow", "auth", "managers", "fab", "security_manager", "override", "FabAirflowSecurityManagerOverride"] + | ["airflow", "www", "security", "FabAirflowSecurityManagerOverride"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.fab.auth_manager.security_manager.override", + name: "FabAirflowSecurityManagerOverride", + provider: "fab", + version: "1.0.0", + } + } // apache-airflow-providers-apache-hdfs - ["airflow", "hooks", "webhdfs_hook", "WebHDFSHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hdfs.hooks.webhdfs.WebHDFSHook", - provider: "apache-hdfs", - version: "1.0.0" - }, - ["airflow", "sensors", "web_hdfs_sensor", "WebHdfsSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hdfs.sensors.web_hdfs.WebHdfsSensor", + ["airflow", "hooks", "webhdfs_hook", "WebHDFSHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hdfs.hooks.webhdfs", + name: "WebHDFSHook", provider: "apache-hdfs", - version: "1.0.0" + version: "1.0.0", }, + ["airflow", "sensors", "web_hdfs_sensor", "WebHdfsSensor"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hdfs.sensors.web_hdfs", + name: "WebHdfsSensor", + provider: "apache-hdfs", + version: "1.0.0", + } + } // apache-airflow-providers-apache-hive - ["airflow", "macros", "hive", rest @ ( - "closest_ds_partition" - | "max_partition" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.apache.hive.macros.hive", - provider: "apache-hive", - version: "5.1.0" - }, - ["airflow", "hooks", "hive_hooks", rest @ ( - "HiveCliHook" - | "HiveMetastoreHook" - | "HiveServer2Hook" - | "HIVE_QUEUE_PRIORITIES" - )] => ProviderReplacement::SourceModuleMovedToProvider { + ["airflow", "hooks", "hive_hooks", rest @ ("HiveCliHook" + | "HiveMetastoreHook" + | "HiveServer2Hook" + | "HIVE_QUEUE_PRIORITIES")] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.apache.hive.hooks.hive", provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "operators", "hive_operator", "HiveOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.operators.hive.HiveOperator", - provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "operators", "hive_stats_operator", "HiveStatsCollectionOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.operators.hive_stats.HiveStatsCollectionOperator", - provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "operators", "hive_to_mysql", "HiveToMySqlOperator" | "HiveToMySqlTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator", - provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "operators", "hive_to_samba_operator", "HiveToSambaOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.transfers.hive_to_samba.HiveToSambaOperator", - provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "operators", "mssql_to_hive", "MsSqlToHiveOperator" | "MsSqlToHiveTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator", - provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "operators", "mysql_to_hive", "MySqlToHiveOperator" | "MySqlToHiveTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator", - provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "operators", "s3_to_hive_operator", "S3ToHiveOperator" | "S3ToHiveTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator", - provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "sensors", "hive_partition_sensor", "HivePartitionSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.sensors.hive_partition.HivePartitionSensor", - provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "sensors", "metastore_partition_sensor", "MetastorePartitionSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.sensors.metastore_partition.MetastorePartitionSensor", - provider: "apache-hive", - version: "1.0.0" - }, - ["airflow", "sensors", "named_hive_partition_sensor", "NamedHivePartitionSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.hive.sensors.named_hive_partition.NamedHivePartitionSensor", - provider: "apache-hive", - version: "1.0.0" + version: "1.0.0", }, + ["airflow", "macros", "hive", rest @ ("closest_ds_partition" | "max_partition")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.apache.hive.macros.hive", + provider: "apache-hive", + version: "5.1.0", + } + } + ["airflow", "operators", "hive_operator", "HiveOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.operators.hive", + name: "HiveOperator", + provider: "apache-hive", + version: "1.0.0", + } + } + ["airflow", "operators", "hive_stats_operator", "HiveStatsCollectionOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.operators.hive_stats", + name: "HiveStatsCollectionOperator", + provider: "apache-hive", + version: "1.0.0", + } + } + ["airflow", "operators", "hive_to_mysql", "HiveToMySqlOperator" | "HiveToMySqlTransfer"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.transfers.hive_to_mysql", + name: "HiveToMySqlOperator", + provider: "apache-hive", + version: "1.0.0", + } + } + ["airflow", "operators", "hive_to_samba_operator", "HiveToSambaOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.transfers.hive_to_samba", + name: "HiveToSambaOperator", + provider: "apache-hive", + version: "1.0.0", + } + } + ["airflow", "operators", "mssql_to_hive", "MsSqlToHiveOperator" | "MsSqlToHiveTransfer"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.transfers.mssql_to_hive", + name: "MsSqlToHiveOperator", + provider: "apache-hive", + version: "1.0.0", + } + } + ["airflow", "operators", "mysql_to_hive", "MySqlToHiveOperator" | "MySqlToHiveTransfer"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.transfers.mysql_to_hive", + name: "MySqlToHiveOperator", + provider: "apache-hive", + version: "1.0.0", + } + } + ["airflow", "operators", "s3_to_hive_operator", "S3ToHiveOperator" | "S3ToHiveTransfer"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.transfers.s3_to_hive", + name: "S3ToHiveOperator", + provider: "apache-hive", + version: "1.0.0", + } + } + ["airflow", "sensors", "hive_partition_sensor", "HivePartitionSensor"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.sensors.hive_partition", + name: "HivePartitionSensor", + provider: "apache-hive", + version: "1.0.0", + } + } + ["airflow", "sensors", "metastore_partition_sensor", "MetastorePartitionSensor"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.sensors.metastore_partition", + name: "MetastorePartitionSensor", + provider: "apache-hive", + version: "1.0.0", + } + } + ["airflow", "sensors", "named_hive_partition_sensor", "NamedHivePartitionSensor"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.hive.sensors.named_hive_partition", + name: "NamedHivePartitionSensor", + provider: "apache-hive", + version: "1.0.0", + } + } // apache-airflow-providers-http - ["airflow", "hooks", "http_hook", "HttpHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.http.hooks.http.HttpHook", - provider: "http", - version: "1.0.0" - }, - ["airflow", "operators", "http_operator", "SimpleHttpOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.http.operators.http.HttpOperator", + ["airflow", "hooks", "http_hook", "HttpHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.http.hooks.http", + name: "HttpHook", provider: "http", - version: "5.0.0" + version: "1.0.0", }, - ["airflow", "sensors", "http_sensor", "HttpSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.http.sensors.http.HttpSensor", + ["airflow", "operators", "http_operator", "SimpleHttpOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.http.operators.http", + name: "HttpOperator", + provider: "http", + version: "5.0.0", + } + } + ["airflow", "sensors", "http_sensor", "HttpSensor"] => ProviderReplacement::AutoImport { + module: "airflow.providers.http.sensors.http", + name: "HttpSensor", provider: "http", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-jdbc - ["airflow", "hooks", "jdbc_hook", rest @ ( - "JdbcHook" - | "jaydebeapi" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.jdbc.hooks.jdbc", - provider: "jdbc", - version: "1.0.0" - }, + ["airflow", "hooks", "jdbc_hook", rest @ ("JdbcHook" | "jaydebeapi")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.jdbc.hooks.jdbc", + provider: "jdbc", + version: "1.0.0", + } + } // apache-airflow-providers-cncf-kubernetes - ["airflow", "executors", "kubernetes_executor_types", rest @ ( - "ALL_NAMESPACES" - | "POD_EXECUTOR_DONE_KEY" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "k8s_model", rest @ ( - "K8SModel" - | "append_to_pod" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.cncf.kubernetes.k8s_model", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "kube_client", rest @ ( - "_disable_verify_ssl" - | "_enable_tcp_keepalive" - | "get_kube_client" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.cncf.kubernetes.kube_client", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "kubernetes_helper_functions", "add_pod_suffix"] => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_unique_suffix", - provider: "cncf-kubernetes", - version: "10.0.0" - }, - ["airflow", "kubernetes", "kubernetes_helper_functions", "create_pod_id"] => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions.create_unique_id", - provider: "cncf-kubernetes", - version: "10.0.0" - }, - ["airflow", "kubernetes", "kubernetes_helper_functions", rest @ ( - | "annotations_for_logging_task_metadata" - | "annotations_to_key" - | "get_logs_task_metadata" - | "rand_str" - )] => ProviderReplacement::SourceModuleMovedToProvider { + ["airflow", "executors", "kubernetes_executor_types", rest @ ("ALL_NAMESPACES" | "POD_EXECUTOR_DONE_KEY")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types", + provider: "cncf-kubernetes", + version: "7.4.0", + } + } + ["airflow", "kubernetes", "k8s_model", rest @ ("K8SModel" | "append_to_pod")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.cncf.kubernetes.k8s_model", + provider: "cncf-kubernetes", + version: "7.4.0", + } + } + ["airflow", "kubernetes", "kube_client", rest @ ("_disable_verify_ssl" | "_enable_tcp_keepalive" | "get_kube_client")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.cncf.kubernetes.kube_client", + provider: "cncf-kubernetes", + version: "7.4.0", + } + } + ["airflow", "kubernetes", "kubernetes_helper_functions", "add_pod_suffix"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", + name: "add_unique_suffix", + provider: "cncf-kubernetes", + version: "10.0.0", + } + } + ["airflow", "kubernetes", "kubernetes_helper_functions", "create_pod_id"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", + name: "create_unique_id", + provider: "cncf-kubernetes", + version: "10.0.0", + } + } + ["airflow", "kubernetes", "kubernetes_helper_functions", rest @ ("annotations_for_logging_task_metadata" + | "annotations_to_key" + | "get_logs_task_metadata" + | "rand_str")] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, ["airflow", "kubernetes", "pod", rest] => match *rest { - "Port" =>ProviderReplacement::ProviderName { - name: "kubernetes.client.models.V1ContainerPort", + "Port" => ProviderReplacement::AutoImport { + module: "kubernetes.client.models", + name: "V1ContainerPort", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, - "Resources" => ProviderReplacement::ProviderName { - name: "kubernetes.client.models.V1ResourceRequirements", + "Resources" => ProviderReplacement::AutoImport { + module: "kubernetes.client.models", + name: "V1ResourceRequirements", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, - _ => return + _ => return, }, - ["airflow", "kubernetes", "pod_generator", rest ] => match *rest { + ["airflow", "kubernetes", "pod_generator", rest] => match *rest { "datetime_to_label_safe_datestring" | "extend_object_field" | "label_safe_datestring_to_datetime" @@ -528,248 +644,316 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan name: (*rest).to_string(), module: "airflow.providers.cncf.kubernetes.pod_generator", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, - "PodDefaults" => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults", + "PodDefaults" => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.utils.xcom_sidecar", + name: "PodDefaults", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, - "PodGeneratorDeprecated" => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.pod_generator.PodGenerator", + "PodGeneratorDeprecated" => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.pod_generator", + name: "PodGenerator", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, - "add_pod_suffix" => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_unique_suffix", + "add_pod_suffix" => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", + name: "add_unique_suffix", provider: "cncf-kubernetes", - version: "10.0.0" + version: "10.0.0", }, "rand_str" => ProviderReplacement::SourceModuleMovedToProvider { module: "airflow.providers.cncf.kubernetes.kubernetes_helper_functions", name: "rand_str".to_string(), provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, _ => return, }, - ["airflow", "kubernetes", "pod_generator_deprecated", rest @ ( - "make_safe_label_value" - | "PodGenerator" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.cncf.kubernetes.pod_generator", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "pod_generator_deprecated" | "pod_launcher_deprecated", "PodDefaults"] => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "pod_launcher_deprecated", "get_kube_client"] => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.kube_client.get_kube_client", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "pod_launcher" | "pod_launcher_deprecated", "PodLauncher"] => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.utils.pod_manager.PodManager", - provider: "cncf-kubernetes", - version: "3.0.0" - }, - ["airflow", "kubernetes", "pod_launcher" | "pod_launcher_deprecated", "PodStatus"] => ProviderReplacement::ProviderName { - name: " airflow.providers.cncf.kubernetes.utils.pod_manager.PodPhase", - provider: "cncf-kubernetes", - version: "3.0.0" - }, - ["airflow", "kubernetes", "pod_runtime_info_env", "PodRuntimeInfoEnv"] => ProviderReplacement::ProviderName { - name: "kubernetes.client.models.V1EnvVar", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "secret", rest ] => match *rest { - "K8SModel" => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.k8s_model.K8SModel", + ["airflow", "kubernetes", "pod_generator_deprecated", rest @ ("make_safe_label_value" | "PodGenerator")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.cncf.kubernetes.pod_generator", + provider: "cncf-kubernetes", + version: "7.4.0", + } + } + ["airflow", "kubernetes", "pod_generator_deprecated" | "pod_launcher_deprecated", "PodDefaults"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.utils.xcom_sidecar", + name: "PodDefaults", + provider: "cncf-kubernetes", + version: "7.4.0", + } + } + ["airflow", "kubernetes", "pod_launcher_deprecated", "get_kube_client"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.kube_client", + name: "get_kube_client", + provider: "cncf-kubernetes", + version: "7.4.0", + } + } + ["airflow", "kubernetes", "pod_launcher" | "pod_launcher_deprecated", "PodLauncher"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.utils.pod_manager", + name: "PodManager", provider: "cncf-kubernetes", - version: "7.4.0" + version: "3.0.0", + } + } + ["airflow", "kubernetes", "pod_launcher" | "pod_launcher_deprecated", "PodStatus"] => { + ProviderReplacement::AutoImport { + module: " airflow.providers.cncf.kubernetes.utils.pod_manager", + name: "PodPhase", + provider: "cncf-kubernetes", + version: "3.0.0", + } + } + ["airflow", "kubernetes", "pod_runtime_info_env", "PodRuntimeInfoEnv"] => { + ProviderReplacement::AutoImport { + module: "kubernetes.client.models", + name: "V1EnvVar", + provider: "cncf-kubernetes", + version: "7.4.0", + } + } + ["airflow", "kubernetes", "secret", rest] => match *rest { + "K8SModel" => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.k8s_model", + name: "K8SModel", + provider: "cncf-kubernetes", + version: "7.4.0", }, - "Secret" => ProviderReplacement::ProviderName { - name: "airflow.providers.cncf.kubernetes.secret.Secret", + "Secret" => ProviderReplacement::AutoImport { + module: "airflow.providers.cncf.kubernetes.secret", + name: "Secret", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, _ => return, }, - ["airflow", "kubernetes", "volume", "Volume"] => ProviderReplacement::ProviderName { - name: "kubernetes.client.models.V1Volume", - provider: "cncf-kubernetes", - version: "7.4.0" - }, - ["airflow", "kubernetes", "volume_mount", "VolumeMount"] => ProviderReplacement::ProviderName { - name: "kubernetes.client.models.V1VolumeMount", + ["airflow", "kubernetes", "volume", "Volume"] => ProviderReplacement::AutoImport { + module: "kubernetes.client.models", + name: "V1Volume", provider: "cncf-kubernetes", - version: "7.4.0" + version: "7.4.0", }, + ["airflow", "kubernetes", "volume_mount", "VolumeMount"] => { + ProviderReplacement::AutoImport { + module: "kubernetes.client.models", + name: "V1VolumeMount", + provider: "cncf-kubernetes", + version: "7.4.0", + } + } // apache-airflow-providers-microsoft-mssql - ["airflow", "hooks", "mssql_hook", "MsSqlHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.microsoft.mssql.hooks.mssql.MsSqlHook", + ["airflow", "hooks", "mssql_hook", "MsSqlHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.microsoft.mssql.hooks.mssql", + name: "MsSqlHook", provider: "microsoft-mssql", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-mysql - ["airflow", "hooks", "mysql_hook", "MySqlHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.mysql.hooks.mysql.MySqlHook", - provider: "mysql", - version: "1.0.0" - }, - ["airflow", "operators", "presto_to_mysql", "PrestoToMySqlOperator" | "PrestoToMySqlTransfer"] => ProviderReplacement::ProviderName { - name: "airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator", + ["airflow", "hooks", "mysql_hook", "MySqlHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.mysql.hooks.mysql", + name: "MySqlHook", provider: "mysql", - version: "1.0.0" + version: "1.0.0", }, + ["airflow", "operators", "presto_to_mysql", "PrestoToMySqlOperator" | "PrestoToMySqlTransfer"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.mysql.transfers.presto_to_mysql", + name: "PrestoToMySqlOperator", + provider: "mysql", + version: "1.0.0", + } + } // apache-airflow-providers-oracle - ["airflow", "hooks", "oracle_hook", "OracleHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.oracle.hooks.oracle.OracleHook", + ["airflow", "hooks", "oracle_hook", "OracleHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.oracle.hooks.oracle", + name: "OracleHook", provider: "oracle", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-papermill - ["airflow", "operators", "papermill_operator", "PapermillOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.papermill.operators.papermill.PapermillOperator", - provider: "papermill", - version: "1.0.0" - }, + ["airflow", "operators", "papermill_operator", "PapermillOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.papermill.operators.papermill", + name: "PapermillOperator", + provider: "papermill", + version: "1.0.0", + } + } // apache-airflow-providers-apache-pig - ["airflow", "hooks", "pig_hook", "PigCliHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.pig.hooks.pig.PigCliHook", - provider: "apache-pig", - version: "1.0.0" - }, - ["airflow", "operators", "pig_operator", "PigOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.apache.pig.operators.pig.PigOperator", + ["airflow", "hooks", "pig_hook", "PigCliHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.apache.pig.hooks.pig", + name: "PigCliHook", provider: "apache-pig", - version: "1.0.0" + version: "1.0.0", }, + ["airflow", "operators", "pig_operator", "PigOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.apache.pig.operators.pig", + name: "PigOperator", + provider: "apache-pig", + version: "1.0.0", + } + } // apache-airflow-providers-postgres - ["airflow", "hooks", "postgres_hook", "PostgresHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.postgres.hooks.postgres.PostgresHook", + ["airflow", "hooks", "postgres_hook", "PostgresHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.postgres.hooks.postgres", + name: "PostgresHook", provider: "postgres", - version: "1.0.0" + version: "1.0.0", }, ["airflow", "operators", "postgres_operator", "Mapping"] => ProviderReplacement::None, // apache-airflow-providers-presto - ["airflow", "hooks", "presto_hook", "PrestoHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.presto.hooks.presto.PrestoHook", + ["airflow", "hooks", "presto_hook", "PrestoHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.presto.hooks.presto", + name: "PrestoHook", provider: "presto", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-samba - ["airflow", "hooks", "samba_hook", "SambaHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.samba.hooks.samba.SambaHook", + ["airflow", "hooks", "samba_hook", "SambaHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.samba.hooks.samba", + name: "SambaHook", provider: "samba", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-slack - ["airflow", "hooks", "slack_hook", "SlackHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.slack.hooks.slack.SlackHook", - provider: "slack", - version: "1.0.0" - }, - ["airflow", "operators", "slack_operator", rest @ ( - "SlackAPIOperator" - | "SlackAPIPostOperator" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.slack.operators.slack", + ["airflow", "hooks", "slack_hook", "SlackHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.slack.hooks.slack", + name: "SlackHook", provider: "slack", - version: "1.0.0" + version: "1.0.0", }, + ["airflow", "operators", "slack_operator", rest @ ("SlackAPIOperator" | "SlackAPIPostOperator")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.slack.operators.slack", + provider: "slack", + version: "1.0.0", + } + } // apache-airflow-providers-smtp - ["airflow", "operators", "email_operator" | "email", "EmailOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.smtp.operators.smtp.EmailOperator", - provider: "smtp", - version: "1.0.0", - }, + ["airflow", "operators", "email_operator" | "email", "EmailOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.smtp.operators.smtp", + name: "EmailOperator", + provider: "smtp", + version: "1.0.0", + } + } // apache-airflow-providers-sqlite - ["airflow", "hooks", "sqlite_hook", "SqliteHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.sqlite.hooks.sqlite.SqliteHook", + ["airflow", "hooks", "sqlite_hook", "SqliteHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.sqlite.hooks.sqlite", + name: "SqliteHook", provider: "sqlite", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-zendesk - ["airflow", "hooks", "zendesk_hook", "ZendeskHook"] => - ProviderReplacement::ProviderName { - name: "airflow.providers.zendesk.hooks.zendesk.ZendeskHook", + ["airflow", "hooks", "zendesk_hook", "ZendeskHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.zendesk.hooks.zendesk", + name: "ZendeskHook", provider: "zendesk", - version: "1.0.0" + version: "1.0.0", }, // apache-airflow-providers-standard - ["airflow", "operators", "bash_operator", "BashOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.bash.BashOperator", - provider: "standard", - version: "0.0.1" - }, - ["airflow", "operators", "dagrun_operator", rest @ ( - "TriggerDagRunLink" - | "TriggerDagRunOperator" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.operators.trigger_dagrun", - provider: "standard", - version: "0.0.2" - }, - ["airflow", "operators", "dummy" | "dummy_operator", "EmptyOperator" | "DummyOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.empty.EmptyOperator", - provider: "standard", - version: "0.0.2" - }, - ["airflow", "operators", "latest_only_operator", "LatestOnlyOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.latest_only.LatestOnlyOperator", - provider: "standard", - version: "0.0.3" - }, - ["airflow", "operators", "python_operator", rest @ ( - "BranchPythonOperator" - | "PythonOperator" - | "PythonVirtualenvOperator" - | "ShortCircuitOperator" - )] => ProviderReplacement::SourceModuleMovedToProvider { + ["airflow", "operators", "bash_operator", "BashOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.bash", + name: "BashOperator", + provider: "standard", + version: "0.0.1", + } + } + ["airflow", "operators", "dagrun_operator", rest @ ("TriggerDagRunLink" | "TriggerDagRunOperator")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.standard.operators.trigger_dagrun", + provider: "standard", + version: "0.0.2", + } + } + ["airflow", "operators", "dummy" | "dummy_operator", "EmptyOperator" | "DummyOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.empty", + name: "EmptyOperator", + provider: "standard", + version: "0.0.2", + } + } + ["airflow", "operators", "latest_only_operator", "LatestOnlyOperator"] => { + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.latest_only", + name: "LatestOnlyOperator", + provider: "standard", + version: "0.0.3", + } + } + ["airflow", "operators", "python_operator", rest @ ("BranchPythonOperator" + | "PythonOperator" + | "PythonVirtualenvOperator" + | "ShortCircuitOperator")] => ProviderReplacement::SourceModuleMovedToProvider { name: (*rest).to_string(), module: "airflow.providers.standard.operators.python", provider: "standard", - version: "0.0.1" - }, - ["airflow", "sensors", "external_task_sensor", rest @ ( - "ExternalTaskMarker" - | "ExternalTaskSensor" - | "ExternalTaskSensorLink" - )] => ProviderReplacement::SourceModuleMovedToProvider { - name: (*rest).to_string(), - module: "airflow.providers.standard.sensors.external_task", - provider: "standard", - version: "0.0.3" + version: "0.0.1", }, + ["airflow", "sensors", "external_task_sensor", rest @ ("ExternalTaskMarker" | "ExternalTaskSensor" | "ExternalTaskSensorLink")] => { + ProviderReplacement::SourceModuleMovedToProvider { + name: (*rest).to_string(), + module: "airflow.providers.standard.sensors.external_task", + provider: "standard", + version: "0.0.3", + } + } _ => return, }; - checker.report_diagnostic(Diagnostic::new( + + let mut diagnostic = Diagnostic::new( Airflow3MovedToProvider { deprecated: qualified_name.to_string(), - replacement, + replacement: replacement.clone(), }, ranged.range(), - )); + ); + + if let ProviderReplacement::AutoImport { + module, + name, + provider: _, + version: _, + } = replacement + { + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer().get_or_import_symbol( + &ImportRequest::import_from(module, name), + expr.start(), + checker.semantic(), + )?; + let replacement_edit = Edit::range_replacement(binding, ranged.range()); + Ok(Fix::safe_edits(import_edit, [replacement_edit])) + }); + } + + checker.report_diagnostic(diagnostic); } diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs index 8b990d0b15680e..b23e031c639faa 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs @@ -49,14 +49,21 @@ impl Violation for Airflow3SuggestedToMoveToProvider { provider, version: _, } + | ProviderReplacement::AutoImport { + name: _, + module: _, + provider, + version: _, + } | ProviderReplacement::SourceModuleMovedToProvider { name: _, module: _, provider, version: _, } => { - format!("`{deprecated}` is deprecated and moved into `{provider}` provider in Airflow 3.0; \ - It still works in Airflow 3.0 but is expected to be removed in a future version." + format!( + "`{deprecated}` is deprecated and moved into `{provider}` provider in Airflow 3.0; \ + It still works in Airflow 3.0 but is expected to be removed in a future version." ) } } @@ -75,6 +82,14 @@ impl Violation for Airflow3SuggestedToMoveToProvider { "Install `apache-airflow-providers-{provider}>={version}` and use `{name}` instead." )) }, + ProviderReplacement::AutoImport { + module, + name, + provider, + version, + } => { + Some(format!("Install `apache-airflow-providers-{provider}>={version}` and use `{module}.{name}` instead.")) + }, ProviderReplacement::SourceModuleMovedToProvider { module, name, diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap deleted file mode 100644 index 6d8ce502c7b59c..00000000000000 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302.py.snap +++ /dev/null @@ -1,1646 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/airflow/mod.rs ---- -AIR302.py:226:1: AIR302 `airflow.hooks.S3_hook.provide_bucket_name` is moved into `amazon` provider in Airflow 3.0; - | -225 | # apache-airflow-providers-amazon -226 | provide_bucket_name() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -227 | GCSToS3Operator() -228 | GoogleApiToS3Operator() - | - = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.hooks.s3.provide_bucket_name` instead. - -AIR302.py:227:1: AIR302 `airflow.operators.gcs_to_s3.GCSToS3Operator` is moved into `amazon` provider in Airflow 3.0; - | -225 | # apache-airflow-providers-amazon -226 | provide_bucket_name() -227 | GCSToS3Operator() - | ^^^^^^^^^^^^^^^ AIR302 -228 | GoogleApiToS3Operator() -229 | GoogleApiToS3Transfer() - | - = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.gcs_to_s3.GCSToS3Operator` instead. - -AIR302.py:228:1: AIR302 `airflow.operators.google_api_to_s3_transfer.GoogleApiToS3Operator` is moved into `amazon` provider in Airflow 3.0; - | -226 | provide_bucket_name() -227 | GCSToS3Operator() -228 | GoogleApiToS3Operator() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -229 | GoogleApiToS3Transfer() -230 | RedshiftToS3Operator() - | - = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator` instead. - -AIR302.py:229:1: AIR302 `airflow.operators.google_api_to_s3_transfer.GoogleApiToS3Transfer` is moved into `amazon` provider in Airflow 3.0; - | -227 | GCSToS3Operator() -228 | GoogleApiToS3Operator() -229 | GoogleApiToS3Transfer() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -230 | RedshiftToS3Operator() -231 | RedshiftToS3Transfer() - | - = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator` instead. - -AIR302.py:230:1: AIR302 `airflow.operators.redshift_to_s3_operator.RedshiftToS3Operator` is moved into `amazon` provider in Airflow 3.0; - | -228 | GoogleApiToS3Operator() -229 | GoogleApiToS3Transfer() -230 | RedshiftToS3Operator() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -231 | RedshiftToS3Transfer() -232 | S3FileTransformOperator() - | - = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator` instead. - -AIR302.py:231:1: AIR302 `airflow.operators.redshift_to_s3_operator.RedshiftToS3Transfer` is moved into `amazon` provider in Airflow 3.0; - | -229 | GoogleApiToS3Transfer() -230 | RedshiftToS3Operator() -231 | RedshiftToS3Transfer() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -232 | S3FileTransformOperator() -233 | S3Hook() - | - = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator` instead. - -AIR302.py:232:1: AIR302 `airflow.operators.s3_file_transform_operator.S3FileTransformOperator` is moved into `amazon` provider in Airflow 3.0; - | -230 | RedshiftToS3Operator() -231 | RedshiftToS3Transfer() -232 | S3FileTransformOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -233 | S3Hook() -234 | SSQLTableCheckOperator3KeySensor() - | - = help: Install `apache-airflow-providers-amazon>=3.0.0` and use `airflow.providers.amazon.aws.operators.s3.S3FileTransformOperator` instead. - -AIR302.py:233:1: AIR302 `airflow.hooks.S3_hook.S3Hook` is moved into `amazon` provider in Airflow 3.0; - | -231 | RedshiftToS3Transfer() -232 | S3FileTransformOperator() -233 | S3Hook() - | ^^^^^^ AIR302 -234 | SSQLTableCheckOperator3KeySensor() -235 | S3ToRedshiftOperator() - | - = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.hooks.s3.S3Hook` instead. - -AIR302.py:235:1: AIR302 `airflow.operators.s3_to_redshift_operator.S3ToRedshiftOperator` is moved into `amazon` provider in Airflow 3.0; - | -233 | S3Hook() -234 | SSQLTableCheckOperator3KeySensor() -235 | S3ToRedshiftOperator() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -236 | S3ToRedshiftTransfer() - | - = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator` instead. - -AIR302.py:236:1: AIR302 `airflow.operators.s3_to_redshift_operator.S3ToRedshiftTransfer` is moved into `amazon` provider in Airflow 3.0; - | -234 | SSQLTableCheckOperator3KeySensor() -235 | S3ToRedshiftOperator() -236 | S3ToRedshiftTransfer() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -237 | -238 | # apache-airflow-providers-celery - | - = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator` instead. - -AIR302.py:239:1: AIR302 `airflow.config_templates.default_celery.DEFAULT_CELERY_CONFIG` is moved into `celery` provider in Airflow 3.0; - | -238 | # apache-airflow-providers-celery -239 | DEFAULT_CELERY_CONFIG - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -240 | app -241 | CeleryExecutor() - | - = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.default_celery.DEFAULT_CELERY_CONFIG` instead. - -AIR302.py:240:1: AIR302 `airflow.executors.celery_executor.app` is moved into `celery` provider in Airflow 3.0; - | -238 | # apache-airflow-providers-celery -239 | DEFAULT_CELERY_CONFIG -240 | app - | ^^^ AIR302 -241 | CeleryExecutor() -242 | CeleryKubernetesExecutor() - | - = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_executor_utils.app` instead. - -AIR302.py:241:1: AIR302 `airflow.executors.celery_executor.CeleryExecutor` is moved into `celery` provider in Airflow 3.0; - | -239 | DEFAULT_CELERY_CONFIG -240 | app -241 | CeleryExecutor() - | ^^^^^^^^^^^^^^ AIR302 -242 | CeleryKubernetesExecutor() - | - = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_executor.CeleryExecutor` instead. - -AIR302.py:242:1: AIR302 `airflow.executors.celery_kubernetes_executor.CeleryKubernetesExecutor` is moved into `celery` provider in Airflow 3.0; - | -240 | app -241 | CeleryExecutor() -242 | CeleryKubernetesExecutor() - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -243 | -244 | # apache-airflow-providers-common-sql - | - = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_kubernetes_executor.CeleryKubernetesExecutor` instead. - -AIR302.py:245:1: AIR302 `airflow.operators.sql._convert_to_float_if_possible` is moved into `common-sql` provider in Airflow 3.0; - | -244 | # apache-airflow-providers-common-sql -245 | _convert_to_float_if_possible() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -246 | parse_boolean() -247 | BaseSQLOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql._convert_to_float_if_possible` instead. - -AIR302.py:246:1: AIR302 `airflow.operators.sql.parse_boolean` is moved into `common-sql` provider in Airflow 3.0; - | -244 | # apache-airflow-providers-common-sql -245 | _convert_to_float_if_possible() -246 | parse_boolean() - | ^^^^^^^^^^^^^ AIR302 -247 | BaseSQLOperator() -248 | BashOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.parse_boolean` instead. - -AIR302.py:247:1: AIR302 `airflow.operators.sql.BaseSQLOperator` is moved into `common-sql` provider in Airflow 3.0; - | -245 | _convert_to_float_if_possible() -246 | parse_boolean() -247 | BaseSQLOperator() - | ^^^^^^^^^^^^^^^ AIR302 -248 | BashOperator() -249 | LegacyBashOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BaseSQLOperator` instead. - -AIR302.py:249:1: AIR302 `airflow.operators.bash_operator.BashOperator` is moved into `standard` provider in Airflow 3.0; - | -247 | BaseSQLOperator() -248 | BashOperator() -249 | LegacyBashOperator() - | ^^^^^^^^^^^^^^^^^^ AIR302 -250 | BranchSQLOperator() -251 | CheckOperator() - | - = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.bash.BashOperator` instead. - -AIR302.py:250:1: AIR302 `airflow.operators.sql.BranchSQLOperator` is moved into `common-sql` provider in Airflow 3.0; - | -248 | BashOperator() -249 | LegacyBashOperator() -250 | BranchSQLOperator() - | ^^^^^^^^^^^^^^^^^ AIR302 -251 | CheckOperator() -252 | ConnectorProtocol() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BranchSQLOperator` instead. - -AIR302.py:251:1: AIR302 `airflow.operators.check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -249 | LegacyBashOperator() -250 | BranchSQLOperator() -251 | CheckOperator() - | ^^^^^^^^^^^^^ AIR302 -252 | ConnectorProtocol() -253 | DbApiHook() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:252:1: AIR302 `airflow.hooks.dbapi.ConnectorProtocol` is moved into `common-sql` provider in Airflow 3.0; - | -250 | BranchSQLOperator() -251 | CheckOperator() -252 | ConnectorProtocol() - | ^^^^^^^^^^^^^^^^^ AIR302 -253 | DbApiHook() -254 | DbApiHook2() - | - = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.ConnectorProtocol` instead. - -AIR302.py:253:1: AIR302 `airflow.hooks.dbapi.DbApiHook` is moved into `common-sql` provider in Airflow 3.0; - | -251 | CheckOperator() -252 | ConnectorProtocol() -253 | DbApiHook() - | ^^^^^^^^^ AIR302 -254 | DbApiHook2() -255 | IntervalCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.DbApiHook` instead. - -AIR302.py:254:1: AIR302 `airflow.hooks.dbapi_hook.DbApiHook` is moved into `common-sql` provider in Airflow 3.0; - | -252 | ConnectorProtocol() -253 | DbApiHook() -254 | DbApiHook2() - | ^^^^^^^^^^ AIR302 -255 | IntervalCheckOperator() -256 | PrestoCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.DbApiHook` instead. - -AIR302.py:255:1: AIR302 `airflow.operators.check_operator.IntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -253 | DbApiHook() -254 | DbApiHook2() -255 | IntervalCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -256 | PrestoCheckOperator() -257 | PrestoIntervalCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. - -AIR302.py:256:1: AIR302 `airflow.operators.presto_check_operator.PrestoCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -254 | DbApiHook2() -255 | IntervalCheckOperator() -256 | PrestoCheckOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -257 | PrestoIntervalCheckOperator() -258 | PrestoValueCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:257:1: AIR302 `airflow.operators.presto_check_operator.PrestoIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -255 | IntervalCheckOperator() -256 | PrestoCheckOperator() -257 | PrestoIntervalCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -258 | PrestoValueCheckOperator() -259 | SQLCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. - -AIR302.py:258:1: AIR302 `airflow.operators.presto_check_operator.PrestoValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -256 | PrestoCheckOperator() -257 | PrestoIntervalCheckOperator() -258 | PrestoValueCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -259 | SQLCheckOperator() -260 | SQLCheckOperator2() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. - -AIR302.py:259:1: AIR302 `airflow.operators.check_operator.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -257 | PrestoIntervalCheckOperator() -258 | PrestoValueCheckOperator() -259 | SQLCheckOperator() - | ^^^^^^^^^^^^^^^^ AIR302 -260 | SQLCheckOperator2() -261 | SQLCheckOperator3() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:260:1: AIR302 `airflow.operators.presto_check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -258 | PrestoValueCheckOperator() -259 | SQLCheckOperator() -260 | SQLCheckOperator2() - | ^^^^^^^^^^^^^^^^^ AIR302 -261 | SQLCheckOperator3() -262 | SQLColumnCheckOperator2() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:261:1: AIR302 `airflow.operators.sql.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -259 | SQLCheckOperator() -260 | SQLCheckOperator2() -261 | SQLCheckOperator3() - | ^^^^^^^^^^^^^^^^^ AIR302 -262 | SQLColumnCheckOperator2() -263 | SQLIntervalCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:262:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -260 | SQLCheckOperator2() -261 | SQLCheckOperator3() -262 | SQLColumnCheckOperator2() - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -263 | SQLIntervalCheckOperator() -264 | SQLIntervalCheckOperator2() - | - = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.SQLColumnCheckOperator` instead. - -AIR302.py:263:1: AIR302 `airflow.operators.check_operator.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -261 | SQLCheckOperator3() -262 | SQLColumnCheckOperator2() -263 | SQLIntervalCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -264 | SQLIntervalCheckOperator2() -265 | SQLIntervalCheckOperator3() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. - -AIR302.py:264:1: AIR302 `airflow.operators.presto_check_operator.IntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -262 | SQLColumnCheckOperator2() -263 | SQLIntervalCheckOperator() -264 | SQLIntervalCheckOperator2() - | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -265 | SQLIntervalCheckOperator3() -266 | SQLTableCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. - -AIR302.py:265:1: AIR302 `airflow.operators.sql.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -263 | SQLIntervalCheckOperator() -264 | SQLIntervalCheckOperator2() -265 | SQLIntervalCheckOperator3() - | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -266 | SQLTableCheckOperator() -267 | SQLThresholdCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. - -AIR302.py:267:1: AIR302 `airflow.operators.check_operator.SQLThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -265 | SQLIntervalCheckOperator3() -266 | SQLTableCheckOperator() -267 | SQLThresholdCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -268 | SQLThresholdCheckOperator2() -269 | SQLValueCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. - -AIR302.py:268:1: AIR302 `airflow.operators.sql.SQLThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -266 | SQLTableCheckOperator() -267 | SQLThresholdCheckOperator() -268 | SQLThresholdCheckOperator2() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -269 | SQLValueCheckOperator() -270 | SQLValueCheckOperator2() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. - -AIR302.py:269:1: AIR302 `airflow.operators.check_operator.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -267 | SQLThresholdCheckOperator() -268 | SQLThresholdCheckOperator2() -269 | SQLValueCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -270 | SQLValueCheckOperator2() -271 | SQLValueCheckOperator3() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. - -AIR302.py:270:1: AIR302 `airflow.operators.presto_check_operator.ValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -268 | SQLThresholdCheckOperator2() -269 | SQLValueCheckOperator() -270 | SQLValueCheckOperator2() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -271 | SQLValueCheckOperator3() -272 | SqlSensor() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. - -AIR302.py:271:1: AIR302 `airflow.operators.sql.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -269 | SQLValueCheckOperator() -270 | SQLValueCheckOperator2() -271 | SQLValueCheckOperator3() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -272 | SqlSensor() -273 | SqlSensor2() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. - -AIR302.py:272:1: AIR302 `airflow.sensors.sql.SqlSensor` is moved into `common-sql` provider in Airflow 3.0; - | -270 | SQLValueCheckOperator2() -271 | SQLValueCheckOperator3() -272 | SqlSensor() - | ^^^^^^^^^ AIR302 -273 | SqlSensor2() -274 | ThresholdCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.sensors.sql.SqlSensor` instead. - -AIR302.py:274:1: AIR302 `airflow.operators.check_operator.ThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -272 | SqlSensor() -273 | SqlSensor2() -274 | ThresholdCheckOperator() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -275 | ValueCheckOperator() - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. - -AIR302.py:275:1: AIR302 `airflow.operators.check_operator.ValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -273 | SqlSensor2() -274 | ThresholdCheckOperator() -275 | ValueCheckOperator() - | ^^^^^^^^^^^^^^^^^^ AIR302 -276 | -277 | # apache-airflow-providers-daskexecutor - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. - -AIR302.py:278:1: AIR302 `airflow.executors.dask_executor.DaskExecutor` is moved into `daskexecutor` provider in Airflow 3.0; - | -277 | # apache-airflow-providers-daskexecutor -278 | DaskExecutor() - | ^^^^^^^^^^^^ AIR302 -279 | -280 | # apache-airflow-providers-docker - | - = help: Install `apache-airflow-providers-daskexecutor>=1.0.0` and use `airflow.providers.daskexecutor.executors.dask_executor.DaskExecutor` instead. - -AIR302.py:281:1: AIR302 `airflow.hooks.docker_hook.DockerHook` is moved into `docker` provider in Airflow 3.0; - | -280 | # apache-airflow-providers-docker -281 | DockerHook() - | ^^^^^^^^^^ AIR302 -282 | DockerOperator() - | - = help: Install `apache-airflow-providers-docker>=1.0.0` and use `airflow.providers.docker.hooks.docker.DockerHook` instead. - -AIR302.py:282:1: AIR302 `airflow.operators.docker_operator.DockerOperator` is moved into `docker` provider in Airflow 3.0; - | -280 | # apache-airflow-providers-docker -281 | DockerHook() -282 | DockerOperator() - | ^^^^^^^^^^^^^^ AIR302 -283 | -284 | # apache-airflow-providers-apache-druid - | - = help: Install `apache-airflow-providers-docker>=1.0.0` and use `airflow.providers.docker.operators.docker.DockerOperator` instead. - -AIR302.py:285:1: AIR302 `airflow.hooks.druid_hook.DruidDbApiHook` is moved into `apache-druid` provider in Airflow 3.0; - | -284 | # apache-airflow-providers-apache-druid -285 | DruidDbApiHook() - | ^^^^^^^^^^^^^^ AIR302 -286 | DruidHook() -287 | DruidCheckOperator() - | - = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidDbApiHook` instead. - -AIR302.py:286:1: AIR302 `airflow.hooks.druid_hook.DruidHook` is moved into `apache-druid` provider in Airflow 3.0; - | -284 | # apache-airflow-providers-apache-druid -285 | DruidDbApiHook() -286 | DruidHook() - | ^^^^^^^^^ AIR302 -287 | DruidCheckOperator() - | - = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidHook` instead. - -AIR302.py:287:1: AIR302 `airflow.operators.druid_check_operator.DruidCheckOperator` is moved into `common-sql` provider in Airflow 3.0; - | -285 | DruidDbApiHook() -286 | DruidHook() -287 | DruidCheckOperator() - | ^^^^^^^^^^^^^^^^^^ AIR302 -288 | -289 | # apache-airflow-providers-apache-hdfs - | - = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. - -AIR302.py:290:1: AIR302 `airflow.hooks.webhdfs_hook.WebHDFSHook` is moved into `apache-hdfs` provider in Airflow 3.0; - | -289 | # apache-airflow-providers-apache-hdfs -290 | WebHDFSHook() - | ^^^^^^^^^^^ AIR302 -291 | WebHdfsSensor() - | - = help: Install `apache-airflow-providers-apache-hdfs>=1.0.0` and use `airflow.providers.apache.hdfs.hooks.webhdfs.WebHDFSHook` instead. - -AIR302.py:291:1: AIR302 `airflow.sensors.web_hdfs_sensor.WebHdfsSensor` is moved into `apache-hdfs` provider in Airflow 3.0; - | -289 | # apache-airflow-providers-apache-hdfs -290 | WebHDFSHook() -291 | WebHdfsSensor() - | ^^^^^^^^^^^^^ AIR302 -292 | -293 | # apache-airflow-providers-apache-hive - | - = help: Install `apache-airflow-providers-apache-hdfs>=1.0.0` and use `airflow.providers.apache.hdfs.sensors.web_hdfs.WebHdfsSensor` instead. - -AIR302.py:294:1: AIR302 `airflow.hooks.hive_hooks.HIVE_QUEUE_PRIORITIES` is moved into `apache-hive` provider in Airflow 3.0; - | -293 | # apache-airflow-providers-apache-hive -294 | HIVE_QUEUE_PRIORITIES - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -295 | closest_ds_partition() -296 | max_partition() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HIVE_QUEUE_PRIORITIES` instead. - -AIR302.py:295:1: AIR302 `airflow.macros.hive.closest_ds_partition` is moved into `apache-hive` provider in Airflow 3.0; - | -293 | # apache-airflow-providers-apache-hive -294 | HIVE_QUEUE_PRIORITIES -295 | closest_ds_partition() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -296 | max_partition() -297 | HiveCliHook() - | - = help: Install `apache-airflow-providers-apache-hive>=5.1.0` and use `airflow.providers.apache.hive.macros.hive.closest_ds_partition` instead. - -AIR302.py:296:1: AIR302 `airflow.macros.hive.max_partition` is moved into `apache-hive` provider in Airflow 3.0; - | -294 | HIVE_QUEUE_PRIORITIES -295 | closest_ds_partition() -296 | max_partition() - | ^^^^^^^^^^^^^ AIR302 -297 | HiveCliHook() -298 | HiveMetastoreHook() - | - = help: Install `apache-airflow-providers-apache-hive>=5.1.0` and use `airflow.providers.apache.hive.macros.hive.max_partition` instead. - -AIR302.py:297:1: AIR302 `airflow.hooks.hive_hooks.HiveCliHook` is moved into `apache-hive` provider in Airflow 3.0; - | -295 | closest_ds_partition() -296 | max_partition() -297 | HiveCliHook() - | ^^^^^^^^^^^ AIR302 -298 | HiveMetastoreHook() -299 | HiveOperator() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveCliHook` instead. - -AIR302.py:298:1: AIR302 `airflow.hooks.hive_hooks.HiveMetastoreHook` is moved into `apache-hive` provider in Airflow 3.0; - | -296 | max_partition() -297 | HiveCliHook() -298 | HiveMetastoreHook() - | ^^^^^^^^^^^^^^^^^ AIR302 -299 | HiveOperator() -300 | HivePartitionSensor() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveMetastoreHook` instead. - -AIR302.py:299:1: AIR302 `airflow.operators.hive_operator.HiveOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -297 | HiveCliHook() -298 | HiveMetastoreHook() -299 | HiveOperator() - | ^^^^^^^^^^^^ AIR302 -300 | HivePartitionSensor() -301 | HiveServer2Hook() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.operators.hive.HiveOperator` instead. - -AIR302.py:300:1: AIR302 `airflow.sensors.hive_partition_sensor.HivePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; - | -298 | HiveMetastoreHook() -299 | HiveOperator() -300 | HivePartitionSensor() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -301 | HiveServer2Hook() -302 | HiveStatsCollectionOperator() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.hive_partition.HivePartitionSensor` instead. - -AIR302.py:301:1: AIR302 `airflow.hooks.hive_hooks.HiveServer2Hook` is moved into `apache-hive` provider in Airflow 3.0; - | -299 | HiveOperator() -300 | HivePartitionSensor() -301 | HiveServer2Hook() - | ^^^^^^^^^^^^^^^ AIR302 -302 | HiveStatsCollectionOperator() -303 | HiveToDruidOperator() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveServer2Hook` instead. - -AIR302.py:302:1: AIR302 `airflow.operators.hive_stats_operator.HiveStatsCollectionOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -300 | HivePartitionSensor() -301 | HiveServer2Hook() -302 | HiveStatsCollectionOperator() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -303 | HiveToDruidOperator() -304 | HiveToDruidTransfer() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.operators.hive_stats.HiveStatsCollectionOperator` instead. - -AIR302.py:303:1: AIR302 `airflow.operators.hive_to_druid.HiveToDruidOperator` is moved into `apache-druid` provider in Airflow 3.0; - | -301 | HiveServer2Hook() -302 | HiveStatsCollectionOperator() -303 | HiveToDruidOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -304 | HiveToDruidTransfer() -305 | HiveToSambaOperator() - | - = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator` instead. - -AIR302.py:304:1: AIR302 `airflow.operators.hive_to_druid.HiveToDruidTransfer` is moved into `apache-druid` provider in Airflow 3.0; - | -302 | HiveStatsCollectionOperator() -303 | HiveToDruidOperator() -304 | HiveToDruidTransfer() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -305 | HiveToSambaOperator() -306 | S3ToHiveOperator() - | - = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator` instead. - -AIR302.py:305:1: AIR302 `airflow.operators.hive_to_samba_operator.HiveToSambaOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -303 | HiveToDruidOperator() -304 | HiveToDruidTransfer() -305 | HiveToSambaOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -306 | S3ToHiveOperator() -307 | S3ToHiveTransfer() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_samba.HiveToSambaOperator` instead. - -AIR302.py:306:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -304 | HiveToDruidTransfer() -305 | HiveToSambaOperator() -306 | S3ToHiveOperator() - | ^^^^^^^^^^^^^^^^ AIR302 -307 | S3ToHiveTransfer() -308 | MetastorePartitionSensor() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator` instead. - -AIR302.py:307:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; - | -305 | HiveToSambaOperator() -306 | S3ToHiveOperator() -307 | S3ToHiveTransfer() - | ^^^^^^^^^^^^^^^^ AIR302 -308 | MetastorePartitionSensor() -309 | NamedHivePartitionSensor() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator` instead. - -AIR302.py:308:1: AIR302 `airflow.sensors.metastore_partition_sensor.MetastorePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; - | -306 | S3ToHiveOperator() -307 | S3ToHiveTransfer() -308 | MetastorePartitionSensor() - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -309 | NamedHivePartitionSensor() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.metastore_partition.MetastorePartitionSensor` instead. - -AIR302.py:309:1: AIR302 `airflow.sensors.named_hive_partition_sensor.NamedHivePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; - | -307 | S3ToHiveTransfer() -308 | MetastorePartitionSensor() -309 | NamedHivePartitionSensor() - | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -310 | -311 | # apache-airflow-providers-http - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.named_hive_partition.NamedHivePartitionSensor` instead. - -AIR302.py:312:1: AIR302 `airflow.hooks.http_hook.HttpHook` is moved into `http` provider in Airflow 3.0; - | -311 | # apache-airflow-providers-http -312 | HttpHook() - | ^^^^^^^^ AIR302 -313 | HttpSensor() -314 | SimpleHttpOperator() - | - = help: Install `apache-airflow-providers-http>=1.0.0` and use `airflow.providers.http.hooks.http.HttpHook` instead. - -AIR302.py:313:1: AIR302 `airflow.sensors.http_sensor.HttpSensor` is moved into `http` provider in Airflow 3.0; - | -311 | # apache-airflow-providers-http -312 | HttpHook() -313 | HttpSensor() - | ^^^^^^^^^^ AIR302 -314 | SimpleHttpOperator() - | - = help: Install `apache-airflow-providers-http>=1.0.0` and use `airflow.providers.http.sensors.http.HttpSensor` instead. - -AIR302.py:314:1: AIR302 `airflow.operators.http_operator.SimpleHttpOperator` is moved into `http` provider in Airflow 3.0; - | -312 | HttpHook() -313 | HttpSensor() -314 | SimpleHttpOperator() - | ^^^^^^^^^^^^^^^^^^ AIR302 -315 | -316 | # apache-airflow-providers-jdbc - | - = help: Install `apache-airflow-providers-http>=5.0.0` and use `airflow.providers.http.operators.http.HttpOperator` instead. - -AIR302.py:317:1: AIR302 `airflow.hooks.jdbc_hook.jaydebeapi` is moved into `jdbc` provider in Airflow 3.0; - | -316 | # apache-airflow-providers-jdbc -317 | jaydebeapi - | ^^^^^^^^^^ AIR302 -318 | JdbcHook() -319 | JdbcOperator() - | - = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.jaydebeapi` instead. - -AIR302.py:318:1: AIR302 `airflow.hooks.jdbc_hook.JdbcHook` is moved into `jdbc` provider in Airflow 3.0; - | -316 | # apache-airflow-providers-jdbc -317 | jaydebeapi -318 | JdbcHook() - | ^^^^^^^^ AIR302 -319 | JdbcOperator() - | - = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.JdbcHook` instead. - -AIR302.py:319:1: AIR302 `airflow.operators.jdbc_operator.JdbcOperator` is moved into `common-sql` provider in Airflow 3.0; - | -317 | jaydebeapi -318 | JdbcHook() -319 | JdbcOperator() - | ^^^^^^^^^^^^ AIR302 -320 | -321 | # apache-airflow-providers-fab - | - = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. - -AIR302.py:322:12: AIR302 `airflow.api.auth.backend.basic_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; - | -321 | # apache-airflow-providers-fab -322 | basic_auth.CLIENT_AUTH - | ^^^^^^^^^^^ AIR302 -323 | basic_auth.init_app -324 | basic_auth.auth_current_user - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.CLIENT_AUTH` instead. - -AIR302.py:323:12: AIR302 `airflow.api.auth.backend.basic_auth.init_app` is moved into `fab` provider in Airflow 3.0; - | -321 | # apache-airflow-providers-fab -322 | basic_auth.CLIENT_AUTH -323 | basic_auth.init_app - | ^^^^^^^^ AIR302 -324 | basic_auth.auth_current_user -325 | basic_auth.requires_authentication - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.init_app` instead. - -AIR302.py:324:12: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; - | -322 | basic_auth.CLIENT_AUTH -323 | basic_auth.init_app -324 | basic_auth.auth_current_user - | ^^^^^^^^^^^^^^^^^ AIR302 -325 | basic_auth.requires_authentication - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. - -AIR302.py:325:12: AIR302 `airflow.api.auth.backend.basic_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; - | -323 | basic_auth.init_app -324 | basic_auth.auth_current_user -325 | basic_auth.requires_authentication - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -326 | -327 | kerberos_auth.log - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.requires_authentication` instead. - -AIR302.py:327:15: AIR302 `airflow.api.auth.backend.kerberos_auth.log` is moved into `fab` provider in Airflow 3.0; - | -325 | basic_auth.requires_authentication -326 | -327 | kerberos_auth.log - | ^^^ AIR302 -328 | kerberos_auth.CLIENT_AUTH -329 | kerberos_auth.find_user - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.log` instead. - -AIR302.py:328:15: AIR302 `airflow.api.auth.backend.kerberos_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; - | -327 | kerberos_auth.log -328 | kerberos_auth.CLIENT_AUTH - | ^^^^^^^^^^^ AIR302 -329 | kerberos_auth.find_user -330 | kerberos_auth.init_app - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.CLIENT_AUTH` instead. - -AIR302.py:329:15: AIR302 `airflow.api.auth.backend.kerberos_auth.find_user` is moved into `fab` provider in Airflow 3.0; - | -327 | kerberos_auth.log -328 | kerberos_auth.CLIENT_AUTH -329 | kerberos_auth.find_user - | ^^^^^^^^^ AIR302 -330 | kerberos_auth.init_app -331 | kerberos_auth.requires_authentication - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.find_user` instead. - -AIR302.py:330:15: AIR302 `airflow.api.auth.backend.kerberos_auth.init_app` is moved into `fab` provider in Airflow 3.0; - | -328 | kerberos_auth.CLIENT_AUTH -329 | kerberos_auth.find_user -330 | kerberos_auth.init_app - | ^^^^^^^^ AIR302 -331 | kerberos_auth.requires_authentication -332 | auth_current_user - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.init_app` instead. - -AIR302.py:331:15: AIR302 `airflow.api.auth.backend.kerberos_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; - | -329 | kerberos_auth.find_user -330 | kerberos_auth.init_app -331 | kerberos_auth.requires_authentication - | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -332 | auth_current_user -333 | backend_kerberos_auth - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.requires_authentication` instead. - -AIR302.py:332:1: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; - | -330 | kerberos_auth.init_app -331 | kerberos_auth.requires_authentication -332 | auth_current_user - | ^^^^^^^^^^^^^^^^^ AIR302 -333 | backend_kerberos_auth -334 | fab_override - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. - -AIR302.py:335:1: AIR302 `airflow.auth.managers.fab.fab_auth_manager.FabAuthManager` is moved into `fab` provider in Airflow 3.0; - | -333 | backend_kerberos_auth -334 | fab_override -335 | FabAuthManager() - | ^^^^^^^^^^^^^^ AIR302 -336 | FabAirflowSecurityManagerOverride() - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager` instead. - -AIR302.py:336:1: AIR302 `airflow.www.security.FabAirflowSecurityManagerOverride` is moved into `fab` provider in Airflow 3.0; - | -334 | fab_override -335 | FabAuthManager() -336 | FabAirflowSecurityManagerOverride() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -337 | -338 | # check whether attribute access - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.override.FabAirflowSecurityManagerOverride` instead. - -AIR302.py:339:12: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; - | -338 | # check whether attribute access -339 | basic_auth.auth_current_user - | ^^^^^^^^^^^^^^^^^ AIR302 -340 | -341 | # apache-airflow-providers-cncf-kubernetes - | - = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. - -AIR302.py:342:1: AIR302 `airflow.executors.kubernetes_executor_types.ALL_NAMESPACES` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -341 | # apache-airflow-providers-cncf-kubernetes -342 | ALL_NAMESPACES - | ^^^^^^^^^^^^^^ AIR302 -343 | POD_EXECUTOR_DONE_KEY -344 | _disable_verify_ssl() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.ALL_NAMESPACES` instead. - -AIR302.py:343:1: AIR302 `airflow.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -341 | # apache-airflow-providers-cncf-kubernetes -342 | ALL_NAMESPACES -343 | POD_EXECUTOR_DONE_KEY - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -344 | _disable_verify_ssl() -345 | _enable_tcp_keepalive() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` instead. - -AIR302.py:344:1: AIR302 `airflow.kubernetes.kube_client._disable_verify_ssl` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -342 | ALL_NAMESPACES -343 | POD_EXECUTOR_DONE_KEY -344 | _disable_verify_ssl() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -345 | _enable_tcp_keepalive() -346 | append_to_pod() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client._disable_verify_ssl` instead. - -AIR302.py:345:1: AIR302 `airflow.kubernetes.kube_client._enable_tcp_keepalive` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -343 | POD_EXECUTOR_DONE_KEY -344 | _disable_verify_ssl() -345 | _enable_tcp_keepalive() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -346 | append_to_pod() -347 | annotations_for_logging_task_metadata() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client._enable_tcp_keepalive` instead. - -AIR302.py:346:1: AIR302 `airflow.kubernetes.k8s_model.append_to_pod` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -344 | _disable_verify_ssl() -345 | _enable_tcp_keepalive() -346 | append_to_pod() - | ^^^^^^^^^^^^^ AIR302 -347 | annotations_for_logging_task_metadata() -348 | annotations_to_key() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.append_to_pod` instead. - -AIR302.py:347:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -345 | _enable_tcp_keepalive() -346 | append_to_pod() -347 | annotations_for_logging_task_metadata() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -348 | annotations_to_key() -349 | create_pod_id() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata` instead. - -AIR302.py:348:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.annotations_to_key` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -346 | append_to_pod() -347 | annotations_for_logging_task_metadata() -348 | annotations_to_key() - | ^^^^^^^^^^^^^^^^^^ AIR302 -349 | create_pod_id() -350 | datetime_to_label_safe_datestring() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_to_key` instead. - -AIR302.py:349:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.create_pod_id` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -347 | annotations_for_logging_task_metadata() -348 | annotations_to_key() -349 | create_pod_id() - | ^^^^^^^^^^^^^ AIR302 -350 | datetime_to_label_safe_datestring() -351 | extend_object_field() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.create_unique_id` instead. - -AIR302.py:350:1: AIR302 `airflow.kubernetes.pod_generator.datetime_to_label_safe_datestring` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -348 | annotations_to_key() -349 | create_pod_id() -350 | datetime_to_label_safe_datestring() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -351 | extend_object_field() -352 | get_logs_task_metadata() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.datetime_to_label_safe_datestring` instead. - -AIR302.py:351:1: AIR302 `airflow.kubernetes.pod_generator.extend_object_field` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -349 | create_pod_id() -350 | datetime_to_label_safe_datestring() -351 | extend_object_field() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -352 | get_logs_task_metadata() -353 | label_safe_datestring_to_datetime() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.extend_object_field` instead. - -AIR302.py:352:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.get_logs_task_metadata` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -350 | datetime_to_label_safe_datestring() -351 | extend_object_field() -352 | get_logs_task_metadata() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -353 | label_safe_datestring_to_datetime() -354 | merge_objects() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.get_logs_task_metadata` instead. - -AIR302.py:353:1: AIR302 `airflow.kubernetes.pod_generator.label_safe_datestring_to_datetime` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -351 | extend_object_field() -352 | get_logs_task_metadata() -353 | label_safe_datestring_to_datetime() - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 -354 | merge_objects() -355 | Port() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.label_safe_datestring_to_datetime` instead. - -AIR302.py:354:1: AIR302 `airflow.kubernetes.pod_generator.merge_objects` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -352 | get_logs_task_metadata() -353 | label_safe_datestring_to_datetime() -354 | merge_objects() - | ^^^^^^^^^^^^^ AIR302 -355 | Port() -356 | Resources() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.merge_objects` instead. - -AIR302.py:355:1: AIR302 `airflow.kubernetes.pod.Port` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -353 | label_safe_datestring_to_datetime() -354 | merge_objects() -355 | Port() - | ^^^^ AIR302 -356 | Resources() -357 | PodRuntimeInfoEnv() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1ContainerPort` instead. - -AIR302.py:356:1: AIR302 `airflow.kubernetes.pod.Resources` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -354 | merge_objects() -355 | Port() -356 | Resources() - | ^^^^^^^^^ AIR302 -357 | PodRuntimeInfoEnv() -358 | PodGeneratorDeprecated() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1ResourceRequirements` instead. - -AIR302.py:357:1: AIR302 `airflow.kubernetes.pod_runtime_info_env.PodRuntimeInfoEnv` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -355 | Port() -356 | Resources() -357 | PodRuntimeInfoEnv() - | ^^^^^^^^^^^^^^^^^ AIR302 -358 | PodGeneratorDeprecated() -359 | Volume() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1EnvVar` instead. - -AIR302.py:358:1: AIR302 `airflow.kubernetes.pod_generator.PodGeneratorDeprecated` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -356 | Resources() -357 | PodRuntimeInfoEnv() -358 | PodGeneratorDeprecated() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -359 | Volume() -360 | VolumeMount() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. - -AIR302.py:359:1: AIR302 `airflow.kubernetes.volume.Volume` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -357 | PodRuntimeInfoEnv() -358 | PodGeneratorDeprecated() -359 | Volume() - | ^^^^^^ AIR302 -360 | VolumeMount() -361 | Secret() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1Volume` instead. - -AIR302.py:360:1: AIR302 `airflow.kubernetes.volume_mount.VolumeMount` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -358 | PodGeneratorDeprecated() -359 | Volume() -360 | VolumeMount() - | ^^^^^^^^^^^ AIR302 -361 | Secret() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1VolumeMount` instead. - -AIR302.py:361:1: AIR302 `airflow.kubernetes.secret.Secret` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -359 | Volume() -360 | VolumeMount() -361 | Secret() - | ^^^^^^ AIR302 -362 | -363 | add_pod_suffix() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.secret.Secret` instead. - -AIR302.py:363:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -361 | Secret() -362 | -363 | add_pod_suffix() - | ^^^^^^^^^^^^^^ AIR302 -364 | add_pod_suffix2() -365 | get_kube_client() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_unique_suffix` instead. - -AIR302.py:364:1: AIR302 `airflow.kubernetes.pod_generator.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -363 | add_pod_suffix() -364 | add_pod_suffix2() - | ^^^^^^^^^^^^^^^ AIR302 -365 | get_kube_client() -366 | get_kube_client2() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_unique_suffix` instead. - -AIR302.py:365:1: AIR302 `airflow.kubernetes.kube_client.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -363 | add_pod_suffix() -364 | add_pod_suffix2() -365 | get_kube_client() - | ^^^^^^^^^^^^^^^ AIR302 -366 | get_kube_client2() -367 | make_safe_label_value() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. - -AIR302.py:366:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -364 | add_pod_suffix2() -365 | get_kube_client() -366 | get_kube_client2() - | ^^^^^^^^^^^^^^^^ AIR302 -367 | make_safe_label_value() -368 | make_safe_label_value2() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. - -AIR302.py:367:1: AIR302 `airflow.kubernetes.pod_generator.make_safe_label_value` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -365 | get_kube_client() -366 | get_kube_client2() -367 | make_safe_label_value() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -368 | make_safe_label_value2() -369 | rand_str() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.make_safe_label_value` instead. - -AIR302.py:368:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.make_safe_label_value` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -366 | get_kube_client2() -367 | make_safe_label_value() -368 | make_safe_label_value2() - | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 -369 | rand_str() -370 | rand_str2() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.make_safe_label_value` instead. - -AIR302.py:369:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -367 | make_safe_label_value() -368 | make_safe_label_value2() -369 | rand_str() - | ^^^^^^^^ AIR302 -370 | rand_str2() -371 | K8SModel() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str` instead. - -AIR302.py:370:1: AIR302 `airflow.kubernetes.pod_generator.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -368 | make_safe_label_value2() -369 | rand_str() -370 | rand_str2() - | ^^^^^^^^^ AIR302 -371 | K8SModel() -372 | K8SModel2() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str` instead. - -AIR302.py:371:1: AIR302 `airflow.kubernetes.k8s_model.K8SModel` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -369 | rand_str() -370 | rand_str2() -371 | K8SModel() - | ^^^^^^^^ AIR302 -372 | K8SModel2() -373 | PodLauncher() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.K8SModel` instead. - -AIR302.py:373:1: AIR302 `airflow.kubernetes.pod_launcher.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -371 | K8SModel() -372 | K8SModel2() -373 | PodLauncher() - | ^^^^^^^^^^^ AIR302 -374 | PodLauncher2() -375 | PodStatus() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use `airflow.providers.cncf.kubernetes.utils.pod_manager.PodManager` instead. - -AIR302.py:374:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -372 | K8SModel2() -373 | PodLauncher() -374 | PodLauncher2() - | ^^^^^^^^^^^^ AIR302 -375 | PodStatus() -376 | PodStatus2() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use `airflow.providers.cncf.kubernetes.utils.pod_manager.PodManager` instead. - -AIR302.py:375:1: AIR302 `airflow.kubernetes.pod_launcher.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -373 | PodLauncher() -374 | PodLauncher2() -375 | PodStatus() - | ^^^^^^^^^ AIR302 -376 | PodStatus2() -377 | PodDefaults() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use ` airflow.providers.cncf.kubernetes.utils.pod_manager.PodPhase` instead. - -AIR302.py:376:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -374 | PodLauncher2() -375 | PodStatus() -376 | PodStatus2() - | ^^^^^^^^^^ AIR302 -377 | PodDefaults() -378 | PodDefaults2() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use ` airflow.providers.cncf.kubernetes.utils.pod_manager.PodPhase` instead. - -AIR302.py:377:1: AIR302 `airflow.kubernetes.pod_generator.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -375 | PodStatus() -376 | PodStatus2() -377 | PodDefaults() - | ^^^^^^^^^^^ AIR302 -378 | PodDefaults2() -379 | PodDefaults3() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults` instead. - -AIR302.py:378:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -376 | PodStatus2() -377 | PodDefaults() -378 | PodDefaults2() - | ^^^^^^^^^^^^ AIR302 -379 | PodDefaults3() -380 | PodGenerator() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults` instead. - -AIR302.py:379:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -377 | PodDefaults() -378 | PodDefaults2() -379 | PodDefaults3() - | ^^^^^^^^^^^^ AIR302 -380 | PodGenerator() -381 | PodGenerator2() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults` instead. - -AIR302.py:380:1: AIR302 `airflow.kubernetes.pod_generator.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -378 | PodDefaults2() -379 | PodDefaults3() -380 | PodGenerator() - | ^^^^^^^^^^^^ AIR302 -381 | PodGenerator2() - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. - -AIR302.py:381:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; - | -379 | PodDefaults3() -380 | PodGenerator() -381 | PodGenerator2() - | ^^^^^^^^^^^^^ AIR302 - | - = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. - -AIR302.py:385:1: AIR302 `airflow.hooks.mssql_hook.MsSqlHook` is moved into `microsoft-mssql` provider in Airflow 3.0; - | -384 | # apache-airflow-providers-microsoft-mssql -385 | MsSqlHook() - | ^^^^^^^^^ AIR302 -386 | MsSqlOperator() -387 | MsSqlToHiveOperator() - | - = help: Install `apache-airflow-providers-microsoft-mssql>=1.0.0` and use `airflow.providers.microsoft.mssql.hooks.mssql.MsSqlHook` instead. - -AIR302.py:386:1: AIR302 `airflow.operators.mssql_operator.MsSqlOperator` is moved into `common-sql` provider in Airflow 3.0; - | -384 | # apache-airflow-providers-microsoft-mssql -385 | MsSqlHook() -386 | MsSqlOperator() - | ^^^^^^^^^^^^^ AIR302 -387 | MsSqlToHiveOperator() -388 | MsSqlToHiveTransfer() - | - = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. - -AIR302.py:387:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -385 | MsSqlHook() -386 | MsSqlOperator() -387 | MsSqlToHiveOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -388 | MsSqlToHiveTransfer() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator` instead. - -AIR302.py:388:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; - | -386 | MsSqlOperator() -387 | MsSqlToHiveOperator() -388 | MsSqlToHiveTransfer() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -389 | -390 | # apache-airflow-providers-mysql - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator` instead. - -AIR302.py:391:1: AIR302 `airflow.operators.hive_to_mysql.HiveToMySqlOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -390 | # apache-airflow-providers-mysql -391 | HiveToMySqlOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -392 | HiveToMySqlTransfer() -393 | MySqlHook() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator` instead. - -AIR302.py:392:1: AIR302 `airflow.operators.hive_to_mysql.HiveToMySqlTransfer` is moved into `apache-hive` provider in Airflow 3.0; - | -390 | # apache-airflow-providers-mysql -391 | HiveToMySqlOperator() -392 | HiveToMySqlTransfer() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -393 | MySqlHook() -394 | MySqlOperator() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator` instead. - -AIR302.py:393:1: AIR302 `airflow.hooks.mysql_hook.MySqlHook` is moved into `mysql` provider in Airflow 3.0; - | -391 | HiveToMySqlOperator() -392 | HiveToMySqlTransfer() -393 | MySqlHook() - | ^^^^^^^^^ AIR302 -394 | MySqlOperator() -395 | MySqlToHiveOperator() - | - = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.hooks.mysql.MySqlHook` instead. - -AIR302.py:394:1: AIR302 `airflow.operators.mysql_operator.MySqlOperator` is moved into `common-sql` provider in Airflow 3.0; - | -392 | HiveToMySqlTransfer() -393 | MySqlHook() -394 | MySqlOperator() - | ^^^^^^^^^^^^^ AIR302 -395 | MySqlToHiveOperator() -396 | MySqlToHiveTransfer() - | - = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. - -AIR302.py:395:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; - | -393 | MySqlHook() -394 | MySqlOperator() -395 | MySqlToHiveOperator() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -396 | MySqlToHiveTransfer() -397 | PrestoToMySqlOperator() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator` instead. - -AIR302.py:396:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; - | -394 | MySqlOperator() -395 | MySqlToHiveOperator() -396 | MySqlToHiveTransfer() - | ^^^^^^^^^^^^^^^^^^^ AIR302 -397 | PrestoToMySqlOperator() -398 | PrestoToMySqlTransfer() - | - = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator` instead. - -AIR302.py:397:1: AIR302 `airflow.operators.presto_to_mysql.PrestoToMySqlOperator` is moved into `mysql` provider in Airflow 3.0; - | -395 | MySqlToHiveOperator() -396 | MySqlToHiveTransfer() -397 | PrestoToMySqlOperator() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -398 | PrestoToMySqlTransfer() - | - = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator` instead. - -AIR302.py:398:1: AIR302 `airflow.operators.presto_to_mysql.PrestoToMySqlTransfer` is moved into `mysql` provider in Airflow 3.0; - | -396 | MySqlToHiveTransfer() -397 | PrestoToMySqlOperator() -398 | PrestoToMySqlTransfer() - | ^^^^^^^^^^^^^^^^^^^^^ AIR302 -399 | -400 | # apache-airflow-providers-oracle - | - = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator` instead. - -AIR302.py:401:1: AIR302 `airflow.hooks.oracle_hook.OracleHook` is moved into `oracle` provider in Airflow 3.0; - | -400 | # apache-airflow-providers-oracle -401 | OracleHook() - | ^^^^^^^^^^ AIR302 -402 | OracleOperator() - | - = help: Install `apache-airflow-providers-oracle>=1.0.0` and use `airflow.providers.oracle.hooks.oracle.OracleHook` instead. - -AIR302.py:402:1: AIR302 `airflow.operators.oracle_operator.OracleOperator` is moved into `common-sql` provider in Airflow 3.0; - | -400 | # apache-airflow-providers-oracle -401 | OracleHook() -402 | OracleOperator() - | ^^^^^^^^^^^^^^ AIR302 -403 | -404 | # apache-airflow-providers-papermill - | - = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. - -AIR302.py:405:1: AIR302 `airflow.operators.papermill_operator.PapermillOperator` is moved into `papermill` provider in Airflow 3.0; - | -404 | # apache-airflow-providers-papermill -405 | PapermillOperator() - | ^^^^^^^^^^^^^^^^^ AIR302 -406 | -407 | # apache-airflow-providers-apache-pig - | - = help: Install `apache-airflow-providers-papermill>=1.0.0` and use `airflow.providers.papermill.operators.papermill.PapermillOperator` instead. - -AIR302.py:408:1: AIR302 `airflow.hooks.pig_hook.PigCliHook` is moved into `apache-pig` provider in Airflow 3.0; - | -407 | # apache-airflow-providers-apache-pig -408 | PigCliHook() - | ^^^^^^^^^^ AIR302 -409 | PigOperator() - | - = help: Install `apache-airflow-providers-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.hooks.pig.PigCliHook` instead. - -AIR302.py:409:1: AIR302 `airflow.operators.pig_operator.PigOperator` is moved into `apache-pig` provider in Airflow 3.0; - | -407 | # apache-airflow-providers-apache-pig -408 | PigCliHook() -409 | PigOperator() - | ^^^^^^^^^^^ AIR302 -410 | -411 | # apache-airflow-providers-postgres - | - = help: Install `apache-airflow-providers-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.operators.pig.PigOperator` instead. - -AIR302.py:412:1: AIR302 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0 - | -411 | # apache-airflow-providers-postgres -412 | Mapping - | ^^^^^^^ AIR302 -413 | PostgresHook() -414 | PostgresOperator() - | - -AIR302.py:413:1: AIR302 `airflow.hooks.postgres_hook.PostgresHook` is moved into `postgres` provider in Airflow 3.0; - | -411 | # apache-airflow-providers-postgres -412 | Mapping -413 | PostgresHook() - | ^^^^^^^^^^^^ AIR302 -414 | PostgresOperator() - | - = help: Install `apache-airflow-providers-postgres>=1.0.0` and use `airflow.providers.postgres.hooks.postgres.PostgresHook` instead. - -AIR302.py:414:1: AIR302 `airflow.operators.postgres_operator.PostgresOperator` is moved into `common-sql` provider in Airflow 3.0; - | -412 | Mapping -413 | PostgresHook() -414 | PostgresOperator() - | ^^^^^^^^^^^^^^^^ AIR302 -415 | -416 | # apache-airflow-providers-presto - | - = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. - -AIR302.py:417:1: AIR302 `airflow.hooks.presto_hook.PrestoHook` is moved into `presto` provider in Airflow 3.0; - | -416 | # apache-airflow-providers-presto -417 | PrestoHook() - | ^^^^^^^^^^ AIR302 -418 | -419 | # apache-airflow-providers-samba - | - = help: Install `apache-airflow-providers-presto>=1.0.0` and use `airflow.providers.presto.hooks.presto.PrestoHook` instead. - -AIR302.py:420:1: AIR302 `airflow.hooks.samba_hook.SambaHook` is moved into `samba` provider in Airflow 3.0; - | -419 | # apache-airflow-providers-samba -420 | SambaHook() - | ^^^^^^^^^ AIR302 -421 | -422 | # apache-airflow-providers-slack - | - = help: Install `apache-airflow-providers-samba>=1.0.0` and use `airflow.providers.samba.hooks.samba.SambaHook` instead. - -AIR302.py:423:1: AIR302 `airflow.hooks.slack_hook.SlackHook` is moved into `slack` provider in Airflow 3.0; - | -422 | # apache-airflow-providers-slack -423 | SlackHook() - | ^^^^^^^^^ AIR302 -424 | SlackAPIOperator() -425 | SlackAPIPostOperator() - | - = help: Install `apache-airflow-providers-slack>=1.0.0` and use `airflow.providers.slack.hooks.slack.SlackHook` instead. - -AIR302.py:424:1: AIR302 `airflow.operators.slack_operator.SlackAPIOperator` is moved into `slack` provider in Airflow 3.0; - | -422 | # apache-airflow-providers-slack -423 | SlackHook() -424 | SlackAPIOperator() - | ^^^^^^^^^^^^^^^^ AIR302 -425 | SlackAPIPostOperator() - | - = help: Install `apache-airflow-providers-slack>=1.0.0` and use `airflow.providers.slack.operators.slack.SlackAPIOperator` instead. - -AIR302.py:425:1: AIR302 `airflow.operators.slack_operator.SlackAPIPostOperator` is moved into `slack` provider in Airflow 3.0; - | -423 | SlackHook() -424 | SlackAPIOperator() -425 | SlackAPIPostOperator() - | ^^^^^^^^^^^^^^^^^^^^ AIR302 -426 | -427 | # apache-airflow-providers-sqlite - | - = help: Install `apache-airflow-providers-slack>=1.0.0` and use `airflow.providers.slack.operators.slack.SlackAPIPostOperator` instead. - -AIR302.py:428:1: AIR302 `airflow.hooks.sqlite_hook.SqliteHook` is moved into `sqlite` provider in Airflow 3.0; - | -427 | # apache-airflow-providers-sqlite -428 | SqliteHook() - | ^^^^^^^^^^ AIR302 -429 | SqliteOperator() - | - = help: Install `apache-airflow-providers-sqlite>=1.0.0` and use `airflow.providers.sqlite.hooks.sqlite.SqliteHook` instead. - -AIR302.py:429:1: AIR302 `airflow.operators.sqlite_operator.SqliteOperator` is moved into `common-sql` provider in Airflow 3.0; - | -427 | # apache-airflow-providers-sqlite -428 | SqliteHook() -429 | SqliteOperator() - | ^^^^^^^^^^^^^^ AIR302 -430 | -431 | # apache-airflow-providers-zendesk - | - = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. - -AIR302.py:432:1: AIR302 `airflow.hooks.zendesk_hook.ZendeskHook` is moved into `zendesk` provider in Airflow 3.0; - | -431 | # apache-airflow-providers-zendesk -432 | ZendeskHook() - | ^^^^^^^^^^^ AIR302 -433 | -434 | # apache-airflow-providers-smtp - | - = help: Install `apache-airflow-providers-zendesk>=1.0.0` and use `airflow.providers.zendesk.hooks.zendesk.ZendeskHook` instead. - -AIR302.py:435:1: AIR302 `airflow.operators.email_operator.EmailOperator` is moved into `smtp` provider in Airflow 3.0; - | -434 | # apache-airflow-providers-smtp -435 | EmailOperator() - | ^^^^^^^^^^^^^ AIR302 -436 | -437 | # apache-airflow-providers-standard - | - = help: Install `apache-airflow-providers-smtp>=1.0.0` and use `airflow.providers.smtp.operators.smtp.EmailOperator` instead. - -AIR302.py:451:1: AIR302 `airflow.operators.dummy.DummyOperator` is moved into `standard` provider in Airflow 3.0; - | -449 | TimeDeltaSensor() -450 | DayOfWeekSensor() -451 | DummyOperator() - | ^^^^^^^^^^^^^ AIR302 -452 | EmptyOperator() -453 | ExternalTaskMarker() - | - = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. - -AIR302.py:452:1: AIR302 `airflow.operators.dummy.EmptyOperator` is moved into `standard` provider in Airflow 3.0; - | -450 | DayOfWeekSensor() -451 | DummyOperator() -452 | EmptyOperator() - | ^^^^^^^^^^^^^ AIR302 -453 | ExternalTaskMarker() -454 | ExternalTaskSensor() - | - = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_amazon.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_amazon.py.snap new file mode 100644 index 00000000000000..f7f18a458b1e0f --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_amazon.py.snap @@ -0,0 +1,113 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_amazon.py:23:1: AIR302 `airflow.hooks.S3_hook.S3Hook` is moved into `amazon` provider in Airflow 3.0; + | +21 | from airflow.sensors.s3_key_sensor import S3KeySensor +22 | +23 | S3Hook() + | ^^^^^^ AIR302 +24 | provide_bucket_name() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.hooks.s3.S3Hook` instead. + +AIR302_amazon.py:24:1: AIR302 `airflow.hooks.S3_hook.provide_bucket_name` is moved into `amazon` provider in Airflow 3.0; + | +23 | S3Hook() +24 | provide_bucket_name() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +25 | +26 | GCSToS3Operator() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.hooks.s3.provide_bucket_name` instead. + +AIR302_amazon.py:26:1: AIR302 `airflow.operators.gcs_to_s3.GCSToS3Operator` is moved into `amazon` provider in Airflow 3.0; + | +24 | provide_bucket_name() +25 | +26 | GCSToS3Operator() + | ^^^^^^^^^^^^^^^ AIR302 +27 | +28 | GoogleApiToS3Operator() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.gcs_to_s3.GCSToS3Operator` instead. + +AIR302_amazon.py:28:1: AIR302 `airflow.operators.google_api_to_s3_transfer.GoogleApiToS3Operator` is moved into `amazon` provider in Airflow 3.0; + | +26 | GCSToS3Operator() +27 | +28 | GoogleApiToS3Operator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +29 | GoogleApiToS3Transfer() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator` instead. + +AIR302_amazon.py:29:1: AIR302 `airflow.operators.google_api_to_s3_transfer.GoogleApiToS3Transfer` is moved into `amazon` provider in Airflow 3.0; + | +28 | GoogleApiToS3Operator() +29 | GoogleApiToS3Transfer() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +30 | +31 | RedshiftToS3Operator() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.google_api_to_s3.GoogleApiToS3Operator` instead. + +AIR302_amazon.py:31:1: AIR302 `airflow.operators.redshift_to_s3_operator.RedshiftToS3Operator` is moved into `amazon` provider in Airflow 3.0; + | +29 | GoogleApiToS3Transfer() +30 | +31 | RedshiftToS3Operator() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +32 | RedshiftToS3Transfer() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator` instead. + +AIR302_amazon.py:32:1: AIR302 `airflow.operators.redshift_to_s3_operator.RedshiftToS3Transfer` is moved into `amazon` provider in Airflow 3.0; + | +31 | RedshiftToS3Operator() +32 | RedshiftToS3Transfer() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +33 | +34 | S3FileTransformOperator() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.redshift_to_s3.RedshiftToS3Operator` instead. + +AIR302_amazon.py:34:1: AIR302 `airflow.operators.s3_file_transform_operator.S3FileTransformOperator` is moved into `amazon` provider in Airflow 3.0; + | +32 | RedshiftToS3Transfer() +33 | +34 | S3FileTransformOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +35 | +36 | S3ToRedshiftOperator() + | + = help: Install `apache-airflow-providers-amazon>=3.0.0` and use `airflow.providers.amazon.aws.operators.s3.S3FileTransformOperator` instead. + +AIR302_amazon.py:36:1: AIR302 `airflow.operators.s3_to_redshift_operator.S3ToRedshiftOperator` is moved into `amazon` provider in Airflow 3.0; + | +34 | S3FileTransformOperator() +35 | +36 | S3ToRedshiftOperator() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +37 | S3ToRedshiftTransfer() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator` instead. + +AIR302_amazon.py:37:1: AIR302 `airflow.operators.s3_to_redshift_operator.S3ToRedshiftTransfer` is moved into `amazon` provider in Airflow 3.0; + | +36 | S3ToRedshiftOperator() +37 | S3ToRedshiftTransfer() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +38 | +39 | S3KeySensor() + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.transfers.s3_to_redshift.S3ToRedshiftOperator` instead. + +AIR302_amazon.py:39:1: AIR302 `airflow.sensors.s3_key_sensor.S3KeySensor` is moved into `amazon` provider in Airflow 3.0; + | +37 | S3ToRedshiftTransfer() +38 | +39 | S3KeySensor() + | ^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-amazon>=1.0.0` and use `airflow.providers.amazon.aws.sensors.s3.S3KeySensor` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_celery.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_celery.py.snap new file mode 100644 index 00000000000000..554871e39e481c --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_celery.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_celery.py:9:1: AIR302 `airflow.config_templates.default_celery.DEFAULT_CELERY_CONFIG` is moved into `celery` provider in Airflow 3.0; + | + 7 | ) + 8 | + 9 | DEFAULT_CELERY_CONFIG + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +10 | +11 | app + | + = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.default_celery.DEFAULT_CELERY_CONFIG` instead. + +AIR302_celery.py:11:1: AIR302 `airflow.executors.celery_executor.app` is moved into `celery` provider in Airflow 3.0; + | + 9 | DEFAULT_CELERY_CONFIG +10 | +11 | app + | ^^^ AIR302 +12 | CeleryExecutor() + | + = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_executor_utils.app` instead. + +AIR302_celery.py:12:1: AIR302 `airflow.executors.celery_executor.CeleryExecutor` is moved into `celery` provider in Airflow 3.0; + | +11 | app +12 | CeleryExecutor() + | ^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-celery>=3.3.0` and use `airflow.providers.celery.executors.celery_executor.CeleryExecutor` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_common_sql.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_common_sql.py.snap new file mode 100644 index 00000000000000..2a15a223299c7b --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_common_sql.py.snap @@ -0,0 +1,352 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_common_sql.py:10:1: AIR302 `airflow.hooks.dbapi.ConnectorProtocol` is moved into `common-sql` provider in Airflow 3.0; + | + 8 | from airflow.operators.check_operator import SQLCheckOperator + 9 | +10 | ConnectorProtocol() + | ^^^^^^^^^^^^^^^^^ AIR302 +11 | DbApiHook() +12 | SQLCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.ConnectorProtocol` instead. + +AIR302_common_sql.py:11:1: AIR302 `airflow.hooks.dbapi_hook.DbApiHook` is moved into `common-sql` provider in Airflow 3.0; + | +10 | ConnectorProtocol() +11 | DbApiHook() + | ^^^^^^^^^ AIR302 +12 | SQLCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.hooks.sql.DbApiHook` instead. + +AIR302_common_sql.py:12:1: AIR302 `airflow.operators.check_operator.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +10 | ConnectorProtocol() +11 | DbApiHook() +12 | SQLCheckOperator() + | ^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + +AIR302_common_sql.py:18:1: AIR302 `airflow.operators.sql.SQLCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +16 | from airflow.operators.sql import SQLCheckOperator +17 | +18 | SQLCheckOperator() + | ^^^^^^^^^^^^^^^^ AIR302 +19 | CheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + +AIR302_common_sql.py:19:1: AIR302 `airflow.operators.check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +18 | SQLCheckOperator() +19 | CheckOperator() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + +AIR302_common_sql.py:24:1: AIR302 `airflow.operators.druid_check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +22 | from airflow.operators.druid_check_operator import CheckOperator +23 | +24 | CheckOperator() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + +AIR302_common_sql.py:29:1: AIR302 `airflow.operators.presto_check_operator.CheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +27 | from airflow.operators.presto_check_operator import CheckOperator +28 | +29 | CheckOperator() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + +AIR302_common_sql.py:39:1: AIR302 `airflow.operators.druid_check_operator.DruidCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +37 | from airflow.operators.presto_check_operator import PrestoCheckOperator +38 | +39 | DruidCheckOperator() + | ^^^^^^^^^^^^^^^^^^ AIR302 +40 | PrestoCheckOperator() +41 | IntervalCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + +AIR302_common_sql.py:40:1: AIR302 `airflow.operators.presto_check_operator.PrestoCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +39 | DruidCheckOperator() +40 | PrestoCheckOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +41 | IntervalCheckOperator() +42 | SQLIntervalCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLCheckOperator` instead. + +AIR302_common_sql.py:41:1: AIR302 `airflow.operators.check_operator.IntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +39 | DruidCheckOperator() +40 | PrestoCheckOperator() +41 | IntervalCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +42 | SQLIntervalCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. + +AIR302_common_sql.py:42:1: AIR302 `airflow.operators.check_operator.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +40 | PrestoCheckOperator() +41 | IntervalCheckOperator() +42 | SQLIntervalCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. + +AIR302_common_sql.py:51:1: AIR302 `airflow.operators.presto_check_operator.IntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +49 | from airflow.operators.sql import SQLIntervalCheckOperator +50 | +51 | IntervalCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +52 | SQLIntervalCheckOperator() +53 | PrestoIntervalCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. + +AIR302_common_sql.py:52:1: AIR302 `airflow.operators.sql.SQLIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +51 | IntervalCheckOperator() +52 | SQLIntervalCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +53 | PrestoIntervalCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. + +AIR302_common_sql.py:53:1: AIR302 `airflow.operators.presto_check_operator.PrestoIntervalCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +51 | IntervalCheckOperator() +52 | SQLIntervalCheckOperator() +53 | PrestoIntervalCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLIntervalCheckOperator` instead. + +AIR302_common_sql.py:61:1: AIR302 `airflow.operators.check_operator.SQLThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +59 | ) +60 | +61 | SQLThresholdCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +62 | ThresholdCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. + +AIR302_common_sql.py:62:1: AIR302 `airflow.operators.check_operator.ThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +61 | SQLThresholdCheckOperator() +62 | ThresholdCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. + +AIR302_common_sql.py:67:1: AIR302 `airflow.operators.sql.SQLThresholdCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +65 | from airflow.operators.sql import SQLThresholdCheckOperator +66 | +67 | SQLThresholdCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLThresholdCheckOperator` instead. + +AIR302_common_sql.py:75:1: AIR302 `airflow.operators.check_operator.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +73 | ) +74 | +75 | SQLValueCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +76 | ValueCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + +AIR302_common_sql.py:76:1: AIR302 `airflow.operators.check_operator.ValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +75 | SQLValueCheckOperator() +76 | ValueCheckOperator() + | ^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + +AIR302_common_sql.py:85:1: AIR302 `airflow.operators.sql.SQLValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +83 | from airflow.operators.sql import SQLValueCheckOperator +84 | +85 | SQLValueCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +86 | ValueCheckOperator() +87 | PrestoValueCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + +AIR302_common_sql.py:86:1: AIR302 `airflow.operators.presto_check_operator.ValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +85 | SQLValueCheckOperator() +86 | ValueCheckOperator() + | ^^^^^^^^^^^^^^^^^^ AIR302 +87 | PrestoValueCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + +AIR302_common_sql.py:87:1: AIR302 `airflow.operators.presto_check_operator.PrestoValueCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +85 | SQLValueCheckOperator() +86 | ValueCheckOperator() +87 | PrestoValueCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLValueCheckOperator` instead. + +AIR302_common_sql.py:99:1: AIR302 `airflow.operators.sql.BaseSQLOperator` is moved into `common-sql` provider in Airflow 3.0; + | + 97 | ) + 98 | + 99 | BaseSQLOperator() + | ^^^^^^^^^^^^^^^ AIR302 +100 | BranchSQLOperator() +101 | SQLTablecheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BaseSQLOperator` instead. + +AIR302_common_sql.py:100:1: AIR302 `airflow.operators.sql.BranchSQLOperator` is moved into `common-sql` provider in Airflow 3.0; + | + 99 | BaseSQLOperator() +100 | BranchSQLOperator() + | ^^^^^^^^^^^^^^^^^ AIR302 +101 | SQLTablecheckOperator() +102 | SQLColumnCheckOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.BranchSQLOperator` instead. + +AIR302_common_sql.py:101:1: AIR302 `airflow.operators.sql.SQLTablecheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | + 99 | BaseSQLOperator() +100 | BranchSQLOperator() +101 | SQLTablecheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +102 | SQLColumnCheckOperator() +103 | _convert_to_float_if_possible() + | + = help: Install `apache-airflow-providers-common-sql>=1.1.0` and use `airflow.providers.common.sql.operators.sql.SQLTablecheckOperator` instead. + +AIR302_common_sql.py:102:1: AIR302 `airflow.operators.sql.SQLColumnCheckOperator` is moved into `common-sql` provider in Airflow 3.0; + | +100 | BranchSQLOperator() +101 | SQLTablecheckOperator() +102 | SQLColumnCheckOperator() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 +103 | _convert_to_float_if_possible() +104 | parse_boolean() + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.SQLColumnCheckOperator` instead. + +AIR302_common_sql.py:103:1: AIR302 `airflow.operators.sql._convert_to_float_if_possible` is moved into `common-sql` provider in Airflow 3.0; + | +101 | SQLTablecheckOperator() +102 | SQLColumnCheckOperator() +103 | _convert_to_float_if_possible() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +104 | parse_boolean() + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql._convert_to_float_if_possible` instead. + +AIR302_common_sql.py:104:1: AIR302 `airflow.operators.sql.parse_boolean` is moved into `common-sql` provider in Airflow 3.0; + | +102 | SQLColumnCheckOperator() +103 | _convert_to_float_if_possible() +104 | parse_boolean() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.operators.sql.parse_boolean` instead. + +AIR302_common_sql.py:109:1: AIR302 `airflow.sensors.sql.SqlSensor` is moved into `common-sql` provider in Airflow 3.0; + | +107 | from airflow.sensors.sql import SqlSensor +108 | +109 | SqlSensor() + | ^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.sensors.sql.SqlSensor` instead. + +AIR302_common_sql.py:114:1: AIR302 `airflow.sensors.sql_sensor.SqlSensor` is moved into `common-sql` provider in Airflow 3.0; + | +112 | from airflow.sensors.sql_sensor import SqlSensor +113 | +114 | SqlSensor() + | ^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.0.0` and use `airflow.providers.common.sql.sensors.sql.SqlSensor` instead. + +AIR302_common_sql.py:124:1: AIR302 `airflow.operators.jdbc_operator.JdbcOperator` is moved into `common-sql` provider in Airflow 3.0; + | +122 | from airflow.operators.sqlite_operator import SqliteOperator +123 | +124 | JdbcOperator() + | ^^^^^^^^^^^^ AIR302 +125 | MsSqlOperator() +126 | MySqlOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. + +AIR302_common_sql.py:125:1: AIR302 `airflow.operators.mssql_operator.MsSqlOperator` is moved into `common-sql` provider in Airflow 3.0; + | +124 | JdbcOperator() +125 | MsSqlOperator() + | ^^^^^^^^^^^^^ AIR302 +126 | MySqlOperator() +127 | OracleOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. + +AIR302_common_sql.py:126:1: AIR302 `airflow.operators.mysql_operator.MySqlOperator` is moved into `common-sql` provider in Airflow 3.0; + | +124 | JdbcOperator() +125 | MsSqlOperator() +126 | MySqlOperator() + | ^^^^^^^^^^^^^ AIR302 +127 | OracleOperator() +128 | PostgresOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. + +AIR302_common_sql.py:127:1: AIR302 `airflow.operators.oracle_operator.OracleOperator` is moved into `common-sql` provider in Airflow 3.0; + | +125 | MsSqlOperator() +126 | MySqlOperator() +127 | OracleOperator() + | ^^^^^^^^^^^^^^ AIR302 +128 | PostgresOperator() +129 | SqliteOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. + +AIR302_common_sql.py:128:1: AIR302 `airflow.operators.postgres_operator.PostgresOperator` is moved into `common-sql` provider in Airflow 3.0; + | +126 | MySqlOperator() +127 | OracleOperator() +128 | PostgresOperator() + | ^^^^^^^^^^^^^^^^ AIR302 +129 | SqliteOperator() + | + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. + +AIR302_common_sql.py:129:1: AIR302 `airflow.operators.sqlite_operator.SqliteOperator` is moved into `common-sql` provider in Airflow 3.0; + | +127 | OracleOperator() +128 | PostgresOperator() +129 | SqliteOperator() + | ^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-common-sql>=1.3.0` and use `airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_daskexecutor.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_daskexecutor.py.snap new file mode 100644 index 00000000000000..ba40037189fb61 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_daskexecutor.py.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_daskexecutor.py:5:1: AIR302 `airflow.executors.dask_executor.DaskExecutor` is moved into `daskexecutor` provider in Airflow 3.0; + | +3 | from airflow.executors.dask_executor import DaskExecutor +4 | +5 | DaskExecutor() + | ^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-daskexecutor>=1.0.0` and use `airflow.providers.daskexecutor.executors.dask_executor.DaskExecutor` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_druid.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_druid.py.snap new file mode 100644 index 00000000000000..992a7d3da508dc --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_druid.py.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_druid.py:12:1: AIR302 `airflow.hooks.druid_hook.DruidDbApiHook` is moved into `apache-druid` provider in Airflow 3.0; + | +10 | ) +11 | +12 | DruidDbApiHook() + | ^^^^^^^^^^^^^^ AIR302 +13 | DruidHook() + | + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidDbApiHook` instead. + +AIR302_druid.py:13:1: AIR302 `airflow.hooks.druid_hook.DruidHook` is moved into `apache-druid` provider in Airflow 3.0; + | +12 | DruidDbApiHook() +13 | DruidHook() + | ^^^^^^^^^ AIR302 +14 | +15 | HiveToDruidOperator() + | + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.hooks.druid.DruidHook` instead. + +AIR302_druid.py:15:1: AIR302 `airflow.operators.hive_to_druid.HiveToDruidOperator` is moved into `apache-druid` provider in Airflow 3.0; + | +13 | DruidHook() +14 | +15 | HiveToDruidOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +16 | HiveToDruidTransfer() + | + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator` instead. + +AIR302_druid.py:16:1: AIR302 `airflow.operators.hive_to_druid.HiveToDruidTransfer` is moved into `apache-druid` provider in Airflow 3.0; + | +15 | HiveToDruidOperator() +16 | HiveToDruidTransfer() + | ^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-apache-druid>=1.0.0` and use `airflow.providers.apache.druid.transfers.hive_to_druid.HiveToDruidOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_fab.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_fab.py.snap new file mode 100644 index 00000000000000..0889673ff422de --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_fab.py.snap @@ -0,0 +1,190 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_fab.py:10:1: AIR302 `airflow.api.auth.backend.basic_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; + | + 8 | ) + 9 | +10 | CLIENT_AUTH + | ^^^^^^^^^^^ AIR302 +11 | init_app() +12 | auth_current_user() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.CLIENT_AUTH` instead. + +AIR302_fab.py:11:1: AIR302 `airflow.api.auth.backend.basic_auth.init_app` is moved into `fab` provider in Airflow 3.0; + | +10 | CLIENT_AUTH +11 | init_app() + | ^^^^^^^^ AIR302 +12 | auth_current_user() +13 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.init_app` instead. + +AIR302_fab.py:12:1: AIR302 `airflow.api.auth.backend.basic_auth.auth_current_user` is moved into `fab` provider in Airflow 3.0; + | +10 | CLIENT_AUTH +11 | init_app() +12 | auth_current_user() + | ^^^^^^^^^^^^^^^^^ AIR302 +13 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.auth_current_user` instead. + +AIR302_fab.py:13:1: AIR302 `airflow.api.auth.backend.basic_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; + | +11 | init_app() +12 | auth_current_user() +13 | requires_authentication() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +14 | +15 | from airflow.api.auth.backend.kerberos_auth import ( + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth.requires_authentication` instead. + +AIR302_fab.py:23:1: AIR302 `airflow.api.auth.backend.kerberos_auth.log` is moved into `fab` provider in Airflow 3.0; + | +21 | ) +22 | +23 | log() + | ^^^ AIR302 +24 | CLIENT_AUTH +25 | find_user() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.log` instead. + +AIR302_fab.py:24:1: AIR302 `airflow.api.auth.backend.kerberos_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; + | +23 | log() +24 | CLIENT_AUTH + | ^^^^^^^^^^^ AIR302 +25 | find_user() +26 | init_app() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.CLIENT_AUTH` instead. + +AIR302_fab.py:25:1: AIR302 `airflow.api.auth.backend.kerberos_auth.find_user` is moved into `fab` provider in Airflow 3.0; + | +23 | log() +24 | CLIENT_AUTH +25 | find_user() + | ^^^^^^^^^ AIR302 +26 | init_app() +27 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.find_user` instead. + +AIR302_fab.py:26:1: AIR302 `airflow.api.auth.backend.kerberos_auth.init_app` is moved into `fab` provider in Airflow 3.0; + | +24 | CLIENT_AUTH +25 | find_user() +26 | init_app() + | ^^^^^^^^ AIR302 +27 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.init_app` instead. + +AIR302_fab.py:27:1: AIR302 `airflow.api.auth.backend.kerberos_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; + | +25 | find_user() +26 | init_app() +27 | requires_authentication() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +28 | +29 | from airflow.auth.managers.fab.api.auth.backend.kerberos_auth import ( + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.requires_authentication` instead. + +AIR302_fab.py:37:1: AIR302 `airflow.auth.managers.fab.api.auth.backend.kerberos_auth.log` is moved into `fab` provider in Airflow 3.0; + | +35 | ) +36 | +37 | log() + | ^^^ AIR302 +38 | CLIENT_AUTH +39 | find_user() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.log` instead. + +AIR302_fab.py:38:1: AIR302 `airflow.auth.managers.fab.api.auth.backend.kerberos_auth.CLIENT_AUTH` is moved into `fab` provider in Airflow 3.0; + | +37 | log() +38 | CLIENT_AUTH + | ^^^^^^^^^^^ AIR302 +39 | find_user() +40 | init_app() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.CLIENT_AUTH` instead. + +AIR302_fab.py:39:1: AIR302 `airflow.auth.managers.fab.api.auth.backend.kerberos_auth.find_user` is moved into `fab` provider in Airflow 3.0; + | +37 | log() +38 | CLIENT_AUTH +39 | find_user() + | ^^^^^^^^^ AIR302 +40 | init_app() +41 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.find_user` instead. + +AIR302_fab.py:40:1: AIR302 `airflow.auth.managers.fab.api.auth.backend.kerberos_auth.init_app` is moved into `fab` provider in Airflow 3.0; + | +38 | CLIENT_AUTH +39 | find_user() +40 | init_app() + | ^^^^^^^^ AIR302 +41 | requires_authentication() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.init_app` instead. + +AIR302_fab.py:41:1: AIR302 `airflow.auth.managers.fab.api.auth.backend.kerberos_auth.requires_authentication` is moved into `fab` provider in Airflow 3.0; + | +39 | find_user() +40 | init_app() +41 | requires_authentication() + | ^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +42 | +43 | from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth.requires_authentication` instead. + +AIR302_fab.py:49:1: AIR302 `airflow.auth.managers.fab.fab_auth_manager.FabAuthManager` is moved into `fab` provider in Airflow 3.0; + | +47 | ) +48 | +49 | FabAuthManager() + | ^^^^^^^^^^^^^^ AIR302 +50 | MAX_NUM_DATABASE_USER_SESSIONS +51 | FabAirflowSecurityManagerOverride() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager` instead. + +AIR302_fab.py:50:1: AIR302 `airflow.auth.managers.fab.security_manager.override.MAX_NUM_DATABASE_USER_SESSIONS` is moved into `fab` provider in Airflow 3.0; + | +49 | FabAuthManager() +50 | MAX_NUM_DATABASE_USER_SESSIONS + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +51 | FabAirflowSecurityManagerOverride() + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.override.MAX_NUM_DATABASE_USER_SESSIONS` instead. + +AIR302_fab.py:51:1: AIR302 `airflow.auth.managers.fab.security_manager.override.FabAirflowSecurityManagerOverride` is moved into `fab` provider in Airflow 3.0; + | +49 | FabAuthManager() +50 | MAX_NUM_DATABASE_USER_SESSIONS +51 | FabAirflowSecurityManagerOverride() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +52 | +53 | from airflow.www.security import FabAirflowSecurityManagerOverride + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.override.FabAirflowSecurityManagerOverride` instead. + +AIR302_fab.py:55:1: AIR302 `airflow.www.security.FabAirflowSecurityManagerOverride` is moved into `fab` provider in Airflow 3.0; + | +53 | from airflow.www.security import FabAirflowSecurityManagerOverride +54 | +55 | FabAirflowSecurityManagerOverride() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.override.FabAirflowSecurityManagerOverride` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hdfs.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hdfs.py.snap new file mode 100644 index 00000000000000..8ff613fea1cdbf --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hdfs.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_hdfs.py:6:1: AIR302 `airflow.hooks.webhdfs_hook.WebHDFSHook` is moved into `apache-hdfs` provider in Airflow 3.0; + | +4 | from airflow.sensors.web_hdfs_sensor import WebHdfsSensor +5 | +6 | WebHDFSHook() + | ^^^^^^^^^^^ AIR302 +7 | WebHdfsSensor() + | + = help: Install `apache-airflow-providers-apache-hdfs>=1.0.0` and use `airflow.providers.apache.hdfs.hooks.webhdfs.WebHDFSHook` instead. + +AIR302_hdfs.py:7:1: AIR302 `airflow.sensors.web_hdfs_sensor.WebHdfsSensor` is moved into `apache-hdfs` provider in Airflow 3.0; + | +6 | WebHDFSHook() +7 | WebHdfsSensor() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-apache-hdfs>=1.0.0` and use `airflow.providers.apache.hdfs.sensors.web_hdfs.WebHdfsSensor` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hive.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hive.py.snap new file mode 100644 index 00000000000000..aa035c4ff715cd --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_hive.py.snap @@ -0,0 +1,208 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_hive.py:36:1: AIR302 `airflow.macros.hive.closest_ds_partition` is moved into `apache-hive` provider in Airflow 3.0; + | +34 | from airflow.sensors.named_hive_partition_sensor import NamedHivePartitionSensor +35 | +36 | closest_ds_partition() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +37 | max_partition() + | + = help: Install `apache-airflow-providers-apache-hive>=5.1.0` and use `airflow.providers.apache.hive.macros.hive.closest_ds_partition` instead. + +AIR302_hive.py:37:1: AIR302 `airflow.macros.hive.max_partition` is moved into `apache-hive` provider in Airflow 3.0; + | +36 | closest_ds_partition() +37 | max_partition() + | ^^^^^^^^^^^^^ AIR302 +38 | +39 | HiveCliHook() + | + = help: Install `apache-airflow-providers-apache-hive>=5.1.0` and use `airflow.providers.apache.hive.macros.hive.max_partition` instead. + +AIR302_hive.py:39:1: AIR302 `airflow.hooks.hive_hooks.HiveCliHook` is moved into `apache-hive` provider in Airflow 3.0; + | +37 | max_partition() +38 | +39 | HiveCliHook() + | ^^^^^^^^^^^ AIR302 +40 | HiveMetastoreHook() +41 | HiveServer2Hook() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveCliHook` instead. + +AIR302_hive.py:40:1: AIR302 `airflow.hooks.hive_hooks.HiveMetastoreHook` is moved into `apache-hive` provider in Airflow 3.0; + | +39 | HiveCliHook() +40 | HiveMetastoreHook() + | ^^^^^^^^^^^^^^^^^ AIR302 +41 | HiveServer2Hook() +42 | HIVE_QUEUE_PRIORITIES + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveMetastoreHook` instead. + +AIR302_hive.py:41:1: AIR302 `airflow.hooks.hive_hooks.HiveServer2Hook` is moved into `apache-hive` provider in Airflow 3.0; + | +39 | HiveCliHook() +40 | HiveMetastoreHook() +41 | HiveServer2Hook() + | ^^^^^^^^^^^^^^^ AIR302 +42 | HIVE_QUEUE_PRIORITIES + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HiveServer2Hook` instead. + +AIR302_hive.py:42:1: AIR302 `airflow.hooks.hive_hooks.HIVE_QUEUE_PRIORITIES` is moved into `apache-hive` provider in Airflow 3.0; + | +40 | HiveMetastoreHook() +41 | HiveServer2Hook() +42 | HIVE_QUEUE_PRIORITIES + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +43 | +44 | HiveOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.hooks.hive.HIVE_QUEUE_PRIORITIES` instead. + +AIR302_hive.py:44:1: AIR302 `airflow.operators.hive_operator.HiveOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +42 | HIVE_QUEUE_PRIORITIES +43 | +44 | HiveOperator() + | ^^^^^^^^^^^^ AIR302 +45 | +46 | HiveStatsCollectionOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.operators.hive.HiveOperator` instead. + +AIR302_hive.py:46:1: AIR302 `airflow.operators.hive_stats_operator.HiveStatsCollectionOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +44 | HiveOperator() +45 | +46 | HiveStatsCollectionOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +47 | +48 | HiveToMySqlOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.operators.hive_stats.HiveStatsCollectionOperator` instead. + +AIR302_hive.py:48:1: AIR302 `airflow.operators.hive_to_mysql.HiveToMySqlOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +46 | HiveStatsCollectionOperator() +47 | +48 | HiveToMySqlOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +49 | HiveToMySqlTransfer() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator` instead. + +AIR302_hive.py:49:1: AIR302 `airflow.operators.hive_to_mysql.HiveToMySqlTransfer` is moved into `apache-hive` provider in Airflow 3.0; + | +48 | HiveToMySqlOperator() +49 | HiveToMySqlTransfer() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +50 | +51 | HiveToSambaOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_mysql.HiveToMySqlOperator` instead. + +AIR302_hive.py:51:1: AIR302 `airflow.operators.hive_to_samba_operator.HiveToSambaOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +49 | HiveToMySqlTransfer() +50 | +51 | HiveToSambaOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +52 | +53 | MsSqlToHiveOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.hive_to_samba.HiveToSambaOperator` instead. + +AIR302_hive.py:53:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +51 | HiveToSambaOperator() +52 | +53 | MsSqlToHiveOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +54 | MsSqlToHiveTransfer() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator` instead. + +AIR302_hive.py:54:1: AIR302 `airflow.operators.mssql_to_hive.MsSqlToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; + | +53 | MsSqlToHiveOperator() +54 | MsSqlToHiveTransfer() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +55 | +56 | MySqlToHiveOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mssql_to_hive.MsSqlToHiveOperator` instead. + +AIR302_hive.py:56:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +54 | MsSqlToHiveTransfer() +55 | +56 | MySqlToHiveOperator() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +57 | MySqlToHiveTransfer() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator` instead. + +AIR302_hive.py:57:1: AIR302 `airflow.operators.mysql_to_hive.MySqlToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; + | +56 | MySqlToHiveOperator() +57 | MySqlToHiveTransfer() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +58 | +59 | S3ToHiveOperator() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.mysql_to_hive.MySqlToHiveOperator` instead. + +AIR302_hive.py:59:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveOperator` is moved into `apache-hive` provider in Airflow 3.0; + | +57 | MySqlToHiveTransfer() +58 | +59 | S3ToHiveOperator() + | ^^^^^^^^^^^^^^^^ AIR302 +60 | S3ToHiveTransfer() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator` instead. + +AIR302_hive.py:60:1: AIR302 `airflow.operators.s3_to_hive_operator.S3ToHiveTransfer` is moved into `apache-hive` provider in Airflow 3.0; + | +59 | S3ToHiveOperator() +60 | S3ToHiveTransfer() + | ^^^^^^^^^^^^^^^^ AIR302 +61 | +62 | HivePartitionSensor() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.transfers.s3_to_hive.S3ToHiveOperator` instead. + +AIR302_hive.py:62:1: AIR302 `airflow.sensors.hive_partition_sensor.HivePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; + | +60 | S3ToHiveTransfer() +61 | +62 | HivePartitionSensor() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +63 | +64 | MetastorePartitionSensor() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.hive_partition.HivePartitionSensor` instead. + +AIR302_hive.py:64:1: AIR302 `airflow.sensors.metastore_partition_sensor.MetastorePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; + | +62 | HivePartitionSensor() +63 | +64 | MetastorePartitionSensor() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +65 | +66 | NamedHivePartitionSensor() + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.metastore_partition.MetastorePartitionSensor` instead. + +AIR302_hive.py:66:1: AIR302 `airflow.sensors.named_hive_partition_sensor.NamedHivePartitionSensor` is moved into `apache-hive` provider in Airflow 3.0; + | +64 | MetastorePartitionSensor() +65 | +66 | NamedHivePartitionSensor() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-apache-hive>=1.0.0` and use `airflow.providers.apache.hive.sensors.named_hive_partition.NamedHivePartitionSensor` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_http.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_http.py.snap new file mode 100644 index 00000000000000..d8ab77304dd638 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_http.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_http.py:7:1: AIR302 `airflow.hooks.http_hook.HttpHook` is moved into `http` provider in Airflow 3.0; + | +5 | from airflow.sensors.http_sensor import HttpSensor +6 | +7 | HttpHook() + | ^^^^^^^^ AIR302 +8 | SimpleHttpOperator() +9 | HttpSensor() + | + = help: Install `apache-airflow-providers-http>=1.0.0` and use `airflow.providers.http.hooks.http.HttpHook` instead. + +AIR302_http.py:8:1: AIR302 [*] `airflow.operators.http_operator.SimpleHttpOperator` is moved into `http` provider in Airflow 3.0; + | +7 | HttpHook() +8 | SimpleHttpOperator() + | ^^^^^^^^^^^^^^^^^^ AIR302 +9 | HttpSensor() + | + = help: Install `apache-airflow-providers-http>=5.0.0` and use `airflow.providers.http.operators.http.HttpOperator` instead. + +ℹ Safe fix +3 3 | from airflow.hooks.http_hook import HttpHook +4 4 | from airflow.operators.http_operator import SimpleHttpOperator +5 5 | from airflow.sensors.http_sensor import HttpSensor + 6 |+from airflow.providers.http.operators.http import HttpOperator +6 7 | +7 8 | HttpHook() +8 |-SimpleHttpOperator() + 9 |+HttpOperator() +9 10 | HttpSensor() + +AIR302_http.py:9:1: AIR302 `airflow.sensors.http_sensor.HttpSensor` is moved into `http` provider in Airflow 3.0; + | +7 | HttpHook() +8 | SimpleHttpOperator() +9 | HttpSensor() + | ^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-http>=1.0.0` and use `airflow.providers.http.sensors.http.HttpSensor` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_jdbc.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_jdbc.py.snap new file mode 100644 index 00000000000000..ab257a2b2479fc --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_jdbc.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_jdbc.py:8:1: AIR302 `airflow.hooks.jdbc_hook.JdbcHook` is moved into `jdbc` provider in Airflow 3.0; + | +6 | ) +7 | +8 | JdbcHook() + | ^^^^^^^^ AIR302 +9 | jaydebeapi() + | + = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.JdbcHook` instead. + +AIR302_jdbc.py:9:1: AIR302 `airflow.hooks.jdbc_hook.jaydebeapi` is moved into `jdbc` provider in Airflow 3.0; + | +8 | JdbcHook() +9 | jaydebeapi() + | ^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-jdbc>=1.0.0` and use `airflow.providers.jdbc.hooks.jdbc.jaydebeapi` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_kubernetes.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_kubernetes.py.snap new file mode 100644 index 00000000000000..cd35c51ba2a928 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_kubernetes.py.snap @@ -0,0 +1,599 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_kubernetes.py:29:1: AIR302 `airflow.executors.kubernetes_executor_types.ALL_NAMESPACES` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +27 | ) +28 | +29 | ALL_NAMESPACES + | ^^^^^^^^^^^^^^ AIR302 +30 | POD_EXECUTOR_DONE_KEY + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.ALL_NAMESPACES` instead. + +AIR302_kubernetes.py:30:1: AIR302 `airflow.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +29 | ALL_NAMESPACES +30 | POD_EXECUTOR_DONE_KEY + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +31 | +32 | K8SModel() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` instead. + +AIR302_kubernetes.py:32:1: AIR302 `airflow.kubernetes.k8s_model.K8SModel` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +30 | POD_EXECUTOR_DONE_KEY +31 | +32 | K8SModel() + | ^^^^^^^^ AIR302 +33 | append_to_pod() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.K8SModel` instead. + +AIR302_kubernetes.py:33:1: AIR302 `airflow.kubernetes.k8s_model.append_to_pod` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +32 | K8SModel() +33 | append_to_pod() + | ^^^^^^^^^^^^^ AIR302 +34 | +35 | _disable_verify_ssl() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.append_to_pod` instead. + +AIR302_kubernetes.py:35:1: AIR302 `airflow.kubernetes.kube_client._disable_verify_ssl` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +33 | append_to_pod() +34 | +35 | _disable_verify_ssl() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +36 | _enable_tcp_keepalive() +37 | get_kube_client() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client._disable_verify_ssl` instead. + +AIR302_kubernetes.py:36:1: AIR302 `airflow.kubernetes.kube_client._enable_tcp_keepalive` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +35 | _disable_verify_ssl() +36 | _enable_tcp_keepalive() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +37 | get_kube_client() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client._enable_tcp_keepalive` instead. + +AIR302_kubernetes.py:37:1: AIR302 `airflow.kubernetes.kube_client.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +35 | _disable_verify_ssl() +36 | _enable_tcp_keepalive() +37 | get_kube_client() + | ^^^^^^^^^^^^^^^ AIR302 +38 | +39 | add_pod_suffix() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. + +AIR302_kubernetes.py:39:1: AIR302 [*] `airflow.kubernetes.kubernetes_helper_functions.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +37 | get_kube_client() +38 | +39 | add_pod_suffix() + | ^^^^^^^^^^^^^^ AIR302 +40 | create_pod_id() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_unique_suffix` instead. + +ℹ Safe fix +25 25 | Port, +26 26 | Resources, +27 27 | ) + 28 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import add_unique_suffix +28 29 | +29 30 | ALL_NAMESPACES +30 31 | POD_EXECUTOR_DONE_KEY +-------------------------------------------------------------------------------- +36 37 | _enable_tcp_keepalive() +37 38 | get_kube_client() +38 39 | +39 |-add_pod_suffix() + 40 |+add_unique_suffix() +40 41 | create_pod_id() +41 42 | +42 43 | annotations_for_logging_task_metadata() + +AIR302_kubernetes.py:40:1: AIR302 [*] `airflow.kubernetes.kubernetes_helper_functions.create_pod_id` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +39 | add_pod_suffix() +40 | create_pod_id() + | ^^^^^^^^^^^^^ AIR302 +41 | +42 | annotations_for_logging_task_metadata() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.create_unique_id` instead. + +ℹ Safe fix +25 25 | Port, +26 26 | Resources, +27 27 | ) + 28 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import create_unique_id +28 29 | +29 30 | ALL_NAMESPACES +30 31 | POD_EXECUTOR_DONE_KEY +-------------------------------------------------------------------------------- +37 38 | get_kube_client() +38 39 | +39 40 | add_pod_suffix() +40 |-create_pod_id() + 41 |+create_unique_id() +41 42 | +42 43 | annotations_for_logging_task_metadata() +43 44 | annotations_to_key() + +AIR302_kubernetes.py:42:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +40 | create_pod_id() +41 | +42 | annotations_for_logging_task_metadata() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +43 | annotations_to_key() +44 | get_logs_task_metadata() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata` instead. + +AIR302_kubernetes.py:43:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.annotations_to_key` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +42 | annotations_for_logging_task_metadata() +43 | annotations_to_key() + | ^^^^^^^^^^^^^^^^^^ AIR302 +44 | get_logs_task_metadata() +45 | rand_str() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_to_key` instead. + +AIR302_kubernetes.py:44:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.get_logs_task_metadata` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +42 | annotations_for_logging_task_metadata() +43 | annotations_to_key() +44 | get_logs_task_metadata() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 +45 | rand_str() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.get_logs_task_metadata` instead. + +AIR302_kubernetes.py:45:1: AIR302 `airflow.kubernetes.kubernetes_helper_functions.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +43 | annotations_to_key() +44 | get_logs_task_metadata() +45 | rand_str() + | ^^^^^^^^ AIR302 +46 | +47 | Port() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str` instead. + +AIR302_kubernetes.py:47:1: AIR302 [*] `airflow.kubernetes.pod.Port` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +45 | rand_str() +46 | +47 | Port() + | ^^^^ AIR302 +48 | Resources() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1ContainerPort` instead. + +ℹ Safe fix +25 25 | Port, +26 26 | Resources, +27 27 | ) + 28 |+from kubernetes.client.models import V1ContainerPort +28 29 | +29 30 | ALL_NAMESPACES +30 31 | POD_EXECUTOR_DONE_KEY +-------------------------------------------------------------------------------- +44 45 | get_logs_task_metadata() +45 46 | rand_str() +46 47 | +47 |-Port() + 48 |+V1ContainerPort() +48 49 | Resources() +49 50 | +50 51 | + +AIR302_kubernetes.py:48:1: AIR302 [*] `airflow.kubernetes.pod.Resources` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +47 | Port() +48 | Resources() + | ^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1ResourceRequirements` instead. + +ℹ Safe fix +25 25 | Port, +26 26 | Resources, +27 27 | ) + 28 |+from kubernetes.client.models import V1ResourceRequirements +28 29 | +29 30 | ALL_NAMESPACES +30 31 | POD_EXECUTOR_DONE_KEY +-------------------------------------------------------------------------------- +45 46 | rand_str() +46 47 | +47 48 | Port() +48 |-Resources() + 49 |+V1ResourceRequirements() +49 50 | +50 51 | +51 52 | from airflow.kubernetes.pod_generator import ( + +AIR302_kubernetes.py:64:1: AIR302 `airflow.kubernetes.pod_generator.datetime_to_label_safe_datestring` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +62 | ) +63 | +64 | datetime_to_label_safe_datestring() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +65 | extend_object_field() +66 | label_safe_datestring_to_datetime() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.datetime_to_label_safe_datestring` instead. + +AIR302_kubernetes.py:65:1: AIR302 `airflow.kubernetes.pod_generator.extend_object_field` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +64 | datetime_to_label_safe_datestring() +65 | extend_object_field() + | ^^^^^^^^^^^^^^^^^^^ AIR302 +66 | label_safe_datestring_to_datetime() +67 | make_safe_label_value() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.extend_object_field` instead. + +AIR302_kubernetes.py:66:1: AIR302 `airflow.kubernetes.pod_generator.label_safe_datestring_to_datetime` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +64 | datetime_to_label_safe_datestring() +65 | extend_object_field() +66 | label_safe_datestring_to_datetime() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +67 | make_safe_label_value() +68 | merge_objects() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.label_safe_datestring_to_datetime` instead. + +AIR302_kubernetes.py:67:1: AIR302 `airflow.kubernetes.pod_generator.make_safe_label_value` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +65 | extend_object_field() +66 | label_safe_datestring_to_datetime() +67 | make_safe_label_value() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +68 | merge_objects() +69 | PodGenerator() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.make_safe_label_value` instead. + +AIR302_kubernetes.py:68:1: AIR302 `airflow.kubernetes.pod_generator.merge_objects` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +66 | label_safe_datestring_to_datetime() +67 | make_safe_label_value() +68 | merge_objects() + | ^^^^^^^^^^^^^ AIR302 +69 | PodGenerator() +70 | PodDefaults() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.merge_objects` instead. + +AIR302_kubernetes.py:69:1: AIR302 `airflow.kubernetes.pod_generator.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +67 | make_safe_label_value() +68 | merge_objects() +69 | PodGenerator() + | ^^^^^^^^^^^^ AIR302 +70 | PodDefaults() +71 | PodGeneratorDeprecated() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. + +AIR302_kubernetes.py:70:1: AIR302 `airflow.kubernetes.pod_generator.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +68 | merge_objects() +69 | PodGenerator() +70 | PodDefaults() + | ^^^^^^^^^^^ AIR302 +71 | PodGeneratorDeprecated() +72 | add_pod_suffix() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults` instead. + +AIR302_kubernetes.py:71:1: AIR302 `airflow.kubernetes.pod_generator.PodGeneratorDeprecated` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +69 | PodGenerator() +70 | PodDefaults() +71 | PodGeneratorDeprecated() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 +72 | add_pod_suffix() +73 | rand_str() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. + +AIR302_kubernetes.py:72:1: AIR302 [*] `airflow.kubernetes.pod_generator.add_pod_suffix` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +70 | PodDefaults() +71 | PodGeneratorDeprecated() +72 | add_pod_suffix() + | ^^^^^^^^^^^^^^ AIR302 +73 | rand_str() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=10.0.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_unique_suffix` instead. + +ℹ Safe fix +60 60 | merge_objects, +61 61 | rand_str, +62 62 | ) + 63 |+from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import add_unique_suffix +63 64 | +64 65 | datetime_to_label_safe_datestring() +65 66 | extend_object_field() +-------------------------------------------------------------------------------- +69 70 | PodGenerator() +70 71 | PodDefaults() +71 72 | PodGeneratorDeprecated() +72 |-add_pod_suffix() + 73 |+add_unique_suffix() +73 74 | rand_str() +74 75 | +75 76 | + +AIR302_kubernetes.py:73:1: AIR302 `airflow.kubernetes.pod_generator.rand_str` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +71 | PodGeneratorDeprecated() +72 | add_pod_suffix() +73 | rand_str() + | ^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str` instead. + +AIR302_kubernetes.py:86:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +84 | ) +85 | +86 | PodDefaults() + | ^^^^^^^^^^^ AIR302 +87 | PodGenerator() +88 | make_safe_label_value() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults` instead. + +AIR302_kubernetes.py:87:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.PodGenerator` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +86 | PodDefaults() +87 | PodGenerator() + | ^^^^^^^^^^^^ AIR302 +88 | make_safe_label_value() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.PodGenerator` instead. + +AIR302_kubernetes.py:88:1: AIR302 `airflow.kubernetes.pod_generator_deprecated.make_safe_label_value` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +86 | PodDefaults() +87 | PodGenerator() +88 | make_safe_label_value() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +89 | +90 | PodLauncher() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.pod_generator.make_safe_label_value` instead. + +AIR302_kubernetes.py:90:1: AIR302 [*] `airflow.kubernetes.pod_launcher.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +88 | make_safe_label_value() +89 | +90 | PodLauncher() + | ^^^^^^^^^^^ AIR302 +91 | PodStatus() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use `airflow.providers.cncf.kubernetes.utils.pod_manager.PodManager` instead. + +ℹ Safe fix +82 82 | PodLauncher, +83 83 | PodStatus, +84 84 | ) + 85 |+from airflow.providers.cncf.kubernetes.utils.pod_manager import PodManager +85 86 | +86 87 | PodDefaults() +87 88 | PodGenerator() +88 89 | make_safe_label_value() +89 90 | +90 |-PodLauncher() + 91 |+PodManager() +91 92 | PodStatus() +92 93 | +93 94 | + +AIR302_kubernetes.py:91:1: AIR302 [*] `airflow.kubernetes.pod_launcher.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +90 | PodLauncher() +91 | PodStatus() + | ^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use ` airflow.providers.cncf.kubernetes.utils.pod_manager.PodPhase` instead. + +ℹ Safe fix +82 82 | PodLauncher, +83 83 | PodStatus, +84 84 | ) + 85 |+from airflow.providers.cncf.kubernetes.utils.pod_manager import PodPhase +85 86 | +86 87 | PodDefaults() +87 88 | PodGenerator() +88 89 | make_safe_label_value() +89 90 | +90 91 | PodLauncher() +91 |-PodStatus() + 92 |+PodPhase() +92 93 | +93 94 | +94 95 | from airflow.kubernetes.pod_launcher_deprecated import ( + +AIR302_kubernetes.py:108:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.PodDefaults` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +106 | from airflow.kubernetes.volume_mount import VolumeMount +107 | +108 | PodDefaults() + | ^^^^^^^^^^^ AIR302 +109 | PodLauncher() +110 | PodStatus() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.utils.xcom_sidecar.PodDefaults` instead. + +AIR302_kubernetes.py:109:1: AIR302 [*] `airflow.kubernetes.pod_launcher_deprecated.PodLauncher` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +108 | PodDefaults() +109 | PodLauncher() + | ^^^^^^^^^^^ AIR302 +110 | PodStatus() +111 | get_kube_client() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use `airflow.providers.cncf.kubernetes.utils.pod_manager.PodManager` instead. + +ℹ Safe fix +104 104 | ) +105 105 | from airflow.kubernetes.volume import Volume +106 106 | from airflow.kubernetes.volume_mount import VolumeMount + 107 |+from airflow.providers.cncf.kubernetes.utils.pod_manager import PodManager +107 108 | +108 109 | PodDefaults() +109 |-PodLauncher() + 110 |+PodManager() +110 111 | PodStatus() +111 112 | get_kube_client() +112 113 | + +AIR302_kubernetes.py:110:1: AIR302 [*] `airflow.kubernetes.pod_launcher_deprecated.PodStatus` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +108 | PodDefaults() +109 | PodLauncher() +110 | PodStatus() + | ^^^^^^^^^ AIR302 +111 | get_kube_client() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=3.0.0` and use ` airflow.providers.cncf.kubernetes.utils.pod_manager.PodPhase` instead. + +ℹ Safe fix +104 104 | ) +105 105 | from airflow.kubernetes.volume import Volume +106 106 | from airflow.kubernetes.volume_mount import VolumeMount + 107 |+from airflow.providers.cncf.kubernetes.utils.pod_manager import PodPhase +107 108 | +108 109 | PodDefaults() +109 110 | PodLauncher() +110 |-PodStatus() + 111 |+PodPhase() +111 112 | get_kube_client() +112 113 | +113 114 | PodRuntimeInfoEnv() + +AIR302_kubernetes.py:111:1: AIR302 `airflow.kubernetes.pod_launcher_deprecated.get_kube_client` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +109 | PodLauncher() +110 | PodStatus() +111 | get_kube_client() + | ^^^^^^^^^^^^^^^ AIR302 +112 | +113 | PodRuntimeInfoEnv() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.kube_client.get_kube_client` instead. + +AIR302_kubernetes.py:113:1: AIR302 [*] `airflow.kubernetes.pod_runtime_info_env.PodRuntimeInfoEnv` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +111 | get_kube_client() +112 | +113 | PodRuntimeInfoEnv() + | ^^^^^^^^^^^^^^^^^ AIR302 +114 | K8SModel() +115 | Secret() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1EnvVar` instead. + +ℹ Safe fix +104 104 | ) +105 105 | from airflow.kubernetes.volume import Volume +106 106 | from airflow.kubernetes.volume_mount import VolumeMount + 107 |+from kubernetes.client.models import V1EnvVar +107 108 | +108 109 | PodDefaults() +109 110 | PodLauncher() +110 111 | PodStatus() +111 112 | get_kube_client() +112 113 | +113 |-PodRuntimeInfoEnv() + 114 |+V1EnvVar() +114 115 | K8SModel() +115 116 | Secret() +116 117 | Volume() + +AIR302_kubernetes.py:114:1: AIR302 `airflow.kubernetes.secret.K8SModel` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +113 | PodRuntimeInfoEnv() +114 | K8SModel() + | ^^^^^^^^ AIR302 +115 | Secret() +116 | Volume() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.k8s_model.K8SModel` instead. + +AIR302_kubernetes.py:115:1: AIR302 `airflow.kubernetes.secret.Secret` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +113 | PodRuntimeInfoEnv() +114 | K8SModel() +115 | Secret() + | ^^^^^^ AIR302 +116 | Volume() +117 | VolumeMount() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `airflow.providers.cncf.kubernetes.secret.Secret` instead. + +AIR302_kubernetes.py:116:1: AIR302 [*] `airflow.kubernetes.volume.Volume` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +114 | K8SModel() +115 | Secret() +116 | Volume() + | ^^^^^^ AIR302 +117 | VolumeMount() + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1Volume` instead. + +ℹ Safe fix +104 104 | ) +105 105 | from airflow.kubernetes.volume import Volume +106 106 | from airflow.kubernetes.volume_mount import VolumeMount + 107 |+from kubernetes.client.models import V1Volume +107 108 | +108 109 | PodDefaults() +109 110 | PodLauncher() +-------------------------------------------------------------------------------- +113 114 | PodRuntimeInfoEnv() +114 115 | K8SModel() +115 116 | Secret() +116 |-Volume() + 117 |+V1Volume() +117 118 | VolumeMount() + +AIR302_kubernetes.py:117:1: AIR302 [*] `airflow.kubernetes.volume_mount.VolumeMount` is moved into `cncf-kubernetes` provider in Airflow 3.0; + | +115 | Secret() +116 | Volume() +117 | VolumeMount() + | ^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-cncf-kubernetes>=7.4.0` and use `kubernetes.client.models.V1VolumeMount` instead. + +ℹ Safe fix +104 104 | ) +105 105 | from airflow.kubernetes.volume import Volume +106 106 | from airflow.kubernetes.volume_mount import VolumeMount + 107 |+from kubernetes.client.models import V1VolumeMount +107 108 | +108 109 | PodDefaults() +109 110 | PodLauncher() +-------------------------------------------------------------------------------- +114 115 | K8SModel() +115 116 | Secret() +116 117 | Volume() +117 |-VolumeMount() + 118 |+V1VolumeMount() diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_mysql.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_mysql.py.snap new file mode 100644 index 00000000000000..d40283a7dd8a39 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_mysql.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_mysql.py:9:1: AIR302 `airflow.hooks.mysql_hook.MySqlHook` is moved into `mysql` provider in Airflow 3.0; + | + 7 | ) + 8 | + 9 | MySqlHook() + | ^^^^^^^^^ AIR302 +10 | PrestoToMySqlOperator() +11 | PrestoToMySqlTransfer() + | + = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.hooks.mysql.MySqlHook` instead. + +AIR302_mysql.py:10:1: AIR302 `airflow.operators.presto_to_mysql.PrestoToMySqlOperator` is moved into `mysql` provider in Airflow 3.0; + | + 9 | MySqlHook() +10 | PrestoToMySqlOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +11 | PrestoToMySqlTransfer() + | + = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator` instead. + +AIR302_mysql.py:11:1: AIR302 `airflow.operators.presto_to_mysql.PrestoToMySqlTransfer` is moved into `mysql` provider in Airflow 3.0; + | + 9 | MySqlHook() +10 | PrestoToMySqlOperator() +11 | PrestoToMySqlTransfer() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-mysql>=1.0.0` and use `airflow.providers.mysql.transfers.presto_to_mysql.PrestoToMySqlOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_oracle.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_oracle.py.snap new file mode 100644 index 00000000000000..6b087c471593eb --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_oracle.py.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_oracle.py:5:1: AIR302 `airflow.hooks.oracle_hook.OracleHook` is moved into `oracle` provider in Airflow 3.0; + | +3 | from airflow.hooks.oracle_hook import OracleHook +4 | +5 | OracleHook() + | ^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-oracle>=1.0.0` and use `airflow.providers.oracle.hooks.oracle.OracleHook` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_papermill.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_papermill.py.snap new file mode 100644 index 00000000000000..b0ebbed9d619dd --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_papermill.py.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_papermill.py:5:1: AIR302 `airflow.operators.papermill_operator.PapermillOperator` is moved into `papermill` provider in Airflow 3.0; + | +3 | from airflow.operators.papermill_operator import PapermillOperator +4 | +5 | PapermillOperator() + | ^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-papermill>=1.0.0` and use `airflow.providers.papermill.operators.papermill.PapermillOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_pig.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_pig.py.snap new file mode 100644 index 00000000000000..10c5ee05fb1a3a --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_pig.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_pig.py:6:1: AIR302 `airflow.hooks.pig_hook.PigCliHook` is moved into `apache-pig` provider in Airflow 3.0; + | +4 | from airflow.operators.pig_operator import PigOperator +5 | +6 | PigCliHook() + | ^^^^^^^^^^ AIR302 +7 | PigOperator() + | + = help: Install `apache-airflow-providers-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.hooks.pig.PigCliHook` instead. + +AIR302_pig.py:7:1: AIR302 `airflow.operators.pig_operator.PigOperator` is moved into `apache-pig` provider in Airflow 3.0; + | +6 | PigCliHook() +7 | PigOperator() + | ^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-apache-pig>=1.0.0` and use `airflow.providers.apache.pig.operators.pig.PigOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_postgres.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_postgres.py.snap new file mode 100644 index 00000000000000..f54468142b7144 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_postgres.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_postgres.py:6:1: AIR302 `airflow.hooks.postgres_hook.PostgresHook` is moved into `postgres` provider in Airflow 3.0; + | +4 | from airflow.operators.postgres_operator import Mapping +5 | +6 | PostgresHook() + | ^^^^^^^^^^^^ AIR302 +7 | Mapping() + | + = help: Install `apache-airflow-providers-postgres>=1.0.0` and use `airflow.providers.postgres.hooks.postgres.PostgresHook` instead. + +AIR302_postgres.py:7:1: AIR302 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0 + | +6 | PostgresHook() +7 | Mapping() + | ^^^^^^^ AIR302 + | diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_presto.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_presto.py.snap new file mode 100644 index 00000000000000..cb03cfaf254a1b --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_presto.py.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_presto.py:5:1: AIR302 `airflow.hooks.presto_hook.PrestoHook` is moved into `presto` provider in Airflow 3.0; + | +3 | from airflow.hooks.presto_hook import PrestoHook +4 | +5 | PrestoHook() + | ^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-presto>=1.0.0` and use `airflow.providers.presto.hooks.presto.PrestoHook` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_samba.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_samba.py.snap new file mode 100644 index 00000000000000..470f3faa0ed13d --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_samba.py.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_samba.py:5:1: AIR302 `airflow.hooks.samba_hook.SambaHook` is moved into `samba` provider in Airflow 3.0; + | +3 | from airflow.hooks.samba_hook import SambaHook +4 | +5 | SambaHook() + | ^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-samba>=1.0.0` and use `airflow.providers.samba.hooks.samba.SambaHook` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_slack.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_slack.py.snap new file mode 100644 index 00000000000000..40b728f9f505c0 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_slack.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_slack.py:6:1: AIR302 `airflow.hooks.slack_hook.SlackHook` is moved into `slack` provider in Airflow 3.0; + | +4 | from airflow.operators.slack_operator import SlackAPIOperator, SlackAPIPostOperator +5 | +6 | SlackHook() + | ^^^^^^^^^ AIR302 +7 | SlackAPIOperator() +8 | SlackAPIPostOperator() + | + = help: Install `apache-airflow-providers-slack>=1.0.0` and use `airflow.providers.slack.hooks.slack.SlackHook` instead. + +AIR302_slack.py:7:1: AIR302 `airflow.operators.slack_operator.SlackAPIOperator` is moved into `slack` provider in Airflow 3.0; + | +6 | SlackHook() +7 | SlackAPIOperator() + | ^^^^^^^^^^^^^^^^ AIR302 +8 | SlackAPIPostOperator() + | + = help: Install `apache-airflow-providers-slack>=1.0.0` and use `airflow.providers.slack.operators.slack.SlackAPIOperator` instead. + +AIR302_slack.py:8:1: AIR302 `airflow.operators.slack_operator.SlackAPIPostOperator` is moved into `slack` provider in Airflow 3.0; + | +6 | SlackHook() +7 | SlackAPIOperator() +8 | SlackAPIPostOperator() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-slack>=1.0.0` and use `airflow.providers.slack.operators.slack.SlackAPIPostOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_smtp.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_smtp.py.snap new file mode 100644 index 00000000000000..5bf6522a4d8f47 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_smtp.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_smtp.py:5:1: AIR302 `airflow.operators.email_operator.EmailOperator` is moved into `smtp` provider in Airflow 3.0; + | +3 | from airflow.operators.email_operator import EmailOperator +4 | +5 | EmailOperator() + | ^^^^^^^^^^^^^ AIR302 +6 | +7 | from airflow.operators.email import EmailOperator + | + = help: Install `apache-airflow-providers-smtp>=1.0.0` and use `airflow.providers.smtp.operators.smtp.EmailOperator` instead. + +AIR302_smtp.py:9:1: AIR302 `airflow.operators.email.EmailOperator` is moved into `smtp` provider in Airflow 3.0; + | +7 | from airflow.operators.email import EmailOperator +8 | +9 | EmailOperator() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-smtp>=1.0.0` and use `airflow.providers.smtp.operators.smtp.EmailOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_sqlite.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_sqlite.py.snap new file mode 100644 index 00000000000000..6b3b7dcbc4570b --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_sqlite.py.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_sqlite.py:5:1: AIR302 `airflow.hooks.sqlite_hook.SqliteHook` is moved into `sqlite` provider in Airflow 3.0; + | +3 | from airflow.hooks.sqlite_hook import SqliteHook +4 | +5 | SqliteHook() + | ^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-sqlite>=1.0.0` and use `airflow.providers.sqlite.hooks.sqlite.SqliteHook` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_standard.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_standard.py.snap new file mode 100644 index 00000000000000..f922fd2ef856aa --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_standard.py.snap @@ -0,0 +1,157 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_standard.py:25:1: AIR302 `airflow.operators.bash_operator.BashOperator` is moved into `standard` provider in Airflow 3.0; + | +23 | ) +24 | +25 | BashOperator() + | ^^^^^^^^^^^^ AIR302 +26 | +27 | TriggerDagRunLink() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.bash.BashOperator` instead. + +AIR302_standard.py:27:1: AIR302 `airflow.operators.dagrun_operator.TriggerDagRunLink` is moved into `standard` provider in Airflow 3.0; + | +25 | BashOperator() +26 | +27 | TriggerDagRunLink() + | ^^^^^^^^^^^^^^^^^ AIR302 +28 | TriggerDagRunOperator() +29 | DummyOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunLink` instead. + +AIR302_standard.py:28:1: AIR302 `airflow.operators.dagrun_operator.TriggerDagRunOperator` is moved into `standard` provider in Airflow 3.0; + | +27 | TriggerDagRunLink() +28 | TriggerDagRunOperator() + | ^^^^^^^^^^^^^^^^^^^^^ AIR302 +29 | DummyOperator() +30 | EmptyOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunOperator` instead. + +AIR302_standard.py:29:1: AIR302 `airflow.operators.dummy.DummyOperator` is moved into `standard` provider in Airflow 3.0; + | +27 | TriggerDagRunLink() +28 | TriggerDagRunOperator() +29 | DummyOperator() + | ^^^^^^^^^^^^^ AIR302 +30 | EmptyOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. + +AIR302_standard.py:30:1: AIR302 `airflow.operators.dummy.EmptyOperator` is moved into `standard` provider in Airflow 3.0; + | +28 | TriggerDagRunOperator() +29 | DummyOperator() +30 | EmptyOperator() + | ^^^^^^^^^^^^^ AIR302 +31 | +32 | LatestOnlyOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. + +AIR302_standard.py:32:1: AIR302 `airflow.operators.latest_only_operator.LatestOnlyOperator` is moved into `standard` provider in Airflow 3.0; + | +30 | EmptyOperator() +31 | +32 | LatestOnlyOperator() + | ^^^^^^^^^^^^^^^^^^ AIR302 +33 | +34 | BranchPythonOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.operators.latest_only.LatestOnlyOperator` instead. + +AIR302_standard.py:34:1: AIR302 `airflow.operators.python_operator.BranchPythonOperator` is moved into `standard` provider in Airflow 3.0; + | +32 | LatestOnlyOperator() +33 | +34 | BranchPythonOperator() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +35 | PythonOperator() +36 | PythonVirtualenvOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.python.BranchPythonOperator` instead. + +AIR302_standard.py:35:1: AIR302 `airflow.operators.python_operator.PythonOperator` is moved into `standard` provider in Airflow 3.0; + | +34 | BranchPythonOperator() +35 | PythonOperator() + | ^^^^^^^^^^^^^^ AIR302 +36 | PythonVirtualenvOperator() +37 | ShortCircuitOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.python.PythonOperator` instead. + +AIR302_standard.py:36:1: AIR302 `airflow.operators.python_operator.PythonVirtualenvOperator` is moved into `standard` provider in Airflow 3.0; + | +34 | BranchPythonOperator() +35 | PythonOperator() +36 | PythonVirtualenvOperator() + | ^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 +37 | ShortCircuitOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.python.PythonVirtualenvOperator` instead. + +AIR302_standard.py:37:1: AIR302 `airflow.operators.python_operator.ShortCircuitOperator` is moved into `standard` provider in Airflow 3.0; + | +35 | PythonOperator() +36 | PythonVirtualenvOperator() +37 | ShortCircuitOperator() + | ^^^^^^^^^^^^^^^^^^^^ AIR302 +38 | +39 | ExternalTaskMarker() + | + = help: Install `apache-airflow-providers-standard>=0.0.1` and use `airflow.providers.standard.operators.python.ShortCircuitOperator` instead. + +AIR302_standard.py:39:1: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskMarker` is moved into `standard` provider in Airflow 3.0; + | +37 | ShortCircuitOperator() +38 | +39 | ExternalTaskMarker() + | ^^^^^^^^^^^^^^^^^^ AIR302 +40 | ExternalTaskSensor() +41 | ExternalTaskSensorLink() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskMarker` instead. + +AIR302_standard.py:40:1: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensor` is moved into `standard` provider in Airflow 3.0; + | +39 | ExternalTaskMarker() +40 | ExternalTaskSensor() + | ^^^^^^^^^^^^^^^^^^ AIR302 +41 | ExternalTaskSensorLink() + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskSensor` instead. + +AIR302_standard.py:41:1: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` is moved into `standard` provider in Airflow 3.0; + | +39 | ExternalTaskMarker() +40 | ExternalTaskSensor() +41 | ExternalTaskSensorLink() + | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 +42 | +43 | from airflow.operators.dummy_operator import ( + | + = help: Install `apache-airflow-providers-standard>=0.0.3` and use `airflow.providers.standard.sensors.external_task.ExternalTaskSensorLink` instead. + +AIR302_standard.py:48:1: AIR302 `airflow.operators.dummy_operator.DummyOperator` is moved into `standard` provider in Airflow 3.0; + | +46 | ) +47 | +48 | DummyOperator() + | ^^^^^^^^^^^^^ AIR302 +49 | EmptyOperator() + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. + +AIR302_standard.py:49:1: AIR302 `airflow.operators.dummy_operator.EmptyOperator` is moved into `standard` provider in Airflow 3.0; + | +48 | DummyOperator() +49 | EmptyOperator() + | ^^^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-standard>=0.0.2` and use `airflow.providers.standard.operators.empty.EmptyOperator` instead. diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_zendesk.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_zendesk.py.snap new file mode 100644 index 00000000000000..731774ac962e46 --- /dev/null +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_zendesk.py.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/rules/airflow/mod.rs +--- +AIR302_zendesk.py:5:1: AIR302 `airflow.hooks.zendesk_hook.ZendeskHook` is moved into `zendesk` provider in Airflow 3.0; + | +3 | from airflow.hooks.zendesk_hook import ZendeskHook +4 | +5 | ZendeskHook() + | ^^^^^^^^^^^ AIR302 + | + = help: Install `apache-airflow-providers-zendesk>=1.0.0` and use `airflow.providers.zendesk.hooks.zendesk.ZendeskHook` instead. From ae7691b026b166b577d6b645fad0a9762c3e94b5 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 28 Apr 2025 16:29:00 -0500 Subject: [PATCH 0165/1161] Add Python 3.14 to configuration options (#17647) A small PR that just updates the various settings/configurations to allow Python 3.14. At the moment selecting that target version will have no impact compared to Python 3.13 - except that a warning is emitted if the user does so with `preview` disabled. --- crates/ruff_linter/src/linter.rs | 13 +++++++++++-- crates/ruff_linter/src/preview.rs | 4 ++++ crates/ruff_linter/src/settings/types.rs | 3 +++ crates/ruff_python_ast/src/python_version.rs | 6 ++++++ docs/configuration.md | 4 ++-- knot.schema.json | 4 ++++ ruff.schema.json | 3 ++- 7 files changed, 32 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 00d07892dc261d..1fd72b16d858e6 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -30,14 +30,14 @@ use crate::fix::{fix_file, FixResult}; use crate::message::Message; use crate::noqa::add_noqa; use crate::package::PackageRoot; -use crate::preview::is_unsupported_syntax_enabled; +use crate::preview::{is_py314_support_enabled, is_unsupported_syntax_enabled}; use crate::registry::{AsRule, Rule, RuleSet}; #[cfg(any(feature = "test-rules", test))] use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES}; use crate::settings::types::UnsafeFixes; use crate::settings::{flags, LinterSettings}; use crate::source_kind::SourceKind; -use crate::{directives, fs, Locator}; +use crate::{directives, fs, warn_user_once, Locator}; pub struct LinterResult { /// A collection of diagnostic messages generated by the linter. @@ -450,6 +450,11 @@ pub fn lint_only( source: ParseSource, ) -> LinterResult { let target_version = settings.resolve_target_version(path); + + if matches!(target_version, PythonVersion::PY314) && !is_py314_support_enabled(settings) { + warn_user_once!("Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning."); + } + let parsed = source.into_parsed(source_kind, source_type, target_version); // Map row and column locations to byte slices (lazily). @@ -559,6 +564,10 @@ pub fn lint_fix<'a>( let target_version = settings.resolve_target_version(path); + if matches!(target_version, PythonVersion::PY314) && !is_py314_support_enabled(settings) { + warn_user_once!("Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning."); + } + // Continuously fix until the source code stabilizes. loop { // Parse once. diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 9973eada40d909..0edf643f123683 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -18,6 +18,10 @@ pub(crate) const fn is_unsupported_syntax_enabled(settings: &LinterSettings) -> settings.preview.is_enabled() } +pub(crate) const fn is_py314_support_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + // Rule-specific behavior // https://github.com/astral-sh/ruff/pull/17136 diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 7087d9856d5126..9a6fd859e09ebc 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -34,6 +34,7 @@ pub enum PythonVersion { Py311, Py312, Py313, + Py314, } impl Default for PythonVersion { @@ -55,6 +56,7 @@ impl TryFrom for PythonVersion { ast::PythonVersion::PY311 => Ok(Self::Py311), ast::PythonVersion::PY312 => Ok(Self::Py312), ast::PythonVersion::PY313 => Ok(Self::Py313), + ast::PythonVersion::PY314 => Ok(Self::Py314), _ => Err(format!("unrecognized python version {value}")), } } @@ -84,6 +86,7 @@ impl PythonVersion { Self::Py311 => (3, 11), Self::Py312 => (3, 12), Self::Py313 => (3, 13), + Self::Py314 => (3, 14), } } } diff --git a/crates/ruff_python_ast/src/python_version.rs b/crates/ruff_python_ast/src/python_version.rs index e5d1406ded2771..20906a25100269 100644 --- a/crates/ruff_python_ast/src/python_version.rs +++ b/crates/ruff_python_ast/src/python_version.rs @@ -30,6 +30,10 @@ impl PythonVersion { major: 3, minor: 13, }; + pub const PY314: PythonVersion = PythonVersion { + major: 3, + minor: 14, + }; pub fn iter() -> impl Iterator { [ @@ -40,6 +44,7 @@ impl PythonVersion { PythonVersion::PY311, PythonVersion::PY312, PythonVersion::PY313, + PythonVersion::PY314, ] .into_iter() } @@ -49,6 +54,7 @@ impl PythonVersion { Self::PY37 } + // TODO: change this to 314 when it is released pub const fn latest() -> Self { Self::PY313 } diff --git a/docs/configuration.md b/docs/configuration.md index 953a4baf59a8c4..ade0b0ced3c5d2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -608,7 +608,7 @@ Options: RUFF_OUTPUT_FILE=] --target-version The minimum Python version that should be supported [possible values: - py37, py38, py39, py310, py311, py312, py313] + py37, py38, py39, py310, py311, py312, py313, py314] --preview Enable preview mode; checks will include unstable rules and fixes. Use `--no-preview` to disable @@ -723,7 +723,7 @@ Options: notebooks, use `--extension ipy:ipynb` --target-version The minimum Python version that should be supported [possible values: - py37, py38, py39, py310, py311, py312, py313] + py37, py38, py39, py310, py311, py312, py313, py314] --preview Enable preview mode; enables unstable formatting. Use `--no-preview` to disable diff --git a/knot.schema.json b/knot.schema.json index 7349373c0e467d..28a09099c689a2 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -210,6 +210,10 @@ { "description": "Python 3.13", "const": "3.13" + }, + { + "description": "Python 3.14", + "const": "3.14" } ] }, diff --git a/ruff.schema.json b/ruff.schema.json index 096df71618c5f1..446ce35e58902a 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2848,7 +2848,8 @@ "py310", "py311", "py312", - "py313" + "py313", + "py314" ] }, "Quote": { From 5096824793865b5c1676f9c9bcdc1213264a31be Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Tue, 29 Apr 2025 00:07:22 +0200 Subject: [PATCH 0166/1161] [`ruff`] add fix safety section (`RUF017`) (#17480) The PR add the `fix safety` section for rule `RUF017` (#15584 ) --- .../src/rules/ruff/rules/quadratic_list_summation.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs b/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs index 53d4bf44cecd59..6e123e1960c6d1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/quadratic_list_summation.rs @@ -46,6 +46,14 @@ use crate::importer::ImportRequest; /// functools.reduce(operator.iadd, lists, []) /// ``` /// +/// ## Fix safety +/// +/// This fix is always marked as unsafe because `sum` uses the `__add__` magic method while +/// `operator.iadd` uses the `__iadd__` magic method, and these behave differently on lists. +/// The former requires the right summand to be a list, whereas the latter allows for any iterable. +/// Therefore, the fix could inadvertently cause code that previously raised an error to silently +/// succeed. Moreover, the fix could remove comments from the original code. +/// /// ## References /// - [_How Not to Flatten a List of Lists in Python_](https://mathieularose.com/how-not-to-flatten-a-list-of-lists-in-python) /// - [_How do I make a flat list out of a list of lists?_](https://stackoverflow.com/questions/952914/how-do-i-make-a-flat-list-out-of-a-list-of-lists/953097#953097) From c953e7d143b5e9a58ed97350c8b3f35d09ee1547 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 29 Apr 2025 10:26:41 +0200 Subject: [PATCH 0167/1161] [red-knot] Improve log message for default python platform (#17700) --- crates/red_knot_project/src/metadata/options.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/red_knot_project/src/metadata/options.rs b/crates/red_knot_project/src/metadata/options.rs index eeff13808289c6..47a24dd81d8ce6 100644 --- a/crates/red_knot_project/src/metadata/options.rs +++ b/crates/red_knot_project/src/metadata/options.rs @@ -68,9 +68,7 @@ impl Options { .and_then(|env| env.python_platform.as_deref().cloned()) .unwrap_or_else(|| { let default = PythonPlatform::default(); - tracing::info!( - "Defaulting to default python version for this platform: '{default}'", - ); + tracing::info!("Defaulting to python-platform `{default}`"); default }); ProgramSettings { From 31e6576971aa95354ec39f58ca886cd0a67bffdd Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 29 Apr 2025 13:35:53 +0100 Subject: [PATCH 0168/1161] [red-knot] micro-optimise `ClassLiteral::is_protocol` (#17703) --- crates/red_knot_python_semantic/src/types/class.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index b3c28473fcde0f..0a4870447550ed 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -534,7 +534,7 @@ impl<'db> ClassLiteral<'db> { /// Determine if this class is a protocol. pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool { - self.explicit_bases(db).iter().any(|base| { + self.explicit_bases(db).iter().rev().take(3).any(|base| { matches!( base, Type::KnownInstance(KnownInstanceType::Protocol) From 3c460a7b9ab0361e3109216de10b4b0293222480 Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 29 Apr 2025 07:55:30 -0500 Subject: [PATCH 0169/1161] Make syntax error for unparenthesized except tuples version specific to before 3.14 (#17660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What it says on the tin 😄 --- ...> except_stmt_unparenthesized_tuple_as.py} | 4 - ..._stmt_unparenthesized_tuple_no_as_py313.py | 9 + ..._stmt_unparenthesized_tuple_no_as_py314.py | 9 + crates/ruff_python_parser/src/error.rs | 41 +++ .../src/parser/statement.rs | 63 +++-- ...@except_stmt_unparenthesized_tuple.py.snap | 251 ------------------ ...cept_stmt_unparenthesized_tuple_as.py.snap | 154 +++++++++++ ..._unparenthesized_tuple_no_as_py313.py.snap | 144 ++++++++++ ..._unparenthesized_tuple_no_as_py314.py.snap | 125 +++++++++ 9 files changed, 526 insertions(+), 274 deletions(-) rename crates/ruff_python_parser/resources/inline/err/{except_stmt_unparenthesized_tuple.py => except_stmt_unparenthesized_tuple_as.py} (65%) create mode 100644 crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_no_as_py313.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/except_stmt_unparenthesized_tuple_no_as_py314.py delete mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_as.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_no_as_py313.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_unparenthesized_tuple_no_as_py314.py.snap diff --git a/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple.py b/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_as.py similarity index 65% rename from crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple.py rename to crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_as.py index e1ca280b7692b1..4e2122b1d54638 100644 --- a/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple.py +++ b/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_as.py @@ -1,12 +1,8 @@ try: pass -except x, y: - pass except x, y as exc: pass try: pass -except* x, y: - pass except* x, y as eg: pass diff --git a/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_no_as_py313.py b/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_no_as_py313.py new file mode 100644 index 00000000000000..11da8cf313f01e --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_no_as_py313.py @@ -0,0 +1,9 @@ +# parse_options: {"target-version": "3.13"} +try: + pass +except x, y: + pass +try: + pass +except* x, y: + pass diff --git a/crates/ruff_python_parser/resources/inline/ok/except_stmt_unparenthesized_tuple_no_as_py314.py b/crates/ruff_python_parser/resources/inline/ok/except_stmt_unparenthesized_tuple_no_as_py314.py new file mode 100644 index 00000000000000..6d646a7de5397c --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/except_stmt_unparenthesized_tuple_no_as_py314.py @@ -0,0 +1,9 @@ +# parse_options: {"target-version": "3.14"} +try: + pass +except x, y: + pass +try: + pass +except* x, y: + pass diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index bc170ba838be5b..671432a8f5801c 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -813,6 +813,41 @@ pub enum UnsupportedSyntaxErrorKind { /// [PEG parser rewrite]: https://peps.python.org/pep-0617/ /// [Python 3.11 release]: https://docs.python.org/3/whatsnew/3.11.html#other-language-changes UnparenthesizedUnpackInFor, + /// Represents the use of multiple exception names in an except clause without an `as` binding, before Python 3.14. + /// + /// ## Examples + /// Before Python 3.14, catching multiple exceptions required + /// parentheses like so: + /// + /// ```python + /// try: + /// ... + /// except (ExceptionA, ExceptionB, ExceptionC): + /// ... + /// ``` + /// + /// Starting with Python 3.14, thanks to [PEP 758], it was permitted + /// to omit the parentheses: + /// + /// ```python + /// try: + /// ... + /// except ExceptionA, ExceptionB, ExceptionC: + /// ... + /// ``` + /// + /// However, parentheses are still required in the presence of an `as`: + /// + /// ```python + /// try: + /// ... + /// except (ExceptionA, ExceptionB, ExceptionC) as e: + /// ... + /// ``` + /// + /// + /// [PEP 758]: https://peps.python.org/pep-0758/ + UnparenthesizedExceptionTypes, } impl Display for UnsupportedSyntaxError { @@ -888,6 +923,9 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => { "Cannot use iterable unpacking in `for` statements" } + UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { + "Multiple exception types must be parenthesized" + } }; write!( @@ -955,6 +993,9 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => { Change::Added(PythonVersion::PY39) } + UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { + Change::Added(PythonVersion::PY314) + } } } diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index cb1d4d5e570ba2..7f3f0c74e29952 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -1581,25 +1581,50 @@ impl<'src> Parser<'src> { .. }) ) { - // test_err except_stmt_unparenthesized_tuple - // try: - // pass - // except x, y: - // pass - // except x, y as exc: - // pass - // try: - // pass - // except* x, y: - // pass - // except* x, y as eg: - // pass - self.add_error( - ParseErrorType::OtherError( - "Multiple exception types must be parenthesized".to_string(), - ), - &parsed_expr, - ); + if self.at(TokenKind::As) { + // test_err except_stmt_unparenthesized_tuple_as + // try: + // pass + // except x, y as exc: + // pass + // try: + // pass + // except* x, y as eg: + // pass + self.add_error( + ParseErrorType::OtherError( + "Multiple exception types must be parenthesized when using `as`" + .to_string(), + ), + &parsed_expr, + ); + } else { + // test_err except_stmt_unparenthesized_tuple_no_as_py313 + // # parse_options: {"target-version": "3.13"} + // try: + // pass + // except x, y: + // pass + // try: + // pass + // except* x, y: + // pass + + // test_ok except_stmt_unparenthesized_tuple_no_as_py314 + // # parse_options: {"target-version": "3.14"} + // try: + // pass + // except x, y: + // pass + // try: + // pass + // except* x, y: + // pass + self.add_unsupported_syntax_error( + UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes, + parsed_expr.range(), + ); + } } Some(Box::new(parsed_expr.expr)) } else { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple.py.snap deleted file mode 100644 index 9a142e87afcb73..00000000000000 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple.py.snap +++ /dev/null @@ -1,251 +0,0 @@ ---- -source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple.py -snapshot_kind: text ---- -## AST - -``` -Module( - ModModule { - range: 0..131, - body: [ - Try( - StmtTry { - range: 0..64, - body: [ - Pass( - StmtPass { - range: 9..13, - }, - ), - ], - handlers: [ - ExceptHandler( - ExceptHandlerExceptHandler { - range: 14..35, - type_: Some( - Tuple( - ExprTuple { - range: 21..25, - elts: [ - Name( - ExprName { - range: 21..22, - id: Name("x"), - ctx: Load, - }, - ), - Name( - ExprName { - range: 24..25, - id: Name("y"), - ctx: Load, - }, - ), - ], - ctx: Load, - parenthesized: false, - }, - ), - ), - name: None, - body: [ - Pass( - StmtPass { - range: 31..35, - }, - ), - ], - }, - ), - ExceptHandler( - ExceptHandlerExceptHandler { - range: 36..64, - type_: Some( - Tuple( - ExprTuple { - range: 43..47, - elts: [ - Name( - ExprName { - range: 43..44, - id: Name("x"), - ctx: Load, - }, - ), - Name( - ExprName { - range: 46..47, - id: Name("y"), - ctx: Load, - }, - ), - ], - ctx: Load, - parenthesized: false, - }, - ), - ), - name: Some( - Identifier { - id: Name("exc"), - range: 51..54, - }, - ), - body: [ - Pass( - StmtPass { - range: 60..64, - }, - ), - ], - }, - ), - ], - orelse: [], - finalbody: [], - is_star: false, - }, - ), - Try( - StmtTry { - range: 65..130, - body: [ - Pass( - StmtPass { - range: 74..78, - }, - ), - ], - handlers: [ - ExceptHandler( - ExceptHandlerExceptHandler { - range: 79..101, - type_: Some( - Tuple( - ExprTuple { - range: 87..91, - elts: [ - Name( - ExprName { - range: 87..88, - id: Name("x"), - ctx: Load, - }, - ), - Name( - ExprName { - range: 90..91, - id: Name("y"), - ctx: Load, - }, - ), - ], - ctx: Load, - parenthesized: false, - }, - ), - ), - name: None, - body: [ - Pass( - StmtPass { - range: 97..101, - }, - ), - ], - }, - ), - ExceptHandler( - ExceptHandlerExceptHandler { - range: 102..130, - type_: Some( - Tuple( - ExprTuple { - range: 110..114, - elts: [ - Name( - ExprName { - range: 110..111, - id: Name("x"), - ctx: Load, - }, - ), - Name( - ExprName { - range: 113..114, - id: Name("y"), - ctx: Load, - }, - ), - ], - ctx: Load, - parenthesized: false, - }, - ), - ), - name: Some( - Identifier { - id: Name("eg"), - range: 118..120, - }, - ), - body: [ - Pass( - StmtPass { - range: 126..130, - }, - ), - ], - }, - ), - ], - orelse: [], - finalbody: [], - is_star: true, - }, - ), - ], - }, -) -``` -## Errors - - | -1 | try: -2 | pass -3 | except x, y: - | ^^^^ Syntax Error: Multiple exception types must be parenthesized -4 | pass -5 | except x, y as exc: - | - - - | -3 | except x, y: -4 | pass -5 | except x, y as exc: - | ^^^^ Syntax Error: Multiple exception types must be parenthesized -6 | pass -7 | try: - | - - - | - 7 | try: - 8 | pass - 9 | except* x, y: - | ^^^^ Syntax Error: Multiple exception types must be parenthesized -10 | pass -11 | except* x, y as eg: - | - - - | - 9 | except* x, y: -10 | pass -11 | except* x, y as eg: - | ^^^^ Syntax Error: Multiple exception types must be parenthesized -12 | pass - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_as.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_as.py.snap new file mode 100644 index 00000000000000..928495cb4dde98 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_as.py.snap @@ -0,0 +1,154 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_as.py +--- +## AST + +``` +Module( + ModModule { + range: 0..86, + body: [ + Try( + StmtTry { + range: 0..42, + body: [ + Pass( + StmtPass { + range: 9..13, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 14..42, + type_: Some( + Tuple( + ExprTuple { + range: 21..25, + elts: [ + Name( + ExprName { + range: 21..22, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 24..25, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: Some( + Identifier { + id: Name("exc"), + range: 29..32, + }, + ), + body: [ + Pass( + StmtPass { + range: 38..42, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: false, + }, + ), + Try( + StmtTry { + range: 43..85, + body: [ + Pass( + StmtPass { + range: 52..56, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 57..85, + type_: Some( + Tuple( + ExprTuple { + range: 65..69, + elts: [ + Name( + ExprName { + range: 65..66, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 68..69, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: Some( + Identifier { + id: Name("eg"), + range: 73..75, + }, + ), + body: [ + Pass( + StmtPass { + range: 81..85, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: true, + }, + ), + ], + }, +) +``` +## Errors + + | +1 | try: +2 | pass +3 | except x, y as exc: + | ^^^^ Syntax Error: Multiple exception types must be parenthesized when using `as` +4 | pass +5 | try: + | + + + | +5 | try: +6 | pass +7 | except* x, y as eg: + | ^^^^ Syntax Error: Multiple exception types must be parenthesized when using `as` +8 | pass + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_no_as_py313.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_no_as_py313.py.snap new file mode 100644 index 00000000000000..1a74da4a6767cb --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_stmt_unparenthesized_tuple_no_as_py313.py.snap @@ -0,0 +1,144 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/except_stmt_unparenthesized_tuple_no_as_py313.py +--- +## AST + +``` +Module( + ModModule { + range: 0..117, + body: [ + Try( + StmtTry { + range: 44..79, + body: [ + Pass( + StmtPass { + range: 53..57, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 58..79, + type_: Some( + Tuple( + ExprTuple { + range: 65..69, + elts: [ + Name( + ExprName { + range: 65..66, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 68..69, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: None, + body: [ + Pass( + StmtPass { + range: 75..79, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: false, + }, + ), + Try( + StmtTry { + range: 80..116, + body: [ + Pass( + StmtPass { + range: 89..93, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 94..116, + type_: Some( + Tuple( + ExprTuple { + range: 102..106, + elts: [ + Name( + ExprName { + range: 102..103, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 105..106, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: None, + body: [ + Pass( + StmtPass { + range: 112..116, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: true, + }, + ), + ], + }, +) +``` +## Unsupported Syntax Errors + + | +2 | try: +3 | pass +4 | except x, y: + | ^^^^ Syntax Error: Multiple exception types must be parenthesized on Python 3.13 (syntax was added in Python 3.14) +5 | pass +6 | try: + | + + + | +6 | try: +7 | pass +8 | except* x, y: + | ^^^^ Syntax Error: Multiple exception types must be parenthesized on Python 3.13 (syntax was added in Python 3.14) +9 | pass + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_unparenthesized_tuple_no_as_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_unparenthesized_tuple_no_as_py314.py.snap new file mode 100644 index 00000000000000..044e166fa61e43 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@except_stmt_unparenthesized_tuple_no_as_py314.py.snap @@ -0,0 +1,125 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/except_stmt_unparenthesized_tuple_no_as_py314.py +--- +## AST + +``` +Module( + ModModule { + range: 0..117, + body: [ + Try( + StmtTry { + range: 44..79, + body: [ + Pass( + StmtPass { + range: 53..57, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 58..79, + type_: Some( + Tuple( + ExprTuple { + range: 65..69, + elts: [ + Name( + ExprName { + range: 65..66, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 68..69, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: None, + body: [ + Pass( + StmtPass { + range: 75..79, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: false, + }, + ), + Try( + StmtTry { + range: 80..116, + body: [ + Pass( + StmtPass { + range: 89..93, + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 94..116, + type_: Some( + Tuple( + ExprTuple { + range: 102..106, + elts: [ + Name( + ExprName { + range: 102..103, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 105..106, + id: Name("y"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + ), + name: None, + body: [ + Pass( + StmtPass { + range: 112..116, + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: true, + }, + ), + ], + }, +) +``` From ca4fdf452d61387a6b39ec44ea6bc3607ad3fda5 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 29 Apr 2025 09:03:06 -0400 Subject: [PATCH 0170/1161] Create `TypeVarInstance` type for legacy typevars (#16538) We are currently representing type variables using a `KnownInstance` variant, which wraps a `TypeVarInstance` that contains the information about the typevar (name, bounds, constraints, default type). We were previously only constructing that type for PEP 695 typevars. This PR constructs that type for legacy typevars as well. It also detects functions that are generic because they use legacy typevars in their parameter list. With the existing logic for inferring specializations of function calls (#17301), that means that we are correctly detecting that the definition of `reveal_type` in the typeshed is generic, and inferring the correct specialization of `_T` for each call site. This does not yet handle legacy generic classes; that will come in a follow-on PR. --- .../resources/mdtest/function/return_type.md | 2 +- .../resources/mdtest/generics/functions.md | 33 +++ .../resources/mdtest/generics/legacy.md | 58 +++- .../resources/mdtest/generics/pep695.md | 43 ++- .../resources/mdtest/generics/scoping.md | 3 +- ...functions_-_Inferring_a_bound_typevar.snap | 86 ++++++ ...ons_-_Inferring_a_constrained_typevar.snap | 101 +++++++ ...ion_return_type_-_Invalid_return_type.snap | 13 +- .../resources/mdtest/subscript/lists.md | 2 +- .../type_properties/is_assignable_to.md | 2 + .../src/semantic_index/builder.rs | 25 +- .../src/semantic_index/expression.rs | 11 + crates/red_knot_python_semantic/src/types.rs | 198 ++++++++++++- .../src/types/call/bind.rs | 190 +++++++++---- .../src/types/diagnostic.rs | 33 ++- .../src/types/display.rs | 2 +- .../src/types/generics.rs | 126 ++++++-- .../src/types/infer.rs | 269 +++++++++++++----- .../src/types/known_instance.rs | 11 +- .../src/types/signatures.rs | 38 ++- crates/ruff_benchmark/benches/red_knot.rs | 23 +- knot.schema.json | 10 + 22 files changed, 1094 insertions(+), 185 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap 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 1365807e95b08e..8e83d036543e5e 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 @@ -203,7 +203,7 @@ from typing import TypeVar T = TypeVar("T") -# TODO: `invalid-return-type` error should be emitted +# error: [invalid-return-type] def m(x: T) -> T: ... ``` 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 10732bf3a243ea..22c62b1d35f398 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md @@ -71,6 +71,39 @@ def f[T](x: list[T]) -> T: reveal_type(f([1.0, 2.0])) # revealed: Unknown ``` +## Inferring a bound typevar + + + +```py +from typing_extensions import reveal_type + +def f[T: int](x: T) -> T: + return x + +reveal_type(f(1)) # revealed: Literal[1] +reveal_type(f(True)) # revealed: Literal[True] +# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`" +reveal_type(f("string")) # revealed: Unknown +``` + +## Inferring a constrained typevar + + + +```py +from typing_extensions import reveal_type + +def f[T: (int, None)](x: T) -> T: + return x + +reveal_type(f(1)) # revealed: int +reveal_type(f(True)) # revealed: int +reveal_type(f(None)) # revealed: None +# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`" +reveal_type(f("string")) # revealed: Unknown +``` + ## Typevar constraints If a type parameter has an upper bound, that upper bound constrains which types can be used for that diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md index 6aea25ed820c2f..17021dd3985baa 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md @@ -19,6 +19,9 @@ in newer Python releases. from typing import TypeVar T = TypeVar("T") +reveal_type(type(T)) # revealed: Literal[TypeVar] +reveal_type(T) # revealed: typing.TypeVar +reveal_type(T.__name__) # revealed: Literal["T"] ``` ### Directly assigned to a variable @@ -29,7 +32,12 @@ T = TypeVar("T") ```py from typing import TypeVar -# TODO: error +T = TypeVar("T") +# TODO: no error +# error: [invalid-legacy-type-variable] +U: TypeVar = TypeVar("U") + +# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable" TestList = list[TypeVar("W")] ``` @@ -40,7 +48,7 @@ TestList = list[TypeVar("W")] ```py from typing import TypeVar -# TODO: error +# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)" T = TypeVar("Q") ``` @@ -57,6 +65,52 @@ T = TypeVar("T") T = TypeVar("T") ``` +### Type variables with a default + +Note that the `__default__` property is only available in Python ≥3.13. + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import TypeVar + +T = TypeVar("T", default=int) +reveal_type(T.__default__) # revealed: int +reveal_type(T.__bound__) # revealed: None +reveal_type(T.__constraints__) # revealed: tuple[()] + +S = TypeVar("S") +reveal_type(S.__default__) # revealed: NoDefault +``` + +### Type variables with an upper bound + +```py +from typing import TypeVar + +T = TypeVar("T", bound=int) +reveal_type(T.__bound__) # revealed: int +reveal_type(T.__constraints__) # revealed: tuple[()] + +S = TypeVar("S") +reveal_type(S.__bound__) # revealed: None +``` + +### Type variables with constraints + +```py +from typing import TypeVar + +T = TypeVar("T", int, str) +reveal_type(T.__constraints__) # revealed: tuple[int, str] + +S = TypeVar("S") +reveal_type(S.__constraints__) # revealed: tuple[()] +``` + ### Cannot have only one constraint > `TypeVar` supports constraining parametric types to a fixed set of possible types...There should 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 67e69940f3129a..67a4fdc4f08844 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -17,10 +17,51 @@ instances of `typing.TypeVar`, just like legacy type variables. ```py def f[T](): reveal_type(type(T)) # revealed: Literal[TypeVar] - reveal_type(T) # revealed: T + reveal_type(T) # revealed: typing.TypeVar reveal_type(T.__name__) # revealed: Literal["T"] ``` +### Type variables with a default + +Note that the `__default__` property is only available in Python ≥3.13. + +```toml +[environment] +python-version = "3.13" +``` + +```py +def f[T = int](): + reveal_type(T.__default__) # revealed: int + reveal_type(T.__bound__) # revealed: None + reveal_type(T.__constraints__) # revealed: tuple[()] + +def g[S](): + reveal_type(S.__default__) # revealed: NoDefault +``` + +### Type variables with an upper bound + +```py +def f[T: int](): + reveal_type(T.__bound__) # revealed: int + reveal_type(T.__constraints__) # revealed: tuple[()] + +def g[S](): + reveal_type(S.__bound__) # revealed: None +``` + +### Type variables with constraints + +```py +def f[T: (int, str)](): + reveal_type(T.__constraints__) # revealed: tuple[int, str] + reveal_type(T.__bound__) # revealed: None + +def g[S](): + reveal_type(S.__constraints__) # revealed: tuple[()] +``` + ### Cannot have only one constraint > `TypeVar` supports constraining parametric types to a fixed set of possible types...There should 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 9712be7213f41c..0300cadc87d9af 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -142,8 +142,7 @@ class Legacy(Generic[T]): return y legacy: Legacy[int] = Legacy() -# TODO: revealed: str -reveal_type(legacy.m(1, "string")) # revealed: @Todo(Support for `typing.TypeVar` instances in type expressions) +reveal_type(legacy.m(1, "string")) # revealed: Literal["string"] ``` With PEP 695 syntax, it is clearer that the method uses a separate typevar: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap new file mode 100644 index 00000000000000..1fcfed30588f79 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap @@ -0,0 +1,86 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: functions.md - Generic functions - Inferring a bound typevar +mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing_extensions import reveal_type +2 | +3 | def f[T: int](x: T) -> T: +4 | return x +5 | +6 | reveal_type(f(1)) # revealed: Literal[1] +7 | reveal_type(f(True)) # revealed: Literal[True] +8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`" +9 | reveal_type(f("string")) # revealed: Unknown +``` + +# Diagnostics + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:6:1 + | +4 | return x +5 | +6 | reveal_type(f(1)) # revealed: Literal[1] + | ^^^^^^^^^^^^^^^^^ `Literal[1]` +7 | reveal_type(f(True)) # revealed: Literal[True] +8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bo... + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:7:1 + | +6 | reveal_type(f(1)) # revealed: Literal[1] +7 | reveal_type(f(True)) # revealed: Literal[True] + | ^^^^^^^^^^^^^^^^^^^^ `Literal[True]` +8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b... +9 | reveal_type(f("string")) # revealed: Unknown + | + +``` + +``` +error: lint:invalid-argument-type: Argument to this function is incorrect + --> src/mdtest_snippet.py:9:15 + | +7 | reveal_type(f(True)) # revealed: Literal[True] +8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b... +9 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound of type variable `T` + | +info: Type variable defined here + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import reveal_type +2 | +3 | def f[T: int](x: T) -> T: + | ^^^^^^ +4 | return x + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:9:1 + | +7 | reveal_type(f(True)) # revealed: Literal[True] +8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b... +9 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap new file mode 100644 index 00000000000000..2e2802007b5b10 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap @@ -0,0 +1,101 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: functions.md - Generic functions - Inferring a constrained typevar +mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import reveal_type + 2 | + 3 | def f[T: (int, None)](x: T) -> T: + 4 | return x + 5 | + 6 | reveal_type(f(1)) # revealed: int + 7 | reveal_type(f(True)) # revealed: int + 8 | reveal_type(f(None)) # revealed: None + 9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`" +10 | reveal_type(f("string")) # revealed: Unknown +``` + +# Diagnostics + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:6:1 + | +4 | return x +5 | +6 | reveal_type(f(1)) # revealed: int + | ^^^^^^^^^^^^^^^^^ `int` +7 | reveal_type(f(True)) # revealed: int +8 | reveal_type(f(None)) # revealed: None + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:7:1 + | +6 | reveal_type(f(1)) # revealed: int +7 | reveal_type(f(True)) # revealed: int + | ^^^^^^^^^^^^^^^^^^^^ `int` +8 | reveal_type(f(None)) # revealed: None +9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra... + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:8:1 + | + 6 | reveal_type(f(1)) # revealed: int + 7 | reveal_type(f(True)) # revealed: int + 8 | reveal_type(f(None)) # revealed: None + | ^^^^^^^^^^^^^^^^^^^^ `None` + 9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra... +10 | reveal_type(f("string")) # revealed: Unknown + | + +``` + +``` +error: lint:invalid-argument-type: Argument to this function is incorrect + --> src/mdtest_snippet.py:10:15 + | + 8 | reveal_type(f(None)) # revealed: None + 9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra... +10 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints of type variable `T` + | +info: Type variable defined here + --> src/mdtest_snippet.py:3:7 + | +1 | from typing_extensions import reveal_type +2 | +3 | def f[T: (int, None)](x: T) -> T: + | ^^^^^^^^^^^^^^ +4 | return x + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:10:1 + | + 8 | reveal_type(f(None)) # revealed: None + 9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra... +10 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap index 67a88e4dc8d335..7e439527f1e84d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap @@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty 14 | 15 | T = TypeVar("T") 16 | -17 | # TODO: `invalid-return-type` error should be emitted +17 | # error: [invalid-return-type] 18 | def m(x: T) -> T: ... ``` @@ -79,3 +79,14 @@ error: lint:invalid-return-type: Return type does not match returned value | ``` + +``` +error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `T` + --> src/mdtest_snippet.py:18:16 + | +17 | # error: [invalid-return-type] +18 | def m(x: T) -> T: ... + | ^ + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md index 192dc4a88e10e0..d074d1b82669a8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md @@ -12,7 +12,7 @@ x = [1, 2, 3] reveal_type(x) # revealed: list # TODO reveal int -reveal_type(x[0]) # revealed: @Todo(Support for `typing.TypeVar` instances in type expressions) +reveal_type(x[0]) # revealed: Unknown # TODO reveal list reveal_type(x[0:1]) # revealed: @Todo(specialized non-generic class) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index ce7198ba697e12..f8b8adf5f8b832 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -583,6 +583,8 @@ from functools import partial def f(x: int, y: str) -> None: ... +# TODO: no error +# error: [invalid-assignment] "Object of type `partial` is not assignable to `(int, /) -> None`" c1: Callable[[int], None] = partial(f, y="a") ``` diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index a17e4523f360a4..22af0fda06d8c2 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -754,19 +754,35 @@ impl<'db> SemanticIndexBuilder<'db> { /// Record an expression that needs to be a Salsa ingredient, because we need to infer its type /// standalone (type narrowing tests, RHS of an assignment.) fn add_standalone_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> { - self.add_standalone_expression_impl(expression_node, ExpressionKind::Normal) + self.add_standalone_expression_impl(expression_node, ExpressionKind::Normal, None) + } + + /// Record an expression that is immediately assigned to a target, and that needs to be a Salsa + /// ingredient, because we need to infer its type standalone (type narrowing tests, RHS of an + /// assignment.) + fn add_standalone_assigned_expression( + &mut self, + expression_node: &ast::Expr, + assigned_to: &ast::StmtAssign, + ) -> Expression<'db> { + self.add_standalone_expression_impl( + expression_node, + ExpressionKind::Normal, + Some(assigned_to), + ) } /// Same as [`SemanticIndexBuilder::add_standalone_expression`], but marks the expression as a /// *type* expression, which makes sure that it will later be inferred as such. fn add_standalone_type_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> { - self.add_standalone_expression_impl(expression_node, ExpressionKind::TypeExpression) + self.add_standalone_expression_impl(expression_node, ExpressionKind::TypeExpression, None) } fn add_standalone_expression_impl( &mut self, expression_node: &ast::Expr, expression_kind: ExpressionKind, + assigned_to: Option<&ast::StmtAssign>, ) -> Expression<'db> { let expression = Expression::new( self.db, @@ -776,6 +792,9 @@ impl<'db> SemanticIndexBuilder<'db> { unsafe { AstNodeRef::new(self.module.clone(), expression_node) }, + #[allow(unsafe_code)] + assigned_to + .map(|assigned_to| unsafe { AstNodeRef::new(self.module.clone(), assigned_to) }), expression_kind, countme::Count::default(), ); @@ -1377,7 +1396,7 @@ where debug_assert_eq!(&self.current_assignments, &[]); self.visit_expr(&node.value); - let value = self.add_standalone_expression(&node.value); + let value = self.add_standalone_assigned_expression(&node.value, node); for target in &node.targets { self.add_unpackable_assignment(&Unpackable::Assign(node), target, value); diff --git a/crates/red_knot_python_semantic/src/semantic_index/expression.rs b/crates/red_knot_python_semantic/src/semantic_index/expression.rs index 9ac1fd30b81eb7..8c50ea5bb9c917 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/expression.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/expression.rs @@ -44,6 +44,17 @@ pub(crate) struct Expression<'db> { #[return_ref] pub(crate) node_ref: AstNodeRef, + /// An assignment statement, if this expression is immediately used as the rhs of that + /// assignment. + /// + /// (Note that this is the _immediately_ containing assignment — if a complex expression is + /// assigned to some target, only the outermost expression node has this set. The inner + /// expressions are used to build up the assignment result, and are not "immediately assigned" + /// to the target, and so have `None` for this field.) + #[no_eq] + #[tracked] + pub(crate) assigned_to: Option>, + /// Should this expression be inferred as a normal expression or a type expression? pub(crate) kind: ExpressionKind, diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1c2e15a3cd9aef..adc6ac478efc2a 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -348,6 +348,19 @@ impl<'db> PropertyInstanceType<'db> { .map(|ty| ty.apply_specialization(db, specialization)); Self::new(db, getter, setter) } + + fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + if let Some(ty) = self.getter(db) { + ty.find_legacy_typevars(db, typevars); + } + if let Some(ty) = self.setter(db) { + ty.find_legacy_typevars(db, typevars); + } + } } bitflags! { @@ -923,6 +936,7 @@ impl<'db> Type<'db> { typevar.definition(db), Some(TypeVarBoundOrConstraints::UpperBound(bound.normalized(db))), typevar.default_ty(db), + typevar.kind(db), )) } Some(TypeVarBoundOrConstraints::Constraints(union)) => { @@ -932,6 +946,7 @@ impl<'db> Type<'db> { typevar.definition(db), Some(TypeVarBoundOrConstraints::Constraints(union.normalized(db))), typevar.default_ty(db), + typevar.kind(db), )) } None => self, @@ -3799,6 +3814,56 @@ impl<'db> Type<'db> { Signatures::single(signature) } + Some(KnownClass::TypeVar) => { + // ```py + // class TypeVar: + // def __new__( + // cls, + // name: str, + // *constraints: Any, + // bound: Any | None = None, + // contravariant: bool = False, + // covariant: bool = False, + // infer_variance: bool = False, + // default: Any = ..., + // ) -> Self: ... + // ``` + let signature = CallableSignature::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_or_keyword(Name::new_static("name")) + .with_annotated_type(Type::LiteralString), + Parameter::variadic(Name::new_static("constraints")) + .type_form() + .with_annotated_type(Type::any()), + Parameter::keyword_only(Name::new_static("bound")) + .type_form() + .with_annotated_type(UnionType::from_elements( + db, + [Type::any(), Type::none(db)], + )) + .with_default_type(Type::none(db)), + Parameter::keyword_only(Name::new_static("default")) + .type_form() + .with_annotated_type(Type::any()) + .with_default_type(KnownClass::NoneType.to_instance(db)), + Parameter::keyword_only(Name::new_static("contravariant")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("covariant")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("infer_variance")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + ]), + Some(KnownClass::TypeVar.to_instance(db)), + ), + ); + Signatures::single(signature) + } + Some(KnownClass::Property) => { let getter_signature = Signature::new( Parameters::new([ @@ -4834,6 +4899,93 @@ impl<'db> Type<'db> { } } + /// Locates any legacy `TypeVar`s in this type, and adds them to a set. This is used to build + /// up a generic context from any legacy `TypeVar`s that appear in a function parameter list or + /// `Generic` specialization. + pub(crate) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + match self { + Type::TypeVar(typevar) => { + if typevar.is_legacy(db) { + typevars.insert(typevar); + } + } + + Type::FunctionLiteral(function) => function.find_legacy_typevars(db, typevars), + + Type::BoundMethod(method) => { + method.self_instance(db).find_legacy_typevars(db, typevars); + method.function(db).find_legacy_typevars(db, typevars); + } + + Type::MethodWrapper( + MethodWrapperKind::FunctionTypeDunderGet(function) + | MethodWrapperKind::FunctionTypeDunderCall(function), + ) => { + function.find_legacy_typevars(db, typevars); + } + + Type::MethodWrapper( + MethodWrapperKind::PropertyDunderGet(property) + | MethodWrapperKind::PropertyDunderSet(property), + ) => { + property.find_legacy_typevars(db, typevars); + } + + Type::Callable(callable) => { + callable.find_legacy_typevars(db, typevars); + } + + Type::PropertyInstance(property) => { + property.find_legacy_typevars(db, typevars); + } + + Type::Union(union) => { + for element in union.iter(db) { + element.find_legacy_typevars(db, typevars); + } + } + Type::Intersection(intersection) => { + for positive in intersection.positive(db) { + positive.find_legacy_typevars(db, typevars); + } + for negative in intersection.negative(db) { + negative.find_legacy_typevars(db, typevars); + } + } + Type::Tuple(tuple) => { + for element in tuple.iter(db) { + element.find_legacy_typevars(db, typevars); + } + } + + Type::Dynamic(_) + | Type::Never + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::GenericAlias(_) + | Type::SubclassOf(_) + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::LiteralString + | Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::SliceLiteral(_) + | Type::BoundSuper(_) + | Type::Instance(_) + | Type::KnownInstance(_) => {} + } + } + /// Return the string representation of this type when converted to string as it would be /// provided by the `__str__` method. /// @@ -4844,9 +4996,7 @@ impl<'db> Type<'db> { match self { Type::IntLiteral(_) | Type::BooleanLiteral(_) => self.repr(db), Type::StringLiteral(_) | Type::LiteralString => *self, - Type::KnownInstance(known_instance) => { - Type::string_literal(db, known_instance.repr(db)) - } + Type::KnownInstance(known_instance) => Type::string_literal(db, known_instance.repr()), // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), } @@ -4864,9 +5014,7 @@ impl<'db> Type<'db> { Type::string_literal(db, &format!("'{}'", literal.value(db).escape_default())) } Type::LiteralString => Type::LiteralString, - Type::KnownInstance(known_instance) => { - Type::string_literal(db, known_instance.repr(db)) - } + Type::KnownInstance(known_instance) => Type::string_literal(db, known_instance.repr()), // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), } @@ -5235,12 +5383,12 @@ impl<'db> InvalidTypeExpression<'db> { InvalidTypeExpression::TypeQualifier(qualifier) => write!( f, "Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions)", - q = qualifier.repr(self.db) + q = qualifier.repr() ), InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) => write!( f, "Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)", - q = qualifier.repr(self.db) + q = qualifier.repr() ), InvalidTypeExpression::InvalidType(ty) => write!( f, @@ -5255,6 +5403,13 @@ impl<'db> InvalidTypeExpression<'db> { } } +/// Whether this typecar was created via the legacy `TypeVar` constructor, or using PEP 695 syntax. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum TypeVarKind { + Legacy, + Pep695, +} + /// Data regarding a single type variable. /// /// This is referenced by `KnownInstanceType::TypeVar` (to represent the singleton type of the @@ -5276,9 +5431,15 @@ pub struct TypeVarInstance<'db> { /// The default type for this TypeVar default_ty: Option>, + + pub kind: TypeVarKind, } impl<'db> TypeVarInstance<'db> { + pub(crate) fn is_legacy(self, db: &'db dyn Db) -> bool { + matches!(self.kind(db), TypeVarKind::Legacy) + } + #[allow(unused)] pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { @@ -6368,6 +6529,17 @@ impl<'db> FunctionType<'db> { ) } + fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + let signatures = self.signature(db); + for signature in signatures { + signature.find_legacy_typevars(db, typevars); + } + } + /// Returns `self` as [`OverloadedFunction`] if it is overloaded, [`None`] otherwise. /// /// ## Note @@ -6698,6 +6870,16 @@ impl<'db> CallableType<'db> { ) } + fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for signature in self.signatures(db) { + signature.find_legacy_typevars(db, typevars); + } + } + /// Check whether this callable type is fully static. /// /// See [`Type::is_fully_static`] for more details. diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 8f5fb666589fd0..14e6039c3165cd 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -16,7 +16,7 @@ use crate::types::diagnostic::{ NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, }; -use crate::types::generics::{Specialization, SpecializationBuilder}; +use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError}; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ todo_type, BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, @@ -295,54 +295,76 @@ impl<'db> Bindings<'db> { } } - Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderGet) => { - match overload.parameter_types() { - [Some(property @ Type::PropertyInstance(_)), Some(instance), ..] - if instance.is_none(db) => - { - overload.set_return_type(*property); - } - [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias))), ..] - if property.getter(db).is_some_and(|getter| { - getter - .into_function_literal() - .is_some_and(|f| f.name(db) == "__name__") - }) => - { - overload.set_return_type(Type::string_literal(db, type_alias.name(db))); - } - [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeVar(type_var))), ..] - if property.getter(db).is_some_and(|getter| { - getter - .into_function_literal() - .is_some_and(|f| f.name(db) == "__name__") - }) => + Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderGet) => match overload + .parameter_types() + { + [Some(property @ Type::PropertyInstance(_)), Some(instance), ..] + if instance.is_none(db) => + { + overload.set_return_type(*property); + } + [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias))), ..] + if property.getter(db).is_some_and(|getter| { + getter + .into_function_literal() + .is_some_and(|f| f.name(db) == "__name__") + }) => + { + overload.set_return_type(Type::string_literal(db, type_alias.name(db))); + } + [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), ..] => { + match property + .getter(db) + .and_then(Type::into_function_literal) + .map(|f| f.name(db).as_str()) { - overload.set_return_type(Type::string_literal(db, type_var.name(db))); + Some("__name__") => { + overload + .set_return_type(Type::string_literal(db, typevar.name(db))); + } + Some("__bound__") => { + overload.set_return_type( + typevar.upper_bound(db).unwrap_or_else(|| Type::none(db)), + ); + } + Some("__constraints__") => { + overload.set_return_type(TupleType::from_elements( + db, + typevar.constraints(db).into_iter().flatten(), + )); + } + Some("__default__") => { + overload.set_return_type( + typevar.default_ty(db).unwrap_or_else(|| { + KnownClass::NoDefaultType.to_instance(db) + }), + ); + } + _ => {} } - [Some(Type::PropertyInstance(property)), Some(instance), ..] => { - if let Some(getter) = property.getter(db) { - if let Ok(return_ty) = getter - .try_call(db, CallArgumentTypes::positional([*instance])) - .map(|binding| binding.return_type(db)) - { - overload.set_return_type(return_ty); - } else { - overload.errors.push(BindingError::InternalCallError( - "calling the getter failed", - )); - overload.set_return_type(Type::unknown()); - } + } + [Some(Type::PropertyInstance(property)), Some(instance), ..] => { + if let Some(getter) = property.getter(db) { + if let Ok(return_ty) = getter + .try_call(db, CallArgumentTypes::positional([*instance])) + .map(|binding| binding.return_type(db)) + { + overload.set_return_type(return_ty); } else { overload.errors.push(BindingError::InternalCallError( - "property has no getter", + "calling the getter failed", )); - overload.set_return_type(Type::Never); + overload.set_return_type(Type::unknown()); } + } else { + overload + .errors + .push(BindingError::InternalCallError("property has no getter")); + overload.set_return_type(Type::Never); } - _ => {} } - } + _ => {} + }, Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => { match overload.parameter_types() { @@ -1150,12 +1172,28 @@ impl<'db> Binding<'db> { signature: &Signature<'db>, argument_types: &CallArgumentTypes<'_, 'db>, ) { + let mut num_synthetic_args = 0; + let get_argument_index = |argument_index: usize, num_synthetic_args: usize| { + if argument_index >= num_synthetic_args { + // Adjust the argument index to skip synthetic args, which don't appear at the call + // site and thus won't be in the Call node arguments list. + Some(argument_index - num_synthetic_args) + } else { + // we are erroring on a synthetic argument, we'll just emit the diagnostic on the + // entire Call node, since there's no argument node for this argument at the call site + None + } + }; + // If this overload is generic, first see if we can infer a specialization of the function // from the arguments that were passed in. let parameters = signature.parameters(); if signature.generic_context.is_some() || signature.inherited_generic_context.is_some() { let mut builder = SpecializationBuilder::new(db); - for (argument_index, (_, argument_type)) in argument_types.iter().enumerate() { + for (argument_index, (argument, argument_type)) in argument_types.iter().enumerate() { + if matches!(argument, Argument::Synthetic) { + num_synthetic_args += 1; + } let Some(parameter_index) = self.argument_parameters[argument_index] else { // There was an error with argument when matching parameters, so don't bother // type-checking it. @@ -1165,7 +1203,12 @@ impl<'db> Binding<'db> { let Some(expected_type) = parameter.annotated_type() else { continue; }; - builder.infer(expected_type, argument_type); + if let Err(error) = builder.infer(expected_type, argument_type) { + self.errors.push(BindingError::SpecializationError { + error, + argument_index: get_argument_index(argument_index, num_synthetic_args), + }); + } } self.specialization = signature.generic_context.map(|gc| builder.build(gc)); self.inherited_specialization = signature @@ -1173,18 +1216,7 @@ impl<'db> Binding<'db> { .map(|gc| builder.build(gc)); } - let mut num_synthetic_args = 0; - let get_argument_index = |argument_index: usize, num_synthetic_args: usize| { - if argument_index >= num_synthetic_args { - // Adjust the argument index to skip synthetic args, which don't appear at the call - // site and thus won't be in the Call node arguments list. - Some(argument_index - num_synthetic_args) - } else { - // we are erroring on a synthetic argument, we'll just emit the diagnostic on the - // entire Call node, since there's no argument node for this argument at the call site - None - } - }; + num_synthetic_args = 0; for (argument_index, (argument, argument_type)) in argument_types.iter().enumerate() { if matches!(argument, Argument::Synthetic) { num_synthetic_args += 1; @@ -1250,6 +1282,20 @@ impl<'db> Binding<'db> { &self.parameter_tys } + pub(crate) fn arguments_for_parameter<'a>( + &'a self, + argument_types: &'a CallArgumentTypes<'a, 'db>, + parameter_index: usize, + ) -> impl Iterator, Type<'db>)> + 'a { + argument_types + .iter() + .zip(&self.argument_parameters) + .filter(move |(_, argument_parameter)| { + argument_parameter.is_some_and(|ap| ap == parameter_index) + }) + .map(|(arg_and_type, _)| arg_and_type) + } + fn report_diagnostics( &self, context: &InferContext<'db>, @@ -1398,6 +1444,11 @@ pub(crate) enum BindingError<'db> { argument_index: Option, parameter: ParameterContext, }, + /// An inferred specialization was invalid. + SpecializationError { + error: SpecializationError<'db>, + argument_index: Option, + }, /// The call itself might be well constructed, but an error occurred while evaluating the call. /// We use this variant to report errors in `property.__get__` and `property.__set__`, which /// can occur when the call to the underlying getter/setter fails. @@ -1510,6 +1561,35 @@ impl<'db> BindingError<'db> { } } + Self::SpecializationError { + error, + argument_index, + } => { + let range = Self::get_node(node, *argument_index); + let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else { + return; + }; + + let typevar = error.typevar(); + let argument_type = error.argument_type(); + let argument_ty_display = argument_type.display(context.db()); + + let mut diag = builder.into_diagnostic("Argument to this function is incorrect"); + diag.set_primary_message(format_args!( + "Argument type `{argument_ty_display}` does not satisfy {} of type variable `{}`", + match error { + SpecializationError::MismatchedBound {..} => "upper bound", + SpecializationError::MismatchedConstraint {..} => "constraints", + }, + typevar.name(context.db()), + )); + + let typevar_range = typevar.definition(context.db()).full_range(context.db()); + let mut sub = SubDiagnostic::new(Severity::Info, "Type variable defined here"); + sub.annotate(Annotation::primary(typevar_range.into())); + diag.sub(sub); + } + Self::InternalCallError(reason) => { let node = Self::get_node(node, None); if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index e051de715bd5bc..63d5a18e4baef0 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -35,6 +35,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_CONTEXT_MANAGER); registry.register_lint(&INVALID_DECLARATION); registry.register_lint(&INVALID_EXCEPTION_CAUGHT); + registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE); registry.register_lint(&INVALID_METACLASS); registry.register_lint(&INVALID_PARAMETER_DEFAULT); registry.register_lint(&INVALID_PROTOCOL); @@ -391,6 +392,34 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for the creation of invalid legacy `TypeVar`s + /// + /// ## Why is this bad? + /// There are several requirements that you must follow when creating a legacy `TypeVar`. + /// + /// ## Examples + /// ```python + /// from typing import TypeVar + /// + /// T = TypeVar("T") # okay + /// Q = TypeVar("S") # error: TypeVar name must match the variable it's assigned to + /// T = TypeVar("T") # error: TypeVars should not be redefined + /// + /// # error: TypeVar must be immediately assigned to a variable + /// def f(t: TypeVar("U")): ... + /// ``` + /// + /// ## References + /// - [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) + pub(crate) static INVALID_LEGACY_TYPE_VARIABLE = { + summary: "detects invalid legacy type variables", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for arguments to `metaclass=` that are invalid. @@ -1314,7 +1343,7 @@ pub(crate) fn report_invalid_arguments_to_annotated( builder.into_diagnostic(format_args!( "Special form `{}` expected at least 2 arguments \ (one type and at least one metadata element)", - KnownInstanceType::Annotated.repr(context.db()) + KnownInstanceType::Annotated.repr() )); } @@ -1362,7 +1391,7 @@ pub(crate) fn report_invalid_arguments_to_callable( }; builder.into_diagnostic(format_args!( "Special form `{}` expected exactly two arguments (parameter types and return type)", - KnownInstanceType::Callable.repr(context.db()) + KnownInstanceType::Callable.repr() )); } diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 4220b0cf41f138..2633938ae9285f 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -94,7 +94,7 @@ impl Display for DisplayRepresentation<'_> { SubclassOfInner::Class(class) => write!(f, "type[{}]", class.name(self.db)), SubclassOfInner::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), }, - Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)), + Type::KnownInstance(known_instance) => f.write_str(known_instance.repr()), Type::FunctionLiteral(function) => { let signature = function.signature(self.db); diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index b1c0355ebf240b..8589f14a5cb5a0 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -5,9 +5,9 @@ use crate::semantic_index::SemanticIndex; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ declaration_type, KnownInstanceType, Type, TypeVarBoundOrConstraints, TypeVarInstance, - UnionBuilder, UnionType, + UnionType, }; -use crate::Db; +use crate::{Db, FxOrderSet}; /// A list of formal type variables for a generic function, class, or type alias. /// @@ -20,6 +20,7 @@ pub struct GenericContext<'db> { } impl<'db> GenericContext<'db> { + /// Creates a generic context from a list of PEP-695 type parameters. pub(crate) fn from_type_params( db: &'db dyn Db, index: &'db SemanticIndex<'db>, @@ -53,6 +54,32 @@ impl<'db> GenericContext<'db> { } } + /// Creates a generic context from the legecy `TypeVar`s that appear in a function parameter + /// list. + pub(crate) fn from_function_params( + db: &'db dyn Db, + parameters: &Parameters<'db>, + return_type: Option>, + ) -> Option { + let mut variables = FxOrderSet::default(); + for param in parameters { + if let Some(ty) = param.annotated_type() { + ty.find_legacy_typevars(db, &mut variables); + } + if let Some(ty) = param.default_type() { + ty.find_legacy_typevars(db, &mut variables); + } + } + if let Some(ty) = return_type { + ty.find_legacy_typevars(db, &mut variables); + } + if variables.is_empty() { + return None; + } + let variables: Box<[_]> = variables.into_iter().collect(); + Some(Self::new(db, variables)) + } + pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { let parameters = Parameters::new( self.variables(db) @@ -303,7 +330,7 @@ impl<'db> Specialization<'db> { /// specialization of a generic function. pub(crate) struct SpecializationBuilder<'db> { db: &'db dyn Db, - types: FxHashMap, UnionBuilder<'db>>, + types: FxHashMap, Type<'db>>, } impl<'db> SpecializationBuilder<'db> { @@ -320,8 +347,8 @@ impl<'db> SpecializationBuilder<'db> { .iter() .map(|variable| { self.types - .remove(variable) - .map(UnionBuilder::build) + .get(variable) + .copied() .unwrap_or(variable.default_ty(self.db).unwrap_or(Type::unknown())) }) .collect(); @@ -329,17 +356,25 @@ impl<'db> SpecializationBuilder<'db> { } fn add_type_mapping(&mut self, typevar: TypeVarInstance<'db>, ty: Type<'db>) { - let builder = self - .types + self.types .entry(typevar) - .or_insert_with(|| UnionBuilder::new(self.db)); - builder.add_in_place(ty); + .and_modify(|existing| { + *existing = UnionType::from_elements(self.db, [*existing, ty]); + }) + .or_insert(ty); } - pub(crate) fn infer(&mut self, formal: Type<'db>, actual: Type<'db>) { - // If the actual type is already assignable to the formal type, then return without adding - // any new type mappings. (Note that if the formal type contains any typevars, this check - // will fail, since no non-typevar types are assignable to a typevar.) + pub(crate) fn infer( + &mut self, + formal: Type<'db>, + actual: Type<'db>, + ) -> Result<(), SpecializationError<'db>> { + // If the actual type is a subtype of the formal type, then return without adding any new + // type mappings. (Note that if the formal type contains any typevars, this check will + // fail, since no non-typevar types are assignable to a typevar. Also note that we are + // checking _subtyping_, not _assignability_, so that we do specialize typevars to dynamic + // argument types; and we have a special case for `Never`, which is a subtype of all types, + // but which we also do want as a specialization candidate.) // // In particular, this handles a case like // @@ -350,12 +385,37 @@ impl<'db> SpecializationBuilder<'db> { // ``` // // without specializing `T` to `None`. - if actual.is_assignable_to(self.db, formal) { - return; + if !actual.is_never() && actual.is_subtype_of(self.db, formal) { + return Ok(()); } match (formal, actual) { - (Type::TypeVar(typevar), _) => self.add_type_mapping(typevar, actual), + (Type::TypeVar(typevar), _) => match typevar.bound_or_constraints(self.db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + if !actual.is_assignable_to(self.db, bound) { + return Err(SpecializationError::MismatchedBound { + typevar, + argument: actual, + }); + } + self.add_type_mapping(typevar, actual); + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + for constraint in constraints.iter(self.db) { + if actual.is_assignable_to(self.db, *constraint) { + self.add_type_mapping(typevar, *constraint); + return Ok(()); + } + } + return Err(SpecializationError::MismatchedConstraint { + typevar, + argument: actual, + }); + } + _ => { + self.add_type_mapping(typevar, actual); + } + }, (Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => { let formal_elements = formal_tuple.elements(self.db); @@ -364,7 +424,7 @@ impl<'db> SpecializationBuilder<'db> { for (formal_element, actual_element) in formal_elements.iter().zip(actual_elements) { - self.infer(*formal_element, *actual_element); + self.infer(*formal_element, *actual_element)?; } } } @@ -397,12 +457,42 @@ impl<'db> SpecializationBuilder<'db> { // actual type must also be disjoint from every negative element of the // intersection, but that doesn't help us infer any type mappings.) for positive in formal.iter_positive(self.db) { - self.infer(positive, actual); + self.infer(positive, actual)?; } } // TODO: Add more forms that we can structurally induct into: type[C], callables _ => {} } + + Ok(()) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum SpecializationError<'db> { + MismatchedBound { + typevar: TypeVarInstance<'db>, + argument: Type<'db>, + }, + MismatchedConstraint { + typevar: TypeVarInstance<'db>, + argument: Type<'db>, + }, +} + +impl<'db> SpecializationError<'db> { + pub(crate) fn typevar(&self) -> TypeVarInstance<'db> { + match self { + Self::MismatchedBound { typevar, .. } => *typevar, + Self::MismatchedConstraint { typevar, .. } => *typevar, + } + } + + pub(crate) fn argument_type(&self) -> Type<'db> { + match self { + Self::MismatchedBound { argument, .. } => *argument, + Self::MismatchedConstraint { argument, .. } => *argument, + } } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index e9448d83b09698..db5b2a53197f05 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -73,9 +73,9 @@ use crate::types::diagnostic::{ CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, - INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS, - POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, - UNSUPPORTED_OPERATOR, + INVALID_LEGACY_TYPE_VARIABLE, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, + INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, + UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR, }; use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; @@ -87,7 +87,8 @@ use crate::types::{ MemberLookupPolicy, MetaclassCandidate, Parameter, ParameterForm, Parameters, Signature, Signatures, SliceLiteralType, StringLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, - TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType, + TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, UnionBuilder, + UnionType, }; use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; @@ -2204,6 +2205,7 @@ impl<'db> TypeInferenceBuilder<'db> { definition, bound_or_constraint, default_ty, + TypeVarKind::Pep695, ))); self.add_declaration_with_binding( node.into(), @@ -3733,7 +3735,9 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::Named(named) => self.infer_named_expression(named), ast::Expr::If(if_expression) => self.infer_if_expression(if_expression), ast::Expr::Lambda(lambda_expression) => self.infer_lambda_expression(lambda_expression), - ast::Expr::Call(call_expression) => self.infer_call_expression(call_expression), + ast::Expr::Call(call_expression) => { + self.infer_call_expression(expression, call_expression) + } ast::Expr::Starred(starred) => self.infer_starred_expression(starred), ast::Expr::Yield(yield_expression) => self.infer_yield_expression(yield_expression), ast::Expr::YieldFrom(yield_from) => self.infer_yield_from_expression(yield_from), @@ -4277,7 +4281,11 @@ impl<'db> TypeInferenceBuilder<'db> { }) } - fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> { + fn infer_call_expression( + &mut self, + call_expression_node: &ast::Expr, + call_expression: &ast::ExprCall, + ) -> Type<'db> { let ast::ExprCall { range: _, func, @@ -4332,6 +4340,7 @@ impl<'db> TypeInferenceBuilder<'db> { | KnownClass::Object | KnownClass::Property | KnownClass::Super + | KnownClass::TypeVar ) ) { @@ -4543,70 +4552,179 @@ impl<'db> TypeInferenceBuilder<'db> { _ => {} } } - Type::ClassLiteral(class) - if class.is_known(self.db(), KnownClass::Super) => - { - // Handle the case where `super()` is called with no arguments. - // In this case, we need to infer the two arguments: - // 1. The nearest enclosing class - // 2. The first parameter of the current function (typically `self` or `cls`) - match overload.parameter_types() { - [] => { - let scope = self.scope(); - - let Some(enclosing_class) = self.enclosing_class_symbol(scope) + + Type::ClassLiteral(class) => { + let Some(known_class) = class.known(self.db()) else { + continue; + }; + + match known_class { + KnownClass::Super => { + // Handle the case where `super()` is called with no arguments. + // In this case, we need to infer the two arguments: + // 1. The nearest enclosing class + // 2. The first parameter of the current function (typically `self` or `cls`) + match overload.parameter_types() { + [] => { + let scope = self.scope(); + + let Some(enclosing_class) = + self.enclosing_class_symbol(scope) + else { + overload.set_return_type(Type::unknown()); + BoundSuperError::UnavailableImplicitArguments + .report_diagnostic( + &self.context, + call_expression.into(), + ); + continue; + }; + + let Some(first_param) = + self.first_param_type_in_scope(scope) + else { + overload.set_return_type(Type::unknown()); + BoundSuperError::UnavailableImplicitArguments + .report_diagnostic( + &self.context, + call_expression.into(), + ); + continue; + }; + + let bound_super = BoundSuperType::build( + self.db(), + enclosing_class, + first_param, + ) + .unwrap_or_else(|err| { + err.report_diagnostic( + &self.context, + call_expression.into(), + ); + Type::unknown() + }); + + overload.set_return_type(bound_super); + } + [Some(pivot_class_type), Some(owner_type)] => { + let bound_super = BoundSuperType::build( + self.db(), + *pivot_class_type, + *owner_type, + ) + .unwrap_or_else(|err| { + err.report_diagnostic( + &self.context, + call_expression.into(), + ); + Type::unknown() + }); + + overload.set_return_type(bound_super); + } + _ => (), + } + } + + KnownClass::TypeVar => { + let assigned_to = (self.index) + .try_expression(call_expression_node) + .and_then(|expr| expr.assigned_to(self.db())); + + let Some(target) = + assigned_to.as_ref().and_then(|assigned_to| { + match assigned_to.node().targets.as_slice() { + [ast::Expr::Name(target)] => Some(target), + _ => None, + } + }) else { - overload.set_return_type(Type::unknown()); - BoundSuperError::UnavailableImplicitArguments - .report_diagnostic( - &self.context, - call_expression.into(), - ); + if let Some(builder) = self.context.report_lint( + &INVALID_LEGACY_TYPE_VARIABLE, + call_expression, + ) { + builder.into_diagnostic(format_args!( + "A legacy `typing.TypeVar` must be immediately assigned to a variable", + )); + } continue; }; - let Some(first_param) = self.first_param_type_in_scope(scope) + let [Some(name_param), constraints, bound, default, _contravariant, _covariant, _infer_variance] = + overload.parameter_types() else { - overload.set_return_type(Type::unknown()); - BoundSuperError::UnavailableImplicitArguments - .report_diagnostic( - &self.context, - call_expression.into(), - ); continue; }; - let bound_super = BoundSuperType::build( - self.db(), - enclosing_class, - first_param, - ) - .unwrap_or_else(|err| { - err.report_diagnostic( - &self.context, - call_expression.into(), - ); - Type::unknown() - }); - - overload.set_return_type(bound_super); - } - [Some(pivot_class_type), Some(owner_type)] => { - let bound_super = BoundSuperType::build( - self.db(), - *pivot_class_type, - *owner_type, - ) - .unwrap_or_else(|err| { - err.report_diagnostic( - &self.context, - call_expression.into(), - ); - Type::unknown() - }); - - overload.set_return_type(bound_super); + let name_param = name_param + .into_string_literal() + .map(|name| name.value(self.db()).as_ref()); + if name_param.is_none_or(|name_param| name_param != target.id) { + if let Some(builder) = self.context.report_lint( + &INVALID_LEGACY_TYPE_VARIABLE, + call_expression, + ) { + builder.into_diagnostic(format_args!( + "The name of a legacy `typing.TypeVar`{} must match \ + the name of the variable it is assigned to (`{}`)", + if let Some(name_param) = name_param { + format!(" (`{name_param}`)") + } else { + String::new() + }, + target.id, + )); + } + continue; + } + + let bound_or_constraint = match (bound, constraints) { + (Some(bound), None) => { + Some(TypeVarBoundOrConstraints::UpperBound(*bound)) + } + + (None, Some(_constraints)) => { + // We don't use UnionType::from_elements or UnionBuilder here, + // because we don't want to simplify the list of constraints like + // we do with the elements of an actual union type. + // TODO: Consider using a new `OneOfType` connective here instead, + // since that more accurately represents the actual semantics of + // typevar constraints. + let elements = UnionType::new( + self.db(), + overload + .arguments_for_parameter( + &call_argument_types, + 1, + ) + .map(|(_, ty)| ty) + .collect::>(), + ); + Some(TypeVarBoundOrConstraints::Constraints(elements)) + } + + // TODO: Emit a diagnostic that TypeVar cannot be both bounded and + // constrained + (Some(_), Some(_)) => continue, + + (None, None) => None, + }; + + let containing_assignment = + self.index.expect_single_definition(target); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::TypeVar(TypeVarInstance::new( + self.db(), + target.id.clone(), + containing_assignment, + bound_or_constraint, + *default, + TypeVarKind::Legacy, + )), + )); } + _ => (), } } @@ -6509,7 +6627,12 @@ impl<'db> TypeInferenceBuilder<'db> { if class.generic_context(self.db()).is_some() { // TODO: specialize the generic class using these explicit type - // variable assignments + // variable assignments. This branch is only encountered when an + // explicit class specialization appears inside of some other subscript + // expression, e.g. `tuple[list[int], ...]`. We have already inferred + // the type of the outer subscript slice as a value expression, which + // means we can't re-infer the inner specialization here as a type + // expression. return value_ty; } } @@ -6753,7 +6876,7 @@ impl<'db> TypeInferenceBuilder<'db> { builder.into_diagnostic(format_args!( "Type qualifier `{type_qualifier}` \ expects exactly one type parameter", - type_qualifier = known_instance.repr(self.db()), + type_qualifier = known_instance.repr(), )); } Type::unknown().into() @@ -7111,7 +7234,7 @@ impl<'db> TypeInferenceBuilder<'db> { } ast::Expr::Call(call_expr) => { - self.infer_call_expression(call_expr); + self.infer_call_expression(expression, call_expr); self.report_invalid_type_expression( expression, format_args!("Function calls are not allowed in type expressions"), @@ -7531,7 +7654,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected exactly one type parameter", - known_instance.repr(db) + known_instance.repr() )); } Type::unknown() @@ -7558,7 +7681,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected exactly one type parameter", - known_instance.repr(db) + known_instance.repr() )); } Type::unknown() @@ -7574,7 +7697,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected exactly one type parameter", - known_instance.repr(db) + known_instance.repr() )); } Type::unknown() @@ -7607,7 +7730,7 @@ impl<'db> TypeInferenceBuilder<'db> { "Expected the first argument to `{}` \ to be a callable object, \ but got an object of type `{}`", - known_instance.repr(db), + known_instance.repr(), argument_type.display(db) )); } @@ -7672,7 +7795,7 @@ impl<'db> TypeInferenceBuilder<'db> { builder.into_diagnostic(format_args!( "Type qualifier `{}` is not allowed in type expressions \ (only in annotation expressions)", - known_instance.repr(db) + known_instance.repr() )); } self.infer_type_expression(arguments_slice) @@ -7715,7 +7838,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", - known_instance.repr(db) + known_instance.repr() )); } Type::unknown() @@ -7729,7 +7852,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected no type parameter", - known_instance.repr(db) + known_instance.repr() )); } Type::unknown() @@ -7740,7 +7863,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let mut diag = builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", - known_instance.repr(db) + known_instance.repr() )); diag.info("Did you mean to use `Literal[...]` instead?"); } @@ -8288,7 +8411,7 @@ mod tests { constraints: Option<&[&'static str]>, default: Option<&'static str>| { let var_ty = get_symbol(&db, "src/a.py", &["f"], var).expect_type(); - assert_eq!(var_ty.display(&db).to_string(), var); + assert_eq!(var_ty.display(&db).to_string(), "typing.TypeVar"); let expected_name_ty = format!(r#"Literal["{var}"]"#); let name_ty = var_ty.member(&db, "__name__").symbol.expect_type(); diff --git a/crates/red_knot_python_semantic/src/types/known_instance.rs b/crates/red_knot_python_semantic/src/types/known_instance.rs index 01316c1c5698bb..d6da1d76d42998 100644 --- a/crates/red_knot_python_semantic/src/types/known_instance.rs +++ b/crates/red_knot_python_semantic/src/types/known_instance.rs @@ -109,6 +109,10 @@ impl<'db> KnownInstanceType<'db> { | Self::Literal | Self::LiteralString | Self::Optional + // This is a legacy `TypeVar` _outside_ of any generic class or function, so it's + // AlwaysTrue. The truthiness of a typevar inside of a generic class or function + // depends on its bounds and constraints; but that's represented by `Type::TypeVar` and + // handled in elsewhere. | Self::TypeVar(_) | Self::Union | Self::NoReturn @@ -152,7 +156,7 @@ impl<'db> KnownInstanceType<'db> { } /// Return the repr of the symbol at runtime - pub(crate) fn repr(self, db: &'db dyn Db) -> &'db str { + pub(crate) fn repr(self) -> &'db str { match self { Self::Annotated => "typing.Annotated", Self::Literal => "typing.Literal", @@ -188,7 +192,10 @@ impl<'db> KnownInstanceType<'db> { Self::Protocol => "typing.Protocol", Self::Generic => "typing.Generic", Self::ReadOnly => "typing.ReadOnly", - Self::TypeVar(typevar) => typevar.name(db), + // This is a legacy `TypeVar` _outside_ of any generic class or function, so we render + // it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll + // have a `Type::TypeVar(_)`, which is rendered as the typevar's name. + Self::TypeVar(_) => "typing.TypeVar", Self::TypeAliasType(_) => "typing.TypeAliasType", Self::Unknown => "knot_extensions.Unknown", Self::AlwaysTruthy => "knot_extensions.AlwaysTruthy", diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 811965cde91041..a44720d38c3945 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -18,8 +18,8 @@ use smallvec::{smallvec, SmallVec}; use super::{definition_expression_type, DynamicType, Type}; use crate::semantic_index::definition::Definition; use crate::types::generics::{GenericContext, Specialization}; -use crate::types::todo_type; -use crate::Db; +use crate::types::{todo_type, TypeVarInstance}; +use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; /// The signature of a possible union of callables. @@ -267,6 +267,8 @@ impl<'db> Signature<'db> { definition: Definition<'db>, function_node: &ast::StmtFunctionDef, ) -> Self { + let parameters = + Parameters::from_parameters(db, definition, function_node.parameters.as_ref()); let return_ty = function_node.returns.as_ref().map(|returns| { if function_node.is_async { todo_type!("generic types.CoroutineType") @@ -274,15 +276,17 @@ impl<'db> Signature<'db> { definition_expression_type(db, definition, returns.as_ref()) } }); + let legacy_generic_context = + GenericContext::from_function_params(db, ¶meters, return_ty); + + if generic_context.is_some() && legacy_generic_context.is_some() { + // TODO: Raise a diagnostic! + } Self { - generic_context, + generic_context: generic_context.or(legacy_generic_context), inherited_generic_context, - parameters: Parameters::from_parameters( - db, - definition, - function_node.parameters.as_ref(), - ), + parameters, return_ty, } } @@ -315,6 +319,24 @@ impl<'db> Signature<'db> { } } + pub(crate) fn find_legacy_typevars( + &self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for param in &self.parameters { + if let Some(ty) = param.annotated_type() { + ty.find_legacy_typevars(db, typevars); + } + if let Some(ty) = param.default_type() { + ty.find_legacy_typevars(db, typevars); + } + } + if let Some(ty) = self.return_ty { + ty.find_legacy_typevars(db, typevars); + } + } + /// Return the parameters in this signature. pub(crate) fn parameters(&self) -> &Parameters<'db> { &self.parameters diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs index e0b0da246ba744..6dab343b05e1dc 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -59,13 +59,22 @@ type KeyDiagnosticFields = ( Severity, ); -static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[( - DiagnosticId::lint("unused-ignore-comment"), - Some("/src/tomllib/_parser.py"), - Some(22299..22333), - "Unused blanket `type: ignore` directive", - Severity::Warning, -)]; +static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[ + ( + DiagnosticId::lint("no-matching-overload"), + Some("/src/tomllib/_parser.py"), + Some(2329..2358), + "No overload of bound method `__init__` matches arguments", + Severity::Error, + ), + ( + DiagnosticId::lint("unused-ignore-comment"), + Some("/src/tomllib/_parser.py"), + Some(22299..22333), + "Unused blanket `type: ignore` directive", + Severity::Warning, + ), +]; fn tomllib_path(file: &TestFile) -> SystemPathBuf { SystemPathBuf::from("src").join(file.name()) diff --git a/knot.schema.json b/knot.schema.json index 28a09099c689a2..66e0a5b2e304a8 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -450,6 +450,16 @@ } ] }, + "invalid-legacy-type-variable": { + "title": "detects invalid legacy type variables", + "description": "## What it does\nChecks for the creation of invalid legacy `TypeVar`s\n\n## Why is this bad?\nThere are several requirements that you must follow when creating a legacy `TypeVar`.\n\n## Examples\n```python\nfrom typing import TypeVar\n\nT = TypeVar(\"T\") # okay\nQ = TypeVar(\"S\") # error: TypeVar name must match the variable it's assigned to\nT = TypeVar(\"T\") # error: TypeVars should not be redefined\n\n# error: TypeVar must be immediately assigned to a variable\ndef f(t: TypeVar(\"U\")): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-metaclass": { "title": "detects invalid `metaclass=` arguments", "description": "## What it does\nChecks for arguments to `metaclass=` that are invalid.\n\n## Why is this bad?\nPython allows arbitrary expressions to be used as the argument to `metaclass=`.\nThese expressions, however, need to be callable and accept the same arguments\nas `type.__new__`.\n\n## Example\n\n```python\ndef f(): ...\n\n# TypeError: f() takes 0 positional arguments but 3 were given\nclass B(metaclass=f): ...\n```\n\n## References\n- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)", From 79f8473e51dee76d3ffd193ceb36923edf02f295 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 29 Apr 2025 15:04:22 +0200 Subject: [PATCH 0171/1161] [red-knot] Assignability of class literals to Callables (#17704) ## Summary Subtyping was already modeled, but assignability also needs an explicit branch. Removes 921 ecosystem false positives. ## Test Plan New Markdown tests. --- .../mdtest/type_properties/is_assignable_to.md | 12 ++++++++++++ crates/red_knot_python_semantic/src/types.rs | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index f8b8adf5f8b832..68c7e8ce1a6ab8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -529,6 +529,18 @@ c: Callable[[Any], str] = A().f c: Callable[[Any], str] = A().g ``` +### Class literal types + +```py +from typing import Any, Callable + +c: Callable[[str], Any] = str +c: Callable[[str], Any] = int + +# error: [invalid-assignment] +c: Callable[[str], Any] = object +``` + ### Overloads `overloaded.pyi`: diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index adc6ac478efc2a..335e4d8e1c13fd 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1503,6 +1503,13 @@ impl<'db> Type<'db> { } } + (Type::ClassLiteral(class_literal), Type::Callable(_)) => { + if let Some(callable) = class_literal.into_callable(db) { + return callable.is_assignable_to(db, target); + } + false + } + (Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => { self_function_literal .into_callable_type(db) From 9b9d16c3ba70567f2c90f7342b29befc60ed1f0b Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 29 Apr 2025 22:07:16 +0800 Subject: [PATCH 0172/1161] [red-knot] colorize concise output diagnostics (#17232) (#17479) Co-authored-by: Micha Reiser Co-authored-by: Andrew Gallant --- Cargo.lock | 1 + crates/ruff_db/Cargo.toml | 1 + crates/ruff_db/src/diagnostic/mod.rs | 1 + crates/ruff_db/src/diagnostic/render.rs | 57 ++++++++++++--- crates/ruff_db/src/diagnostic/stylesheet.rs | 80 +++++++++++++++++++++ 5 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 crates/ruff_db/src/diagnostic/stylesheet.rs diff --git a/Cargo.lock b/Cargo.lock index 34302a319ba588..5acc1c77d89fcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2865,6 +2865,7 @@ dependencies = [ name = "ruff_db" version = "0.0.0" dependencies = [ + "anstyle", "camino", "countme", "dashmap 6.1.0", diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml index b37af0f97866d5..3507efa545f3ab 100644 --- a/crates/ruff_db/Cargo.toml +++ b/crates/ruff_db/Cargo.toml @@ -20,6 +20,7 @@ ruff_python_trivia = { workspace = true } ruff_source_file = { workspace = true } ruff_text_size = { workspace = true } +anstyle = { workspace = true } camino = { workspace = true } countme = { workspace = true } dashmap = { workspace = true } diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index ffcdf2a026742b..412eec753059ee 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -11,6 +11,7 @@ use crate::Db; use self::render::FileResolver; mod render; +mod stylesheet; /// A collection of information that can be rendered into a diagnostic. /// diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 56e3ea4eaaf691..e20e8f48aa837c 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -7,6 +7,7 @@ use ruff_annotate_snippets::{ use ruff_source_file::{LineIndex, OneIndexed, SourceCode}; use ruff_text_size::{TextRange, TextSize}; +use crate::diagnostic::stylesheet::{fmt_styled, DiagnosticStylesheet}; use crate::{ files::File, source::{line_index, source_text, SourceText}, @@ -48,6 +49,7 @@ impl<'a> DisplayDiagnostic<'a> { } else { AnnotateRenderer::plain() }; + DisplayDiagnostic { config, resolver, @@ -59,31 +61,64 @@ impl<'a> DisplayDiagnostic<'a> { impl std::fmt::Display for DisplayDiagnostic<'_> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let stylesheet = if self.config.color { + DiagnosticStylesheet::styled() + } else { + DiagnosticStylesheet::plain() + }; + if matches!(self.config.format, DiagnosticFormat::Concise) { - match self.diag.severity() { - Severity::Info => f.write_str("info")?, - Severity::Warning => f.write_str("warning")?, - Severity::Error => f.write_str("error")?, - Severity::Fatal => f.write_str("fatal")?, - } + let (severity, severity_style) = match self.diag.severity() { + Severity::Info => ("info", stylesheet.info), + Severity::Warning => ("warning", stylesheet.warning), + Severity::Error => ("error", stylesheet.error), + Severity::Fatal => ("fatal", stylesheet.error), + }; + + write!( + f, + "{severity}[{id}]", + severity = fmt_styled(severity, severity_style), + id = fmt_styled(self.diag.id(), stylesheet.emphasis) + )?; - write!(f, "[{rule}]", rule = self.diag.id())?; if let Some(span) = self.diag.primary_span() { - write!(f, " {path}", path = self.resolver.path(span.file()))?; + write!( + f, + " {path}", + path = fmt_styled(self.resolver.path(span.file()), stylesheet.emphasis) + )?; if let Some(range) = span.range() { let input = self.resolver.input(span.file()); let start = input.as_source_code().line_column(range.start()); - write!(f, ":{line}:{col}", line = start.line, col = start.column)?; + + write!( + f, + ":{line}:{col}", + line = fmt_styled(start.line, stylesheet.emphasis), + col = fmt_styled(start.column, stylesheet.emphasis), + )?; } write!(f, ":")?; } - return writeln!(f, " {}", self.diag.concise_message()); + return writeln!(f, " {message}", message = self.diag.concise_message()); } + let mut renderer = self.annotate_renderer.clone(); + renderer = renderer + .error(stylesheet.error) + .warning(stylesheet.warning) + .info(stylesheet.info) + .note(stylesheet.note) + .help(stylesheet.help) + .line_no(stylesheet.line_no) + .emphasis(stylesheet.emphasis) + .none(stylesheet.none); + let resolved = Resolved::new(&self.resolver, self.diag); let renderable = resolved.to_renderable(self.config.context); for diag in renderable.diagnostics.iter() { - writeln!(f, "{}", self.annotate_renderer.render(diag.to_annotate()))?; + writeln!(f, "{}", renderer.render(diag.to_annotate()))?; } writeln!(f) } diff --git a/crates/ruff_db/src/diagnostic/stylesheet.rs b/crates/ruff_db/src/diagnostic/stylesheet.rs new file mode 100644 index 00000000000000..f3d451030bcb8a --- /dev/null +++ b/crates/ruff_db/src/diagnostic/stylesheet.rs @@ -0,0 +1,80 @@ +use anstyle::{AnsiColor, Effects, Style}; +use std::fmt::Formatter; + +pub(super) const fn fmt_styled<'a, T>( + content: T, + style: anstyle::Style, +) -> impl std::fmt::Display + 'a +where + T: std::fmt::Display + 'a, +{ + struct FmtStyled { + content: T, + style: anstyle::Style, + } + + impl std::fmt::Display for FmtStyled + where + T: std::fmt::Display, + { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{style_start}{content}{style_end}", + style_start = self.style.render(), + content = self.content, + style_end = self.style.render_reset() + ) + } + } + + FmtStyled { content, style } +} + +#[derive(Clone, Debug)] +pub struct DiagnosticStylesheet { + pub(crate) error: Style, + pub(crate) warning: Style, + pub(crate) info: Style, + pub(crate) note: Style, + pub(crate) help: Style, + pub(crate) line_no: Style, + pub(crate) emphasis: Style, + pub(crate) none: Style, +} + +impl Default for DiagnosticStylesheet { + fn default() -> Self { + Self::plain() + } +} + +impl DiagnosticStylesheet { + /// Default terminal styling + pub fn styled() -> Self { + let bright_blue = AnsiColor::BrightBlue.on_default(); + Self { + error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD), + warning: AnsiColor::Yellow.on_default().effects(Effects::BOLD), + info: bright_blue.effects(Effects::BOLD), + note: AnsiColor::BrightGreen.on_default().effects(Effects::BOLD), + help: AnsiColor::BrightCyan.on_default().effects(Effects::BOLD), + line_no: bright_blue.effects(Effects::BOLD), + emphasis: Style::new().effects(Effects::BOLD), + none: Style::new(), + } + } + + pub fn plain() -> Self { + Self { + error: Style::new(), + warning: Style::new(), + info: Style::new(), + note: Style::new(), + help: Style::new(), + line_no: Style::new(), + emphasis: Style::new(), + none: Style::new(), + } + } +} From c9a6b1a9d0d131c637af04977421d052b70e0e93 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 29 Apr 2025 15:14:08 +0100 Subject: [PATCH 0173/1161] [red-knot] Make `Type::signatures()` exhaustive (#17706) --- crates/red_knot_python_semantic/src/types.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 335e4d8e1c13fd..dc7a789939dcf0 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4049,7 +4049,25 @@ impl<'db> Type<'db> { Signatures::single(CallableSignature::todo("Type::Intersection.call()")) } - _ => Signatures::not_callable(self), + // TODO: these are actually callable + Type::MethodWrapper(_) | Type::DataclassDecorator(_) => Signatures::not_callable(self), + + // TODO: some `KnownInstance`s are callable (e.g. TypedDicts) + Type::KnownInstance(_) => Signatures::not_callable(self), + + Type::PropertyInstance(_) + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::IntLiteral(_) + | Type::StringLiteral(_) + | Type::BytesLiteral(_) + | Type::BooleanLiteral(_) + | Type::LiteralString + | Type::SliceLiteral(_) + | Type::Tuple(_) + | Type::BoundSuper(_) + | Type::TypeVar(_) + | Type::ModuleLiteral(_) => Signatures::not_callable(self), } } From 7d46579808f9f0f3a8779a1ace28bd28fbbdcfc8 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 29 Apr 2025 23:58:39 +0800 Subject: [PATCH 0174/1161] [docs] fix duplicated 'are' in comment for PTH123 rule (#17714) --- .../rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index 7d44916c890073..07e3bf10c1667b 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -99,7 +99,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { // PTH123 ["" | "builtins", "open"] => { // `closefd` and `opener` are not supported by pathlib, so check if they are - // are set to non-default values. + // set to non-default values. // https://github.com/astral-sh/ruff/issues/7620 // Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open): // ```text From 1d788981cdb2fc64b7163ea5d97704fc0723a8de Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 29 Apr 2025 18:58:58 +0200 Subject: [PATCH 0175/1161] [red-knot] Capture backtrace in "check-failed" diagnostic (#17641) Co-authored-by: David Peter --- crates/red_knot_project/src/lib.rs | 55 +++++++++---- crates/red_knot_test/src/lib.rs | 4 +- crates/ruff_db/src/panic.rs | 123 ++++++++++++++++++++++------- 3 files changed, 137 insertions(+), 45 deletions(-) diff --git a/crates/red_knot_project/src/lib.rs b/crates/red_knot_project/src/lib.rs index 48a5de4d48662a..25897e6d72c6a9 100644 --- a/crates/red_knot_project/src/lib.rs +++ b/crates/red_knot_project/src/lib.rs @@ -20,7 +20,7 @@ use ruff_db::system::{SystemPath, SystemPathBuf}; use rustc_hash::FxHashSet; use salsa::Durability; use salsa::Setter; -use std::panic::{catch_unwind, AssertUnwindSafe, UnwindSafe}; +use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::sync::Arc; use thiserror::Error; use tracing::error; @@ -577,26 +577,29 @@ fn catch(db: &dyn Db, file: File, f: F) -> Result, Diagnostic> where F: FnOnce() -> R + UnwindSafe, { - match catch_unwind(|| { + match ruff_db::panic::catch_unwind(|| { // Ignore salsa errors salsa::Cancelled::catch(f).ok() }) { Ok(result) => Ok(result), Err(error) => { - let payload = if let Some(s) = error.downcast_ref::<&str>() { - Some((*s).to_string()) - } else { - error.downcast_ref::().cloned() - }; + use std::fmt::Write; + let mut message = String::new(); + message.push_str("Panicked"); - let message = if let Some(payload) = payload { - format!( - "Panicked while checking `{file}`: `{payload}`", - file = file.path(db) - ) - } else { - format!("Panicked while checking `{file}`", file = { file.path(db) }) - }; + if let Some(location) = error.location { + let _ = write!(&mut message, " at {location}"); + } + + let _ = write!( + &mut message, + " when checking `{file}`", + file = file.path(db) + ); + + if let Some(payload) = error.payload.as_str() { + let _ = write!(&mut message, ": `{payload}`"); + } let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message); diagnostic.sub(SubDiagnostic::new( @@ -606,6 +609,28 @@ where let report_message = "If you could open an issue at https://github.com/astral-sh/ruff/issues/new?title=%5Bred-knot%5D:%20panic we'd be very appreciative!"; diagnostic.sub(SubDiagnostic::new(Severity::Info, report_message)); + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + format!( + "Platform: {os} {arch}", + os = std::env::consts::OS, + arch = std::env::consts::ARCH + ), + )); + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + format!( + "Args: {args:?}", + args = std::env::args().collect::>() + ), + )); + + if let Some(backtrace) = error.backtrace { + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + format!("Backtrace:\n{backtrace}"), + )); + } Err(diagnostic) } diff --git a/crates/red_knot_test/src/lib.rs b/crates/red_knot_test/src/lib.rs index 0efa683f2e8381..f57c559b87402c 100644 --- a/crates/red_knot_test/src/lib.rs +++ b/crates/red_knot_test/src/lib.rs @@ -324,8 +324,8 @@ fn run_test( Some(location) => messages.push(format!("panicked at {location}")), None => messages.push("panicked at unknown location".to_string()), } - match info.payload { - Some(payload) => messages.push(payload), + match info.payload.as_str() { + Some(message) => messages.push(message.to_string()), // Mimic the default panic hook's rendering of the panic payload if it's // not a string. None => messages.push("Box".to_string()), diff --git a/crates/ruff_db/src/panic.rs b/crates/ruff_db/src/panic.rs index 576425a99e0fe0..3deda2d741fc82 100644 --- a/crates/ruff_db/src/panic.rs +++ b/crates/ruff_db/src/panic.rs @@ -2,20 +2,35 @@ use std::cell::Cell; use std::panic::Location; use std::sync::OnceLock; -#[derive(Default, Debug)] +#[derive(Debug)] pub struct PanicError { pub location: Option, - pub payload: Option, + pub payload: Payload, pub backtrace: Option, } +#[derive(Debug)] +pub struct Payload(Box); + +impl Payload { + pub fn as_str(&self) -> Option<&str> { + if let Some(s) = self.0.downcast_ref::() { + Some(s) + } else if let Some(s) = self.0.downcast_ref::<&str>() { + Some(s) + } else { + None + } + } +} + impl std::fmt::Display for PanicError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "panicked at")?; if let Some(location) = &self.location { write!(f, " {location}")?; } - if let Some(payload) = &self.payload { + if let Some(payload) = self.payload.as_str() { write!(f, ":\n{payload}")?; } if let Some(backtrace) = &self.backtrace { @@ -27,8 +42,7 @@ impl std::fmt::Display for PanicError { thread_local! { static CAPTURE_PANIC_INFO: Cell = const { Cell::new(false) }; - static OUR_HOOK_RAN: Cell = const { Cell::new(false) }; - static LAST_PANIC: Cell> = const { Cell::new(None) }; + static LAST_BACKTRACE: Cell<(Option, Option)> = const { Cell::new((None, None)) }; } fn install_hook() { @@ -36,25 +50,15 @@ fn install_hook() { ONCE.get_or_init(|| { let prev = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { - OUR_HOOK_RAN.with(|cell| cell.set(true)); let should_capture = CAPTURE_PANIC_INFO.with(Cell::get); if !should_capture { return (*prev)(info); } - let payload = if let Some(s) = info.payload().downcast_ref::<&str>() { - Some(s.to_string()) - } else { - info.payload().downcast_ref::().cloned() - }; + let location = info.location().map(Location::to_string); - let backtrace = std::backtrace::Backtrace::force_capture(); - LAST_PANIC.with(|cell| { - cell.set(Some(PanicError { - payload, - location, - backtrace: Some(backtrace), - })); - }); + let backtrace = Some(std::backtrace::Backtrace::force_capture()); + + LAST_BACKTRACE.set((backtrace, location)); })); }); } @@ -70,7 +74,7 @@ fn install_hook() { /// stderr). /// /// We assume that there is nothing else running in this process that needs to install a competing -/// panic hook. We are careful to install our custom hook only once, and we do not ever restore +/// panic hook. We are careful to install our custom hook only once, and we do not ever restore /// the previous hook (since you can always retain the previous hook's behavior by not calling this /// wrapper). pub fn catch_unwind(f: F) -> Result @@ -78,15 +82,78 @@ where F: FnOnce() -> R + std::panic::UnwindSafe, { install_hook(); - OUR_HOOK_RAN.with(|cell| cell.set(false)); - let prev_should_capture = CAPTURE_PANIC_INFO.with(|cell| cell.replace(true)); - let result = std::panic::catch_unwind(f).map_err(|_| { - let our_hook_ran = OUR_HOOK_RAN.with(Cell::get); - if !our_hook_ran { - panic!("detected a competing panic hook"); + let prev_should_capture = CAPTURE_PANIC_INFO.replace(true); + let result = std::panic::catch_unwind(f).map_err(|payload| { + // Try to get the backtrace and location from our custom panic hook. + // The custom panic hook only runs once when `panic!` is called (or similar). It doesn't + // run when the panic is propagated with `std::panic::resume_unwind`. The panic hook + // is also not called when the panic is raised with `std::panic::resum_unwind` as is the + // case for salsa unwinds (see the ignored test below). + // Because of that, always take the payload from `catch_unwind` because it may have been transformed + // by an inner `std::panic::catch_unwind` handlers and only use the information + // from the custom handler to enrich the error with the backtrace and location. + let (backtrace, location) = LAST_BACKTRACE.with(Cell::take); + + PanicError { + location, + payload: Payload(payload), + backtrace, } - LAST_PANIC.with(Cell::take).unwrap_or_default() }); - CAPTURE_PANIC_INFO.with(|cell| cell.set(prev_should_capture)); + CAPTURE_PANIC_INFO.set(prev_should_capture); result } + +#[cfg(test)] +mod tests { + use salsa::{Database, Durability}; + + #[test] + #[ignore = "super::catch_unwind installs a custom panic handler, which could effect test isolation"] + fn no_backtrace_for_salsa_cancelled() { + #[salsa::input] + struct Input { + value: u32, + } + + #[salsa::tracked] + fn test_query(db: &dyn Database, input: Input) -> u32 { + loop { + // This should throw a cancelled error + let _ = input.value(db); + } + } + + let db = salsa::DatabaseImpl::new(); + + let input = Input::new(&db, 42); + + let result = std::thread::scope(move |scope| { + { + let mut db = db.clone(); + scope.spawn(move || { + // This will cancel the other thread by throwing a `salsa::Cancelled` error. + db.synthetic_write(Durability::MEDIUM); + }); + } + + { + scope.spawn(move || { + super::catch_unwind(|| { + test_query(&db, input); + }) + }) + } + .join() + .unwrap() + }); + + match result { + Ok(_) => panic!("Expected query to panic"), + Err(err) => { + // Panics triggered with `resume_unwind` have no backtrace. + assert!(err.backtrace.is_none()); + } + } + } +} From 93d6a3567be875c2d8590f90901a23c319b4a63e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 29 Apr 2025 19:27:49 +0100 Subject: [PATCH 0176/1161] [red-knot] mdtest.py: Watch for changes in `red_knot_vendored` and `red_knot_test` as well as in `red_knot_python_semantic` (#17718) --- crates/red_knot_python_semantic/mdtest.py | 29 +++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/red_knot_python_semantic/mdtest.py b/crates/red_knot_python_semantic/mdtest.py index a82edee78263b2..9869295479ec4f 100644 --- a/crates/red_knot_python_semantic/mdtest.py +++ b/crates/red_knot_python_semantic/mdtest.py @@ -20,6 +20,12 @@ CRATE_NAME: Final = "red_knot_python_semantic" CRATE_ROOT: Final = Path(__file__).resolve().parent +RED_KNOT_VENDORED: Final = CRATE_ROOT.parent / "red_knot_vendored" +DIRS_TO_WATCH: Final = ( + CRATE_ROOT, + RED_KNOT_VENDORED, + CRATE_ROOT.parent / "red_knot_test/src", +) MDTEST_DIR: Final = CRATE_ROOT / "resources" / "mdtest" @@ -158,20 +164,24 @@ def watch(self) -> Never: self._run_mdtest() self.console.print("[dim]Ready to watch for changes...[/dim]") - for changes in watch(CRATE_ROOT): + for changes in watch(*DIRS_TO_WATCH): new_md_files = set() changed_md_files = set() rust_code_has_changed = False + vendored_typeshed_has_changed = False for change, path_str in changes: path = Path(path_str) - if path.suffix == ".rs": - rust_code_has_changed = True - continue - - if path.suffix != ".md": - continue + match path.suffix: + case ".rs": + rust_code_has_changed = True + case ".pyi" if path.is_relative_to(RED_KNOT_VENDORED): + vendored_typeshed_has_changed = True + case ".md": + pass + case _: + continue try: relative_path = Path(path).relative_to(MDTEST_DIR) @@ -199,6 +209,11 @@ def watch(self) -> Never: if rust_code_has_changed: if self._recompile_tests("Rust code has changed, recompiling tests..."): self._run_mdtest() + elif vendored_typeshed_has_changed: + if self._recompile_tests( + "Vendored typeshed has changed, recompiling tests..." + ): + self._run_mdtest() elif new_md_files: files = " ".join(file.as_posix() for file in new_md_files) self._recompile_tests( From 8c68d30c3a50ac4661beff40e466ba85987f62b6 Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Tue, 29 Apr 2025 17:51:38 -0300 Subject: [PATCH 0177/1161] [`flake8-use-pathlib`] Fix `PTH123` false positive when `open` is passed a file descriptor from a function call (#17705) ## Summary Includes minor changes to the semantic type inference to help detect the return type of function call. Fixes #17691 ## Test Plan Snapshot tests --- .../fixtures/flake8_use_pathlib/full_name.py | 13 +++++++ .../rules/replaceable_by_pathlib.rs | 38 ++++++++++++++++--- .../src/analyze/typing.rs | 12 ++++++ 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py index 20197b9703863f..8680212b20c3aa 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py @@ -55,3 +55,16 @@ def opener(path, flags): open(x) def foo(y: int): open(y) + +# https://github.com/astral-sh/ruff/issues/17691 +def f() -> int: + return 1 +open(f()) + +open(b"foo") +byte_str = b"bar" +open(byte_str) + +def bytes_str_func() -> bytes: + return b"foo" +open(bytes_str_func()) diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index 07e3bf10c1667b..58e33f040f8245 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -126,10 +126,9 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { .arguments .find_argument_value("opener", 7) .is_some_and(|expr| !expr.is_none_literal_expr()) - || call - .arguments - .find_positional(0) - .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) + || call.arguments.find_positional(0).is_some_and(|expr| { + is_file_descriptor_or_bytes_str(expr, checker.semantic()) + }) { return None; } @@ -168,6 +167,10 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { } } +fn is_file_descriptor_or_bytes_str(expr: &Expr, semantic: &SemanticModel) -> bool { + is_file_descriptor(expr, semantic) || is_bytes_string(expr, semantic) +} + /// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer. fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool { if matches!( @@ -180,7 +183,7 @@ fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool { return true; } - let Some(name) = expr.as_name_expr() else { + let Some(name) = get_name_expr(expr) else { return false; }; @@ -190,3 +193,28 @@ fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool { typing::is_int(binding, semantic) } + +/// Returns `true` if the given expression is a bytes string. +fn is_bytes_string(expr: &Expr, semantic: &SemanticModel) -> bool { + if matches!(expr, Expr::BytesLiteral(_)) { + return true; + } + + let Some(name) = get_name_expr(expr) else { + return false; + }; + + let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { + return false; + }; + + typing::is_bytes(binding, semantic) +} + +fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> { + match expr { + Expr::Name(name) => Some(name), + Expr::Call(ast::ExprCall { func, .. }) => get_name_expr(func), + _ => None, + } +} diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 9b1f1c43f9a589..c588da272aff83 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -639,6 +639,18 @@ pub fn check_type(binding: &Binding, semantic: &SemanticModel) - _ => false, }, + BindingKind::FunctionDefinition(_) => match binding.statement(semantic) { + // ```python + // def foo() -> int: + // ... + // ``` + Some(Stmt::FunctionDef(ast::StmtFunctionDef { returns, .. })) => returns + .as_ref() + .is_some_and(|return_ann| T::match_annotation(return_ann, semantic)), + + _ => false, + }, + _ => false, } } From 81fc7d7d3ae334345d2ad66a1b579fcf1607bb2f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 29 Apr 2025 22:15:26 +0100 Subject: [PATCH 0178/1161] Upload red-knot binaries in CI on completion of linux tests (#17720) --- .github/workflows/ci.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 14ada97cac4238..5064763ef3f3fd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -276,6 +276,10 @@ jobs: with: name: ruff path: target/debug/ruff + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: red_knot + path: target/debug/red_knot cargo-test-linux-release: name: "cargo test (linux, release)" From 549ab74bd63fc78a1947e1b680399823dc012d15 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 29 Apr 2025 22:19:29 +0100 Subject: [PATCH 0179/1161] [red-knot] Run py-fuzzer in CI to check for new panics (#17719) --- .github/workflows/ci.yaml | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5064763ef3f3fd..ec094d1d43f166 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -636,6 +636,53 @@ jobs: name: ecosystem-result path: ecosystem-result + fuzz-redknot: + name: "Fuzz for new red-knot panics" + runs-on: depot-ubuntu-22.04-16 + needs: + - cargo-test-linux + - determine_changes + # Only runs on pull requests, since that is the only we way we can find the base version for comparison. + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.red_knot == 'true' }} + timeout-minutes: 20 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + name: Download new red-knot binary + id: redknot-new + with: + name: red_knot + path: target/debug + - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8 + name: Download baseline red-knot binary + with: + name: red_knot + branch: ${{ github.event.pull_request.base.ref }} + workflow: "ci.yaml" + check_artifacts: true + - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + - name: Fuzz + env: + FORCE_COLOR: 1 + NEW_REDKNOT: ${{ steps.redknot-new.outputs.download-path }} + run: | + # Make executable, since artifact download doesn't preserve this + chmod +x "${PWD}/red_knot" "${NEW_REDKNOT}/red_knot" + + ( + uvx \ + --python="${PYTHON_VERSION}" \ + --from=./python/py-fuzzer \ + fuzz \ + --test-executable="${NEW_REDKNOT}/red_knot" \ + --baseline-executable="${PWD}/red_knot" \ + --only-new-bugs \ + --bin=red_knot \ + 0-500 + ) + cargo-shear: name: "cargo shear" runs-on: ubuntu-latest From f11d9cb509fa823b9c52ff799db7077199472677 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 30 Apr 2025 02:53:59 +0530 Subject: [PATCH 0180/1161] [red-knot] Support overloads for callable equivalence (#17698) ## Summary Part of #15383, this PR adds `is_equivalent_to` support for overloaded callables. This is mainly done by delegating it to the subtyping check in that two types A and B are considered equivalent if A is a subtype of B and B is a subtype of A. ## Test Plan Add test cases for overloaded callables in `is_equivalent_to.md` --- .../type_properties/is_equivalent_to.md | 61 ++++++++++++++++++- crates/red_knot_python_semantic/src/types.rs | 17 +++++- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index 3a234bb8a6cd57..69efe5958abc30 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -256,6 +256,65 @@ static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str ### Overloads -TODO +#### One overload + +`overloaded.pyi`: + +```pyi +from typing import overload + +class Grandparent: ... +class Parent(Grandparent): ... +class Child(Parent): ... + +@overload +def overloaded(a: Child) -> None: ... +@overload +def overloaded(a: Parent) -> None: ... +@overload +def overloaded(a: Grandparent) -> None: ... +``` + +```py +from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert +from overloaded import Grandparent, Parent, Child, overloaded + +def grandparent(a: Grandparent) -> None: ... + +static_assert(is_equivalent_to(CallableTypeOf[grandparent], CallableTypeOf[overloaded])) +static_assert(is_equivalent_to(CallableTypeOf[overloaded], CallableTypeOf[grandparent])) +``` + +#### Both overloads + +`overloaded.pyi`: + +```pyi +from typing import overload + +class Grandparent: ... +class Parent(Grandparent): ... +class Child(Parent): ... + +@overload +def pg(a: Parent) -> None: ... +@overload +def pg(a: Grandparent) -> None: ... + +@overload +def cpg(a: Child) -> None: ... +@overload +def cpg(a: Parent) -> None: ... +@overload +def cpg(a: Grandparent) -> None: ... +``` + +```py +from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert +from overloaded import pg, cpg + +static_assert(is_equivalent_to(CallableTypeOf[pg], CallableTypeOf[cpg])) +static_assert(is_equivalent_to(CallableTypeOf[cpg], CallableTypeOf[pg])) +``` [the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index dc7a789939dcf0..1047e929f06b0d 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -6984,11 +6984,22 @@ impl<'db> CallableType<'db> { fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { match (&**self.signatures(db), &**other.signatures(db)) { ([self_signature], [other_signature]) => { + // Common case: both callable types contain a single signature, use the custom + // equivalence check instead of delegating it to the subtype check. self_signature.is_equivalent_to(db, other_signature) } - _ => { - // TODO: overloads - false + (self_signatures, other_signatures) => { + if !self_signatures + .iter() + .chain(other_signatures.iter()) + .all(|signature| signature.is_fully_static(db)) + { + return false; + } + if self == other { + return true; + } + self.is_subtype_of(db, other) && other.is_subtype_of(db, self) } } } From 2bb99df394daf3c9675fdd7dadf8892a64ee2e8b Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 30 Apr 2025 08:58:31 +0200 Subject: [PATCH 0181/1161] [red-knot] Update salsa (#17730) --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- fuzz/Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5acc1c77d89fcc..e3384ec5e1496b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3444,8 +3444,8 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" -version = "0.20.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1" +version = "0.21.0" +source = "git+https://github.com/salsa-rs/salsa.git?rev=af811a34cef26932859b09bbefe50fbd8e8db06a#af811a34cef26932859b09bbefe50fbd8e8db06a" dependencies = [ "boxcar", "compact_str", @@ -3467,13 +3467,13 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.20.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1" +version = "0.21.0" +source = "git+https://github.com/salsa-rs/salsa.git?rev=af811a34cef26932859b09bbefe50fbd8e8db06a#af811a34cef26932859b09bbefe50fbd8e8db06a" [[package]] name = "salsa-macros" -version = "0.20.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1" +version = "0.21.0" +source = "git+https://github.com/salsa-rs/salsa.git?rev=af811a34cef26932859b09bbefe50fbd8e8db06a#af811a34cef26932859b09bbefe50fbd8e8db06a" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 67b5bbc0ff2387..4893c4cad636b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ rayon = { version = "1.10.0" } regex = { version = "1.10.2" } rustc-hash = { version = "2.0.0" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "c75b0161aba55965ab6ad8cc9aaee7dc177967f1" } +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "af811a34cef26932859b09bbefe50fbd8e8db06a" } schemars = { version = "0.8.16" } seahash = { version = "4.1.0" } serde = { version = "1.0.197", features = ["derive"] } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index a0d617b0f56ca5..315805460ccda9 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -29,7 +29,7 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter" } ruff_text_size = { path = "../crates/ruff_text_size" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "c75b0161aba55965ab6ad8cc9aaee7dc177967f1" } +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "af811a34cef26932859b09bbefe50fbd8e8db06a" } similar = { version = "2.5.0" } tracing = { version = "0.1.40" } From 4a621c2c12fb65fae343883ad17ce535411c5712 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 30 Apr 2025 09:32:13 +0200 Subject: [PATCH 0182/1161] [red-knot] Fix recording of negative visibility constraints (#17731) ## Summary We were previously recording wrong reachability constraints for negative branches. Instead of `[cond] AND (NOT [True])` below, we were recording `[cond] AND (NOT ([cond] AND [True]))`, i.e. we were negating not just the last predicate, but the `AND`-ed reachability constraint from last clause. With this fix, we now record the correct constraints for the example from #17723: ```py def _(cond: bool): if cond: # reachability: [cond] if True: # reachability: [cond] AND [True] pass else: # reachability: [cond] AND (NOT [True]) x ``` closes #17723 ## Test Plan * Regression test. * Verified the ecosystem changes --- .../resources/mdtest/unreachable.md | 16 ++++++++++++++++ .../src/semantic_index/builder.rs | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/unreachable.md b/crates/red_knot_python_semantic/resources/mdtest/unreachable.md index f6b70c724ec20a..e01ecdd3141487 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unreachable.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unreachable.md @@ -304,6 +304,22 @@ else: pass ``` +And for nested `if` statements: + +```py +def _(flag: bool): + if flag: + if sys.version_info >= (3, 11): + ExceptionGroup # no error here + else: + pass + + if sys.version_info < (3, 11): + pass + else: + ExceptionGroup # no error here +``` + The same works for ternary expressions: ```py diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index 22af0fda06d8c2..17aff301a5085b 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -635,7 +635,8 @@ impl<'db> SemanticIndexBuilder<'db> { .current_visibility_constraints_mut() .add_atom(predicate_id); self.current_use_def_map_mut() - .record_reachability_constraint(visibility_constraint) + .record_reachability_constraint(visibility_constraint); + visibility_constraint } /// Record the negation of a given reachability/visibility constraint. From 8a6787b39e178ff829d58d775b4d65d83b061c53 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 30 Apr 2025 08:57:49 +0100 Subject: [PATCH 0183/1161] [red-knot] Fix control flow for `assert` statements (#17702) ## Summary @sharkdp and I realised in our 1:1 this morning that our control flow for `assert` statements isn't quite accurate at the moment. Namely, for something like this: ```py def _(x: int | None): assert x is None, reveal_type(x) ``` we currently reveal `None` for `x` here, but this is incorrect. In actual fact, the `msg` expression of an `assert` statement (the expression after the comma) will only be evaluated if the test (`x is None`) evaluates to `False`. As such, we should be adding a constraint of `~None` to `x` in the `msg` expression, which should simplify the inferred type of `x` to `int` in that context (`(int | None) & ~None` -> `int`). ## Test Plan Mdtests added. --------- Co-authored-by: David Peter --- .../resources/mdtest/narrow/assert.md | 61 +++++++++++++++++++ .../resources/mdtest/unreachable.md | 9 +++ .../src/semantic_index/builder.rs | 49 ++++++++++++--- .../src/semantic_index/predicate.rs | 9 +++ 4 files changed, 118 insertions(+), 10 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md index c452e3c71dc76a..0fab83880ee9f9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md @@ -51,3 +51,64 @@ def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]): assert y not in (1, 2) reveal_type(y) # revealed: Literal[3] ``` + +## Assertions with messages + +```py +def _(x: int | None, y: int | None): + reveal_type(x) # revealed: int | None + assert x is None, reveal_type(x) # revealed: int + reveal_type(x) # revealed: None + + reveal_type(y) # revealed: int | None + assert isinstance(y, int), reveal_type(y) # revealed: None + reveal_type(y) # revealed: int +``` + +## Assertions with definitions inside the message + +```py +def one(x: int | None): + assert x is None, (y := x * 42) * reveal_type(y) # revealed: int + + # error: [unresolved-reference] + reveal_type(y) # revealed: Unknown + +def two(x: int | None, y: int | None): + assert x is None, (y := 42) * reveal_type(y) # revealed: Literal[42] + reveal_type(y) # revealed: int | None +``` + +## Assertions with `test` predicates that are statically known to always be `True` + +```py +assert True, (x := 1) + +# error: [unresolved-reference] +reveal_type(x) # revealed: Unknown + +assert False, (y := 1) + +# The `assert` statement is terminal if `test` resolves to `False`, +# so even though we know the `msg` branch will have been taken here +# (we know what the truthiness of `False is!), we also know that the +# `y` definition is not visible from this point in control flow +# (because this point in control flow is unreachable). +# We make sure that this does not emit an `[unresolved-reference]` +# diagnostic by adding a reachability constraint, +# but the inferred type is `Unknown`. +# +reveal_type(y) # revealed: Unknown +``` + +## Assertions with messages that reference definitions from the `test` + +```py +def one(x: int | None): + assert (y := x), reveal_type(y) # revealed: (int & ~AlwaysTruthy) | None + reveal_type(y) # revealed: int & ~AlwaysFalsy + +def two(x: int | None): + assert isinstance((y := x), int), reveal_type(y) # revealed: None + reveal_type(y) # revealed: int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unreachable.md b/crates/red_knot_python_semantic/resources/mdtest/unreachable.md index e01ecdd3141487..0348816b49d6fc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unreachable.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unreachable.md @@ -362,6 +362,15 @@ def f(): ExceptionGroup ``` +Similarly, assertions with statically-known falsy conditions can lead to unreachable code: + +```py +def f(): + assert sys.version_info > (3, 11) + + ExceptionGroup +``` + Finally, not that anyone would ever use it, but it also works for `while` loops: ```py diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index 17aff301a5085b..a633c2c5cfc5d1 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -532,11 +532,8 @@ impl<'db> SemanticIndexBuilder<'db> { /// Negates a predicate and adds it to the list of all predicates, does not record it. fn add_negated_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId { - let negated = Predicate { - node: predicate.node, - is_positive: false, - }; - self.current_use_def_map_mut().add_predicate(negated) + self.current_use_def_map_mut() + .add_predicate(predicate.negated()) } /// Records a previously added narrowing constraint by adding it to all live bindings. @@ -1383,14 +1380,46 @@ where } } - ast::Stmt::Assert(node) => { - self.visit_expr(&node.test); - let predicate = self.record_expression_narrowing_constraint(&node.test); - self.record_visibility_constraint(predicate); + ast::Stmt::Assert(ast::StmtAssert { + test, + msg, + range: _, + }) => { + // We model an `assert test, msg` statement here. Conceptually, we can think of + // this as being equivalent to the following: + // + // ```py + // if not test: + // msg + // + // + // + // ``` + // + // Importantly, the `msg` expression is only evaluated if the `test` expression is + // falsy. This is why we apply the negated `test` predicate as a narrowing and + // reachability constraint on the `msg` expression. + // + // The other important part is the ``. This lets us skip the usual merging of + // flow states and simplification of visibility constraints, since there is no way + // of getting out of that `msg` branch. We simply restore to the post-test state. + + self.visit_expr(test); + let predicate = self.build_predicate(test); - if let Some(msg) = &node.msg { + if let Some(msg) = msg { + let post_test = self.flow_snapshot(); + let negated_predicate = predicate.negated(); + self.record_narrowing_constraint(negated_predicate); + self.record_reachability_constraint(negated_predicate); self.visit_expr(msg); + self.record_visibility_constraint(negated_predicate); + self.flow_restore(post_test); } + + self.record_narrowing_constraint(predicate); + self.record_visibility_constraint(predicate); + self.record_reachability_constraint(predicate); } ast::Stmt::Assign(node) => { diff --git a/crates/red_knot_python_semantic/src/semantic_index/predicate.rs b/crates/red_knot_python_semantic/src/semantic_index/predicate.rs index c2885022e8cef2..1639aeaf5aa34c 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/predicate.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/predicate.rs @@ -49,6 +49,15 @@ pub(crate) struct Predicate<'db> { pub(crate) is_positive: bool, } +impl Predicate<'_> { + pub(crate) fn negated(self) -> Self { + Self { + node: self.node, + is_positive: !self.is_positive, + } + } +} + #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)] pub(crate) enum PredicateNode<'db> { Expression(Expression<'db>), From d94be0e7809aa881c7dc5c9e3c3e2ec0ccd9a968 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 30 Apr 2025 10:26:40 +0200 Subject: [PATCH 0184/1161] [red-knot] Include salsa backtrace in check and mdtest panic messages (#17732) Co-authored-by: David Peter --- Cargo.lock | 6 +-- Cargo.toml | 2 +- crates/red_knot_project/src/lib.rs | 26 ++++++++++-- .../src/types/infer.rs | 2 +- crates/red_knot_test/src/lib.rs | 19 ++++++++- crates/ruff/src/commands/format.rs | 4 +- crates/ruff_db/src/panic.rs | 41 ++++++++++++++++--- fuzz/Cargo.toml | 2 +- 8 files changed, 83 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3384ec5e1496b..2ec41817dc5214 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3445,7 +3445,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" version = "0.21.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=af811a34cef26932859b09bbefe50fbd8e8db06a#af811a34cef26932859b09bbefe50fbd8e8db06a" +source = "git+https://github.com/salsa-rs/salsa.git?rev=79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf#79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" dependencies = [ "boxcar", "compact_str", @@ -3468,12 +3468,12 @@ dependencies = [ [[package]] name = "salsa-macro-rules" version = "0.21.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=af811a34cef26932859b09bbefe50fbd8e8db06a#af811a34cef26932859b09bbefe50fbd8e8db06a" +source = "git+https://github.com/salsa-rs/salsa.git?rev=79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf#79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" [[package]] name = "salsa-macros" version = "0.21.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=af811a34cef26932859b09bbefe50fbd8e8db06a#af811a34cef26932859b09bbefe50fbd8e8db06a" +source = "git+https://github.com/salsa-rs/salsa.git?rev=79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf#79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 4893c4cad636b5..57934aed2517d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ rayon = { version = "1.10.0" } regex = { version = "1.10.2" } rustc-hash = { version = "2.0.0" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "af811a34cef26932859b09bbefe50fbd8e8db06a" } +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" } schemars = { version = "0.8.16" } seahash = { version = "4.1.0" } serde = { version = "1.0.197", features = ["derive"] } diff --git a/crates/red_knot_project/src/lib.rs b/crates/red_knot_project/src/lib.rs index 25897e6d72c6a9..cf953f2b26ec3d 100644 --- a/crates/red_knot_project/src/lib.rs +++ b/crates/red_knot_project/src/lib.rs @@ -20,6 +20,7 @@ use ruff_db::system::{SystemPath, SystemPathBuf}; use rustc_hash::FxHashSet; use salsa::Durability; use salsa::Setter; +use std::backtrace::BacktraceStatus; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::sync::Arc; use thiserror::Error; @@ -626,10 +627,27 @@ where )); if let Some(backtrace) = error.backtrace { - diagnostic.sub(SubDiagnostic::new( - Severity::Info, - format!("Backtrace:\n{backtrace}"), - )); + match backtrace.status() { + BacktraceStatus::Disabled => { + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + "run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information", + )); + } + BacktraceStatus::Captured => { + diagnostic.sub(SubDiagnostic::new( + Severity::Info, + format!("Backtrace:\n{backtrace}"), + )); + } + _ => {} + } + } + + if let Some(backtrace) = error.salsa_backtrace { + salsa::attach(db, || { + diagnostic.sub(SubDiagnostic::new(Severity::Info, backtrace.to_string())); + }); } Err(diagnostic) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index db5b2a53197f05..6016308878674b 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -8483,7 +8483,7 @@ mod tests { db.clear_salsa_events(); assert_file_diagnostics(&db, "src/a.py", &[]); let events = db.take_salsa_events(); - let cycles = salsa::plumbing::attach(&db, || { + let cycles = salsa::attach(&db, || { events .iter() .filter_map(|event| { diff --git a/crates/red_knot_test/src/lib.rs b/crates/red_knot_test/src/lib.rs index f57c559b87402c..66772c113c4dbb 100644 --- a/crates/red_knot_test/src/lib.rs +++ b/crates/red_knot_test/src/lib.rs @@ -18,6 +18,7 @@ use ruff_db::parsed::parsed_module; use ruff_db::system::{DbWithWritableSystem as _, SystemPath, SystemPathBuf}; use ruff_db::testing::{setup_logging, setup_logging_with_filter}; use ruff_source_file::{LineIndex, OneIndexed}; +use std::backtrace::BacktraceStatus; use std::fmt::Write; mod assertion; @@ -331,10 +332,24 @@ fn run_test( None => messages.push("Box".to_string()), } if let Some(backtrace) = info.backtrace { - if std::env::var("RUST_BACKTRACE").is_ok() { - messages.extend(backtrace.to_string().split('\n').map(String::from)); + match backtrace.status() { + BacktraceStatus::Disabled => { + let msg = "run with `RUST_BACKTRACE=1` environment variable to display a backtrace"; + messages.push(msg.to_string()); + } + BacktraceStatus::Captured => { + messages.extend(backtrace.to_string().split('\n').map(String::from)); + } + _ => {} } } + + if let Some(backtrace) = info.salsa_backtrace { + salsa::attach(db, || { + messages.extend(backtrace.to_string().split('\n').map(String::from)); + }); + } + by_line.push(OneIndexed::from_zero_indexed(0), messages); return Some(FileFailures { backtick_offsets: test_file.backtick_offsets, diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 66648fc42a1a46..6b1f5206f47c86 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -160,7 +160,7 @@ pub(crate) fn format( }), Err(error) => Err(FormatCommandError::Panic( Some(resolved_file.path().to_path_buf()), - error, + Box::new(error), )), }, ) @@ -635,7 +635,7 @@ impl<'a> FormatResults<'a> { pub(crate) enum FormatCommandError { Ignore(#[from] ignore::Error), Parse(#[from] DisplayParseError), - Panic(Option, PanicError), + Panic(Option, Box), Read(Option, SourceError), Format(Option, FormatModuleError), Write(Option, SourceError), diff --git a/crates/ruff_db/src/panic.rs b/crates/ruff_db/src/panic.rs index 3deda2d741fc82..67ebf940dc3b94 100644 --- a/crates/ruff_db/src/panic.rs +++ b/crates/ruff_db/src/panic.rs @@ -1,3 +1,4 @@ +use std::backtrace::BacktraceStatus; use std::cell::Cell; use std::panic::Location; use std::sync::OnceLock; @@ -7,6 +8,7 @@ pub struct PanicError { pub location: Option, pub payload: Payload, pub backtrace: Option, + pub salsa_backtrace: Option, } #[derive(Debug)] @@ -34,15 +36,35 @@ impl std::fmt::Display for PanicError { write!(f, ":\n{payload}")?; } if let Some(backtrace) = &self.backtrace { - writeln!(f, "\nBacktrace: {backtrace}")?; + match backtrace.status() { + BacktraceStatus::Disabled => { + writeln!( + f, + "\nrun with `RUST_BACKTRACE=1` environment variable to display a backtrace" + )?; + } + BacktraceStatus::Captured => { + writeln!(f, "\nBacktrace: {backtrace}")?; + } + _ => {} + } } Ok(()) } } +#[derive(Default)] +struct CapturedPanicInfo { + backtrace: Option, + location: Option, + salsa_backtrace: Option, +} + thread_local! { static CAPTURE_PANIC_INFO: Cell = const { Cell::new(false) }; - static LAST_BACKTRACE: Cell<(Option, Option)> = const { Cell::new((None, None)) }; + static LAST_BACKTRACE: Cell = const { + Cell::new(CapturedPanicInfo { backtrace: None, location: None, salsa_backtrace: None }) + }; } fn install_hook() { @@ -56,9 +78,13 @@ fn install_hook() { } let location = info.location().map(Location::to_string); - let backtrace = Some(std::backtrace::Backtrace::force_capture()); + let backtrace = Some(std::backtrace::Backtrace::capture()); - LAST_BACKTRACE.set((backtrace, location)); + LAST_BACKTRACE.set(CapturedPanicInfo { + backtrace, + location, + salsa_backtrace: salsa::Backtrace::capture(), + }); })); }); } @@ -92,12 +118,17 @@ where // Because of that, always take the payload from `catch_unwind` because it may have been transformed // by an inner `std::panic::catch_unwind` handlers and only use the information // from the custom handler to enrich the error with the backtrace and location. - let (backtrace, location) = LAST_BACKTRACE.with(Cell::take); + let CapturedPanicInfo { + location, + backtrace, + salsa_backtrace, + } = LAST_BACKTRACE.with(Cell::take); PanicError { location, payload: Payload(payload), backtrace, + salsa_backtrace, } }); CAPTURE_PANIC_INFO.set(prev_should_capture); diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 315805460ccda9..c8a4a9c7a4418d 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -29,7 +29,7 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter" } ruff_text_size = { path = "../crates/ruff_text_size" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "af811a34cef26932859b09bbefe50fbd8e8db06a" } +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" } similar = { version = "2.5.0" } tracing = { version = "0.1.40" } From b84b58760ee00747afba2a8b437145f0e2c40514 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 30 Apr 2025 11:58:55 +0100 Subject: [PATCH 0185/1161] [red-knot] Computing a type ordering for two non-normalized types is meaningless (#17734) --- .../src/types/type_ordering.rs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 64d9bb726a78a1..ba2adf6b409a7e 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -26,6 +26,17 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( left: &Type<'db>, right: &Type<'db>, ) -> Ordering { + debug_assert_eq!( + *left, + left.normalized(db), + "`left` must be normalized before a meaningful ordering can be established" + ); + debug_assert_eq!( + *right, + right.normalized(db), + "`right` must be normalized before a meaningful ordering can be established" + ); + if left == right { return Ordering::Equal; } @@ -85,19 +96,11 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::DataclassTransformer(_), _) => Ordering::Less, (_, Type::DataclassTransformer(_)) => Ordering::Greater, - (Type::Callable(left), Type::Callable(right)) => { - debug_assert_eq!(*left, left.normalized(db)); - debug_assert_eq!(*right, right.normalized(db)); - left.cmp(right) - } + (Type::Callable(left), Type::Callable(right)) => left.cmp(right), (Type::Callable(_), _) => Ordering::Less, (_, Type::Callable(_)) => Ordering::Greater, - (Type::Tuple(left), Type::Tuple(right)) => { - debug_assert_eq!(*left, left.normalized(db)); - debug_assert_eq!(*right, right.normalized(db)); - left.cmp(right) - } + (Type::Tuple(left), Type::Tuple(right)) => left.cmp(right), (Type::Tuple(_), _) => Ordering::Less, (_, Type::Tuple(_)) => Ordering::Greater, @@ -325,13 +328,6 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( } (Type::Intersection(left), Type::Intersection(right)) => { - debug_assert_eq!(*left, left.normalized(db)); - debug_assert_eq!(*right, right.normalized(db)); - - if left == right { - return Ordering::Equal; - } - // Lexicographically compare the elements of the two unequal intersections. let left_positive = left.positive(db); let right_positive = right.positive(db); @@ -356,7 +352,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( } } - unreachable!("Two equal intersections that both have sorted elements should share the same Salsa ID") + unreachable!("Two equal, normalized intersections should share the same Salsa ID") } } } From d1f359afbb2e616fcb0ba8e69565ed6a5970cc44 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 30 Apr 2025 12:03:10 +0100 Subject: [PATCH 0186/1161] [red-knot] Initial support for protocol types (#17682) --- .../resources/mdtest/import/builtins.md | 4 +- .../resources/mdtest/protocols.md | 345 +++++++++------- .../resources/mdtest/scopes/builtin.md | 4 +- crates/red_knot_python_semantic/src/symbol.rs | 2 +- crates/red_knot_python_semantic/src/types.rs | 368 ++++++++++++------ .../src/types/builder.rs | 8 +- .../src/types/class.rs | 139 +++++-- .../src/types/class_base.rs | 7 +- .../src/types/display.rs | 65 +++- .../src/types/infer.rs | 30 +- .../src/types/instance.rs | 190 ++++++++- .../src/types/known_instance.rs | 4 +- .../src/types/narrow.rs | 10 +- .../src/types/subclass_of.rs | 8 +- .../src/types/type_ordering.rs | 14 +- 15 files changed, 867 insertions(+), 331 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md index 1b2305fb410c2f..7d1a3f5e0bd49a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md +++ b/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md @@ -7,7 +7,7 @@ Builtin symbols can be explicitly imported: ```py import builtins -reveal_type(builtins.chr) # revealed: def chr(i: int | SupportsIndex, /) -> str +reveal_type(builtins.chr) # revealed: def chr(i: SupportsIndex, /) -> str ``` ## Implicit use of builtin @@ -15,7 +15,7 @@ reveal_type(builtins.chr) # revealed: def chr(i: int | SupportsIndex, /) -> str Or used implicitly: ```py -reveal_type(chr) # revealed: def chr(i: int | SupportsIndex, /) -> str +reveal_type(chr) # revealed: def chr(i: SupportsIndex, /) -> str reveal_type(str) # revealed: Literal[str] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 0af5a8b58cdd95..5f124e0549508f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -230,7 +230,7 @@ And it is also an error to use `Protocol` in type expressions: def f( x: Protocol, # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions" y: type[Protocol], # TODO: should emit `[invalid-type-form]` here too -) -> None: +): reveal_type(x) # revealed: Unknown # TODO: should be `type[Unknown]` @@ -266,9 +266,7 @@ class Bar(typing_extensions.Protocol): static_assert(typing_extensions.is_protocol(Foo)) static_assert(typing_extensions.is_protocol(Bar)) - -# TODO: should pass -static_assert(is_equivalent_to(Foo, Bar)) # error: [static-assert-error] +static_assert(is_equivalent_to(Foo, Bar)) ``` The same goes for `typing.runtime_checkable` and `typing_extensions.runtime_checkable`: @@ -284,9 +282,7 @@ class RuntimeCheckableBar(typing_extensions.Protocol): static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo)) static_assert(typing_extensions.is_protocol(RuntimeCheckableBar)) - -# TODO: should pass -static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar)) # error: [static-assert-error] +static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar)) # These should not error because the protocols are decorated with `@runtime_checkable` isinstance(object(), RuntimeCheckableFoo) @@ -488,21 +484,20 @@ class HasX(Protocol): class Foo: x: int -# TODO: these should pass -static_assert(is_subtype_of(Foo, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(Foo, HasX)) # error: [static-assert-error] +static_assert(is_subtype_of(Foo, HasX)) +static_assert(is_assignable_to(Foo, HasX)) class FooSub(Foo): ... -# TODO: these should pass -static_assert(is_subtype_of(FooSub, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(FooSub, HasX)) # error: [static-assert-error] +static_assert(is_subtype_of(FooSub, HasX)) +static_assert(is_assignable_to(FooSub, HasX)) class Bar: x: str -static_assert(not is_subtype_of(Bar, HasX)) -static_assert(not is_assignable_to(Bar, HasX)) +# TODO: these should pass +static_assert(not is_subtype_of(Bar, HasX)) # error: [static-assert-error] +static_assert(not is_assignable_to(Bar, HasX)) # error: [static-assert-error] class Baz: y: int @@ -524,14 +519,16 @@ class A: def x(self) -> int: return 42 -static_assert(not is_subtype_of(A, HasX)) -static_assert(not is_assignable_to(A, HasX)) +# TODO: these should pass +static_assert(not is_subtype_of(A, HasX)) # error: [static-assert-error] +static_assert(not is_assignable_to(A, HasX)) # error: [static-assert-error] class B: x: Final = 42 -static_assert(not is_subtype_of(A, HasX)) -static_assert(not is_assignable_to(A, HasX)) +# TODO: these should pass +static_assert(not is_subtype_of(A, HasX)) # error: [static-assert-error] +static_assert(not is_assignable_to(A, HasX)) # error: [static-assert-error] class IntSub(int): ... @@ -541,8 +538,10 @@ class C: # due to invariance, a type is only a subtype of `HasX` # if its `x` attribute is of type *exactly* `int`: # a subclass of `int` does not satisfy the interface -static_assert(not is_subtype_of(C, HasX)) -static_assert(not is_assignable_to(C, HasX)) +# +# TODO: these should pass +static_assert(not is_subtype_of(C, HasX)) # error: [static-assert-error] +static_assert(not is_assignable_to(C, HasX)) # error: [static-assert-error] ``` All attributes on frozen dataclasses and namedtuples are immutable, so instances of these classes @@ -556,22 +555,23 @@ from typing import NamedTuple class MutableDataclass: x: int -# TODO: these should pass -static_assert(is_subtype_of(MutableDataclass, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(MutableDataclass, HasX)) # error: [static-assert-error] +static_assert(is_subtype_of(MutableDataclass, HasX)) +static_assert(is_assignable_to(MutableDataclass, HasX)) @dataclass(frozen=True) class ImmutableDataclass: x: int -static_assert(not is_subtype_of(ImmutableDataclass, HasX)) -static_assert(not is_assignable_to(ImmutableDataclass, HasX)) +# TODO: these should pass +static_assert(not is_subtype_of(ImmutableDataclass, HasX)) # error: [static-assert-error] +static_assert(not is_assignable_to(ImmutableDataclass, HasX)) # error: [static-assert-error] class NamedTupleWithX(NamedTuple): x: int -static_assert(not is_subtype_of(NamedTupleWithX, HasX)) -static_assert(not is_assignable_to(NamedTupleWithX, HasX)) +# TODO: these should pass +static_assert(not is_subtype_of(NamedTupleWithX, HasX)) # error: [static-assert-error] +static_assert(not is_assignable_to(NamedTupleWithX, HasX)) # error: [static-assert-error] ``` However, a type with a read-write property `x` *does* satisfy the `HasX` protocol. The `HasX` @@ -590,9 +590,8 @@ class XProperty: def x(self, x: int) -> None: self._x = x**2 -# TODO: these should pass -static_assert(is_subtype_of(XProperty, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(XProperty, HasX)) # error: [static-assert-error] +static_assert(is_subtype_of(XProperty, HasX)) +static_assert(is_assignable_to(XProperty, HasX)) ``` Attribute members on protocol classes are allowed to have default values, just like instance @@ -717,9 +716,8 @@ from typing import Protocol class UniversalSet(Protocol): ... -# TODO: these should pass -static_assert(is_assignable_to(object, UniversalSet)) # error: [static-assert-error] -static_assert(is_subtype_of(object, UniversalSet)) # error: [static-assert-error] +static_assert(is_assignable_to(object, UniversalSet)) +static_assert(is_subtype_of(object, UniversalSet)) ``` Which means that `UniversalSet` here is in fact an equivalent type to `object`: @@ -727,8 +725,7 @@ Which means that `UniversalSet` here is in fact an equivalent type to `object`: ```py from knot_extensions import is_equivalent_to -# TODO: this should pass -static_assert(is_equivalent_to(UniversalSet, object)) # error: [static-assert-error] +static_assert(is_equivalent_to(UniversalSet, object)) ``` `object` is a subtype of certain other protocols too. Since all fully static types (whether nominal @@ -739,17 +736,16 @@ means that these protocols are also equivalent to `UniversalSet` and `object`: class SupportsStr(Protocol): def __str__(self) -> str: ... -# TODO: these should pass -static_assert(is_equivalent_to(SupportsStr, UniversalSet)) # error: [static-assert-error] -static_assert(is_equivalent_to(SupportsStr, object)) # error: [static-assert-error] +static_assert(is_equivalent_to(SupportsStr, UniversalSet)) +static_assert(is_equivalent_to(SupportsStr, object)) class SupportsClass(Protocol): - __class__: type + @property + def __class__(self) -> type: ... -# TODO: these should pass -static_assert(is_equivalent_to(SupportsClass, UniversalSet)) # error: [static-assert-error] -static_assert(is_equivalent_to(SupportsClass, SupportsStr)) # error: [static-assert-error] -static_assert(is_equivalent_to(SupportsClass, object)) # error: [static-assert-error] +static_assert(is_equivalent_to(SupportsClass, UniversalSet)) +static_assert(is_equivalent_to(SupportsClass, SupportsStr)) +static_assert(is_equivalent_to(SupportsClass, object)) ``` If a protocol contains members that are not defined on `object`, then that protocol will (like all @@ -786,8 +782,7 @@ class HasX(Protocol): class AlsoHasX(Protocol): x: int -# TODO: this should pass -static_assert(is_equivalent_to(HasX, AlsoHasX)) # error: [static-assert-error] +static_assert(is_equivalent_to(HasX, AlsoHasX)) ``` And unions containing equivalent protocols are recognised as equivalent, even when the order is not @@ -803,8 +798,7 @@ class AlsoHasY(Protocol): class A: ... class B: ... -# TODO: this should pass -static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A)) # error: [static-assert-error] +static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A)) ``` ## Intersections of protocols @@ -882,9 +876,9 @@ from knot_extensions import is_subtype_of, is_assignable_to, static_assert, Type class HasX(Protocol): x: int -# TODO: these should pass +# TODO: this should pass static_assert(is_subtype_of(TypeOf[module], HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(TypeOf[module], HasX)) # error: [static-assert-error] +static_assert(is_assignable_to(TypeOf[module], HasX)) class ExplicitProtocolSubtype(HasX, Protocol): y: int @@ -896,9 +890,8 @@ class ImplicitProtocolSubtype(Protocol): x: int y: str -# TODO: these should pass -static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX)) # error: [static-assert-error] -static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX)) # error: [static-assert-error] +static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX)) +static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX)) class Meta(type): x: int @@ -933,23 +926,24 @@ def f(obj: ClassVarXProto): class InstanceAttrX: x: int -static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto)) -static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto)) +# TODO: these should pass +static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto)) # error: [static-assert-error] +static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto)) # error: [static-assert-error] class PropertyX: @property def x(self) -> int: return 42 -static_assert(not is_assignable_to(PropertyX, ClassVarXProto)) -static_assert(not is_subtype_of(PropertyX, ClassVarXProto)) +# TODO: these should pass +static_assert(not is_assignable_to(PropertyX, ClassVarXProto)) # error: [static-assert-error] +static_assert(not is_subtype_of(PropertyX, ClassVarXProto)) # error: [static-assert-error] class ClassVarX: x: ClassVar[int] = 42 -# TODO: these should pass -static_assert(is_assignable_to(ClassVarX, ClassVarXProto)) # error: [static-assert-error] -static_assert(is_subtype_of(ClassVarX, ClassVarXProto)) # error: [static-assert-error] +static_assert(is_assignable_to(ClassVarX, ClassVarXProto)) +static_assert(is_subtype_of(ClassVarX, ClassVarXProto)) ``` This is mentioned by the @@ -976,18 +970,16 @@ class HasXProperty(Protocol): class XAttr: x: int -# TODO: these should pass -static_assert(is_subtype_of(XAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XAttr, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XAttr, HasXProperty)) +static_assert(is_assignable_to(XAttr, HasXProperty)) class XReadProperty: @property def x(self) -> int: return 42 -# TODO: these should pass -static_assert(is_subtype_of(XReadProperty, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XReadProperty, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XReadProperty, HasXProperty)) +static_assert(is_assignable_to(XReadProperty, HasXProperty)) class XReadWriteProperty: @property @@ -997,22 +989,20 @@ class XReadWriteProperty: @x.setter def x(self, val: int) -> None: ... -# TODO: these should pass -static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) +static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) class XClassVar: x: ClassVar[int] = 42 -static_assert(is_subtype_of(XClassVar, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XClassVar, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XClassVar, HasXProperty)) +static_assert(is_assignable_to(XClassVar, HasXProperty)) class XFinal: x: Final = 42 -# TODO: these should pass -static_assert(is_subtype_of(XFinal, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XFinal, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XFinal, HasXProperty)) +static_assert(is_assignable_to(XFinal, HasXProperty)) ``` A read-only property on a protocol, unlike a mutable attribute, is covariant: `XSub` in the below @@ -1025,9 +1015,8 @@ class MyInt(int): ... class XSub: x: MyInt -# TODO: these should pass -static_assert(is_subtype_of(XSub, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XSub, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XSub, HasXProperty)) +static_assert(is_assignable_to(XSub, HasXProperty)) ``` A read/write property on a protocol, where the getter returns the same type that the setter takes, @@ -1043,17 +1032,17 @@ class HasMutableXProperty(Protocol): class XAttr: x: int -# TODO: these should pass -static_assert(is_subtype_of(XAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XAttr, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XAttr, HasXProperty)) +static_assert(is_assignable_to(XAttr, HasXProperty)) class XReadProperty: @property def x(self) -> int: return 42 -static_assert(not is_subtype_of(XReadProperty, HasXProperty)) -static_assert(not is_assignable_to(XReadProperty, HasXProperty)) +# TODO: these should pass +static_assert(not is_subtype_of(XReadProperty, HasXProperty)) # error: [static-assert-error] +static_assert(not is_assignable_to(XReadProperty, HasXProperty)) # error: [static-assert-error] class XReadWriteProperty: @property @@ -1063,15 +1052,15 @@ class XReadWriteProperty: @x.setter def x(self, val: int) -> None: ... -# TODO: these should pass -static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) +static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) class XSub: x: MyInt -static_assert(not is_subtype_of(XSub, HasXProperty)) -static_assert(not is_assignable_to(XSub, HasXProperty)) +# TODO: should pass +static_assert(not is_subtype_of(XSub, HasXProperty)) # error: [static-assert-error] +static_assert(not is_assignable_to(XSub, HasXProperty)) # error: [static-assert-error] ``` A protocol with a read/write property `x` is exactly equivalent to a protocol with a mutable @@ -1083,16 +1072,13 @@ from knot_extensions import is_equivalent_to class HasMutableXAttr(Protocol): x: int -# TODO: this should pass -static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error: [static-assert-error] +static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) -# TODO: these should pass -static_assert(is_subtype_of(HasMutableXAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(HasMutableXAttr, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(HasMutableXAttr, HasXProperty)) +static_assert(is_assignable_to(HasMutableXAttr, HasXProperty)) -# TODO: these should pass -static_assert(is_subtype_of(HasMutableXProperty, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(HasMutableXProperty, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(HasMutableXProperty, HasXProperty)) +static_assert(is_assignable_to(HasMutableXProperty, HasXProperty)) ``` A read/write property on a protocol, where the setter accepts a subtype of the type returned by the @@ -1119,9 +1105,8 @@ class HasAsymmetricXProperty(Protocol): class XAttr: x: int -# TODO: these should pass -static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty)) +static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty)) ``` The end conclusion of this is that the getter-returned type of a property is always covariant and @@ -1132,9 +1117,8 @@ regular mutable attribute, where the implied getter-returned and setter-accepted class XAttrSub: x: MyInt -# TODO: these should pass -static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty)) +static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty)) class MyIntSub(MyInt): pass @@ -1142,8 +1126,9 @@ class MyIntSub(MyInt): class XAttrSubSub: x: MyIntSub -static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty)) -static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty)) +# TODO: should pass +static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty)) # error: [static-assert-error] +static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty)) # error: [static-assert-error] ``` An asymmetric property on a protocol can also be satisfied by an asymmetric property on a nominal @@ -1159,9 +1144,8 @@ class XAsymmetricProperty: @x.setter def x(self, x: int) -> None: ... -# TODO: these should pass -static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty)) +static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty)) ``` A custom descriptor attribute on the nominal class will also suffice: @@ -1176,9 +1160,8 @@ class Descriptor: class XCustomDescriptor: x: Descriptor = Descriptor() -# TODO: these should pass -static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty)) +static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty)) ``` Moreover, a read-only property on a protocol can be satisfied by a nominal class that defines a @@ -1191,19 +1174,20 @@ class HasGetAttr: def __getattr__(self, attr: str) -> int: return 42 -# TODO: these should pass -static_assert(is_subtype_of(HasGetAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(HasGetAttr, HasXProperty)) # error: [static-assert-error] +static_assert(is_subtype_of(HasGetAttr, HasXProperty)) +static_assert(is_assignable_to(HasGetAttr, HasXProperty)) -static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) -static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) +# TODO: these should pass +static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) # error: [static-assert-error] +static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) # error: [static-assert-error] class HasGetAttrWithUnsuitableReturn: def __getattr__(self, attr: str) -> tuple[int, int]: return (1, 2) -static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty)) -static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty)) +# TODO: these should pass +static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty)) # error: [static-assert-error] +static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty)) # error: [static-assert-error] class HasGetAttrAndSetAttr: def __getattr__(self, attr: str) -> MyInt: @@ -1211,9 +1195,10 @@ class HasGetAttrAndSetAttr: def __setattr__(self, attr: str, value: int) -> None: ... +static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty)) +static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty)) + # TODO: these should pass -static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty)) # error: [static-assert-error] -static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty)) # error: [static-assert-error] static_assert(is_subtype_of(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error] static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error] ``` @@ -1363,12 +1348,12 @@ from knot_extensions import is_subtype_of, is_assignable_to class NominalWithX: x: int = 42 -# TODO: these should pass -static_assert(is_assignable_to(NominalWithX, FullyStatic)) # error: [static-assert-error] -static_assert(is_assignable_to(NominalWithX, NotFullyStatic)) # error: [static-assert-error] -static_assert(is_subtype_of(NominalWithX, FullyStatic)) # error: [static-assert-error] +static_assert(is_assignable_to(NominalWithX, FullyStatic)) +static_assert(is_assignable_to(NominalWithX, NotFullyStatic)) +static_assert(is_subtype_of(NominalWithX, FullyStatic)) -static_assert(not is_subtype_of(NominalWithX, NotFullyStatic)) +# TODO: this should pass +static_assert(not is_subtype_of(NominalWithX, NotFullyStatic)) # error: [static-assert-error] ``` Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to @@ -1418,30 +1403,124 @@ static_assert(not is_fully_static(NoParameterAnnotation)) # error: [static-asse static_assert(not is_fully_static(NoReturnAnnotation)) # error: [static-assert-error] ``` -## `typing.SupportsIndex` and `typing.Sized` +## Callable protocols -`typing.SupportsIndex` is already somewhat supported through some special-casing in red-knot. +An instance of a protocol type is callable if the protocol defines a `__call__` method: ```py -from typing import SupportsIndex, Literal +from typing import Protocol -def _(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex): - a: SupportsIndex = some_int - b: SupportsIndex = some_literal_int - c: SupportsIndex = some_indexable +class CallMeMaybe(Protocol): + def __call__(self, x: int) -> str: ... + +def f(obj: CallMeMaybe): + reveal_type(obj(42)) # revealed: str + obj("bar") # error: [invalid-argument-type] +``` + +An instance of a protocol like this can be assignable to a `Callable` type, but only if it has the +right signature: + +```py +from typing import Callable +from knot_extensions import is_subtype_of, is_assignable_to, static_assert + +static_assert(is_subtype_of(CallMeMaybe, Callable[[int], str])) +static_assert(is_assignable_to(CallMeMaybe, Callable[[int], str])) +static_assert(not is_subtype_of(CallMeMaybe, Callable[[str], str])) +static_assert(not is_assignable_to(CallMeMaybe, Callable[[str], str])) +static_assert(not is_subtype_of(CallMeMaybe, Callable[[CallMeMaybe, int], str])) +static_assert(not is_assignable_to(CallMeMaybe, Callable[[CallMeMaybe, int], str])) + +def g(obj: Callable[[int], str], obj2: CallMeMaybe, obj3: Callable[[str], str]): + obj = obj2 + obj3 = obj2 # error: [invalid-assignment] +``` + +By the same token, a `Callable` type can also be assignable to a protocol-instance type if the +signature implied by the `Callable` type is assignable to the signature of the `__call__` method +specified by the protocol: + +```py +class Foo(Protocol): + def __call__(self, x: int, /) -> str: ... + +# TODO: these fail because we don't yet understand that all `Callable` types have a `__call__` method, +# and we therefore don't think that the `Callable` type is assignable to `Foo`. They should pass. +static_assert(is_subtype_of(Callable[[int], str], Foo)) # error: [static-assert-error] +static_assert(is_assignable_to(Callable[[int], str], Foo)) # error: [static-assert-error] + +static_assert(not is_subtype_of(Callable[[str], str], Foo)) +static_assert(not is_assignable_to(Callable[[str], str], Foo)) +static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo)) +static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo)) + +def h(obj: Callable[[int], str], obj2: Foo, obj3: Callable[[str], str]): + # TODO: this fails because we don't yet understand that all `Callable` types have a `__call__` method, + # and we therefore don't think that the `Callable` type is assignable to `Foo`. It should pass. + obj2 = obj # error: [invalid-assignment] + + # This diagnostic is correct, however. + obj2 = obj3 # error: [invalid-assignment] +``` + +## Protocols are never singleton types, and are never single-valued types + +It *might* be possible to have a singleton protocol-instance type...? + +For example, `WeirdAndWacky` in the following snippet only has a single possible inhabitant: `None`! +It is thus a singleton type. However, going out of our way to recognise it as such is probably not +worth it. Such cases should anyway be exceedingly rare and/or contrived. + +```py +from typing import Protocol, Callable +from knot_extensions import is_singleton, is_single_valued + +class WeirdAndWacky(Protocol): + @property + def __class__(self) -> Callable[[], None]: ... + +reveal_type(is_singleton(WeirdAndWacky)) # revealed: Literal[False] +reveal_type(is_single_valued(WeirdAndWacky)) # revealed: Literal[False] ``` -The same goes for `typing.Sized`: +## Integration test: `typing.SupportsIndex` and `typing.Sized` + +`typing.SupportsIndex` and `typing.Sized` are two protocols that are very commonly used in the wild. ```py -from typing import Sized +from typing import SupportsIndex, Sized, Literal + +def one(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex): + a: SupportsIndex = some_int + b: SupportsIndex = some_literal_int + c: SupportsIndex = some_indexable -def _(some_list: list, some_tuple: tuple[int, str], some_sized: Sized): +def two(some_list: list, some_tuple: tuple[int, str], some_sized: Sized): a: Sized = some_list b: Sized = some_tuple c: Sized = some_sized ``` +## Regression test: narrowing with self-referential protocols + +This snippet caused us to panic on an early version of the implementation for protocols. + +```py +from typing import Protocol + +class A(Protocol): + def x(self) -> "B | A": ... + +class B(Protocol): + def y(self): ... + +obj = something_unresolvable # error: [unresolved-reference] +reveal_type(obj) # revealed: Unknown +if isinstance(obj, (B, A)): + reveal_type(obj) # revealed: (Unknown & B) | (Unknown & A) +``` + ## TODO Add tests for: diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md index 5aa4175f8e6591..fc2ca70f97bab2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md @@ -13,7 +13,7 @@ if returns_bool(): chr: int = 1 def f(): - reveal_type(chr) # revealed: int | (def chr(i: int | SupportsIndex, /) -> str) + reveal_type(chr) # revealed: int | (def chr(i: SupportsIndex, /) -> str) ``` ## Conditionally global or builtin, with annotation @@ -28,5 +28,5 @@ if returns_bool(): chr: int = 1 def f(): - reveal_type(chr) # revealed: int | (def chr(i: int | SupportsIndex, /) -> str) + reveal_type(chr) # revealed: int | (def chr(i: SupportsIndex, /) -> str) ``` diff --git a/crates/red_knot_python_semantic/src/symbol.rs b/crates/red_knot_python_semantic/src/symbol.rs index 3be4ce8d02fc5f..b90103585abe63 100644 --- a/crates/red_knot_python_semantic/src/symbol.rs +++ b/crates/red_knot_python_semantic/src/symbol.rs @@ -1114,7 +1114,7 @@ mod tests { fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Symbol<'db>) { assert!(matches!( symbol, - Symbol::Type(Type::Instance(_), Boundness::Bound) + Symbol::Type(Type::NominalInstance(_), Boundness::Bound) )); assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db)); } diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1047e929f06b0d..826684beb100b7 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -51,7 +51,8 @@ pub(crate) use crate::types::narrow::infer_narrowing_constraint; use crate::types::signatures::{Parameter, ParameterForm, Parameters}; use crate::{Db, FxOrderSet, Module, Program}; pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass}; -pub(crate) use instance::InstanceType; +use instance::Protocol; +pub(crate) use instance::{NominalInstanceType, ProtocolInstanceType}; pub(crate) use known_instance::KnownInstanceType; mod builder; @@ -489,7 +490,10 @@ pub enum Type<'db> { SubclassOf(SubclassOfType<'db>), /// The set of Python objects with the given class in their __class__'s method resolution order. /// Construct this variant using the `Type::instance` constructor function. - Instance(InstanceType<'db>), + NominalInstance(NominalInstanceType<'db>), + /// The set of Python objects that conform to the interface described by a given protocol. + /// Construct this variant using the `Type::instance` constructor function. + ProtocolInstance(ProtocolInstanceType<'db>), /// A single Python object that requires special treatment in the type system KnownInstance(KnownInstanceType<'db>), /// An instance of `builtins.property` @@ -524,7 +528,7 @@ pub enum Type<'db> { TypeVar(TypeVarInstance<'db>), // A bound super object like `super()` or `super(A, A())` // This type doesn't handle an unbound super object like `super(A)`; for that we just use - // a `Type::Instance` of `builtins.super`. + // a `Type::NominalInstance` of `builtins.super`. BoundSuper(BoundSuperType<'db>), // TODO protocols, overloads, generics } @@ -557,17 +561,17 @@ impl<'db> Type<'db> { } fn is_none(&self, db: &'db dyn Db) -> bool { - self.into_instance() + self.into_nominal_instance() .is_some_and(|instance| instance.class().is_known(db, KnownClass::NoneType)) } fn is_bool(&self, db: &'db dyn Db) -> bool { - self.into_instance() + self.into_nominal_instance() .is_some_and(|instance| instance.class().is_known(db, KnownClass::Bool)) } pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { - self.into_instance().is_some_and(|instance| { + self.into_nominal_instance().is_some_and(|instance| { instance .class() .is_known(db, KnownClass::NotImplementedType) @@ -575,7 +579,7 @@ impl<'db> Type<'db> { } pub fn is_object(&self, db: &'db dyn Db) -> bool { - self.into_instance() + self.into_nominal_instance() .is_some_and(|instance| instance.class().is_object(db)) } @@ -597,7 +601,7 @@ impl<'db> Type<'db> { | Self::BooleanLiteral(_) | Self::BytesLiteral(_) | Self::FunctionLiteral(_) - | Self::Instance(_) + | Self::NominalInstance(_) | Self::ModuleLiteral(_) | Self::ClassLiteral(_) | Self::KnownInstance(_) @@ -681,6 +685,8 @@ impl<'db> Type<'db> { .iter() .any(|ty| ty.contains_todo(db)) } + + Self::ProtocolInstance(protocol) => protocol.contains_todo(), } } @@ -894,6 +900,7 @@ impl<'db> Type<'db> { /// as these are irrelevant to whether a callable type `X` is equivalent to a callable type `Y`. /// - Strips the types of default values from parameters in `Callable` types: only whether a parameter /// *has* or *does not have* a default value is relevant to whether two `Callable` types are equivalent. + /// - Converts class-based protocols into synthesized protocols #[must_use] pub fn normalized(self, db: &'db dyn Db) -> Self { match self { @@ -901,8 +908,9 @@ impl<'db> Type<'db> { Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)), Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)), Type::Callable(callable) => Type::Callable(callable.normalized(db)), + Type::ProtocolInstance(protocol) => protocol.normalized(db), Type::LiteralString - | Type::Instance(_) + | Type::NominalInstance(_) | Type::PropertyInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy @@ -996,7 +1004,7 @@ impl<'db> Type<'db> { (_, Type::Never) => false, // Everything is a subtype of `object`. - (_, Type::Instance(instance)) if instance.class().is_object(db) => true, + (_, Type::NominalInstance(instance)) if instance.class().is_object(db) => true, // A fully static typevar is always a subtype of itself, and is never a subtype of any // other typevar, since there is no guarantee that they will be specialized to the same @@ -1160,6 +1168,22 @@ impl<'db> Type<'db> { false } + (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => { + let call_symbol = self.member(db, "__call__").symbol; + match call_symbol { + Symbol::Type(Type::BoundMethod(call_function), _) => call_function + .into_callable_type(db) + .is_subtype_of(db, target), + _ => false, + } + } + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.is_subtype_of(db, right) + } + // A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`. + (Type::ProtocolInstance(_), _) => false, + (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol), + (Type::Callable(_), _) => { // TODO: Implement subtyping between callable types and other types like // function literals, bound methods, class literals, `type[]`, etc.) @@ -1248,7 +1272,7 @@ impl<'db> Type<'db> { metaclass_instance_type.is_subtype_of(db, target) }), - // For example: `Type::KnownInstance(KnownInstanceType::Type)` is a subtype of `Type::Instance(_SpecialForm)`, + // For example: `Type::KnownInstance(KnownInstanceType::Type)` is a subtype of `Type::NominalInstance(_SpecialForm)`, // because `Type::KnownInstance(KnownInstanceType::Type)` is a set with exactly one runtime value in it // (the symbol `typing.Type`), and that symbol is known to be an instance of `typing._SpecialForm` at runtime. (Type::KnownInstance(left), right) => { @@ -1257,20 +1281,10 @@ impl<'db> Type<'db> { // `bool` is a subtype of `int`, because `bool` subclasses `int`, // which means that all instances of `bool` are also instances of `int` - (Type::Instance(self_instance), Type::Instance(target_instance)) => { + (Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => { self_instance.is_subtype_of(db, target_instance) } - (Type::Instance(_), Type::Callable(_)) => { - let call_symbol = self.member(db, "__call__").symbol; - match call_symbol { - Symbol::Type(Type::BoundMethod(call_function), _) => call_function - .into_callable_type(db) - .is_subtype_of(db, target), - _ => false, - } - } - (Type::PropertyInstance(_), _) => KnownClass::Property .to_instance(db) .is_subtype_of(db, target), @@ -1280,7 +1294,7 @@ impl<'db> Type<'db> { // Other than the special cases enumerated above, `Instance` types and typevars are // never subtypes of any other variants - (Type::Instance(_) | Type::TypeVar(_), _) => false, + (Type::NominalInstance(_) | Type::TypeVar(_), _) => false, } } @@ -1302,7 +1316,7 @@ impl<'db> Type<'db> { // All types are assignable to `object`. // TODO this special case might be removable once the below cases are comprehensive - (_, Type::Instance(instance)) if instance.class().is_object(db) => true, + (_, Type::NominalInstance(instance)) if instance.class().is_object(db) => true, // A typevar is always assignable to itself, and is never assignable to any other // typevar, since there is no guarantee that they will be specialized to the same @@ -1436,7 +1450,7 @@ impl<'db> Type<'db> { // subtypes of `type[object]` are `type[...]` types (or `Never`), and `type[Any]` can // materialize to any `type[...]` type (or to `type[Never]`, which is equivalent to // `Never`.) - (Type::SubclassOf(subclass_of_ty), Type::Instance(_)) + (Type::SubclassOf(subclass_of_ty), Type::NominalInstance(_)) if subclass_of_ty.is_dynamic() && (KnownClass::Type .to_instance(db) @@ -1448,44 +1462,14 @@ impl<'db> Type<'db> { // Any type that is assignable to `type[object]` is also assignable to `type[Any]`, // because `type[Any]` can materialize to `type[object]`. - (Type::Instance(_), Type::SubclassOf(subclass_of_ty)) + (Type::NominalInstance(_), Type::SubclassOf(subclass_of_ty)) if subclass_of_ty.is_dynamic() && self.is_assignable_to(db, KnownClass::Type.to_instance(db)) => { true } - // TODO: This is a workaround to avoid false positives (e.g. when checking function calls - // with `SupportsIndex` parameters), which should be removed when we understand protocols. - (lhs, Type::Instance(instance)) - if instance.class().is_known(db, KnownClass::SupportsIndex) => - { - match lhs { - Type::Instance(instance) - if matches!( - instance.class().known(db), - Some(KnownClass::Int | KnownClass::SupportsIndex) - ) => - { - true - } - Type::IntLiteral(_) => true, - _ => false, - } - } - - // TODO: ditto for avoiding false positives when checking function calls with `Sized` parameters. - (lhs, Type::Instance(instance)) if instance.class().is_known(db, KnownClass::Sized) => { - matches!( - lhs.to_meta_type(db).member(db, "__len__"), - SymbolAndQualifiers { - symbol: Symbol::Type(..), - .. - } - ) - } - - (Type::Instance(self_instance), Type::Instance(target_instance)) => { + (Type::NominalInstance(self_instance), Type::NominalInstance(target_instance)) => { self_instance.is_assignable_to(db, target_instance) } @@ -1493,7 +1477,7 @@ impl<'db> Type<'db> { self_callable.is_assignable_to(db, target_callable) } - (Type::Instance(_), Type::Callable(_)) => { + (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => { let call_symbol = self.member(db, "__call__").symbol; match call_symbol { Symbol::Type(Type::BoundMethod(call_function), _) => call_function @@ -1520,6 +1504,15 @@ impl<'db> Type<'db> { .into_callable_type(db) .is_assignable_to(db, target), + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.is_assignable_to(db, right) + } + // Other than the dynamic types such as `Any`/`Unknown`/`Todo` handled above, + // a protocol instance can never be assignable to a nominal type, + // with the *sole* exception of `object`. + (Type::ProtocolInstance(_), _) => false, + (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol), + // TODO other types containing gradual forms _ => self.is_subtype_of(db, target), } @@ -1540,7 +1533,16 @@ impl<'db> Type<'db> { } (Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right), (Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right), - (Type::Instance(left), Type::Instance(right)) => left.is_equivalent_to(db, right), + (Type::NominalInstance(left), Type::NominalInstance(right)) => { + left.is_equivalent_to(db, right) + } + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.is_equivalent_to(db, right) + } + (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) + | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { + n.class().is_object(db) && protocol.normalized(db) == nominal + } _ => self == other && self.is_fully_static(db) && other.is_fully_static(db), } } @@ -1575,7 +1577,7 @@ impl<'db> Type<'db> { (Type::TypeVar(first), Type::TypeVar(second)) => first == second, - (Type::Instance(first), Type::Instance(second)) => { + (Type::NominalInstance(first), Type::NominalInstance(second)) => { first.is_gradual_equivalent_to(db, second) } @@ -1591,6 +1593,13 @@ impl<'db> Type<'db> { first.is_gradual_equivalent_to(db, second) } + (Type::ProtocolInstance(first), Type::ProtocolInstance(second)) => { + first.is_gradual_equivalent_to(db, second) + } + (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) + | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { + n.class().is_object(db) && protocol.normalized(db) == nominal + } _ => false, } } @@ -1791,6 +1800,68 @@ impl<'db> Type<'db> { ty.bool(db).is_always_true() } + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.is_disjoint_from(db, right) + } + + // TODO: we could also consider `protocol` to be disjoint from `nominal` if `nominal` + // has the right member but the type of its member is disjoint from the type of the + // member on `protocol`. + (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) + | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { + n.class().is_final(db) && !nominal.satisfies_protocol(db, protocol) + } + + ( + ty @ (Type::LiteralString + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::BooleanLiteral(..) + | Type::SliceLiteral(..) + | Type::ClassLiteral(..) + | Type::FunctionLiteral(..) + | Type::ModuleLiteral(..) + | Type::GenericAlias(..) + | Type::IntLiteral(..)), + Type::ProtocolInstance(protocol), + ) + | ( + Type::ProtocolInstance(protocol), + ty @ (Type::LiteralString + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::BooleanLiteral(..) + | Type::SliceLiteral(..) + | Type::ClassLiteral(..) + | Type::FunctionLiteral(..) + | Type::ModuleLiteral(..) + | Type::GenericAlias(..) + | Type::IntLiteral(..)), + ) => !ty.satisfies_protocol(db, protocol), + + (Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance)) + | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => { + !known_instance + .instance_fallback(db) + .satisfies_protocol(db, protocol) + } + + (Type::Callable(_), Type::ProtocolInstance(_)) + | (Type::ProtocolInstance(_), Type::Callable(_)) => { + // TODO disjointness between `Callable` and `ProtocolInstance` + false + } + + (Type::Tuple(..), Type::ProtocolInstance(..)) + | (Type::ProtocolInstance(..), Type::Tuple(..)) => { + // Currently we do not make any general assumptions about the disjointness of a `Tuple` type + // and a `ProtocolInstance` type because a `Tuple` type can be an instance of a tuple + // subclass. + // + // TODO when we capture the types of the protocol members, we can improve on this. + false + } + // for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`, // so although the type is dynamic we can still determine disjointedness in some situations (Type::SubclassOf(subclass_of_ty), other) @@ -1803,8 +1874,8 @@ impl<'db> Type<'db> { .is_disjoint_from(db, other), }, - (Type::KnownInstance(known_instance), Type::Instance(instance)) - | (Type::Instance(instance), Type::KnownInstance(known_instance)) => { + (Type::KnownInstance(known_instance), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => { !known_instance.is_instance_of(db, instance.class()) } @@ -1813,8 +1884,8 @@ impl<'db> Type<'db> { known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db)) } - (Type::BooleanLiteral(..), Type::Instance(instance)) - | (Type::Instance(instance), Type::BooleanLiteral(..)) => { + (Type::BooleanLiteral(..), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::BooleanLiteral(..)) => { // A `Type::BooleanLiteral()` must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) !KnownClass::Bool.is_subclass_of(db, instance.class()) @@ -1822,8 +1893,8 @@ impl<'db> Type<'db> { (Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true, - (Type::IntLiteral(..), Type::Instance(instance)) - | (Type::Instance(instance), Type::IntLiteral(..)) => { + (Type::IntLiteral(..), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::IntLiteral(..)) => { // A `Type::IntLiteral()` must be an instance of exactly `int` // (it cannot be an instance of an `int` subclass) !KnownClass::Int.is_subclass_of(db, instance.class()) @@ -1834,8 +1905,8 @@ impl<'db> Type<'db> { (Type::StringLiteral(..), Type::LiteralString) | (Type::LiteralString, Type::StringLiteral(..)) => false, - (Type::StringLiteral(..) | Type::LiteralString, Type::Instance(instance)) - | (Type::Instance(instance), Type::StringLiteral(..) | Type::LiteralString) => { + (Type::StringLiteral(..) | Type::LiteralString, Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::StringLiteral(..) | Type::LiteralString) => { // A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str` // (it cannot be an instance of a `str` subclass) !KnownClass::Str.is_subclass_of(db, instance.class()) @@ -1844,15 +1915,15 @@ impl<'db> Type<'db> { (Type::LiteralString, Type::LiteralString) => false, (Type::LiteralString, _) | (_, Type::LiteralString) => true, - (Type::BytesLiteral(..), Type::Instance(instance)) - | (Type::Instance(instance), Type::BytesLiteral(..)) => { + (Type::BytesLiteral(..), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::BytesLiteral(..)) => { // A `Type::BytesLiteral()` must be an instance of exactly `bytes` // (it cannot be an instance of a `bytes` subclass) !KnownClass::Bytes.is_subclass_of(db, instance.class()) } - (Type::SliceLiteral(..), Type::Instance(instance)) - | (Type::Instance(instance), Type::SliceLiteral(..)) => { + (Type::SliceLiteral(..), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::SliceLiteral(..)) => { // A `Type::SliceLiteral` must be an instance of exactly `slice` // (it cannot be an instance of a `slice` subclass) !KnownClass::Slice.is_subclass_of(db, instance.class()) @@ -1861,17 +1932,19 @@ impl<'db> Type<'db> { // A class-literal type `X` is always disjoint from an instance type `Y`, // unless the type expressing "all instances of `Z`" is a subtype of of `Y`, // where `Z` is `X`'s metaclass. - (Type::ClassLiteral(class), instance @ Type::Instance(_)) - | (instance @ Type::Instance(_), Type::ClassLiteral(class)) => !class - .metaclass_instance_type(db) - .is_subtype_of(db, instance), - (Type::GenericAlias(alias), instance @ Type::Instance(_)) - | (instance @ Type::Instance(_), Type::GenericAlias(alias)) => !ClassType::from(alias) + (Type::ClassLiteral(class), instance @ Type::NominalInstance(_)) + | (instance @ Type::NominalInstance(_), Type::ClassLiteral(class)) => !class .metaclass_instance_type(db) .is_subtype_of(db, instance), + (Type::GenericAlias(alias), instance @ Type::NominalInstance(_)) + | (instance @ Type::NominalInstance(_), Type::GenericAlias(alias)) => { + !ClassType::from(alias) + .metaclass_instance_type(db) + .is_subtype_of(db, instance) + } - (Type::FunctionLiteral(..), Type::Instance(instance)) - | (Type::Instance(instance), Type::FunctionLiteral(..)) => { + (Type::FunctionLiteral(..), Type::NominalInstance(instance)) + | (Type::NominalInstance(instance), Type::FunctionLiteral(..)) => { // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` // (it cannot be an instance of a `types.FunctionType` subclass) !KnownClass::FunctionType.is_subclass_of(db, instance.class()) @@ -1927,13 +2000,15 @@ impl<'db> Type<'db> { false } - (Type::ModuleLiteral(..), other @ Type::Instance(..)) - | (other @ Type::Instance(..), Type::ModuleLiteral(..)) => { + (Type::ModuleLiteral(..), other @ Type::NominalInstance(..)) + | (other @ Type::NominalInstance(..), Type::ModuleLiteral(..)) => { // Modules *can* actually be instances of `ModuleType` subclasses other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db)) } - (Type::Instance(left), Type::Instance(right)) => left.is_disjoint_from(db, right), + (Type::NominalInstance(left), Type::NominalInstance(right)) => { + left.is_disjoint_from(db, right) + } (Type::Tuple(tuple), Type::Tuple(other_tuple)) => { let self_elements = tuple.elements(db); @@ -1945,8 +2020,8 @@ impl<'db> Type<'db> { .any(|(e1, e2)| e1.is_disjoint_from(db, *e2)) } - (Type::Tuple(..), instance @ Type::Instance(_)) - | (instance @ Type::Instance(_), Type::Tuple(..)) => { + (Type::Tuple(..), instance @ Type::NominalInstance(_)) + | (instance @ Type::NominalInstance(_), Type::Tuple(..)) => { // We cannot be sure if the tuple is disjoint from the instance because: // - 'other' might be the homogeneous arbitrary-length tuple type // tuple[T, ...] (which we don't have support for yet); if all of @@ -1992,6 +2067,8 @@ impl<'db> Type<'db> { | Type::AlwaysTruthy | Type::PropertyInstance(_) => true, + Type::ProtocolInstance(protocol) => protocol.is_fully_static(), + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { None => true, Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.is_fully_static(db), @@ -2006,7 +2083,7 @@ impl<'db> Type<'db> { !matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_)) && !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_)) } - Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::Instance(_) => { + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::NominalInstance(_) => { // TODO: Ideally, we would iterate over the MRO of the class, check if all // bases are fully static, and only return `true` if that is the case. // @@ -2057,6 +2134,26 @@ impl<'db> Type<'db> { false } + Type::ProtocolInstance(..) => { + // It *might* be possible to have a singleton protocol-instance type...? + // + // E.g.: + // + // ```py + // from typing import Protocol, Callable + // + // class WeirdAndWacky(Protocol): + // @property + // def __class__(self) -> Callable[[], None]: ... + // ``` + // + // `WeirdAndWacky` only has a single possible inhabitant: `None`! + // It is thus a singleton type. + // However, going out of our way to recognise it as such is probably not worth it. + // Such cases should anyway be exceedingly rare and/or contrived. + false + } + // An unbounded, unconstrained typevar is not a singleton, because it can be // specialized to a non-singleton type. A bounded typevar is not a singleton, even if // the bound is a final singleton class, since it can still be specialized to `Never`. @@ -2112,7 +2209,7 @@ impl<'db> Type<'db> { false } Type::DataclassDecorator(_) | Type::DataclassTransformer(_) => false, - Type::Instance(instance) => instance.is_singleton(db), + Type::NominalInstance(instance) => instance.is_singleton(db), Type::PropertyInstance(_) => false, Type::Tuple(..) => { // The empty tuple is a singleton on CPython and PyPy, but not on other Python @@ -2159,6 +2256,11 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::KnownInstance(..) => true, + Type::ProtocolInstance(..) => { + // See comment in the `Type::ProtocolInstance` branch for `Type::is_singleton`. + false + } + // An unbounded, unconstrained typevar is not single-valued, because it can be // specialized to a multiple-valued type. A bounded typevar is not single-valued, even // if the bound is a final single-valued class, since it can still be specialized to @@ -2184,7 +2286,7 @@ impl<'db> Type<'db> { .iter() .all(|elem| elem.is_single_valued(db)), - Type::Instance(instance) => instance.is_single_valued(db), + Type::NominalInstance(instance) => instance.is_single_valued(db), Type::BoundSuper(_) => { // At runtime two super instances never compare equal, even if their arguments are identical. @@ -2322,10 +2424,11 @@ impl<'db> Type<'db> { .to_class_literal(db) .find_name_in_mro_with_policy(db, name, policy), - // We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type). - // So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the + // We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, + // i.e. Type::NominalInstance(type). So looking up a name in the MRO of + // `Type::NominalInstance(type)` is equivalent to looking up the name in the // MRO of the class `object`. - Type::Instance(instance) if instance.class().is_known(db, KnownClass::Type) => { + Type::NominalInstance(instance) if instance.class().is_known(db, KnownClass::Type) => { KnownClass::Object .to_class_literal(db) .find_name_in_mro_with_policy(db, name, policy) @@ -2350,7 +2453,8 @@ impl<'db> Type<'db> { | Type::SliceLiteral(_) | Type::Tuple(_) | Type::TypeVar(_) - | Type::Instance(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::PropertyInstance(_) => None, } } @@ -2415,7 +2519,18 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Never => Symbol::bound(self).into(), - Type::Instance(instance) => instance.class().instance_member(db, name), + Type::NominalInstance(instance) => instance.class().instance_member(db, name), + + Type::ProtocolInstance(protocol) => match protocol.inner() { + Protocol::FromClass(class) => class.instance_member(db, name), + Protocol::Synthesized(synthesized) => { + if synthesized.members(db).contains(name) { + SymbolAndQualifiers::todo("Capture type of synthesized protocol members") + } else { + Symbol::Unbound.into() + } + } + }, Type::FunctionLiteral(_) => KnownClass::FunctionType .to_instance(db) @@ -2856,7 +2971,7 @@ impl<'db> Type<'db> { .to_instance(db) .member_lookup_with_policy(db, name, policy), - Type::Instance(instance) + Type::NominalInstance(instance) if matches!(name.as_str(), "major" | "minor") && instance.class().is_known(db, KnownClass::VersionInfo) => { @@ -2898,7 +3013,8 @@ impl<'db> Type<'db> { policy, ), - Type::Instance(..) + Type::NominalInstance(..) + | Type::ProtocolInstance(..) | Type::BooleanLiteral(..) | Type::IntLiteral(..) | Type::StringLiteral(..) @@ -2929,7 +3045,7 @@ impl<'db> Type<'db> { // It will need a special handling, so it remember the origin type to properly // resolve the attribute. if matches!( - self.into_instance() + self.into_nominal_instance() .and_then(|instance| instance.class().known(db)), Some(KnownClass::ModuleType | KnownClass::GenericAlias) ) { @@ -3190,11 +3306,13 @@ impl<'db> Type<'db> { } }, - Type::Instance(instance) => match instance.class().known(db) { + Type::NominalInstance(instance) => match instance.class().known(db) { Some(known_class) => known_class.bool(), None => try_dunder_bool()?, }, + Type::ProtocolInstance(_) => try_dunder_bool()?, + Type::KnownInstance(known_instance) => known_instance.bool(), Type::PropertyInstance(_) => Truthiness::AlwaysTrue, @@ -4006,7 +4124,7 @@ impl<'db> Type<'db> { SubclassOfInner::Class(class) => Type::from(class).signatures(db), }, - Type::Instance(_) => { + Type::NominalInstance(_) | Type::ProtocolInstance(_) => { // Note that for objects that have a (possibly not callable!) `__call__` attribute, // we will get the signature of the `__call__` attribute, but will pass in the type // of the original object as the "callable type". That ensures that we get errors @@ -4419,11 +4537,14 @@ impl<'db> Type<'db> { }; let specialized = specialization .map(|specialization| { - Type::instance(ClassType::Generic(GenericAlias::new( + Type::instance( db, - generic_origin, - specialization, - ))) + ClassType::Generic(GenericAlias::new( + db, + generic_origin, + specialization, + )), + ) }) .unwrap_or(instance_ty); Ok(specialized) @@ -4454,9 +4575,9 @@ impl<'db> Type<'db> { pub fn to_instance(&self, db: &'db dyn Db) -> Option> { match self { Type::Dynamic(_) | Type::Never => Some(*self), - Type::ClassLiteral(class) => Some(Type::instance(class.default_specialization(db))), - Type::GenericAlias(alias) => Some(Type::instance(ClassType::from(*alias))), - Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance()), + Type::ClassLiteral(class) => Some(Type::instance(db, class.default_specialization(db))), + Type::GenericAlias(alias) => Some(Type::instance(db, ClassType::from(*alias))), + Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance(db)), Type::Union(union) => { let mut builder = UnionBuilder::new(db); for element in union.elements(db) { @@ -4474,7 +4595,8 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) - | Type::Instance(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::KnownInstance(_) | Type::PropertyInstance(_) | Type::ModuleLiteral(_) @@ -4494,7 +4616,7 @@ impl<'db> Type<'db> { /// /// For example, the builtin `int` as a value expression is of type /// `Type::ClassLiteral(builtins.int)`, that is, it is the `int` class itself. As a type - /// expression, it names the type `Type::Instance(builtins.int)`, that is, all objects whose + /// expression, it names the type `Type::NominalInstance(builtins.int)`, that is, all objects whose /// `__class__` is `int`. pub fn in_type_expression( &self, @@ -4521,11 +4643,11 @@ impl<'db> Type<'db> { KnownClass::Float.to_instance(db), ], ), - _ => Type::instance(class.default_specialization(db)), + _ => Type::instance(db, class.default_specialization(db)), }; Ok(ty) } - Type::GenericAlias(alias) => Ok(Type::instance(ClassType::from(*alias))), + Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))), Type::SubclassOf(_) | Type::BooleanLiteral(_) @@ -4548,6 +4670,7 @@ impl<'db> Type<'db> { | Type::Never | Type::FunctionLiteral(_) | Type::BoundSuper(_) + | Type::ProtocolInstance(_) | Type::PropertyInstance(_) => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(*self)], fallback_type: Type::unknown(), @@ -4672,7 +4795,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) => Ok(*self), - Type::Instance(instance) => match instance.class().known(db) { + Type::NominalInstance(instance) => match instance.class().known(db) { Some(KnownClass::TypeVar) => Ok(todo_type!( "Support for `typing.TypeVar` instances in type expressions" )), @@ -4748,7 +4871,7 @@ impl<'db> Type<'db> { pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> { match self { Type::Never => Type::Never, - Type::Instance(instance) => instance.to_meta_type(db), + Type::NominalInstance(instance) => instance.to_meta_type(db), Type::KnownInstance(known_instance) => known_instance.to_meta_type(db), Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db), Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), @@ -4796,6 +4919,7 @@ impl<'db> Type<'db> { ), Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), + Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), } } @@ -4917,9 +5041,11 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::SliceLiteral(_) | Type::BoundSuper(_) - // Instance contains a ClassType, which has already been specialized if needed, like - // above with BoundMethod's self_instance. - | Type::Instance(_) + // `NominalInstance` contains a ClassType, which has already been specialized if needed, + // like above with BoundMethod's self_instance. + | Type::NominalInstance(_) + // Same for `ProtocolInstance` + | Type::ProtocolInstance(_) | Type::KnownInstance(_) => self, } } @@ -5006,7 +5132,8 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::SliceLiteral(_) | Type::BoundSuper(_) - | Type::Instance(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::KnownInstance(_) => {} } } @@ -5066,7 +5193,7 @@ impl<'db> Type<'db> { Some(TypeDefinition::Class(class_literal.definition(db))) } Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))), - Self::Instance(instance) => { + Self::NominalInstance(instance) => { Some(TypeDefinition::Class(instance.class().definition(db))) } Self::KnownInstance(instance) => match instance { @@ -5100,6 +5227,11 @@ impl<'db> Type<'db> { Self::TypeVar(var) => Some(TypeDefinition::TypeVar(var.definition(db))), + Self::ProtocolInstance(protocol) => match protocol.inner() { + Protocol::FromClass(class) => Some(TypeDefinition::Class(class.definition(db))), + Protocol::Synthesized(_) => None, + }, + Self::Union(_) | Self::Intersection(_) => None, // These types have no definition @@ -7761,7 +7893,7 @@ impl BoundSuperError<'_> { pub enum SuperOwnerKind<'db> { Dynamic(DynamicType), Class(ClassType<'db>), - Instance(InstanceType<'db>), + Instance(NominalInstanceType<'db>), } impl<'db> SuperOwnerKind<'db> { @@ -7795,7 +7927,7 @@ impl<'db> SuperOwnerKind<'db> { Type::ClassLiteral(class_literal) => Some(SuperOwnerKind::Class( class_literal.apply_optional_specialization(db, None), )), - Type::Instance(instance) => Some(SuperOwnerKind::Instance(instance)), + Type::NominalInstance(instance) => Some(SuperOwnerKind::Instance(instance)), Type::BooleanLiteral(_) => { SuperOwnerKind::try_from_type(db, KnownClass::Bool.to_instance(db)) } diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 8f81db8d3f6dc1..3efbd2db0a93d4 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -591,7 +591,7 @@ impl<'db> InnerIntersectionBuilder<'db> { } _ => { let known_instance = new_positive - .into_instance() + .into_nominal_instance() .and_then(|instance| instance.class().known(db)); if known_instance == Some(KnownClass::Object) { @@ -611,7 +611,7 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::AlwaysFalsy if addition_is_bool_instance => { new_positive = Type::BooleanLiteral(false); } - Type::Instance(instance) + Type::NominalInstance(instance) if instance.class().is_known(db, KnownClass::Bool) => { match new_positive { @@ -705,7 +705,7 @@ impl<'db> InnerIntersectionBuilder<'db> { let contains_bool = || { self.positive .iter() - .filter_map(|ty| ty.into_instance()) + .filter_map(|ty| ty.into_nominal_instance()) .filter_map(|instance| instance.class().known(db)) .any(KnownClass::is_bool) }; @@ -722,7 +722,7 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::Never => { // Adding ~Never to an intersection is a no-op. } - Type::Instance(instance) if instance.class().is_object(db) => { + Type::NominalInstance(instance) if instance.class().is_object(db) => { // Adding ~object to an intersection results in Never. *self = Self::default(); self.positive.insert(Type::Never); diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 0a4870447550ed..a2ccc66e8d3e5a 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -533,14 +533,33 @@ impl<'db> ClassLiteral<'db> { } /// Determine if this class is a protocol. + /// + /// This method relies on the accuracy of the [`KnownClass::is_protocol`] method, + /// which hardcodes knowledge about certain special-cased classes. See the docs on + /// that method for why we do this rather than relying on generalised logic for all + /// classes, including the special-cased ones that are included in the [`KnownClass`] + /// enum. pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool { - self.explicit_bases(db).iter().rev().take(3).any(|base| { - matches!( - base, - Type::KnownInstance(KnownInstanceType::Protocol) - | Type::Dynamic(DynamicType::SubscriptedProtocol) - ) - }) + self.known(db) + .map(KnownClass::is_protocol) + .unwrap_or_else(|| { + // Iterate through the last three bases of the class + // searching for `Protocol` or `Protocol[]` in the bases list. + // + // If `Protocol` is present in the bases list of a valid protocol class, it must either: + // + // - be the last base + // - OR be the last-but-one base (with the final base being `Generic[]` or `object`) + // - OR be the last-but-two base (with the penultimate base being `Generic[]` + // and the final base being `object`) + self.explicit_bases(db).iter().rev().take(3).any(|base| { + matches!( + base, + Type::KnownInstance(KnownInstanceType::Protocol) + | Type::Dynamic(DynamicType::SubscriptedProtocol) + ) + }) + }) } /// Return the types of the decorators on this class @@ -1076,6 +1095,7 @@ impl<'db> ClassLiteral<'db> { Parameters::new([Parameter::positional_or_keyword(Name::new_static("other")) // TODO: could be `Self`. .with_annotated_type(Type::instance( + db, self.apply_optional_specialization(db, specialization), ))]), Some(KnownClass::Bool.to_instance(db)), @@ -1711,7 +1731,7 @@ impl<'db> ProtocolClassLiteral<'db> { /// It is illegal for a protocol class to have any instance attributes that are not declared /// in the protocol's class body. If any are assigned to, they are not taken into account in /// the protocol's list of members. - pub(super) fn protocol_members(self, db: &'db dyn Db) -> &'db ordermap::set::Slice { + pub(super) fn protocol_members(self, db: &'db dyn Db) -> &'db FxOrderSet { /// The list of excluded members is subject to change between Python versions, /// especially for dunders, but it probably doesn't matter *too* much if this /// list goes out of date. It's up to date as of Python commit 87b1ea016b1454b1e83b9113fa9435849b7743aa @@ -1748,11 +1768,11 @@ impl<'db> ProtocolClassLiteral<'db> { ) } - #[salsa::tracked(return_ref)] + #[salsa::tracked(return_ref, cycle_fn=proto_members_cycle_recover, cycle_initial=proto_members_cycle_initial)] fn cached_protocol_members<'db>( db: &'db dyn Db, class: ClassLiteral<'db>, - ) -> Box> { + ) -> FxOrderSet { let mut members = FxOrderSet::default(); for parent_protocol in class @@ -1796,9 +1816,24 @@ impl<'db> ProtocolClassLiteral<'db> { } members.sort(); - members.into_boxed_slice() + members.shrink_to_fit(); + members } + fn proto_members_cycle_recover( + _db: &dyn Db, + _value: &FxOrderSet, + _count: u32, + _class: ClassLiteral, + ) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate + } + + fn proto_members_cycle_initial(_db: &dyn Db, _class: ClassLiteral) -> FxOrderSet { + FxOrderSet::default() + } + + let _span = tracing::trace_span!("protocol_members", "class='{}'", self.name(db)).entered(); cached_protocol_members(db, *self) } @@ -1892,8 +1927,6 @@ pub enum KnownClass { TypeAliasType, NoDefaultType, NewType, - Sized, - // TODO: This can probably be removed when we have support for protocols SupportsIndex, // Collections ChainMap, @@ -1977,7 +2010,6 @@ impl<'db> KnownClass { | Self::DefaultDict | Self::Deque | Self::Float - | Self::Sized | Self::Enum | Self::ABCMeta // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 @@ -1988,6 +2020,75 @@ impl<'db> KnownClass { } } + /// Return `true` if this class is a protocol class. + /// + /// In an ideal world, perhaps we wouldn't hardcode this knowledge here; + /// instead, we'd just look at the bases for these classes, as we do for + /// all other classes. However, the special casing here helps us out in + /// two important ways: + /// + /// 1. It helps us avoid Salsa cycles when creating types such as "instance of `str`" + /// and "instance of `sys._version_info`". These types are constructed very early + /// on, but it causes problems if we attempt to infer the types of their bases + /// too soon. + /// 2. It's probably more performant. + const fn is_protocol(self) -> bool { + match self { + Self::SupportsIndex => true, + + Self::Any + | Self::Bool + | Self::Object + | Self::Bytes + | Self::Bytearray + | Self::Tuple + | Self::Int + | Self::Float + | Self::Complex + | Self::FrozenSet + | Self::Str + | Self::Set + | Self::Dict + | Self::List + | Self::Type + | Self::Slice + | Self::Range + | Self::Property + | Self::BaseException + | Self::BaseExceptionGroup + | Self::Classmethod + | Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType + | Self::NoneType + | Self::SpecialForm + | Self::TypeVar + | Self::ParamSpec + | Self::ParamSpecArgs + | Self::ParamSpecKwargs + | Self::TypeVarTuple + | Self::TypeAliasType + | Self::NoDefaultType + | Self::NewType + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict + | Self::Enum + | Self::ABCMeta + | Self::Super + | Self::StdlibAlias + | Self::VersionInfo + | Self::EllipsisType + | Self::NotImplementedType + | Self::UnionType => false, + } + } + pub(crate) fn name(self, db: &'db dyn Db) -> &'static str { match self { Self::Any => "Any", @@ -2033,7 +2134,6 @@ impl<'db> KnownClass { Self::Counter => "Counter", Self::DefaultDict => "defaultdict", Self::Deque => "deque", - Self::Sized => "Sized", Self::OrderedDict => "OrderedDict", Self::Enum => "Enum", Self::ABCMeta => "ABCMeta", @@ -2090,7 +2190,7 @@ impl<'db> KnownClass { pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { self.to_class_literal(db) .to_class_type(db) - .map(Type::instance) + .map(|class| Type::instance(db, class)) .unwrap_or_else(Type::unknown) } @@ -2207,8 +2307,7 @@ impl<'db> KnownClass { | Self::SpecialForm | Self::TypeVar | Self::StdlibAlias - | Self::SupportsIndex - | Self::Sized => KnownModule::Typing, + | Self::SupportsIndex => KnownModule::Typing, Self::TypeAliasType | Self::TypeVarTuple | Self::ParamSpec @@ -2296,7 +2395,6 @@ impl<'db> KnownClass { | Self::ParamSpecArgs | Self::ParamSpecKwargs | Self::TypeVarTuple - | Self::Sized | Self::Enum | Self::ABCMeta | Self::Super @@ -2356,7 +2454,6 @@ impl<'db> KnownClass { | Self::ParamSpecArgs | Self::ParamSpecKwargs | Self::TypeVarTuple - | Self::Sized | Self::Enum | Self::ABCMeta | Self::Super @@ -2418,7 +2515,6 @@ impl<'db> KnownClass { "_SpecialForm" => Self::SpecialForm, "_NoDefaultType" => Self::NoDefaultType, "SupportsIndex" => Self::SupportsIndex, - "Sized" => Self::Sized, "Enum" => Self::Enum, "ABCMeta" => Self::ABCMeta, "super" => Self::Super, @@ -2491,7 +2587,6 @@ impl<'db> KnownClass { | Self::ParamSpecArgs | Self::ParamSpecKwargs | Self::TypeVarTuple - | Self::Sized | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), } } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 9f480ed07eaf05..50ab0a9d78c2a5 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -78,12 +78,14 @@ impl<'db> ClassBase<'db> { Self::Class(literal.default_specialization(db)) }), Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), - Type::Instance(instance) if instance.class().is_known(db, KnownClass::GenericAlias) => { + Type::NominalInstance(instance) + if instance.class().is_known(db, KnownClass::GenericAlias) => + { Self::try_from_type(db, todo_type!("GenericAlias instance")) } Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs? Type::Intersection(_) => None, // TODO -- probably incorrect? - Type::Instance(_) => None, // TODO -- handle `__mro_entries__`? + Type::NominalInstance(_) => None, // TODO -- handle `__mro_entries__`? Type::PropertyInstance(_) => None, Type::Never | Type::BooleanLiteral(_) @@ -104,6 +106,7 @@ impl<'db> ClassBase<'db> { | Type::SubclassOf(_) | Type::TypeVar(_) | Type::BoundSuper(_) + | Type::ProtocolInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy => None, Type::KnownInstance(known_instance) => match known_instance { diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 2633938ae9285f..c878a81539fc30 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -10,15 +10,13 @@ use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ - FunctionSignature, IntersectionType, KnownClass, MethodWrapperKind, StringLiteralType, - SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType, - WrapperDescriptorKind, + CallableType, FunctionSignature, IntersectionType, KnownClass, MethodWrapperKind, Protocol, + StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, + UnionType, WrapperDescriptorKind, }; use crate::Db; use rustc_hash::FxHashMap; -use super::CallableType; - impl<'db> Type<'db> { pub fn display(&self, db: &'db dyn Db) -> DisplayType { DisplayType { ty: self, db } @@ -73,11 +71,32 @@ impl Display for DisplayRepresentation<'_> { match self.ty { Type::Dynamic(dynamic) => dynamic.fmt(f), Type::Never => f.write_str("Never"), - Type::Instance(instance) => match (instance.class(), instance.class().known(self.db)) { - (_, Some(KnownClass::NoneType)) => f.write_str("None"), - (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), - (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)), - (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)), + Type::NominalInstance(instance) => { + match (instance.class(), instance.class().known(self.db)) { + (_, Some(KnownClass::NoneType)) => f.write_str("None"), + (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), + (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)), + (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)), + } + } + Type::ProtocolInstance(protocol) => match protocol.inner() { + Protocol::FromClass(ClassType::NonGeneric(class)) => { + f.write_str(class.name(self.db)) + } + Protocol::FromClass(ClassType::Generic(alias)) => alias.display(self.db).fmt(f), + Protocol::Synthesized(synthetic) => { + f.write_str("') + } }, Type::PropertyInstance(_) => f.write_str("property"), Type::ModuleLiteral(module) => { @@ -761,6 +780,7 @@ mod tests { use ruff_python_ast::name::Name; use crate::db::tests::setup_db; + use crate::symbol::typing_extensions_symbol; use crate::types::{ KnownClass, Parameter, Parameters, Signature, SliceLiteralType, StringLiteralType, Type, }; @@ -832,6 +852,31 @@ mod tests { ); } + #[test] + fn synthesized_protocol_display() { + let db = setup_db(); + + // Call `.normalized()` to turn the class-based protocol into a nameless synthesized one. + let supports_index_synthesized = KnownClass::SupportsIndex.to_instance(&db).normalized(&db); + assert_eq!( + supports_index_synthesized.display(&db).to_string(), + "" + ); + + let iterator_synthesized = typing_extensions_symbol(&db, "Iterator") + .symbol + .ignore_possibly_unbound() + .unwrap() + .to_instance(&db) + .unwrap() + .normalized(&db); // Call `.normalized()` to turn the class-based protocol into a nameless synthesized one. + + assert_eq!( + iterator_synthesized.display(&db).to_string(), + "" + ); + } + fn display_signature<'db>( db: &dyn Db, parameters: impl IntoIterator>, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 6016308878674b..2e275e99924bde 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1065,7 +1065,7 @@ impl<'db> TypeInferenceBuilder<'db> { ) -> bool { match left { Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} - Type::Instance(instance) + Type::NominalInstance(instance) if matches!( instance.class().known(self.db()), Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) @@ -2517,7 +2517,7 @@ impl<'db> TypeInferenceBuilder<'db> { } // Super instances do not allow attribute assignment - Type::Instance(instance) if instance.class().is_known(db, KnownClass::Super) => { + Type::NominalInstance(instance) if instance.class().is_known(db, KnownClass::Super) => { if emit_diagnostics { if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { builder.into_diagnostic(format_args!( @@ -2542,7 +2542,8 @@ impl<'db> TypeInferenceBuilder<'db> { Type::Dynamic(..) | Type::Never => true, - Type::Instance(..) + Type::NominalInstance(..) + | Type::ProtocolInstance(_) | Type::BooleanLiteral(..) | Type::IntLiteral(..) | Type::StringLiteral(..) @@ -3033,7 +3034,7 @@ impl<'db> TypeInferenceBuilder<'db> { } // Handle various singletons. - if let Type::Instance(instance) = declared_ty.inner_type() { + if let Type::NominalInstance(instance) = declared_ty.inner_type() { if instance .class() .is_known(self.db(), KnownClass::SpecialForm) @@ -5125,7 +5126,8 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) - | Type::Instance(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::KnownInstance(_) | Type::PropertyInstance(_) | Type::Union(_) @@ -5405,7 +5407,8 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) - | Type::Instance(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::KnownInstance(_) | Type::PropertyInstance(_) | Type::Intersection(_) @@ -5430,7 +5433,8 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) - | Type::Instance(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::KnownInstance(_) | Type::PropertyInstance(_) | Type::Intersection(_) @@ -5863,13 +5867,13 @@ impl<'db> TypeInferenceBuilder<'db> { right_ty: right, }), }, - (Type::IntLiteral(_), Type::Instance(_)) => self.infer_binary_type_comparison( + (Type::IntLiteral(_), Type::NominalInstance(_)) => self.infer_binary_type_comparison( KnownClass::Int.to_instance(self.db()), op, right, range, ), - (Type::Instance(_), Type::IntLiteral(_)) => self.infer_binary_type_comparison( + (Type::NominalInstance(_), Type::IntLiteral(_)) => self.infer_binary_type_comparison( left, op, KnownClass::Int.to_instance(self.db()), @@ -5995,7 +5999,7 @@ impl<'db> TypeInferenceBuilder<'db> { KnownClass::Bytes.to_instance(self.db()), range, ), - (Type::Tuple(_), Type::Instance(instance)) + (Type::Tuple(_), Type::NominalInstance(instance)) if instance .class() .is_known(self.db(), KnownClass::VersionInfo) => @@ -6007,7 +6011,7 @@ impl<'db> TypeInferenceBuilder<'db> { range, ) } - (Type::Instance(instance), Type::Tuple(_)) + (Type::NominalInstance(instance), Type::Tuple(_)) if instance .class() .is_known(self.db(), KnownClass::VersionInfo) => @@ -6393,7 +6397,7 @@ impl<'db> TypeInferenceBuilder<'db> { ) -> Type<'db> { match (value_ty, slice_ty) { ( - Type::Instance(instance), + Type::NominalInstance(instance), Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::SliceLiteral(_), ) if instance .class() @@ -6699,7 +6703,7 @@ impl<'db> TypeInferenceBuilder<'db> { Err(_) => SliceArg::Unsupported, }, Some(Type::BooleanLiteral(b)) => SliceArg::Arg(Some(i32::from(b))), - Some(Type::Instance(instance)) + Some(Type::NominalInstance(instance)) if instance.class().is_known(self.db(), KnownClass::NoneType) => { SliceArg::Arg(None) diff --git a/crates/red_knot_python_semantic/src/types/instance.rs b/crates/red_knot_python_semantic/src/types/instance.rs index fb1b1deb764a4f..cfd6912597abc3 100644 --- a/crates/red_knot_python_semantic/src/types/instance.rs +++ b/crates/red_knot_python_semantic/src/types/instance.rs @@ -1,30 +1,54 @@ //! Instance types: both nominal and structural. +use ruff_python_ast::name::Name; + use super::{ClassType, KnownClass, SubclassOfType, Type}; -use crate::Db; +use crate::{Db, FxOrderSet}; impl<'db> Type<'db> { - pub(crate) const fn instance(class: ClassType<'db>) -> Self { - Self::Instance(InstanceType { class }) + pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { + if class.class_literal(db).0.is_protocol(db) { + Self::ProtocolInstance(ProtocolInstanceType(Protocol::FromClass(class))) + } else { + Self::NominalInstance(NominalInstanceType { class }) + } } - pub(crate) const fn into_instance(self) -> Option> { + pub(crate) const fn into_nominal_instance(self) -> Option> { match self { - Type::Instance(instance_type) => Some(instance_type), + Type::NominalInstance(instance_type) => Some(instance_type), _ => None, } } + + /// Return `true` if `self` conforms to the interface described by `protocol`. + /// + /// TODO: we may need to split this into two methods in the future, once we start + /// differentiating between fully-static and non-fully-static protocols. + pub(super) fn satisfies_protocol( + self, + db: &'db dyn Db, + protocol: ProtocolInstanceType<'db>, + ) -> bool { + // TODO: this should consider the types of the protocol members + // as well as whether each member *exists* on `self`. + protocol + .0 + .protocol_members(db) + .iter() + .all(|member| !self.member(db, member).symbol.is_unbound()) + } } /// A type representing the set of runtime objects which are instances of a certain nominal class. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)] -pub struct InstanceType<'db> { - // Keep this field private, so that the only way of constructing `InstanceType` instances +pub struct NominalInstanceType<'db> { + // Keep this field private, so that the only way of constructing `NominalInstanceType` instances // is through the `Type::instance` constructor function. class: ClassType<'db>, } -impl<'db> InstanceType<'db> { +impl<'db> NominalInstanceType<'db> { pub(super) fn class(self) -> ClassType<'db> { self.class } @@ -87,8 +111,152 @@ impl<'db> InstanceType<'db> { } } -impl<'db> From> for Type<'db> { - fn from(value: InstanceType<'db>) -> Self { - Self::Instance(value) +impl<'db> From> for Type<'db> { + fn from(value: NominalInstanceType<'db>) -> Self { + Self::NominalInstance(value) + } +} + +/// A `ProtocolInstanceType` represents the set of all possible runtime objects +/// that conform to the interface described by a certain protocol. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, salsa::Update)] +pub struct ProtocolInstanceType<'db>( + // Keep the inner field here private, + // so that the only way of constructing `ProtocolInstanceType` instances + // is through the `Type::instance` constructor function. + Protocol<'db>, +); + +impl<'db> ProtocolInstanceType<'db> { + pub(super) fn inner(self) -> Protocol<'db> { + self.0 } + + /// Return the meta-type of this protocol-instance type. + pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + match self.0 { + Protocol::FromClass(class) => SubclassOfType::from(db, class), + + // TODO: we can and should do better here. + // + // This is supported by mypy, and should be supported by us as well. + // We'll need to come up with a better solution for the meta-type of + // synthesized protocols to solve this: + // + // ```py + // from typing import Callable + // + // def foo(x: Callable[[], int]) -> None: + // reveal_type(type(x)) # mypy: "type[def (builtins.int) -> builtins.str]" + // reveal_type(type(x).__call__) # mypy: "def (*args: Any, **kwds: Any) -> Any" + // ``` + Protocol::Synthesized(_) => KnownClass::Type.to_instance(db), + } + } + + /// Return a "normalized" version of this `Protocol` type. + /// + /// See [`Type::normalized`] for more details. + pub(super) fn normalized(self, db: &'db dyn Db) -> Type<'db> { + let object = KnownClass::Object.to_instance(db); + if object.satisfies_protocol(db, self) { + return object; + } + match self.0 { + Protocol::FromClass(_) => Type::ProtocolInstance(Self(Protocol::Synthesized( + SynthesizedProtocolType::new(db, self.0.protocol_members(db)), + ))), + Protocol::Synthesized(_) => Type::ProtocolInstance(self), + } + } + + /// TODO: this should return `true` if any of the members of this protocol type contain any `Todo` types. + #[expect(clippy::unused_self)] + pub(super) fn contains_todo(self) -> bool { + false + } + + /// Return `true` if this protocol type is fully static. + /// + /// TODO: should not be considered fully static if any members do not have fully static types + #[expect(clippy::unused_self)] + pub(super) fn is_fully_static(self) -> bool { + true + } + + /// Return `true` if this protocol type is a subtype of the protocol `other`. + /// + /// TODO: consider the types of the members as well as their existence + pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { + self.0 + .protocol_members(db) + .is_superset(other.0.protocol_members(db)) + } + + /// Return `true` if this protocol type is assignable to the protocol `other`. + /// + /// TODO: consider the types of the members as well as their existence + pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { + self.is_subtype_of(db, other) + } + + /// Return `true` if this protocol type is equivalent to the protocol `other`. + /// + /// TODO: consider the types of the members as well as their existence + pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.normalized(db) == other.normalized(db) + } + + /// Return `true` if this protocol type is gradually equivalent to the protocol `other`. + /// + /// TODO: consider the types of the members as well as their existence + pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + self.is_equivalent_to(db, other) + } + + /// Return `true` if this protocol type is disjoint from the protocol `other`. + /// + /// TODO: a protocol `X` is disjoint from a protocol `Y` if `X` and `Y` + /// have a member with the same name but disjoint types + #[expect(clippy::unused_self)] + pub(super) fn is_disjoint_from(self, _db: &'db dyn Db, _other: Self) -> bool { + false + } +} + +/// An enumeration of the two kinds of protocol types: those that originate from a class +/// definition in source code, and those that are synthesized from a set of members. +#[derive( + Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, salsa::Supertype, PartialOrd, Ord, +)] +pub(super) enum Protocol<'db> { + FromClass(ClassType<'db>), + Synthesized(SynthesizedProtocolType<'db>), +} + +impl<'db> Protocol<'db> { + /// Return the members of this protocol type + fn protocol_members(self, db: &'db dyn Db) -> &'db FxOrderSet { + match self { + Self::FromClass(class) => class + .class_literal(db) + .0 + .into_protocol_class(db) + .expect("Protocol class literal should be a protocol class") + .protocol_members(db), + Self::Synthesized(synthesized) => synthesized.members(db), + } + } +} + +/// A "synthesized" protocol type that is dissociated from a class definition in source code. +/// +/// Two synthesized protocol types with the same members will share the same Salsa ID, +/// making them easy to compare for equivalence. A synthesized protocol type is therefore +/// returned by [`ProtocolInstanceType::normalized`] so that two protocols with the same members +/// will be understood as equivalent even in the context of differently ordered unions or intersections. +#[salsa::interned(debug)] +pub(super) struct SynthesizedProtocolType<'db> { + #[return_ref] + pub(super) members: FxOrderSet, } diff --git a/crates/red_knot_python_semantic/src/types/known_instance.rs b/crates/red_knot_python_semantic/src/types/known_instance.rs index d6da1d76d42998..7a8cb2e4642ed9 100644 --- a/crates/red_knot_python_semantic/src/types/known_instance.rs +++ b/crates/red_knot_python_semantic/src/types/known_instance.rs @@ -1,6 +1,6 @@ //! The `KnownInstance` type. //! -//! Despite its name, this is quite a different type from [`super::InstanceType`]. +//! Despite its name, this is quite a different type from [`super::NominalInstanceType`]. //! For the vast majority of instance-types in Python, we cannot say how many possible //! inhabitants there are or could be of that type at runtime. Each variant of the //! [`KnownInstanceType`] enum, however, represents a specific runtime symbol @@ -260,7 +260,7 @@ impl<'db> KnownInstanceType<'db> { /// /// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`, /// so `KnownInstanceType::Literal.instance_fallback(db)` - /// returns `Type::Instance(InstanceType { class: })`. + /// returns `Type::NominalInstance(NominalInstanceType { class: })`. pub(super) fn instance_fallback(self, db: &dyn Db) -> Type { self.class().to_instance(db) } diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index 19cb5c2e5d7265..a63d9d55620488 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -159,7 +159,7 @@ impl KnownConstraintFunction { /// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type. fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option> { let constraint_fn = |class| match self { - KnownConstraintFunction::IsInstance => Type::instance(class), + KnownConstraintFunction::IsInstance => Type::instance(db, class), KnownConstraintFunction::IsSubclass => SubclassOfType::from(db, class), }; @@ -472,7 +472,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> { union.map(db, |ty| filter_to_cannot_be_equal(db, *ty, rhs_ty)) } // Treat `bool` as `Literal[True, False]`. - Type::Instance(instance) if instance.class().is_known(db, KnownClass::Bool) => { + Type::NominalInstance(instance) + if instance.class().is_known(db, KnownClass::Bool) => + { UnionType::from_elements( db, [Type::BooleanLiteral(true), Type::BooleanLiteral(false)] @@ -501,7 +503,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> { fn evaluate_expr_ne(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { match (lhs_ty, rhs_ty) { - (Type::Instance(instance), Type::IntLiteral(i)) + (Type::NominalInstance(instance), Type::IntLiteral(i)) if instance.class().is_known(self.db, KnownClass::Bool) => { if i == 0 { @@ -682,7 +684,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> { let symbol = self.expect_expr_name_symbol(id); constraints.insert( symbol, - Type::instance(rhs_class.unknown_specialization(self.db)), + Type::instance(self.db, rhs_class.unknown_specialization(self.db)), ); } } diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/red_knot_python_semantic/src/types/subclass_of.rs index 52dd82aaa665ff..56bccc8b68806f 100644 --- a/crates/red_knot_python_semantic/src/types/subclass_of.rs +++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs @@ -16,8 +16,8 @@ impl<'db> SubclassOfType<'db> { /// This method does not always return a [`Type::SubclassOf`] variant. /// If the class object is known to be a final class, /// this method will return a [`Type::ClassLiteral`] variant; this is a more precise type. - /// If the class object is `builtins.object`, `Type::Instance()` will be returned; - /// this is no more precise, but it is exactly equivalent to `type[object]`. + /// If the class object is `builtins.object`, `Type::NominalInstance()` + /// will be returned; this is no more precise, but it is exactly equivalent to `type[object]`. /// /// The eager normalization here means that we do not need to worry elsewhere about distinguishing /// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants. @@ -94,9 +94,9 @@ impl<'db> SubclassOfType<'db> { } } - pub(crate) fn to_instance(self) -> Type<'db> { + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { match self.subclass_of { - SubclassOfInner::Class(class) => Type::instance(class), + SubclassOfInner::Class(class) => Type::instance(db, class), SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), } } diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index ba2adf6b409a7e..d62baf9b90b3ac 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -129,10 +129,18 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::SubclassOf(_), _) => Ordering::Less, (_, Type::SubclassOf(_)) => Ordering::Greater, - (Type::Instance(left), Type::Instance(right)) => left.class().cmp(&right.class()), - (Type::Instance(_), _) => Ordering::Less, - (_, Type::Instance(_)) => Ordering::Greater, + (Type::NominalInstance(left), Type::NominalInstance(right)) => { + left.class().cmp(&right.class()) + } + (Type::NominalInstance(_), _) => Ordering::Less, + (_, Type::NominalInstance(_)) => Ordering::Greater, + + (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => { + left_proto.cmp(right_proto) + } + (Type::ProtocolInstance(_), _) => Ordering::Less, + (_, Type::ProtocolInstance(_)) => Ordering::Greater, (Type::TypeVar(left), Type::TypeVar(right)) => left.cmp(right), (Type::TypeVar(_), _) => Ordering::Less, From 0861ecfa55188522adead1faf8aa4b97f8c1a1ed Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 30 Apr 2025 13:04:06 +0200 Subject: [PATCH 0187/1161] [red-knot] Use 'full' salsa backtrace output that includes durability and revisions (#17735) --- crates/red_knot_test/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/red_knot_test/src/lib.rs b/crates/red_knot_test/src/lib.rs index 66772c113c4dbb..4ae5dca0332a62 100644 --- a/crates/red_knot_test/src/lib.rs +++ b/crates/red_knot_test/src/lib.rs @@ -346,7 +346,7 @@ fn run_test( if let Some(backtrace) = info.salsa_backtrace { salsa::attach(db, || { - messages.extend(backtrace.to_string().split('\n').map(String::from)); + messages.extend(format!("{backtrace:#}").split('\n').map(String::from)); }); } From ad1a8da4d1126eff1ec59ac4763f2c44e3dc33f4 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 30 Apr 2025 19:37:42 +0530 Subject: [PATCH 0188/1161] [red-knot] Check for invalid overload usages (#17609) ## Summary Part of #15383, this PR adds the core infrastructure to check for invalid overloads and adds a diagnostic to raise if there are < 2 overloads for a given definition. ### Design notes The requirements to check the overloads are: * Requires `FunctionType` which has the `to_overloaded` method * The `FunctionType` **should** be for the function that is either the implementation or the last overload if the implementation doesn't exists * Avoid checking any `FunctionType` that are part of an overload chain * Consider visibility constraints This required a couple of iteration to make sure all of the above requirements are fulfilled. #### 1. Use a set to deduplicate The logic would first collect all the `FunctionType` that are part of the overload chain except for the implementation or the last overload if the implementation doesn't exists. Then, when iterating over all the function declarations within the scope, we'd avoid checking these functions. But, this approach would fail to consider visibility constraints as certain overloads _can_ be behind a version check. Those aren't part of the overload chain but those aren't a separate overload chain either.
    Implementation:

    ```rs fn check_overloaded_functions(&mut self) { let function_definitions = || { self.types .declarations .iter() .filter_map(|(definition, ty)| { // Filter out function literals that result from anything other than a function // definition e.g., imports. if let DefinitionKind::Function(function) = definition.kind(self.db()) { ty.inner_type() .into_function_literal() .map(|ty| (ty, definition.symbol(self.db()), function.node())) } else { None } }) }; // A set of all the functions that are part of an overloaded function definition except for // the implementation function and the last overload in case the implementation doesn't // exists. This allows us to collect all the function definitions that needs to be skipped // when checking for invalid overload usages. let mut overloads: HashSet> = HashSet::default(); for (function, _) in function_definitions() { let Some(overloaded) = function.to_overloaded(self.db()) else { continue; }; if overloaded.implementation.is_some() { overloads.extend(overloaded.overloads.iter().copied()); } else if let Some((_, previous_overloads)) = overloaded.overloads.split_last() { overloads.extend(previous_overloads.iter().copied()); } } for (function, function_node) in function_definitions() { let Some(overloaded) = function.to_overloaded(self.db()) else { continue; }; if overloads.contains(&function) { continue; } // At this point, the `function` variable is either the implementation function or the // last overloaded function if the implementation doesn't exists. if overloaded.overloads.len() < 2 { if let Some(builder) = self .context .report_lint(&INVALID_OVERLOAD, &function_node.name) { let mut diagnostic = builder.into_diagnostic(format_args!( "Function `{}` requires at least two overloads", &function_node.name )); if let Some(first_overload) = overloaded.overloads.first() { diagnostic.annotate( self.context .secondary(first_overload.focus_range(self.db())) .message(format_args!("Only one overload defined here")), ); } } } } } ```

    #### 2. Define a `predecessor` query The `predecessor` query would return the previous `FunctionType` for the given `FunctionType` i.e., the current logic would be extracted to be a query instead. This could then be used to make sure that we're checking the entire overload chain once. The way this would've been implemented is to have a `to_overloaded` implementation which would take the root of the overload chain instead of the leaf. But, this would require updates to the use-def map to somehow be able to return the _following_ functions for a given definition. #### 3. Create a successor link This is what Pyrefly uses, we'd create a forward link between two functions that are involved in an overload chain. This means that for a given function, we can get the successor function. This could be used to find the _leaf_ of the overload chain which can then be used with the `to_overloaded` method to get the entire overload chain. But, this would also require updating the use-def map to be able to "see" the _following_ function. ### Implementation This leads us to the final implementation that this PR implements which is to consider the overloaded functions using: * Collect all the **function symbols** that are defined **and** called within the same file. This could potentially be an overloaded function * Use the public bindings to get the leaf of the overload chain and use that to get the entire overload chain via `to_overloaded` and perform the check This has a limitation that in case a function redefines an overload, then that overload will not be checked. For example: ```py from typing import overload @overload def f() -> None: ... @overload def f(x: int) -> int: ... # The above overload will not be checked as the below function with the same name # shadows it def f(*args: int) -> int: ... ``` ## Test Plan Update existing mdtest and add snapshot diagnostics. --- .../resources/mdtest/overloads.md | 13 +- ...ds_-_Invalid_-_At_least_two_overloads.snap | 65 +++++++++ crates/red_knot_python_semantic/src/types.rs | 31 ++++- .../src/types/diagnostic.rs | 44 ++++++ .../src/types/infer.rs | 127 +++++++++++++++++- knot.schema.json | 10 ++ 6 files changed, 283 insertions(+), 7 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/overloads.md b/crates/red_knot_python_semantic/resources/mdtest/overloads.md index 764b6a44123edb..4bbef31b1d7351 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/overloads.md +++ b/crates/red_knot_python_semantic/resources/mdtest/overloads.md @@ -309,18 +309,29 @@ reveal_type(func("")) # revealed: Literal[""] ### At least two overloads + + At least two `@overload`-decorated definitions must be present. ```py from typing import overload -# TODO: error @overload def func(x: int) -> int: ... + +# error: [invalid-overload] def func(x: int | str) -> int | str: return x ``` +```pyi +from typing import overload + +@overload +# error: [invalid-overload] +def func(x: int) -> int: ... +``` + ### Overload without an implementation #### Regular modules diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap new file mode 100644 index 00000000000000..246dff3c952ba3 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap @@ -0,0 +1,65 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: overloads.md - Overloads - Invalid - At least two overloads +mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import overload +2 | +3 | @overload +4 | def func(x: int) -> int: ... +5 | +6 | # error: [invalid-overload] +7 | def func(x: int | str) -> int | str: +8 | return x +``` + +## mdtest_snippet.pyi + +``` +1 | from typing import overload +2 | +3 | @overload +4 | # error: [invalid-overload] +5 | def func(x: int) -> int: ... +``` + +# Diagnostics + +``` +error: lint:invalid-overload: Overloaded function `func` requires at least two overloads + --> src/mdtest_snippet.py:4:5 + | +3 | @overload +4 | def func(x: int) -> int: ... + | ---- Only one overload defined here +5 | +6 | # error: [invalid-overload] +7 | def func(x: int | str) -> int | str: + | ^^^^ +8 | return x + | + +``` + +``` +error: lint:invalid-overload: Overloaded function `func` requires at least two overloads + --> src/mdtest_snippet.pyi:5:5 + | +3 | @overload +4 | # error: [invalid-overload] +5 | def func(x: int) -> int: ... + | ---- + | | + | Only one overload defined here + | + +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 826684beb100b7..5d2c430fa861af 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -6525,6 +6525,13 @@ pub struct FunctionType<'db> { #[salsa::tracked] impl<'db> FunctionType<'db> { + /// Returns the [`File`] in which this function is defined. + pub(crate) fn file(self, db: &'db dyn Db) -> File { + // NOTE: Do not use `self.definition(db).file(db)` here, as that could create a + // cross-module dependency on the full AST. + self.body_scope(db).file(db) + } + pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { self.decorators(db).contains(decorator) } @@ -6546,21 +6553,41 @@ impl<'db> FunctionType<'db> { Type::BoundMethod(BoundMethodType::new(db, self, self_instance)) } + /// Returns the AST node for this function. + pub(crate) fn node(self, db: &'db dyn Db, file: File) -> &'db ast::StmtFunctionDef { + debug_assert_eq!( + file, + self.file(db), + "FunctionType::node() must be called with the same file as the one where \ + the function is defined." + ); + + self.body_scope(db).node(db).expect_function() + } + /// Returns the [`FileRange`] of the function's name. pub fn focus_range(self, db: &dyn Db) -> FileRange { FileRange::new( - self.body_scope(db).file(db), + self.file(db), self.body_scope(db).node(db).expect_function().name.range, ) } pub fn full_range(self, db: &dyn Db) -> FileRange { FileRange::new( - self.body_scope(db).file(db), + self.file(db), self.body_scope(db).node(db).expect_function().range, ) } + /// Returns the [`Definition`] of this function. + /// + /// ## Warning + /// + /// This uses the semantic index to find the definition of the function. This means that if the + /// calling query is not in the same file as this function is defined in, then this will create + /// a cross-module dependency directly on the full AST which will lead to cache + /// over-invalidation. pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { let body_scope = self.body_scope(db); let index = semantic_index(db, body_scope.file(db)); diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 63d5a18e4baef0..f56300489085d4 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -37,6 +37,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_EXCEPTION_CAUGHT); registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE); registry.register_lint(&INVALID_METACLASS); + registry.register_lint(&INVALID_OVERLOAD); registry.register_lint(&INVALID_PARAMETER_DEFAULT); registry.register_lint(&INVALID_PROTOCOL); registry.register_lint(&INVALID_RAISE); @@ -447,6 +448,49 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for various invalid `@overload` usages. + /// + /// ## Why is this bad? + /// The `@overload` decorator is used to define functions and methods that accepts different + /// combinations of arguments and return different types based on the arguments passed. This is + /// mainly beneficial for type checkers. But, if the `@overload` usage is invalid, the type + /// checker may not be able to provide correct type information. + /// + /// ## Example + /// + /// Defining only one overload: + /// + /// ```py + /// from typing import overload + /// + /// @overload + /// def foo(x: int) -> int: ... + /// def foo(x: int | None) -> int | None: + /// return x + /// ``` + /// + /// Or, not providing an implementation for the overloaded definition: + /// + /// ```py + /// from typing import overload + /// + /// @overload + /// def foo() -> None: ... + /// @overload + /// def foo(x: int) -> int: ... + /// ``` + /// + /// ## References + /// - [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload) + pub(crate) static INVALID_OVERLOAD = { + summary: "detects invalid `@overload` usages", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for default values that can't be assigned to the parameter's annotated type. diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 2e275e99924bde..d9a15f0cc6f2b5 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -101,8 +101,8 @@ use super::diagnostic::{ report_invalid_exception_raised, report_invalid_type_checking_constant, report_non_subscriptable, report_possibly_unresolved_reference, report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero, - report_unresolved_reference, INVALID_METACLASS, INVALID_PROTOCOL, REDUNDANT_CAST, - STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, + report_unresolved_reference, INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, + REDUNDANT_CAST, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, }; use super::slots::check_class_slots; use super::string_annotation::{ @@ -418,7 +418,7 @@ impl<'db> TypeInference<'db> { .copied() .or(self.cycle_fallback_type) .expect( - "definition should belong to this TypeInference region and + "definition should belong to this TypeInference region and \ TypeInferenceBuilder should have inferred a type for it", ) } @@ -430,7 +430,7 @@ impl<'db> TypeInference<'db> { .copied() .or(self.cycle_fallback_type.map(Into::into)) .expect( - "definition should belong to this TypeInference region and + "definition should belong to this TypeInference region and \ TypeInferenceBuilder should have inferred a type for it", ) } @@ -524,6 +524,31 @@ pub(super) struct TypeInferenceBuilder<'db> { /// The returned types and their corresponding ranges of the region, if it is a function body. return_types_and_ranges: Vec>, + /// A set of functions that have been defined **and** called in this region. + /// + /// This is a set because the same function could be called multiple times in the same region. + /// This is mainly used in [`check_overloaded_functions`] to check an overloaded function that + /// is shadowed by a function with the same name in this scope but has been called before. For + /// example: + /// + /// ```py + /// from typing import overload + /// + /// @overload + /// def foo() -> None: ... + /// @overload + /// def foo(x: int) -> int: ... + /// def foo(x: int | None) -> int | None: return x + /// + /// foo() # An overloaded function that was defined in this scope have been called + /// + /// def foo(x: int) -> int: + /// return x + /// ``` + /// + /// [`check_overloaded_functions`]: TypeInferenceBuilder::check_overloaded_functions + called_functions: FxHashSet>, + /// The deferred state of inferring types of certain expressions within the region. /// /// This is different from [`InferenceRegion::Deferred`] which works on the entire definition @@ -556,6 +581,7 @@ impl<'db> TypeInferenceBuilder<'db> { index, region, return_types_and_ranges: vec![], + called_functions: FxHashSet::default(), deferred_state: DeferredExpressionState::None, types: TypeInference::empty(scope), } @@ -718,6 +744,7 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO: Only call this function when diagnostics are enabled. self.check_class_definitions(); + self.check_overloaded_functions(); } /// Iterate over all class definitions to check that the definition will not cause an exception @@ -952,6 +979,86 @@ impl<'db> TypeInferenceBuilder<'db> { } } + /// Check the overloaded functions in this scope. + /// + /// This only checks the overloaded functions that are: + /// 1. Visible publicly at the end of this scope + /// 2. Or, defined and called in this scope + /// + /// For (1), this has the consequence of not checking an overloaded function that is being + /// shadowed by another function with the same name in this scope. + fn check_overloaded_functions(&mut self) { + // Collect all the unique overloaded function symbols in this scope. This requires a set + // because an overloaded function uses the same symbol for each of the overloads and the + // implementation. + let overloaded_function_symbols: FxHashSet<_> = self + .types + .declarations + .iter() + .filter_map(|(definition, ty)| { + // Filter out function literals that result from anything other than a function + // definition e.g., imports which would create a cross-module AST dependency. + if !matches!(definition.kind(self.db()), DefinitionKind::Function(_)) { + return None; + } + let function = ty.inner_type().into_function_literal()?; + if function.has_known_decorator(self.db(), FunctionDecorators::OVERLOAD) { + Some(definition.symbol(self.db())) + } else { + None + } + }) + .collect(); + + let use_def = self + .index + .use_def_map(self.scope().file_scope_id(self.db())); + + let mut public_functions = FxHashSet::default(); + + for symbol in overloaded_function_symbols { + if let Symbol::Type(Type::FunctionLiteral(function), Boundness::Bound) = + symbol_from_bindings(self.db(), use_def.public_bindings(symbol)) + { + if function.file(self.db()) != self.file() { + // If the function is not in this file, we don't need to check it. + // https://github.com/astral-sh/ruff/pull/17609#issuecomment-2839445740 + continue; + } + + // Extend the functions that we need to check with the publicly visible overloaded + // function. This is always going to be either the implementation or the last + // overload if the implementation doesn't exists. + public_functions.insert(function); + } + } + + for function in self.called_functions.union(&public_functions) { + let Some(overloaded) = function.to_overloaded(self.db()) else { + continue; + }; + + // Check that the overloaded function has at least two overloads + if let [single_overload] = overloaded.overloads.as_slice() { + let function_node = function.node(self.db(), self.file()); + if let Some(builder) = self + .context + .report_lint(&INVALID_OVERLOAD, &function_node.name) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Overloaded function `{}` requires at least two overloads", + &function_node.name + )); + diagnostic.annotate( + self.context + .secondary(single_overload.focus_range(self.db())) + .message(format_args!("Only one overload defined here")), + ); + } + } + } + } + fn infer_region_definition(&mut self, definition: Definition<'db>) { match definition.kind(self.db()) { DefinitionKind::Function(function) => { @@ -4299,6 +4406,18 @@ impl<'db> TypeInferenceBuilder<'db> { let mut call_arguments = Self::parse_arguments(arguments); let callable_type = self.infer_expression(func); + if let Type::FunctionLiteral(function) = callable_type { + // Make sure that the `function.definition` is only called when the function is defined + // in the same file as the one we're currently inferring the types for. This is because + // the `definition` method accesses the semantic index, which could create a + // cross-module AST dependency. + if function.file(self.db()) == self.file() + && function.definition(self.db()).scope(self.db()) == self.scope() + { + self.called_functions.insert(function); + } + } + // It might look odd here that we emit an error for class-literals but not `type[]` types. // But it's deliberate! The typing spec explicitly mandates that `type[]` types can be called // even though class-literals cannot. This is because even though a protocol class `SomeProtocol` diff --git a/knot.schema.json b/knot.schema.json index 66e0a5b2e304a8..aeda31fd530bc7 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -470,6 +470,16 @@ } ] }, + "invalid-overload": { + "title": "detects invalid `@overload` usages", + "description": "## What it does\nChecks for various invalid `@overload` usages.\n\n## Why is this bad?\nThe `@overload` decorator is used to define functions and methods that accepts different\ncombinations of arguments and return different types based on the arguments passed. This is\nmainly beneficial for type checkers. But, if the `@overload` usage is invalid, the type\nchecker may not be able to provide correct type information.\n\n## Example\n\nDefining only one overload:\n\n```py\nfrom typing import overload\n\n@overload\ndef foo(x: int) -> int: ...\ndef foo(x: int | None) -> int | None:\n return x\n```\n\nOr, not providing an implementation for the overloaded definition:\n\n```py\nfrom typing import overload\n\n@overload\ndef foo() -> None: ...\n@overload\ndef foo(x: int) -> int: ...\n```\n\n## References\n- [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-parameter-default": { "title": "detects default values that can't be assigned to the parameter's annotated type", "description": "## What it does\nChecks for default values that can't be assigned to the parameter's annotated type.\n\n## Why is this bad?\nTODO #14889", From f584b668247f01229bcbb7bf28e3cfd5221d32d7 Mon Sep 17 00:00:00 2001 From: Max Mynter <32773644+maxmynter@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:14:08 +0200 Subject: [PATCH 0189/1161] Expand Semantic Syntax Coverage (#17725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re: #17526 ## Summary Adds tests to red knot and `linter.rs` for the semantic syntax. Specifically add tests for `ReboundComprehensionVariable`, `DuplicateTypeParameter`, and `MultipleCaseAssignment`. Refactor the `test_async_comprehension_in_sync_comprehension` → `test_semantic_error` to be more general for all semantic syntax test cases. ## Test Plan This is a test. ## Question I'm happy to contribute more tests the coming days. Should that happen here or should we merge this PR such that the refactor `test_async_comprehension_in_sync_comprehension` → `test_semantic_error` is available on main and others can chime in, too? --- .../diagnostics/semantic_syntax_errors.md | 56 +++++++++++++++++++ crates/ruff_linter/src/linter.rs | 54 ++++++++++++++---- ...tion_async_in_sync_error_on_310_3.10.snap} | 0 ...on_async_in_sync_false_positive_3.10.snap} | 0 ...ction_async_in_sync_okay_on_310_3.10.snap} | 0 ...ction_async_in_sync_okay_on_311_3.11.snap} | 0 ...Function_deferred_function_body_3.10.snap} | 1 + ...peParameter_duplicate_type_param_3.12.snap | 8 +++ ...ignment_multiple_case_assignment_3.10.snap | 11 ++++ ...onVariable_rebound_comprehension_3.10.snap | 8 +++ 10 files changed, 128 insertions(+), 10 deletions(-) rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap => ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap} (100%) rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_deferred_function_body_3.10.snap => ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_false_positive_3.10.snap} (100%) rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_310_3.10.snap => ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap} (100%) rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_311_3.11.snap => ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap} (100%) rename crates/ruff_linter/src/snapshots/{ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap => ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap} (98%) create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md index 4c755ac6ccfa77..e380e10018c672 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -130,6 +130,62 @@ async def g(): (x async for x in g()) ``` +## Rebound comprehension variable + +Walrus operators cannot rebind variables already in use as iterators: + +```py +# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" +[x := 2 for x in range(10)] + +# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" +{y := 5 for y in range(10)} +``` + +## Multiple case assignments + +Variable names in pattern matching must be unique within a single pattern: + +```toml +[environment] +python-version = "3.10" +``` + +```py +x = [1, 2] +match x: + # error: [invalid-syntax] "multiple assignments to name `a` in pattern" + case [a, a]: + pass + case _: + pass + +d = {"key": "value"} +match d: + # error: [invalid-syntax] "multiple assignments to name `b` in pattern" + case {"key": b, "other": b}: + pass +``` + +## Duplicate type parameter + +Type parameter names must be unique in a generic class or function definition: + +```toml +[environment] +python-version = "3.12" +``` + +```py +# error: [invalid-syntax] "duplicate type parameter" +class C[T, T]: + pass + +# error: [invalid-syntax] "duplicate type parameter" +def f[X, Y, X](): + pass +``` + ## `await` outside async function This error includes `await`, `async for`, `async with`, and `async` comprehensions. diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 1fd72b16d858e6..2886ea528c7349 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1006,19 +1006,22 @@ mod tests { } #[test_case( - "error_on_310", + "async_in_sync_error_on_310", "async def f(): return [[x async for x in foo(n)] for n in range(3)]", - PythonVersion::PY310 + PythonVersion::PY310, + "AsyncComprehensionOutsideAsyncFunction" )] #[test_case( - "okay_on_311", + "async_in_sync_okay_on_311", "async def f(): return [[x async for x in foo(n)] for n in range(3)]", - PythonVersion::PY311 + PythonVersion::PY311, + "AsyncComprehensionOutsideAsyncFunction" )] #[test_case( - "okay_on_310", + "async_in_sync_okay_on_310", "async def test(): return [[x async for x in elements(n)] async for n in range(3)]", - PythonVersion::PY310 + PythonVersion::PY310, + "AsyncComprehensionOutsideAsyncFunction" )] #[test_case( "deferred_function_body", @@ -1028,15 +1031,46 @@ mod tests { def g(): ... [x async for x in foo()] ", - PythonVersion::PY310 + PythonVersion::PY310, + "AsyncComprehensionOutsideAsyncFunction" )] - #[test_case("false_positive", "[x async for x in y]", PythonVersion::PY310)] - fn test_async_comprehension_in_sync_comprehension( + #[test_case( + "async_in_sync_false_positive", + "[x async for x in y]", + PythonVersion::PY310, + "AsyncComprehensionOutsideAsyncFunction" + )] + #[test_case( + "rebound_comprehension", + "[x:= 2 for x in range(2)]", + PythonVersion::PY310, + "ReboundComprehensionVariable" + )] + #[test_case( + "duplicate_type_param", + "class C[T, T]: pass", + PythonVersion::PY312, + "DuplicateTypeParameter" + )] + #[test_case( + "multiple_case_assignment", + " + match x: + case [a, a]: + pass + case _: + pass + ", + PythonVersion::PY310, + "MultipleCaseAssignment" + )] + fn test_semantic_errors( name: &str, contents: &str, python_version: PythonVersion, + error_type: &str, ) { - let snapshot = format!("async_comprehension_in_sync_comprehension_{name}_{python_version}"); + let snapshot = format!("semantic_syntax_error_{error_type}_{name}_{python_version}"); let messages = test_snippet_syntax_errors( contents, &LinterSettings { diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap similarity index 100% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_error_on_310_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_error_on_310_3.10.snap diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_deferred_function_body_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_false_positive_3.10.snap similarity index 100% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_deferred_function_body_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_false_positive_3.10.snap diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_310_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap similarity index 100% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_310_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_310_3.10.snap diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_311_3.11.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap similarity index 100% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_okay_on_311_3.11.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_async_in_sync_okay_on_311_3.11.snap diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap similarity index 98% rename from crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap rename to crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap index 8d0a8faf7ae2a7..4ba33c756c4946 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension_in_sync_comprehension_false_positive_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_AsyncComprehensionOutsideAsyncFunction_deferred_function_body_3.10.snap @@ -1,3 +1,4 @@ --- source: crates/ruff_linter/src/linter.rs --- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap new file mode 100644 index 00000000000000..5ede497ec67715 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateTypeParameter_duplicate_type_param_3.12.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:1:12: SyntaxError: duplicate type parameter + | +1 | class C[T, T]: pass + | ^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap new file mode 100644 index 00000000000000..6cee83489af497 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_MultipleCaseAssignment_multiple_case_assignment_3.10.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:14: SyntaxError: multiple assignments to name `a` in pattern + | +2 | match x: +3 | case [a, a]: + | ^ +4 | pass +5 | case _: + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap new file mode 100644 index 00000000000000..80fb65620a4c32 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_ReboundComprehensionVariable_rebound_comprehension_3.10.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:1:2: SyntaxError: assignment expression cannot rebind comprehension variable + | +1 | [x:= 2 for x in range(2)] + | ^ + | From 78259759726f30b9aecff165f15ac449e8e4f915 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 30 Apr 2025 19:54:21 +0530 Subject: [PATCH 0190/1161] [red-knot] Check overloads without an implementation (#17681) ## Summary As mentioned in the spec (https://typing.python.org/en/latest/spec/overload.html#invalid-overload-definitions), part of #15383: > The `@overload`-decorated definitions must be followed by an overload implementation, which does not include an `@overload` decorator. Type checkers should report an error or warning if an implementation is missing. Overload definitions within stub files, protocols, and on abstract methods within abstract base classes are exempt from this check. ## Test Plan Remove TODOs from the test; create one diagnostic snapshot. --- .../resources/mdtest/call/methods.md | 2 +- .../resources/mdtest/overloads.md | 10 +++- ...t_an_implementation_-_Regular_modules.snap | 57 +++++++++++++++++++ .../src/types/class.rs | 7 +++ .../src/types/infer.rs | 44 +++++++++++++- 5 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap 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 9ca10a05c7a0ad..3abc3013722973 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -205,7 +205,7 @@ reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed: -```py +```pyi from types import FunctionType, MethodType from typing import overload diff --git a/crates/red_knot_python_semantic/resources/mdtest/overloads.md b/crates/red_knot_python_semantic/resources/mdtest/overloads.md index 4bbef31b1d7351..4089040d9b9c91 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/overloads.md +++ b/crates/red_knot_python_semantic/resources/mdtest/overloads.md @@ -336,23 +336,25 @@ def func(x: int) -> int: ... #### Regular modules + + In regular modules, a series of `@overload`-decorated definitions must be followed by exactly one non-`@overload`-decorated definition (for the same function/method). ```py from typing import overload -# TODO: error because implementation does not exists @overload def func(x: int) -> int: ... @overload +# error: [invalid-overload] "Overloaded non-stub function `func` must have an implementation" def func(x: str) -> str: ... class Foo: - # TODO: error because implementation does not exists @overload def method(self, x: int) -> int: ... @overload + # error: [invalid-overload] "Overloaded non-stub function `method` must have an implementation" def method(self, x: str) -> str: ... ``` @@ -405,12 +407,12 @@ from it. ```py class Foo: - # TODO: Error because implementation does not exists @overload @abstractmethod def f(self, x: int) -> int: ... @overload @abstractmethod + # error: [invalid-overload] def f(self, x: str) -> str: ... ``` @@ -422,6 +424,7 @@ class PartialFoo1(ABC): @abstractmethod def f(self, x: int) -> int: ... @overload + # error: [invalid-overload] def f(self, x: str) -> str: ... class PartialFoo(ABC): @@ -429,6 +432,7 @@ class PartialFoo(ABC): def f(self, x: int) -> int: ... @overload @abstractmethod + # error: [invalid-overload] def f(self, x: str) -> str: ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap new file mode 100644 index 00000000000000..f92141e10cdfb5 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap @@ -0,0 +1,57 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: overloads.md - Overloads - Invalid - Overload without an implementation - Regular modules +mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import overload + 2 | + 3 | @overload + 4 | def func(x: int) -> int: ... + 5 | @overload + 6 | # error: [invalid-overload] "Overloaded non-stub function `func` must have an implementation" + 7 | def func(x: str) -> str: ... + 8 | + 9 | class Foo: +10 | @overload +11 | def method(self, x: int) -> int: ... +12 | @overload +13 | # error: [invalid-overload] "Overloaded non-stub function `method` must have an implementation" +14 | def method(self, x: str) -> str: ... +``` + +# Diagnostics + +``` +error: lint:invalid-overload: Overloaded non-stub function `func` must have an implementation + --> src/mdtest_snippet.py:7:5 + | +5 | @overload +6 | # error: [invalid-overload] "Overloaded non-stub function `func` must have an implementation" +7 | def func(x: str) -> str: ... + | ^^^^ +8 | +9 | class Foo: + | + +``` + +``` +error: lint:invalid-overload: Overloaded non-stub function `method` must have an implementation + --> src/mdtest_snippet.py:14:9 + | +12 | @overload +13 | # error: [invalid-overload] "Overloaded non-stub function `method` must have an implementation" +14 | def method(self, x: str) -> str: ... + | ^^^^^^ + | + +``` diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index a2ccc66e8d3e5a..b68e6b97986320 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -562,6 +562,13 @@ impl<'db> ClassLiteral<'db> { }) } + /// Determine if this is an abstract class. + pub(super) fn is_abstract(self, db: &'db dyn Db) -> bool { + self.metaclass(db) + .into_class_literal() + .is_some_and(|metaclass| metaclass.is_known(db, KnownClass::ABCMeta)) + } + /// Return the types of the decorators on this class #[salsa::tracked(return_ref)] fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index d9a15f0cc6f2b5..b9c56a02af743a 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -744,7 +744,7 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO: Only call this function when diagnostics are enabled. self.check_class_definitions(); - self.check_overloaded_functions(); + self.check_overloaded_functions(node); } /// Iterate over all class definitions to check that the definition will not cause an exception @@ -987,7 +987,7 @@ impl<'db> TypeInferenceBuilder<'db> { /// /// For (1), this has the consequence of not checking an overloaded function that is being /// shadowed by another function with the same name in this scope. - fn check_overloaded_functions(&mut self) { + fn check_overloaded_functions(&mut self, scope: &NodeWithScopeKind) { // Collect all the unique overloaded function symbols in this scope. This requires a set // because an overloaded function uses the same symbol for each of the overloads and the // implementation. @@ -1056,6 +1056,46 @@ impl<'db> TypeInferenceBuilder<'db> { ); } } + + // Check that the overloaded function has an implementation. Overload definitions + // within stub files, protocols, and on abstract methods within abstract base classes + // are exempt from this check. + if overloaded.implementation.is_none() && !self.in_stub() { + let mut implementation_required = true; + + if let NodeWithScopeKind::Class(class_node_ref) = scope { + let class = binding_type( + self.db(), + self.index.expect_single_definition(class_node_ref.node()), + ) + .expect_class_literal(); + + if class.is_protocol(self.db()) + || (class.is_abstract(self.db()) + && overloaded.overloads.iter().all(|overload| { + overload.has_known_decorator( + self.db(), + FunctionDecorators::ABSTRACT_METHOD, + ) + })) + { + implementation_required = false; + } + } + + if implementation_required { + let function_node = function.node(self.db(), self.file()); + if let Some(builder) = self + .context + .report_lint(&INVALID_OVERLOAD, &function_node.name) + { + builder.into_diagnostic(format_args!( + "Overloaded non-stub function `{}` must have an implementation", + &function_node.name + )); + } + } + } } } From 0e85cbdd911f0ed2de72ea76d77ebc556e2c10a2 Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 30 Apr 2025 22:38:57 +0800 Subject: [PATCH 0191/1161] [`flake8-use-pathlib`] Avoid suggesting `Path.iterdir()` for `os.listdir` with file descriptor (`PTH208`) (#17715) ## Summary Fixes: #17695 --------- Co-authored-by: Dhruv Manilawala --- .../fixtures/flake8_use_pathlib/PTH208.py | 3 + .../rules/replaceable_by_pathlib.rs | 282 +++++++++--------- 2 files changed, 143 insertions(+), 142 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH208.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH208.py index 8a31b2b472e056..3bdf05ae45f6c1 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH208.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH208.py @@ -21,3 +21,6 @@ if "file" in os.listdir("dir"): ... + +os.listdir(1) +os.listdir(path=1) diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index 58e33f040f8245..a4bce419397001 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -18,152 +18,150 @@ use crate::rules::flake8_use_pathlib::violations::{ use ruff_python_ast::PythonVersion; pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { - if let Some(diagnostic_kind) = checker - .semantic() - .resolve_qualified_name(&call.func) - .and_then(|qualified_name| match qualified_name.segments() { - // PTH100 - ["os", "path", "abspath"] => Some(OsPathAbspath.into()), - // PTH101 - ["os", "chmod"] => Some(OsChmod.into()), - // PTH102 - ["os", "makedirs"] => Some(OsMakedirs.into()), - // PTH103 - ["os", "mkdir"] => Some(OsMkdir.into()), - // PTH104 - ["os", "rename"] => Some(OsRename.into()), - // PTH105 - ["os", "replace"] => Some(OsReplace.into()), - // PTH106 - ["os", "rmdir"] => Some(OsRmdir.into()), - // PTH107 - ["os", "remove"] => Some(OsRemove.into()), - // PTH108 - ["os", "unlink"] => Some(OsUnlink.into()), - // PTH109 - ["os", "getcwd"] => Some(OsGetcwd.into()), - ["os", "getcwdb"] => Some(OsGetcwd.into()), - // PTH110 - ["os", "path", "exists"] => Some(OsPathExists.into()), - // PTH111 - ["os", "path", "expanduser"] => Some(OsPathExpanduser.into()), - // PTH112 - ["os", "path", "isdir"] => Some(OsPathIsdir.into()), - // PTH113 - ["os", "path", "isfile"] => Some(OsPathIsfile.into()), - // PTH114 - ["os", "path", "islink"] => Some(OsPathIslink.into()), - // PTH116 - ["os", "stat"] => Some(OsStat.into()), - // PTH117 - ["os", "path", "isabs"] => Some(OsPathIsabs.into()), - // PTH118 - ["os", "path", "join"] => Some( - OsPathJoin { - module: "path".to_string(), - joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { - Joiner::Joinpath - } else { - Joiner::Slash - }, - } - .into(), - ), - ["os", "sep", "join"] => Some( - OsPathJoin { - module: "sep".to_string(), - joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { - Joiner::Joinpath - } else { - Joiner::Slash - }, - } - .into(), - ), - // PTH119 - ["os", "path", "basename"] => Some(OsPathBasename.into()), - // PTH120 - ["os", "path", "dirname"] => Some(OsPathDirname.into()), - // PTH121 - ["os", "path", "samefile"] => Some(OsPathSamefile.into()), - // PTH122 - ["os", "path", "splitext"] => Some(OsPathSplitext.into()), - // PTH202 - ["os", "path", "getsize"] => Some(OsPathGetsize.into()), - // PTH203 - ["os", "path", "getatime"] => Some(OsPathGetatime.into()), - // PTH204 - ["os", "path", "getmtime"] => Some(OsPathGetmtime.into()), - // PTH205 - ["os", "path", "getctime"] => Some(OsPathGetctime.into()), - // PTH123 - ["" | "builtins", "open"] => { - // `closefd` and `opener` are not supported by pathlib, so check if they are - // set to non-default values. - // https://github.com/astral-sh/ruff/issues/7620 - // Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open): - // ```text - // 0 1 2 3 4 5 - // open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, - // 6 7 - // closefd=True, opener=None) - // ^^^^ ^^^^ - // ``` - // For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open): - // ```text - // Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) - // ``` - if call + let Some(qualified_name) = checker.semantic().resolve_qualified_name(&call.func) else { + return; + }; + + let diagnostic_kind: DiagnosticKind = match qualified_name.segments() { + // PTH100 + ["os", "path", "abspath"] => OsPathAbspath.into(), + // PTH101 + ["os", "chmod"] => OsChmod.into(), + // PTH102 + ["os", "makedirs"] => OsMakedirs.into(), + // PTH103 + ["os", "mkdir"] => OsMkdir.into(), + // PTH104 + ["os", "rename"] => OsRename.into(), + // PTH105 + ["os", "replace"] => OsReplace.into(), + // PTH106 + ["os", "rmdir"] => OsRmdir.into(), + // PTH107 + ["os", "remove"] => OsRemove.into(), + // PTH108 + ["os", "unlink"] => OsUnlink.into(), + // PTH109 + ["os", "getcwd"] => OsGetcwd.into(), + ["os", "getcwdb"] => OsGetcwd.into(), + // PTH110 + ["os", "path", "exists"] => OsPathExists.into(), + // PTH111 + ["os", "path", "expanduser"] => OsPathExpanduser.into(), + // PTH112 + ["os", "path", "isdir"] => OsPathIsdir.into(), + // PTH113 + ["os", "path", "isfile"] => OsPathIsfile.into(), + // PTH114 + ["os", "path", "islink"] => OsPathIslink.into(), + // PTH116 + ["os", "stat"] => OsStat.into(), + // PTH117 + ["os", "path", "isabs"] => OsPathIsabs.into(), + // PTH118 + ["os", "path", "join"] => OsPathJoin { + module: "path".to_string(), + joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { + Joiner::Joinpath + } else { + Joiner::Slash + }, + } + .into(), + ["os", "sep", "join"] => OsPathJoin { + module: "sep".to_string(), + joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { + Joiner::Joinpath + } else { + Joiner::Slash + }, + } + .into(), + // PTH119 + ["os", "path", "basename"] => OsPathBasename.into(), + // PTH120 + ["os", "path", "dirname"] => OsPathDirname.into(), + // PTH121 + ["os", "path", "samefile"] => OsPathSamefile.into(), + // PTH122 + ["os", "path", "splitext"] => OsPathSplitext.into(), + // PTH202 + ["os", "path", "getsize"] => OsPathGetsize.into(), + // PTH203 + ["os", "path", "getatime"] => OsPathGetatime.into(), + // PTH204 + ["os", "path", "getmtime"] => OsPathGetmtime.into(), + // PTH205 + ["os", "path", "getctime"] => OsPathGetctime.into(), + // PTH123 + ["" | "builtins", "open"] => { + // `closefd` and `opener` are not supported by pathlib, so check if they are + // are set to non-default values. + // https://github.com/astral-sh/ruff/issues/7620 + // Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open): + // ```text + // 0 1 2 3 4 5 + // open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, + // 6 7 + // closefd=True, opener=None) + // ^^^^ ^^^^ + // ``` + // For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open): + // ```text + // Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) + // ``` + if call + .arguments + .find_argument_value("closefd", 6) + .is_some_and(|expr| { + !matches!( + expr, + Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. }) + ) + }) + || call + .arguments + .find_argument_value("opener", 7) + .is_some_and(|expr| !expr.is_none_literal_expr()) + || call .arguments - .find_argument_value("closefd", 6) - .is_some_and(|expr| { - !matches!( - expr, - Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. }) - ) - }) - || call - .arguments - .find_argument_value("opener", 7) - .is_some_and(|expr| !expr.is_none_literal_expr()) - || call.arguments.find_positional(0).is_some_and(|expr| { - is_file_descriptor_or_bytes_str(expr, checker.semantic()) - }) - { - return None; - } - Some(BuiltinOpen.into()) + .find_positional(0) + .is_some_and(|expr| is_file_descriptor_or_bytes_str(expr, checker.semantic())) + { + return; } - // PTH124 - ["py", "path", "local"] => Some(PyPath.into()), - // PTH207 - ["glob", "glob"] => Some( - Glob { - function: "glob".to_string(), - } - .into(), - ), - ["glob", "iglob"] => Some( - Glob { - function: "iglob".to_string(), - } - .into(), - ), - // PTH115 - // Python 3.9+ - ["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => { - Some(OsReadlink.into()) + BuiltinOpen.into() + } + // PTH124 + ["py", "path", "local"] => PyPath.into(), + // PTH207 + ["glob", "glob"] => Glob { + function: "glob".to_string(), + } + .into(), + ["glob", "iglob"] => Glob { + function: "iglob".to_string(), + } + .into(), + // PTH115 + // Python 3.9+ + ["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => OsReadlink.into(), + // PTH208 + ["os", "listdir"] => { + if call + .arguments + .find_argument_value("path", 0) + .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) + { + return; } - // PTH208, - ["os", "listdir"] => Some(OsListdir.into()), - _ => None, - }) - { - let diagnostic = Diagnostic::new::(diagnostic_kind, call.func.range()); - - if checker.enabled(diagnostic.kind.rule()) { - checker.report_diagnostic(diagnostic); + OsListdir.into() } + _ => return, + }; + + if checker.enabled(diagnostic_kind.rule()) { + checker.report_diagnostic(Diagnostic::new(diagnostic_kind, call.func.range())); } } From 7568eeb7a5fcb77af740c8ed0d77eef2f946d479 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 30 Apr 2025 20:34:21 +0530 Subject: [PATCH 0192/1161] [red-knot] Check decorator consistency on overloads (#17684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Part of #15383. As per the spec (https://typing.python.org/en/latest/spec/overload.html#invalid-overload-definitions): For `@staticmethod` and `@classmethod`: > If one overload signature is decorated with `@staticmethod` or `@classmethod`, all overload signatures must be similarly decorated. The implementation, if present, must also have a consistent decorator. Type checkers should report an error if these conditions are not met. For `@final` and `@override`: > If a `@final` or `@override` decorator is supplied for a function with overloads, the decorator should be applied only to the overload implementation if it is present. If an overload implementation isn’t present (for example, in a stub file), the `@final` or `@override` decorator should be applied only to the first overload. Type checkers should enforce these rules and generate an error when they are violated. If a `@final` or `@override` decorator follows these rules, a type checker should treat the decorator as if it is present on all overloads. ## Test Plan Update existing tests; add snapshots. --- .../resources/mdtest/overloads.md | 102 +++++++++----- ...onsistent_decorators_-_`@classmethod`.snap | 128 +++++++++++++++++ ..._-_Inconsistent_decorators_-_`@final`.snap | 111 +++++++++++++++ ...Inconsistent_decorators_-_`@override`.snap | 129 ++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 7 + .../src/types/infer.rs | 97 +++++++++++++ 6 files changed, 539 insertions(+), 35 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap diff --git a/crates/red_knot_python_semantic/resources/mdtest/overloads.md b/crates/red_knot_python_semantic/resources/mdtest/overloads.md index 4089040d9b9c91..c26b9cf42b8ecf 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/overloads.md +++ b/crates/red_knot_python_semantic/resources/mdtest/overloads.md @@ -438,11 +438,10 @@ class PartialFoo(ABC): ### Inconsistent decorators -#### `@staticmethod` / `@classmethod` +#### `@staticmethod` -If one overload signature is decorated with `@staticmethod` or `@classmethod`, all overload -signatures must be similarly decorated. The implementation, if present, must also have a consistent -decorator. +If one overload signature is decorated with `@staticmethod`, all overload signatures must be +similarly decorated. The implementation, if present, must also have a consistent decorator. ```py from __future__ import annotations @@ -486,39 +485,54 @@ class CheckStaticMethod: @staticmethod def method4(x: int | str) -> int | str: return x +``` + +#### `@classmethod` + + + +The same rules apply for `@classmethod` as for [`@staticmethod`](#staticmethod). + +```py +from __future__ import annotations + +from typing import overload class CheckClassMethod: def __init__(self, x: int) -> None: self.x = x - # TODO: error because `@classmethod` does not exist on all overloads + @overload @classmethod def try_from1(cls, x: int) -> CheckClassMethod: ... @overload def try_from1(cls, x: str) -> None: ... @classmethod + # error: [invalid-overload] "Overloaded function `try_from1` does not use the `@classmethod` decorator consistently" def try_from1(cls, x: int | str) -> CheckClassMethod | None: if isinstance(x, int): return cls(x) return None - # TODO: error because `@classmethod` does not exist on all overloads + @overload def try_from2(cls, x: int) -> CheckClassMethod: ... @overload @classmethod def try_from2(cls, x: str) -> None: ... @classmethod + # error: [invalid-overload] def try_from2(cls, x: int | str) -> CheckClassMethod | None: if isinstance(x, int): return cls(x) return None - # TODO: error because `@classmethod` does not exist on the implementation + @overload @classmethod def try_from3(cls, x: int) -> CheckClassMethod: ... @overload @classmethod def try_from3(cls, x: str) -> None: ... + # error: [invalid-overload] def try_from3(cls, x: int | str) -> CheckClassMethod | None: if isinstance(x, int): return cls(x) @@ -537,13 +551,15 @@ class CheckClassMethod: return None ``` -#### `@final` / `@override` +#### `@final` + + -If a `@final` or `@override` decorator is supplied for a function with overloads, the decorator -should be applied only to the overload implementation if it is present. +If a `@final` decorator is supplied for a function with overloads, the decorator should be applied +only to the overload implementation if it is present. ```py -from typing_extensions import final, overload, override +from typing_extensions import final, overload class Foo: @overload @@ -553,22 +569,55 @@ class Foo: @final def method1(self, x: int | str) -> int | str: return x - # TODO: error because `@final` is not on the implementation + @overload @final def method2(self, x: int) -> int: ... @overload def method2(self, x: str) -> str: ... + # error: [invalid-overload] def method2(self, x: int | str) -> int | str: return x - # TODO: error because `@final` is not on the implementation + @overload def method3(self, x: int) -> int: ... @overload @final def method3(self, x: str) -> str: ... + # error: [invalid-overload] def method3(self, x: int | str) -> int | str: return x +``` + +If an overload implementation isn't present (for example, in a stub file), the `@final` decorator +should be applied only to the first overload. + +```pyi +from typing_extensions import final, overload + +class Foo: + @overload + @final + def method1(self, x: int) -> int: ... + @overload + def method1(self, x: str) -> str: ... + + @overload + def method2(self, x: int) -> int: ... + @final + @overload + # error: [invalid-overload] + def method2(self, x: str) -> str: ... +``` + +#### `@override` + + + +The same rules apply for `@override` as for [`@final`](#final). + +```py +from typing_extensions import overload, override class Base: @overload @@ -588,47 +637,30 @@ class Sub1(Base): return x class Sub2(Base): - # TODO: error because `@override` is not on the implementation @overload def method(self, x: int) -> int: ... @overload @override def method(self, x: str) -> str: ... + # error: [invalid-overload] def method(self, x: int | str) -> int | str: return x class Sub3(Base): - # TODO: error because `@override` is not on the implementation @overload @override def method(self, x: int) -> int: ... @overload def method(self, x: str) -> str: ... + # error: [invalid-overload] def method(self, x: int | str) -> int | str: return x ``` -#### `@final` / `@override` in stub files - -If an overload implementation isn’t present (for example, in a stub file), the `@final` or -`@override` decorator should be applied only to the first overload. +And, similarly, in stub files: ```pyi -from typing_extensions import final, overload, override - -class Foo: - @overload - @final - def method1(self, x: int) -> int: ... - @overload - def method1(self, x: str) -> str: ... - - # TODO: error because `@final` is not on the first overload - @overload - def method2(self, x: int) -> int: ... - @final - @overload - def method2(self, x: str) -> str: ... +from typing_extensions import overload, override class Base: @overload @@ -644,10 +676,10 @@ class Sub1(Base): def method(self, x: str) -> str: ... class Sub2(Base): - # TODO: error because `@override` is not on the first overload @overload def method(self, x: int) -> int: ... @overload @override + # error: [invalid-overload] def method(self, x: str) -> str: ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap new file mode 100644 index 00000000000000..aef6a58ff12275 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap @@ -0,0 +1,128 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: overloads.md - Overloads - Invalid - Inconsistent decorators - `@classmethod` +mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from __future__ import annotations + 2 | + 3 | from typing import overload + 4 | + 5 | class CheckClassMethod: + 6 | def __init__(self, x: int) -> None: + 7 | self.x = x + 8 | + 9 | @overload +10 | @classmethod +11 | def try_from1(cls, x: int) -> CheckClassMethod: ... +12 | @overload +13 | def try_from1(cls, x: str) -> None: ... +14 | @classmethod +15 | # error: [invalid-overload] "Overloaded function `try_from1` does not use the `@classmethod` decorator consistently" +16 | def try_from1(cls, x: int | str) -> CheckClassMethod | None: +17 | if isinstance(x, int): +18 | return cls(x) +19 | return None +20 | +21 | @overload +22 | def try_from2(cls, x: int) -> CheckClassMethod: ... +23 | @overload +24 | @classmethod +25 | def try_from2(cls, x: str) -> None: ... +26 | @classmethod +27 | # error: [invalid-overload] +28 | def try_from2(cls, x: int | str) -> CheckClassMethod | None: +29 | if isinstance(x, int): +30 | return cls(x) +31 | return None +32 | +33 | @overload +34 | @classmethod +35 | def try_from3(cls, x: int) -> CheckClassMethod: ... +36 | @overload +37 | @classmethod +38 | def try_from3(cls, x: str) -> None: ... +39 | # error: [invalid-overload] +40 | def try_from3(cls, x: int | str) -> CheckClassMethod | None: +41 | if isinstance(x, int): +42 | return cls(x) +43 | return None +44 | +45 | @overload +46 | @classmethod +47 | def try_from4(cls, x: int) -> CheckClassMethod: ... +48 | @overload +49 | @classmethod +50 | def try_from4(cls, x: str) -> None: ... +51 | @classmethod +52 | def try_from4(cls, x: int | str) -> CheckClassMethod | None: +53 | if isinstance(x, int): +54 | return cls(x) +55 | return None +``` + +# Diagnostics + +``` +error: lint:invalid-overload: Overloaded function `try_from3` does not use the `@classmethod` decorator consistently + --> src/mdtest_snippet.py:40:9 + | +38 | def try_from3(cls, x: str) -> None: ... +39 | # error: [invalid-overload] +40 | def try_from3(cls, x: int | str) -> CheckClassMethod | None: + | --------- + | | + | Missing here +41 | if isinstance(x, int): +42 | return cls(x) + | + +``` + +``` +error: lint:invalid-overload: Overloaded function `try_from1` does not use the `@classmethod` decorator consistently + --> src/mdtest_snippet.py:13:9 + | +11 | def try_from1(cls, x: int) -> CheckClassMethod: ... +12 | @overload +13 | def try_from1(cls, x: str) -> None: ... + | --------- Missing here +14 | @classmethod +15 | # error: [invalid-overload] "Overloaded function `try_from1` does not use the `@classmethod` decorator consistently" +16 | def try_from1(cls, x: int | str) -> CheckClassMethod | None: + | ^^^^^^^^^ +17 | if isinstance(x, int): +18 | return cls(x) + | + +``` + +``` +error: lint:invalid-overload: Overloaded function `try_from2` does not use the `@classmethod` decorator consistently + --> src/mdtest_snippet.py:28:9 + | +26 | @classmethod +27 | # error: [invalid-overload] +28 | def try_from2(cls, x: int | str) -> CheckClassMethod | None: + | ^^^^^^^^^ +29 | if isinstance(x, int): +30 | return cls(x) + | + ::: src/mdtest_snippet.py:22:9 + | +21 | @overload +22 | def try_from2(cls, x: int) -> CheckClassMethod: ... + | --------- Missing here +23 | @overload +24 | @classmethod + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap new file mode 100644 index 00000000000000..97692ad5b8bf93 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap @@ -0,0 +1,111 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: overloads.md - Overloads - Invalid - Inconsistent decorators - `@final` +mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import final, overload + 2 | + 3 | class Foo: + 4 | @overload + 5 | def method1(self, x: int) -> int: ... + 6 | @overload + 7 | def method1(self, x: str) -> str: ... + 8 | @final + 9 | def method1(self, x: int | str) -> int | str: +10 | return x +11 | +12 | @overload +13 | @final +14 | def method2(self, x: int) -> int: ... +15 | @overload +16 | def method2(self, x: str) -> str: ... +17 | # error: [invalid-overload] +18 | def method2(self, x: int | str) -> int | str: +19 | return x +20 | +21 | @overload +22 | def method3(self, x: int) -> int: ... +23 | @overload +24 | @final +25 | def method3(self, x: str) -> str: ... +26 | # error: [invalid-overload] +27 | def method3(self, x: int | str) -> int | str: +28 | return x +``` + +## mdtest_snippet.pyi + +``` + 1 | from typing_extensions import final, overload + 2 | + 3 | class Foo: + 4 | @overload + 5 | @final + 6 | def method1(self, x: int) -> int: ... + 7 | @overload + 8 | def method1(self, x: str) -> str: ... + 9 | +10 | @overload +11 | def method2(self, x: int) -> int: ... +12 | @final +13 | @overload +14 | # error: [invalid-overload] +15 | def method2(self, x: str) -> str: ... +``` + +# Diagnostics + +``` +error: lint:invalid-overload: `@final` decorator should be applied only to the overload implementation + --> src/mdtest_snippet.py:27:9 + | +25 | def method3(self, x: str) -> str: ... +26 | # error: [invalid-overload] +27 | def method3(self, x: int | str) -> int | str: + | ------- + | | + | Implementation defined here +28 | return x + | + +``` + +``` +error: lint:invalid-overload: `@final` decorator should be applied only to the overload implementation + --> src/mdtest_snippet.py:18:9 + | +16 | def method2(self, x: str) -> str: ... +17 | # error: [invalid-overload] +18 | def method2(self, x: int | str) -> int | str: + | ------- + | | + | Implementation defined here +19 | return x + | + +``` + +``` +error: lint:invalid-overload: `@final` decorator should be applied only to the first overload + --> src/mdtest_snippet.pyi:11:9 + | +10 | @overload +11 | def method2(self, x: int) -> int: ... + | ------- First overload defined here +12 | @final +13 | @overload +14 | # error: [invalid-overload] +15 | def method2(self, x: str) -> str: ... + | ^^^^^^^ + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap new file mode 100644 index 00000000000000..bae8cd1bc7a4e1 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap @@ -0,0 +1,129 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: overloads.md - Overloads - Invalid - Inconsistent decorators - `@override` +mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import overload, override + 2 | + 3 | class Base: + 4 | @overload + 5 | def method(self, x: int) -> int: ... + 6 | @overload + 7 | def method(self, x: str) -> str: ... + 8 | def method(self, x: int | str) -> int | str: + 9 | return x +10 | +11 | class Sub1(Base): +12 | @overload +13 | def method(self, x: int) -> int: ... +14 | @overload +15 | def method(self, x: str) -> str: ... +16 | @override +17 | def method(self, x: int | str) -> int | str: +18 | return x +19 | +20 | class Sub2(Base): +21 | @overload +22 | def method(self, x: int) -> int: ... +23 | @overload +24 | @override +25 | def method(self, x: str) -> str: ... +26 | # error: [invalid-overload] +27 | def method(self, x: int | str) -> int | str: +28 | return x +29 | +30 | class Sub3(Base): +31 | @overload +32 | @override +33 | def method(self, x: int) -> int: ... +34 | @overload +35 | def method(self, x: str) -> str: ... +36 | # error: [invalid-overload] +37 | def method(self, x: int | str) -> int | str: +38 | return x +``` + +## mdtest_snippet.pyi + +``` + 1 | from typing_extensions import overload, override + 2 | + 3 | class Base: + 4 | @overload + 5 | def method(self, x: int) -> int: ... + 6 | @overload + 7 | def method(self, x: str) -> str: ... + 8 | + 9 | class Sub1(Base): +10 | @overload +11 | @override +12 | def method(self, x: int) -> int: ... +13 | @overload +14 | def method(self, x: str) -> str: ... +15 | +16 | class Sub2(Base): +17 | @overload +18 | def method(self, x: int) -> int: ... +19 | @overload +20 | @override +21 | # error: [invalid-overload] +22 | def method(self, x: str) -> str: ... +``` + +# Diagnostics + +``` +error: lint:invalid-overload: `@override` decorator should be applied only to the overload implementation + --> src/mdtest_snippet.py:27:9 + | +25 | def method(self, x: str) -> str: ... +26 | # error: [invalid-overload] +27 | def method(self, x: int | str) -> int | str: + | ------ + | | + | Implementation defined here +28 | return x + | + +``` + +``` +error: lint:invalid-overload: `@override` decorator should be applied only to the overload implementation + --> src/mdtest_snippet.py:37:9 + | +35 | def method(self, x: str) -> str: ... +36 | # error: [invalid-overload] +37 | def method(self, x: int | str) -> int | str: + | ------ + | | + | Implementation defined here +38 | return x + | + +``` + +``` +error: lint:invalid-overload: `@override` decorator should be applied only to the first overload + --> src/mdtest_snippet.pyi:18:9 + | +16 | class Sub2(Base): +17 | @overload +18 | def method(self, x: int) -> int: ... + | ------ First overload defined here +19 | @overload +20 | @override +21 | # error: [invalid-overload] +22 | def method(self, x: str) -> str: ... + | ^^^^^^ + | + +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 5d2c430fa861af..007904f177ab48 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -6493,6 +6493,13 @@ struct OverloadedFunction<'db> { implementation: Option>, } +impl<'db> OverloadedFunction<'db> { + /// Returns an iterator over all overloads and the implementation, in that order. + fn all(&self) -> impl Iterator> + '_ { + self.overloads.iter().copied().chain(self.implementation) + } +} + #[salsa::interned(debug)] pub struct FunctionType<'db> { /// Name of the function at definition. diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index b9c56a02af743a..9b5846d83c217b 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1096,6 +1096,103 @@ impl<'db> TypeInferenceBuilder<'db> { } } } + + // TODO: Add `@staticmethod` + for (decorator, name) in [(FunctionDecorators::CLASSMETHOD, "classmethod")] { + let mut decorator_present = false; + let mut decorator_missing = vec![]; + + for function in overloaded.all() { + if function.has_known_decorator(self.db(), decorator) { + decorator_present = true; + } else { + decorator_missing.push(function); + } + } + + if !decorator_present { + // Both overloads and implementation does not have the decorator + continue; + } + if decorator_missing.is_empty() { + // All overloads and implementation have the decorator + continue; + } + + let function_node = function.node(self.db(), self.file()); + if let Some(builder) = self + .context + .report_lint(&INVALID_OVERLOAD, &function_node.name) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Overloaded function `{}` does not use the `@{name}` decorator \ + consistently", + &function_node.name + )); + for function in decorator_missing { + diagnostic.annotate( + self.context + .secondary(function.focus_range(self.db())) + .message(format_args!("Missing here")), + ); + } + } + } + + for (decorator, name) in [ + (FunctionDecorators::FINAL, "final"), + (FunctionDecorators::OVERRIDE, "override"), + ] { + if let Some(implementation) = overloaded.implementation.as_ref() { + for overload in &overloaded.overloads { + if !overload.has_known_decorator(self.db(), decorator) { + continue; + } + let function_node = function.node(self.db(), self.file()); + let Some(builder) = self + .context + .report_lint(&INVALID_OVERLOAD, &function_node.name) + else { + continue; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "`@{name}` decorator should be applied only to the \ + overload implementation" + )); + diagnostic.annotate( + self.context + .secondary(implementation.focus_range(self.db())) + .message(format_args!("Implementation defined here")), + ); + } + } else { + let mut overloads = overloaded.overloads.iter(); + let Some(first_overload) = overloads.next() else { + continue; + }; + for overload in overloads { + if !overload.has_known_decorator(self.db(), decorator) { + continue; + } + let function_node = function.node(self.db(), self.file()); + let Some(builder) = self + .context + .report_lint(&INVALID_OVERLOAD, &function_node.name) + else { + continue; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "`@{name}` decorator should be applied only to the \ + first overload" + )); + diagnostic.annotate( + self.context + .secondary(first_overload.focus_range(self.db())) + .message(format_args!("First overload defined here")), + ); + } + } + } } } From 18bac942265c33f77fe061b2ec9379b8477f1e7b Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 30 Apr 2025 17:27:09 +0200 Subject: [PATCH 0193/1161] [red-knot] Lookup of `__new__` (#17733) ## Summary Model the lookup of `__new__` without going through `Type::try_call_dunder`. The `__new__` method is only looked up on the constructed type itself, not on the meta-type. This now removes ~930 false positives across the ecosystem (vs 255 for https://github.com/astral-sh/ruff/pull/17662). It introduces 30 new false positives related to the construction of enums via something like `Color = enum.Enum("Color", ["RED", "GREEN"])`. This is expected, because we don't handle custom metaclass `__call__` methods. The fact that we previously didn't emit diagnostics there was a coincidence (we incorrectly called `EnumMeta.__new__`, and since we don't fully understand its signature, that happened to work with `str`, `list` arguments). closes #17462 ## Test Plan Regression test --- .../resources/mdtest/call/constructor.md | 68 ++++++++++++--- crates/red_knot_python_semantic/src/symbol.rs | 28 ++++++ crates/red_knot_python_semantic/src/types.rs | 87 ++++++++++++------- .../src/types/call/bind.rs | 18 ++-- .../src/types/class.rs | 28 +++--- .../src/types/infer.rs | 10 +-- 6 files changed, 163 insertions(+), 76 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md b/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md index 56a49fc13f97cf..83ca0093eee360 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md @@ -1,25 +1,25 @@ # Constructor -When classes are instantiated, Python calls the meta-class `__call__` method, which can either be -customized by the user or `type.__call__` is used. +When classes are instantiated, Python calls the metaclass's `__call__` method. The metaclass of most +Python classes is the class `builtins.type`. -The latter calls the `__new__` method of the class, which is responsible for creating the instance -and then calls the `__init__` method on the resulting instance to initialize it with the same -arguments. +`type.__call__` calls the `__new__` method of the class, which is responsible for creating the +instance. `__init__` is then called on the constructed instance with the same arguments that were +passed to `__new__`. -Both `__new__` and `__init__` are looked up using full descriptor protocol, but `__new__` is then -called as an implicit static, rather than bound method with `cls` passed as the first argument. -`__init__` has no special handling, it is fetched as bound method and is called just like any other -dunder method. +Both `__new__` and `__init__` are looked up using the descriptor protocol, i.e., `__get__` is called +if these attributes are descriptors. `__new__` is always treated as a static method, i.e., `cls` is +passed as the first argument. `__init__` has no special handling; it is fetched as a bound method +and called just like any other dunder method. `type.__call__` does other things too, but this is not yet handled by us. Since every class has `object` in it's MRO, the default implementations are `object.__new__` and `object.__init__`. They have some special behavior, namely: -- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for `object`) - \- no arguments are accepted and `TypeError` is raised if any are passed. -- If `__new__` is defined, but `__init__` is not - `object.__init__` will allow arbitrary arguments! +- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for + `object`), no arguments are accepted and `TypeError` is raised if any are passed. +- If `__new__` is defined but `__init__` is not, `object.__init__` will allow arbitrary arguments! As of today there are a number of behaviors that we do not support: @@ -146,6 +146,25 @@ reveal_type(Foo()) # revealed: Foo ### Possibly Unbound +#### Possibly unbound `__new__` method + +```py +def _(flag: bool) -> None: + class Foo: + if flag: + def __new__(cls): + return object.__new__(cls) + + # error: [call-possibly-unbound-method] + reveal_type(Foo()) # revealed: Foo + + # error: [call-possibly-unbound-method] + # error: [too-many-positional-arguments] + reveal_type(Foo(1)) # revealed: Foo +``` + +#### Possibly unbound `__call__` on `__new__` callable + ```py def _(flag: bool) -> None: class Callable: @@ -323,3 +342,28 @@ reveal_type(Foo(1)) # revealed: Foo # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" reveal_type(Foo(1, 2)) # revealed: Foo ``` + +### Lookup of `__new__` + +The `__new__` method is always invoked on the class itself, never on the metaclass. This is +different from how other dunder methods like `__lt__` are implicitly called (always on the +meta-type, never on the type itself). + +```py +from typing_extensions import Literal + +class Meta(type): + def __new__(mcls, name, bases, namespace, /, **kwargs): + return super().__new__(mcls, name, bases, namespace) + + def __lt__(cls, other) -> Literal[True]: + return True + +class C(metaclass=Meta): ... + +# No error is raised here, since we don't implicitly call `Meta.__new__` +reveal_type(C()) # revealed: C + +# Meta.__lt__ is implicitly called here: +reveal_type(C < C) # revealed: Literal[True] +``` diff --git a/crates/red_knot_python_semantic/src/symbol.rs b/crates/red_knot_python_semantic/src/symbol.rs index b90103585abe63..49b404eb711f56 100644 --- a/crates/red_knot_python_semantic/src/symbol.rs +++ b/crates/red_knot_python_semantic/src/symbol.rs @@ -107,6 +107,34 @@ impl<'db> Symbol<'db> { qualifiers, } } + + /// Try to call `__get__(None, owner)` on the type of this symbol (not on the meta type). + /// If it succeeds, return the `__get__` return type. Otherwise, returns the original symbol. + /// This is used to resolve (potential) descriptor attributes. + pub(crate) fn try_call_dunder_get(self, db: &'db dyn Db, owner: Type<'db>) -> Symbol<'db> { + match self { + Symbol::Type(Type::Union(union), boundness) => union.map_with_boundness(db, |elem| { + Symbol::Type(*elem, boundness).try_call_dunder_get(db, owner) + }), + + Symbol::Type(Type::Intersection(intersection), boundness) => intersection + .map_with_boundness(db, |elem| { + Symbol::Type(*elem, boundness).try_call_dunder_get(db, owner) + }), + + Symbol::Type(self_ty, boundness) => { + if let Some((dunder_get_return_ty, _)) = + self_ty.try_call_dunder_get(db, Type::none(db), owner) + { + Symbol::Type(dunder_get_return_ty, boundness) + } else { + self + } + } + + Symbol::Unbound => Symbol::Unbound, + } + } } impl<'db> From> for SymbolAndQualifiers<'db> { diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 007904f177ab48..f4d12a06f540aa 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -155,7 +155,7 @@ fn definition_expression_type<'db>( /// method or a `__delete__` method. This enum is used to categorize attributes into two /// groups: (1) data descriptors and (2) normal attributes or non-data descriptors. #[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, salsa::Update)] -enum AttributeKind { +pub(crate) enum AttributeKind { DataDescriptor, NormalOrNonDataDescriptor, } @@ -2627,7 +2627,7 @@ impl<'db> Type<'db> { /// /// If `__get__` is not defined on the meta-type, this method returns `None`. #[salsa::tracked] - fn try_call_dunder_get( + pub(crate) fn try_call_dunder_get( self, db: &'db dyn Db, instance: Type<'db>, @@ -2643,7 +2643,10 @@ impl<'db> Type<'db> { if let Symbol::Type(descr_get, descr_get_boundness) = descr_get { let return_ty = descr_get - .try_call(db, CallArgumentTypes::positional([self, instance, owner])) + .try_call( + db, + &mut CallArgumentTypes::positional([self, instance, owner]), + ) .map(|bindings| { if descr_get_boundness == Boundness::Bound { bindings.return_type(db) @@ -4198,11 +4201,10 @@ impl<'db> Type<'db> { fn try_call( self, db: &'db dyn Db, - mut argument_types: CallArgumentTypes<'_, 'db>, + argument_types: &mut CallArgumentTypes<'_, 'db>, ) -> Result, CallError<'db>> { let signatures = self.signatures(db); - Bindings::match_parameters(signatures, &mut argument_types) - .check_types(db, &mut argument_types) + Bindings::match_parameters(signatures, argument_types).check_types(db, argument_types) } /// Look up a dunder method on the meta-type of `self` and call it. @@ -4466,16 +4468,27 @@ impl<'db> Type<'db> { // easy to check if that's the one we found? // Note that `__new__` is a static method, so we must inject the `cls` argument. let new_call_outcome = argument_types.with_self(Some(self_type), |argument_types| { - let result = self_type.try_call_dunder_with_policy( - db, - "__new__", - argument_types, - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK - | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, - ); - match result { - Err(CallDunderError::MethodNotAvailable) => None, - _ => Some(result), + let new_method = self_type + .find_name_in_mro_with_policy( + db, + "__new__", + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + )? + .symbol + .try_call_dunder_get(db, self_type); + + match new_method { + Symbol::Type(new_method, boundness) => { + let result = new_method.try_call(db, argument_types); + + if boundness == Boundness::PossiblyUnbound { + return Some(Err(DunderNewCallError::PossiblyUnbound(result.err()))); + } + + Some(result.map_err(DunderNewCallError::CallError)) + } + Symbol::Unbound => None, } }); @@ -6265,12 +6278,23 @@ impl<'db> BoolError<'db> { } } +/// Represents possibly failure modes of implicit `__new__` calls. +#[derive(Debug)] +enum DunderNewCallError<'db> { + /// The call to `__new__` failed. + CallError(CallError<'db>), + /// The `__new__` method could be unbound. If the call to the + /// method has also failed, this variant also includes the + /// corresponding `CallError`. + PossiblyUnbound(Option>), +} + /// Error returned if a class instantiation call failed #[derive(Debug)] enum ConstructorCallError<'db> { Init(Type<'db>, CallDunderError<'db>), - New(Type<'db>, CallDunderError<'db>), - NewAndInit(Type<'db>, CallDunderError<'db>, CallDunderError<'db>), + New(Type<'db>, DunderNewCallError<'db>), + NewAndInit(Type<'db>, DunderNewCallError<'db>, CallDunderError<'db>), } impl<'db> ConstructorCallError<'db> { @@ -6320,13 +6344,8 @@ impl<'db> ConstructorCallError<'db> { } }; - let report_new_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error { - CallDunderError::MethodNotAvailable => { - // We are explicitly checking for `__new__` before attempting to call it, - // so this should never happen. - unreachable!("`__new__` method may not be called if missing"); - } - CallDunderError::PossiblyUnbound(bindings) => { + let report_new_error = |error: &DunderNewCallError<'db>| match error { + DunderNewCallError::PossiblyUnbound(call_error) => { if let Some(builder) = context.report_lint(&CALL_POSSIBLY_UNBOUND_METHOD, context_expression_node) { @@ -6336,22 +6355,24 @@ impl<'db> ConstructorCallError<'db> { )); } - bindings.report_diagnostics(context, context_expression_node); + if let Some(CallError(_kind, bindings)) = call_error { + bindings.report_diagnostics(context, context_expression_node); + } } - CallDunderError::CallError(_, bindings) => { + DunderNewCallError::CallError(CallError(_kind, bindings)) => { bindings.report_diagnostics(context, context_expression_node); } }; match self { - Self::Init(_, call_dunder_error) => { - report_init_error(call_dunder_error); + Self::Init(_, init_call_dunder_error) => { + report_init_error(init_call_dunder_error); } - Self::New(_, call_dunder_error) => { - report_new_error(call_dunder_error); + Self::New(_, new_call_error) => { + report_new_error(new_call_error); } - Self::NewAndInit(_, new_call_dunder_error, init_call_dunder_error) => { - report_new_error(new_call_dunder_error); + Self::NewAndInit(_, new_call_error, init_call_dunder_error) => { + report_new_error(new_call_error); report_init_error(init_call_dunder_error); } } diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 14e6039c3165cd..09bf3b0d34e046 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -346,7 +346,7 @@ impl<'db> Bindings<'db> { [Some(Type::PropertyInstance(property)), Some(instance), ..] => { if let Some(getter) = property.getter(db) { if let Ok(return_ty) = getter - .try_call(db, CallArgumentTypes::positional([*instance])) + .try_call(db, &mut CallArgumentTypes::positional([*instance])) .map(|binding| binding.return_type(db)) { overload.set_return_type(return_ty); @@ -374,7 +374,7 @@ impl<'db> Bindings<'db> { [Some(instance), ..] => { if let Some(getter) = property.getter(db) { if let Ok(return_ty) = getter - .try_call(db, CallArgumentTypes::positional([*instance])) + .try_call(db, &mut CallArgumentTypes::positional([*instance])) .map(|binding| binding.return_type(db)) { overload.set_return_type(return_ty); @@ -400,9 +400,10 @@ impl<'db> Bindings<'db> { overload.parameter_types() { if let Some(setter) = property.setter(db) { - if let Err(_call_error) = setter - .try_call(db, CallArgumentTypes::positional([*instance, *value])) - { + if let Err(_call_error) = setter.try_call( + db, + &mut CallArgumentTypes::positional([*instance, *value]), + ) { overload.errors.push(BindingError::InternalCallError( "calling the setter failed", )); @@ -418,9 +419,10 @@ impl<'db> Bindings<'db> { Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => { if let [Some(instance), Some(value), ..] = overload.parameter_types() { if let Some(setter) = property.setter(db) { - if let Err(_call_error) = setter - .try_call(db, CallArgumentTypes::positional([*instance, *value])) - { + if let Err(_call_error) = setter.try_call( + db, + &mut CallArgumentTypes::positional([*instance, *value]), + ) { overload.errors.push(BindingError::InternalCallError( "calling the setter failed", )); diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index b68e6b97986320..912d3946ac2c68 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -732,9 +732,9 @@ impl<'db> ClassLiteral<'db> { let namespace = KnownClass::Dict.to_instance(db); // TODO: Other keyword arguments? - let arguments = CallArgumentTypes::positional([name, bases, namespace]); + let mut arguments = CallArgumentTypes::positional([name, bases, namespace]); - let return_ty_result = match metaclass.try_call(db, arguments) { + let return_ty_result = match metaclass.try_call(db, &mut arguments) { Ok(bindings) => Ok(bindings.return_type(db)), Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError { @@ -817,17 +817,14 @@ impl<'db> ClassLiteral<'db> { return Some(metaclass_call_function.into_callable_type(db)); } - let new_function_symbol = self_ty - .member_lookup_with_policy( - db, - "__new__".into(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK - | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, - ) - .symbol; + let dunder_new_method = self_ty + .find_name_in_mro(db, "__new__") + .expect("find_name_in_mro always succeeds for class literals") + .symbol + .try_call_dunder_get(db, self_ty); - if let Symbol::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol { - return Some(new_function.into_bound_method_type(db, self.into())); + if let Symbol::Type(Type::FunctionLiteral(dunder_new_method), _) = dunder_new_method { + return Some(dunder_new_method.into_bound_method_type(db, self.into())); } // TODO handle `__init__` also None @@ -905,12 +902,7 @@ impl<'db> ClassLiteral<'db> { continue; } - // HACK: we should implement some more general logic here that supports arbitrary custom - // metaclasses, not just `type` and `ABCMeta`. - if matches!( - class.known(db), - Some(KnownClass::Type | KnownClass::ABCMeta) - ) && policy.meta_class_no_type_fallback() + if class.is_known(db, KnownClass::Type) && policy.meta_class_no_type_fallback() { continue; } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 9b5846d83c217b..69c20c33eb2dc7 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1815,7 +1815,7 @@ impl<'db> TypeInferenceBuilder<'db> { for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { inferred_ty = match decorator_ty - .try_call(self.db(), CallArgumentTypes::positional([inferred_ty])) + .try_call(self.db(), &mut CallArgumentTypes::positional([inferred_ty])) .map(|bindings| bindings.return_type(self.db())) { Ok(return_ty) => return_ty, @@ -2832,7 +2832,7 @@ impl<'db> TypeInferenceBuilder<'db> { let successful_call = meta_dunder_set .try_call( db, - CallArgumentTypes::positional([ + &mut CallArgumentTypes::positional([ meta_attr_ty, object_ty, value_ty, @@ -2973,7 +2973,7 @@ impl<'db> TypeInferenceBuilder<'db> { let successful_call = meta_dunder_set .try_call( db, - CallArgumentTypes::positional([ + &mut CallArgumentTypes::positional([ meta_attr_ty, object_ty, value_ty, @@ -6454,7 +6454,7 @@ impl<'db> TypeInferenceBuilder<'db> { Symbol::Type(contains_dunder, Boundness::Bound) => { // If `__contains__` is available, it is used directly for the membership test. contains_dunder - .try_call(db, CallArgumentTypes::positional([right, left])) + .try_call(db, &mut CallArgumentTypes::positional([right, left])) .map(|bindings| bindings.return_type(db)) .ok() } @@ -6860,7 +6860,7 @@ impl<'db> TypeInferenceBuilder<'db> { match ty.try_call( self.db(), - CallArgumentTypes::positional([value_ty, slice_ty]), + &mut CallArgumentTypes::positional([value_ty, slice_ty]), ) { Ok(bindings) => return bindings.return_type(self.db()), Err(CallError(_, bindings)) => { From b6de01b9a51516d50bd296a8740f8cc7f805a0ba Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 30 Apr 2025 17:01:28 +0100 Subject: [PATCH 0194/1161] [red-knot] Ban direct instantiation of generic protocols as well as non-generic ones (#17741) --- .../resources/mdtest/protocols.md | 15 +++ ...Protocols_-_Calls_to_protocol_classes.snap | 93 +++++++++++++++---- .../src/types/infer.rs | 25 +++-- 3 files changed, 106 insertions(+), 27 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 5f124e0549508f..30afd66e7c4df4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -304,6 +304,11 @@ reveal_type(typing.Protocol is not typing_extensions.Protocol) # revealed: bool Neither `Protocol`, nor any protocol class, can be directly instantiated: +```toml +[environment] +python-version = "3.12" +``` + ```py from typing_extensions import Protocol, reveal_type @@ -315,6 +320,12 @@ class MyProtocol(Protocol): # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" reveal_type(MyProtocol()) # revealed: MyProtocol + +class GenericProtocol[T](Protocol): + x: T + +# error: [call-non-callable] "Cannot instantiate class `GenericProtocol`" +reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] ``` But a non-protocol class can be instantiated, even if it has `Protocol` in its MRO: @@ -323,6 +334,10 @@ But a non-protocol class can be instantiated, even if it has `Protocol` in its M class SubclassOfMyProtocol(MyProtocol): ... reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol + +class SubclassOfGenericProtocol[T](GenericProtocol[T]): ... + +reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] ``` And as a corollary, `type[MyProtocol]` can also be called: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap index 9f5d4e2781e4d3..a223adb68137dd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap @@ -22,11 +22,21 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md 8 | 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" 10 | reveal_type(MyProtocol()) # revealed: MyProtocol -11 | class SubclassOfMyProtocol(MyProtocol): ... -12 | -13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol -14 | def f(x: type[MyProtocol]): -15 | reveal_type(x()) # revealed: MyProtocol +11 | +12 | class GenericProtocol[T](Protocol): +13 | x: T +14 | +15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`" +16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] +17 | class SubclassOfMyProtocol(MyProtocol): ... +18 | +19 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol +20 | +21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ... +22 | +23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] +24 | def f(x: type[MyProtocol]): +25 | reveal_type(x()) # revealed: MyProtocol ``` # Diagnostics @@ -64,7 +74,8 @@ error: lint:call-non-callable: Cannot instantiate class `MyProtocol` 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" 10 | reveal_type(MyProtocol()) # revealed: MyProtocol | ^^^^^^^^^^^^ This call will raise `TypeError` at runtime -11 | class SubclassOfMyProtocol(MyProtocol): ... +11 | +12 | class GenericProtocol[T](Protocol): | info: Protocol classes cannot be instantiated --> src/mdtest_snippet.py:6:7 @@ -85,32 +96,80 @@ info: revealed-type: Revealed type 9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`" 10 | reveal_type(MyProtocol()) # revealed: MyProtocol | ^^^^^^^^^^^^^^^^^^^^^^^^^ `MyProtocol` -11 | class SubclassOfMyProtocol(MyProtocol): ... +11 | +12 | class GenericProtocol[T](Protocol): + | + +``` + +``` +error: lint:call-non-callable: Cannot instantiate class `GenericProtocol` + --> src/mdtest_snippet.py:16:13 + | +15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`" +16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] + | ^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime +17 | class SubclassOfMyProtocol(MyProtocol): ... + | +info: Protocol classes cannot be instantiated + --> src/mdtest_snippet.py:12:7 + | +10 | reveal_type(MyProtocol()) # revealed: MyProtocol +11 | +12 | class GenericProtocol[T](Protocol): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol` declared as a protocol here +13 | x: T + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:16:1 + | +15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`" +16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol[int]` +17 | class SubclassOfMyProtocol(MyProtocol): ... | ``` ``` info: revealed-type: Revealed type - --> src/mdtest_snippet.py:13:1 + --> src/mdtest_snippet.py:19:1 | -11 | class SubclassOfMyProtocol(MyProtocol): ... -12 | -13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol +17 | class SubclassOfMyProtocol(MyProtocol): ... +18 | +19 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfMyProtocol` -14 | def f(x: type[MyProtocol]): -15 | reveal_type(x()) # revealed: MyProtocol +20 | +21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ... + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:23:1 + | +21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ... +22 | +23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfGenericProtocol[int]` +24 | def f(x: type[MyProtocol]): +25 | reveal_type(x()) # revealed: MyProtocol | ``` ``` info: revealed-type: Revealed type - --> src/mdtest_snippet.py:15:5 + --> src/mdtest_snippet.py:25:5 | -13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol -14 | def f(x: type[MyProtocol]): -15 | reveal_type(x()) # revealed: MyProtocol +23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int] +24 | def f(x: type[MyProtocol]): +25 | reveal_type(x()) # revealed: MyProtocol | ^^^^^^^^^^^^^^^^ `MyProtocol` | diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 69c20c33eb2dc7..22b821bdd392ba 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -4555,18 +4555,23 @@ impl<'db> TypeInferenceBuilder<'db> { } } - // It might look odd here that we emit an error for class-literals but not `type[]` types. - // But it's deliberate! The typing spec explicitly mandates that `type[]` types can be called - // even though class-literals cannot. This is because even though a protocol class `SomeProtocol` - // is always an abstract class, `type[SomeProtocol]` can be a concrete subclass of that protocol - // -- and indeed, according to the spec, type checkers must disallow abstract subclasses of the - // protocol to be passed to parameters that accept `type[SomeProtocol]`. + // It might look odd here that we emit an error for class-literals and generic aliases but not + // `type[]` types. But it's deliberate! The typing spec explicitly mandates that `type[]` types + // can be called even though class-literals cannot. This is because even though a protocol class + // `SomeProtocol` is always an abstract class, `type[SomeProtocol]` can be a concrete subclass of + // that protocol -- and indeed, according to the spec, type checkers must disallow abstract + // subclasses of the protocol to be passed to parameters that accept `type[SomeProtocol]`. // . - if let Some(protocol_class) = callable_type - .into_class_literal() - .and_then(|class| class.into_protocol_class(self.db())) + let possible_protocol_class = match callable_type { + Type::ClassLiteral(class) => Some(class), + Type::GenericAlias(generic) => Some(generic.origin(self.db())), + _ => None, + }; + + if let Some(protocol) = + possible_protocol_class.and_then(|class| class.into_protocol_class(self.db())) { - report_attempted_protocol_instantiation(&self.context, call_expression, protocol_class); + report_attempted_protocol_instantiation(&self.context, call_expression, protocol); } // For class literals we model the entire class instantiation logic, so it is handled From a7c358ab5c784953c960365f003ace65c2d04a18 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 30 Apr 2025 18:19:07 +0200 Subject: [PATCH 0195/1161] [red-knot] Update salsa to prevent panic in custom panic-handler (#17742) --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- fuzz/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ec41817dc5214..f78010f0ec8e10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3445,7 +3445,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" version = "0.21.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf#79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" +source = "git+https://github.com/salsa-rs/salsa.git?rev=42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7#42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7" dependencies = [ "boxcar", "compact_str", @@ -3468,12 +3468,12 @@ dependencies = [ [[package]] name = "salsa-macro-rules" version = "0.21.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf#79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" +source = "git+https://github.com/salsa-rs/salsa.git?rev=42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7#42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7" [[package]] name = "salsa-macros" version = "0.21.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf#79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" +source = "git+https://github.com/salsa-rs/salsa.git?rev=42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7#42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 57934aed2517d7..039c225ff82bd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ rayon = { version = "1.10.0" } regex = { version = "1.10.2" } rustc-hash = { version = "2.0.0" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" } +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7" } schemars = { version = "0.8.16" } seahash = { version = "4.1.0" } serde = { version = "1.0.197", features = ["derive"] } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index c8a4a9c7a4418d..d9c16cf9ded4fc 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -29,7 +29,7 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter" } ruff_text_size = { path = "../crates/ruff_text_size" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" } +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7" } similar = { version = "2.5.0" } tracing = { version = "0.1.40" } From 5679bf00bcd7dd193e1dd5008294ed9d329f57f4 Mon Sep 17 00:00:00 2001 From: Brendan Cooley Date: Wed, 30 Apr 2025 12:19:41 -0400 Subject: [PATCH 0196/1161] Fix example syntax for pydocstyle ignore_var_parameters option (#17740) Co-authored-by: Brendan Cooley --- crates/ruff_workspace/src/options.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 6778c2682e24e9..320c7a4187b0c2 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -3118,7 +3118,7 @@ pub struct PydocstyleOptions { default = r#"false"#, value_type = "bool", example = r#" - ignore_var_parameters = true + ignore-var-parameters = true "# )] pub ignore_var_parameters: Option, From f31b1c695c1c449a6e48201cc8b75bd10a7d86f8 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 30 Apr 2025 18:48:31 +0100 Subject: [PATCH 0197/1161] py-fuzzer: fix minimization logic when `--only-new-bugs` is passed (#17739) --- python/py-fuzzer/fuzz.py | 77 +++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/python/py-fuzzer/fuzz.py b/python/py-fuzzer/fuzz.py index 1028ca2d906227..22d7e41a40feca 100644 --- a/python/py-fuzzer/fuzz.py +++ b/python/py-fuzzer/fuzz.py @@ -32,6 +32,7 @@ import enum import subprocess import tempfile +from collections.abc import Callable from dataclasses import KW_ONLY, dataclass from functools import partial from pathlib import Path @@ -50,14 +51,12 @@ def redknot_contains_bug(code: str, *, red_knot_executable: Path) -> bool: """Return `True` if the code triggers a panic in type-checking code.""" with tempfile.TemporaryDirectory() as tempdir: - Path(tempdir, "pyproject.toml").write_text('[project]\n\tname = "fuzz-input"') - Path(tempdir, "input.py").write_text(code) + input_file = Path(tempdir, "input.py") + input_file.write_text(code) completed_process = subprocess.run( - [red_knot_executable, "check", "--project", tempdir], - capture_output=True, - text=True, + [red_knot_executable, "check", input_file], capture_output=True, text=True ) - return completed_process.returncode != 0 and completed_process.returncode != 1 + return completed_process.returncode not in {0, 1, 2} def ruff_contains_bug(code: str, *, ruff_executable: Path) -> bool: @@ -150,39 +149,51 @@ def print_description(self, index: int, num_seeds: int) -> None: def fuzz_code(seed: Seed, args: ResolvedCliArgs) -> FuzzResult: """Return a `FuzzResult` instance describing the fuzzing result from this seed.""" code = generate_random_code(seed) - has_bug = ( - contains_new_bug( - code, + bug_found = False + minimizer_callback: Callable[[str], bool] | None = None + + if args.baseline_executable_path is None: + if contains_bug( + code, executable=args.executable, executable_path=args.test_executable_path + ): + bug_found = True + minimizer_callback = partial( + contains_bug, + executable=args.executable, + executable_path=args.test_executable_path, + ) + elif contains_new_bug( + code, + executable=args.executable, + test_executable_path=args.test_executable_path, + baseline_executable_path=args.baseline_executable_path, + ): + bug_found = True + minimizer_callback = partial( + contains_new_bug, executable=args.executable, test_executable_path=args.test_executable_path, baseline_executable_path=args.baseline_executable_path, ) - if args.baseline_executable_path is not None - else contains_bug( - code, executable=args.executable, executable_path=args.test_executable_path - ) - ) - if has_bug: - callback = partial( - contains_bug, - executable=args.executable, - executable_path=args.test_executable_path, - ) + + if not bug_found: + return FuzzResult(seed, None, args.executable) + + assert minimizer_callback is not None + + try: + maybe_bug = MinimizedSourceCode(minimize_repro(code, minimizer_callback)) + except CouldNotMinimize as e: + # This is to double-check that there isn't a bug in + # `pysource-minimize`/`pysource-codegen`. + # `pysource-minimize` *should* never produce code that's invalid syntax. try: - maybe_bug = MinimizedSourceCode(minimize_repro(code, callback)) - except CouldNotMinimize as e: - # This is to double-check that there isn't a bug in - # `pysource-minimize`/`pysource-codegen`. - # `pysource-minimize` *should* never produce code that's invalid syntax. - try: - ast.parse(code) - except SyntaxError: - raise e from None - else: - maybe_bug = MinimizedSourceCode(code) + ast.parse(code) + except SyntaxError: + raise e from None + else: + maybe_bug = MinimizedSourceCode(code) - else: - maybe_bug = None return FuzzResult(seed, maybe_bug, args.executable) From 0eeb02c0c1ec8ee2a253b6aef5a1a9cd86744af6 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Bodas <55339528+abhijeetbodas2001@users.noreply.github.com> Date: Thu, 1 May 2025 00:34:00 +0530 Subject: [PATCH 0198/1161] [syntax-errors] Detect single starred expression assignment `x = *y` (#17624) ## Summary Part of #17412 Starred expressions cannot be used as values in assignment expressions. Add a new semantic syntax error to catch such instances. Note that we already have `ParseErrorType::InvalidStarredExpressionUsage` to catch some starred expression errors during parsing, but that does not cover top level assignment expressions. ## Test Plan - Added new inline tests for the new rule - Found some examples marked as "valid" in existing tests (`_ = *data`), which are not really valid (per this new rule) and updated them - There was an existing inline test - `assign_stmt_invalid_value_expr` which had instances of `*` expression which would be deemed invalid by this new rule. Converted these to tuples, so that they do not trigger this new rule. --- .../err/assign_stmt_invalid_value_expr.py | 8 +- .../err/assign_stmt_starred_expr_value.py | 4 + .../ok/assign_stmt_starred_expr_value.py | 4 + .../resources/valid/statement/assignment.py | 4 +- .../src/parser/statement.rs | 8 +- .../ruff_python_parser/src/semantic_errors.rs | 15 +- ...tax@assign_stmt_invalid_value_expr.py.snap | 299 ++++++++++-------- ...tax@assign_stmt_starred_expr_value.py.snap | 197 ++++++++++++ ...tax@assign_stmt_starred_expr_value.py.snap | 157 +++++++++ ...valid_syntax@statement__assignment.py.snap | 84 +++-- 10 files changed, 612 insertions(+), 168 deletions(-) create mode 100644 crates/ruff_python_parser/resources/inline/err/assign_stmt_starred_expr_value.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/assign_stmt_starred_expr_value.py create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_stmt_starred_expr_value.py.snap diff --git a/crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_value_expr.py b/crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_value_expr.py index c8cf8e64ee7496..0ffad964a2a304 100644 --- a/crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_value_expr.py +++ b/crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_value_expr.py @@ -1,5 +1,5 @@ -x = *a and b -x = *yield x -x = *yield from x -x = *lambda x: x +x = (*a and b,) +x = (42, *yield x) +x = (42, *yield from x) +x = (*lambda x: x,) x = x := 1 diff --git a/crates/ruff_python_parser/resources/inline/err/assign_stmt_starred_expr_value.py b/crates/ruff_python_parser/resources/inline/err/assign_stmt_starred_expr_value.py new file mode 100644 index 00000000000000..8b3169540758f7 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/assign_stmt_starred_expr_value.py @@ -0,0 +1,4 @@ +_ = *[42] +_ = *{42} +_ = *list() +_ = *(p + q) diff --git a/crates/ruff_python_parser/resources/inline/ok/assign_stmt_starred_expr_value.py b/crates/ruff_python_parser/resources/inline/ok/assign_stmt_starred_expr_value.py new file mode 100644 index 00000000000000..2e8a1919b07b65 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/assign_stmt_starred_expr_value.py @@ -0,0 +1,4 @@ +_ = 4 +_ = [4] +_ = (*[1],) +_ = *[1], diff --git a/crates/ruff_python_parser/resources/valid/statement/assignment.py b/crates/ruff_python_parser/resources/valid/statement/assignment.py index 992e5f8fbae2c9..7e0b4583350208 100644 --- a/crates/ruff_python_parser/resources/valid/statement/assignment.py +++ b/crates/ruff_python_parser/resources/valid/statement/assignment.py @@ -37,7 +37,7 @@ foo = 42 -[] = *data -() = *data +[] = (*data,) +() = (*data,) a, b = ab a = b = c \ No newline at end of file diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 7f3f0c74e29952..3116fc46c52773 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -1127,10 +1127,10 @@ impl<'src> Parser<'src> { // a + b // test_err assign_stmt_invalid_value_expr - // x = *a and b - // x = *yield x - // x = *yield from x - // x = *lambda x: x + // x = (*a and b,) + // x = (42, *yield x) + // x = (42, *yield from x) + // x = (*lambda x: x,) // x = x := 1 let mut value = diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 149292b3b9b854..83366b035faf70 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -90,7 +90,7 @@ impl SemanticSyntaxChecker { Self::duplicate_type_parameter_name(type_params, ctx); } } - Stmt::Assign(ast::StmtAssign { targets, .. }) => { + Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { if let [Expr::Starred(ast::ExprStarred { range, .. })] = targets.as_slice() { // test_ok single_starred_assignment_target // (*a,) = (1,) @@ -105,6 +105,19 @@ impl SemanticSyntaxChecker { *range, ); } + + // test_ok assign_stmt_starred_expr_value + // _ = 4 + // _ = [4] + // _ = (*[1],) + // _ = *[1], + + // test_err assign_stmt_starred_expr_value + // _ = *[42] + // _ = *{42} + // _ = *list() + // _ = *(p + q) + Self::invalid_star_expression(value, ctx); } Stmt::Return(ast::StmtReturn { value, range }) => { if let Some(value) = value { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_value_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_value_expr.py.snap index 306a96376995e7..78ea01ad7e616f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_value_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_value_expr.py.snap @@ -1,18 +1,17 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/assign_stmt_invalid_value_expr.py -snapshot_kind: text --- ## AST ``` Module( ModModule { - range: 0..72, + range: 0..90, body: [ Assign( StmtAssign { - range: 0..12, + range: 0..15, targets: [ Name( ExprName { @@ -22,164 +21,216 @@ Module( }, ), ], - value: Starred( - ExprStarred { - range: 4..12, - value: BoolOp( - ExprBoolOp { - range: 5..12, - op: And, - values: [ - Name( - ExprName { - range: 5..6, - id: Name("a"), - ctx: Load, - }, - ), - Name( - ExprName { - range: 11..12, - id: Name("b"), - ctx: Load, + value: Tuple( + ExprTuple { + range: 4..15, + elts: [ + Starred( + ExprStarred { + range: 5..13, + value: BoolOp( + ExprBoolOp { + range: 6..13, + op: And, + values: [ + Name( + ExprName { + range: 6..7, + id: Name("a"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 12..13, + id: Name("b"), + ctx: Load, + }, + ), + ], }, ), - ], - }, - ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 13..25, + range: 16..34, targets: [ Name( ExprName { - range: 13..14, + range: 16..17, id: Name("x"), ctx: Store, }, ), ], - value: Starred( - ExprStarred { - range: 17..25, - value: Yield( - ExprYield { - range: 18..25, - value: Some( - Name( - ExprName { - range: 24..25, - id: Name("x"), - ctx: Load, + value: Tuple( + ExprTuple { + range: 20..34, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 21..23, + value: Int( + 42, + ), + }, + ), + Starred( + ExprStarred { + range: 25..33, + value: Yield( + ExprYield { + range: 26..33, + value: Some( + Name( + ExprName { + range: 32..33, + id: Name("x"), + ctx: Load, + }, + ), + ), }, ), - ), - }, - ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 26..43, + range: 35..58, targets: [ Name( ExprName { - range: 26..27, + range: 35..36, id: Name("x"), ctx: Store, }, ), ], - value: Starred( - ExprStarred { - range: 30..43, - value: YieldFrom( - ExprYieldFrom { - range: 31..43, - value: Name( - ExprName { - range: 42..43, - id: Name("x"), - ctx: Load, - }, - ), - }, - ), + value: Tuple( + ExprTuple { + range: 39..58, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 40..42, + value: Int( + 42, + ), + }, + ), + Starred( + ExprStarred { + range: 44..57, + value: YieldFrom( + ExprYieldFrom { + range: 45..57, + value: Name( + ExprName { + range: 56..57, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 44..60, + range: 59..78, targets: [ Name( ExprName { - range: 44..45, + range: 59..60, id: Name("x"), ctx: Store, }, ), ], - value: Starred( - ExprStarred { - range: 48..60, - value: Lambda( - ExprLambda { - range: 49..60, - parameters: Some( - Parameters { - range: 56..57, - posonlyargs: [], - args: [ - ParameterWithDefault { - range: 56..57, - parameter: Parameter { - range: 56..57, - name: Identifier { - id: Name("x"), - range: 56..57, - }, - annotation: None, + value: Tuple( + ExprTuple { + range: 63..78, + elts: [ + Starred( + ExprStarred { + range: 64..76, + value: Lambda( + ExprLambda { + range: 65..76, + parameters: Some( + Parameters { + range: 72..73, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 72..73, + parameter: Parameter { + range: 72..73, + name: Identifier { + id: Name("x"), + range: 72..73, + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, }, - default: None, - }, - ], - vararg: None, - kwonlyargs: [], - kwarg: None, - }, - ), - body: Name( - ExprName { - range: 59..60, - id: Name("x"), - ctx: Load, - }, - ), - }, - ), + ), + body: Name( + ExprName { + range: 75..76, + id: Name("x"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 61..66, + range: 79..84, targets: [ Name( ExprName { - range: 61..62, + range: 79..80, id: Name("x"), ctx: Store, }, @@ -187,7 +238,7 @@ Module( ], value: Name( ExprName { - range: 65..66, + range: 83..84, id: Name("x"), ctx: Load, }, @@ -196,10 +247,10 @@ Module( ), Expr( StmtExpr { - range: 70..71, + range: 88..89, value: NumberLiteral( ExprNumberLiteral { - range: 70..71, + range: 88..89, value: Int( 1, ), @@ -214,44 +265,44 @@ Module( ## Errors | -1 | x = *a and b - | ^^^^^^^ Syntax Error: Boolean expression cannot be used here -2 | x = *yield x -3 | x = *yield from x +1 | x = (*a and b,) + | ^^^^^^^ Syntax Error: Boolean expression cannot be used here +2 | x = (42, *yield x) +3 | x = (42, *yield from x) | | -1 | x = *a and b -2 | x = *yield x - | ^^^^^^^ Syntax Error: Yield expression cannot be used here -3 | x = *yield from x -4 | x = *lambda x: x +1 | x = (*a and b,) +2 | x = (42, *yield x) + | ^^^^^^^ Syntax Error: Yield expression cannot be used here +3 | x = (42, *yield from x) +4 | x = (*lambda x: x,) | | -1 | x = *a and b -2 | x = *yield x -3 | x = *yield from x - | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here -4 | x = *lambda x: x +1 | x = (*a and b,) +2 | x = (42, *yield x) +3 | x = (42, *yield from x) + | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here +4 | x = (*lambda x: x,) 5 | x = x := 1 | | -2 | x = *yield x -3 | x = *yield from x -4 | x = *lambda x: x - | ^^^^^^^^^^^ Syntax Error: Lambda expression cannot be used here +2 | x = (42, *yield x) +3 | x = (42, *yield from x) +4 | x = (*lambda x: x,) + | ^^^^^^^^^^^ Syntax Error: Lambda expression cannot be used here 5 | x = x := 1 | | -3 | x = *yield from x -4 | x = *lambda x: x +3 | x = (42, *yield from x) +4 | x = (*lambda x: x,) 5 | x = x := 1 | ^^ Syntax Error: Expected a statement | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap new file mode 100644 index 00000000000000..fc1b6152922b84 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap @@ -0,0 +1,197 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/assign_stmt_starred_expr_value.py +--- +## AST + +``` +Module( + ModModule { + range: 0..45, + body: [ + Assign( + StmtAssign { + range: 0..9, + targets: [ + Name( + ExprName { + range: 0..1, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Starred( + ExprStarred { + range: 4..9, + value: List( + ExprList { + range: 5..9, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 6..8, + value: Int( + 42, + ), + }, + ), + ], + ctx: Load, + }, + ), + ctx: Load, + }, + ), + }, + ), + Assign( + StmtAssign { + range: 10..19, + targets: [ + Name( + ExprName { + range: 10..11, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Starred( + ExprStarred { + range: 14..19, + value: Set( + ExprSet { + range: 15..19, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 16..18, + value: Int( + 42, + ), + }, + ), + ], + }, + ), + ctx: Load, + }, + ), + }, + ), + Assign( + StmtAssign { + range: 20..31, + targets: [ + Name( + ExprName { + range: 20..21, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Starred( + ExprStarred { + range: 24..31, + value: Call( + ExprCall { + range: 25..31, + func: Name( + ExprName { + range: 25..29, + id: Name("list"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 29..31, + args: [], + keywords: [], + }, + }, + ), + ctx: Load, + }, + ), + }, + ), + Assign( + StmtAssign { + range: 32..44, + targets: [ + Name( + ExprName { + range: 32..33, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Starred( + ExprStarred { + range: 36..44, + value: BinOp( + ExprBinOp { + range: 38..43, + left: Name( + ExprName { + range: 38..39, + id: Name("p"), + ctx: Load, + }, + ), + op: Add, + right: Name( + ExprName { + range: 42..43, + id: Name("q"), + ctx: Load, + }, + ), + }, + ), + ctx: Load, + }, + ), + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | _ = *[42] + | ^^^^^ Syntax Error: can't use starred expression here +2 | _ = *{42} +3 | _ = *list() + | + + + | +1 | _ = *[42] +2 | _ = *{42} + | ^^^^^ Syntax Error: can't use starred expression here +3 | _ = *list() +4 | _ = *(p + q) + | + + + | +1 | _ = *[42] +2 | _ = *{42} +3 | _ = *list() + | ^^^^^^^ Syntax Error: can't use starred expression here +4 | _ = *(p + q) + | + + + | +2 | _ = *{42} +3 | _ = *list() +4 | _ = *(p + q) + | ^^^^^^^^ Syntax Error: can't use starred expression here + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_stmt_starred_expr_value.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_stmt_starred_expr_value.py.snap new file mode 100644 index 00000000000000..5b5bf36875ed72 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@assign_stmt_starred_expr_value.py.snap @@ -0,0 +1,157 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/assign_stmt_starred_expr_value.py +--- +## AST + +``` +Module( + ModModule { + range: 0..36, + body: [ + Assign( + StmtAssign { + range: 0..5, + targets: [ + Name( + ExprName { + range: 0..1, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: NumberLiteral( + ExprNumberLiteral { + range: 4..5, + value: Int( + 4, + ), + }, + ), + }, + ), + Assign( + StmtAssign { + range: 6..13, + targets: [ + Name( + ExprName { + range: 6..7, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: List( + ExprList { + range: 10..13, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 11..12, + value: Int( + 4, + ), + }, + ), + ], + ctx: Load, + }, + ), + }, + ), + Assign( + StmtAssign { + range: 14..25, + targets: [ + Name( + ExprName { + range: 14..15, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Tuple( + ExprTuple { + range: 18..25, + elts: [ + Starred( + ExprStarred { + range: 19..23, + value: List( + ExprList { + range: 20..23, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 21..22, + value: Int( + 1, + ), + }, + ), + ], + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: true, + }, + ), + }, + ), + Assign( + StmtAssign { + range: 26..35, + targets: [ + Name( + ExprName { + range: 26..27, + id: Name("_"), + ctx: Store, + }, + ), + ], + value: Tuple( + ExprTuple { + range: 30..35, + elts: [ + Starred( + ExprStarred { + range: 30..34, + value: List( + ExprList { + range: 31..34, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 32..33, + value: Int( + 1, + ), + }, + ), + ], + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assignment.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assignment.py.snap index 8c67f41106456a..606a9ea32422bf 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assignment.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__assignment.py.snap @@ -7,7 +7,7 @@ input_file: crates/ruff_python_parser/resources/valid/statement/assignment.py ``` Module( ModModule { - range: 0..723, + range: 0..729, body: [ Assign( StmtAssign { @@ -802,7 +802,7 @@ Module( ), Assign( StmtAssign { - range: 682..692, + range: 682..695, targets: [ List( ExprList { @@ -812,67 +812,85 @@ Module( }, ), ], - value: Starred( - ExprStarred { - range: 687..692, - value: Name( - ExprName { - range: 688..692, - id: Name("data"), - ctx: Load, - }, - ), + value: Tuple( + ExprTuple { + range: 687..695, + elts: [ + Starred( + ExprStarred { + range: 688..693, + value: Name( + ExprName { + range: 689..693, + id: Name("data"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 693..703, + range: 696..709, targets: [ Tuple( ExprTuple { - range: 693..695, + range: 696..698, elts: [], ctx: Store, parenthesized: true, }, ), ], - value: Starred( - ExprStarred { - range: 698..703, - value: Name( - ExprName { - range: 699..703, - id: Name("data"), - ctx: Load, - }, - ), + value: Tuple( + ExprTuple { + range: 701..709, + elts: [ + Starred( + ExprStarred { + range: 702..707, + value: Name( + ExprName { + range: 703..707, + id: Name("data"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], ctx: Load, + parenthesized: true, }, ), }, ), Assign( StmtAssign { - range: 704..713, + range: 710..719, targets: [ Tuple( ExprTuple { - range: 704..708, + range: 710..714, elts: [ Name( ExprName { - range: 704..705, + range: 710..711, id: Name("a"), ctx: Store, }, ), Name( ExprName { - range: 707..708, + range: 713..714, id: Name("b"), ctx: Store, }, @@ -885,7 +903,7 @@ Module( ], value: Name( ExprName { - range: 711..713, + range: 717..719, id: Name("ab"), ctx: Load, }, @@ -894,18 +912,18 @@ Module( ), Assign( StmtAssign { - range: 714..723, + range: 720..729, targets: [ Name( ExprName { - range: 714..715, + range: 720..721, id: Name("a"), ctx: Store, }, ), Name( ExprName { - range: 718..719, + range: 724..725, id: Name("b"), ctx: Store, }, @@ -913,7 +931,7 @@ Module( ], value: Name( ExprName { - range: 722..723, + range: 728..729, id: Name("c"), ctx: Load, }, From c5e41c278cec292461214e9df32f75926f90ba0f Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Wed, 30 Apr 2025 21:06:25 +0200 Subject: [PATCH 0199/1161] [`ruff`] Add fix safety section (`RUF028`) (#17722) The PR add the fix safety section for rule `RUF028` (https://github.com/astral-sh/ruff/issues/15584 ) See also [here](https://github.com/astral-sh/ruff/issues/15584#issuecomment-2820424485) for the reason behind the _unsafe_ of the fix. --- .../ruff/rules/invalid_formatter_suppression_comment.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs index b3fac6e6ca292b..2293071d0e0eb0 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs @@ -49,6 +49,12 @@ use super::suppression_comment_visitor::{ /// # fmt: on /// # yapf: enable /// ``` +/// +/// ## Fix safety +/// +/// This fix is always marked as unsafe because it deletes the invalid suppression comment, +/// rather than trying to move it to a valid position, which the user more likely intended. +/// #[derive(ViolationMetadata)] pub(crate) struct InvalidFormatterSuppressionComment { reason: IgnoredReason, From 6e765b4527e73375df2e873286b7800d18b84263 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Thu, 1 May 2025 03:53:10 +0800 Subject: [PATCH 0200/1161] [`airflow`] apply Replacement::AutoImport to `AIR312` (#17570) ## Summary This is not yet fixing anything as the names are not changed, but it lays down the foundation for fixing. ## Test Plan the existing test fixture should already cover this change --- .../suggested_to_move_to_provider_in_3.rs | 77 +++++++++++++------ 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs index b23e031c639faa..b0c2389583c358 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/suggested_to_move_to_provider_in_3.rs @@ -1,5 +1,7 @@ +use crate::importer::ImportRequest; + use crate::rules::airflow::helpers::ProviderReplacement; -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{Expr, ExprAttribute}; use ruff_python_semantic::Modules; @@ -34,6 +36,7 @@ pub(crate) struct Airflow3SuggestedToMoveToProvider { } impl Violation for Airflow3SuggestedToMoveToProvider { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; #[derive_message_formats] fn message(&self) -> String { let Airflow3SuggestedToMoveToProvider { @@ -124,14 +127,16 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan let replacement = match qualified_name.segments() { // apache-airflow-providers-standard - ["airflow", "hooks", "filesystem", "FSHook"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.hooks.filesystem.FSHook", + ["airflow", "hooks", "filesystem", "FSHook"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.hooks.filesystem", + name: "FSHook", provider: "standard", version: "0.0.1", }, ["airflow", "hooks", "package_index", "PackageIndexHook"] => { - ProviderReplacement::ProviderName { - name: "airflow.providers.standard.hooks.package_index.PackageIndexHook", + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.hooks.package_index", + name: "PackageIndexHook", provider: "standard", version: "0.0.1", } @@ -144,8 +149,9 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan version: "0.0.3", } } - ["airflow", "operators", "bash", "BashOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.bash.BashOperator", + ["airflow", "operators", "bash", "BashOperator"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.bash", + name: "BashOperator", provider: "standard", version: "0.0.1", }, @@ -165,14 +171,16 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan version: "0.0.2", } } - ["airflow", "operators", "empty", "EmptyOperator"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.empty.EmptyOperator", + ["airflow", "operators", "empty", "EmptyOperator"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.empty", + name: "EmptyOperator", provider: "standard", version: "0.0.2", }, ["airflow", "operators", "latest_only", "LatestOnlyOperator"] => { - ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.latest_only.LatestOnlyOperator", + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.latest_only", + name: "LatestOnlyOperator", provider: "standard", version: "0.0.3", } @@ -187,8 +195,9 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan version: "0.0.1", }, ["airflow", "operators", "weekday", "BranchDayOfWeekOperator"] => { - ProviderReplacement::ProviderName { - name: "airflow.providers.standard.operators.weekday.BranchDayOfWeekOperator", + ProviderReplacement::AutoImport { + module: "airflow.providers.standard.operators.weekday", + name: "BranchDayOfWeekOperator", provider: "standard", version: "0.0.1", } @@ -209,8 +218,9 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan version: "0.0.3", } } - ["airflow", "sensors", "filesystem", "FileSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.sensors.filesystem.FileSensor", + ["airflow", "sensors", "filesystem", "FileSensor"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.sensors.filesystem", + name: "FileSensor", provider: "standard", version: "0.0.2", }, @@ -230,8 +240,9 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan version: "0.0.1", } } - ["airflow", "sensors", "weekday", "DayOfWeekSensor"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.sensors.weekday.DayOfWeekSensor", + ["airflow", "sensors", "weekday", "DayOfWeekSensor"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.sensors.weekday", + name: "DayOfWeekSensor", provider: "standard", version: "0.0.1", }, @@ -243,8 +254,9 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan version: "0.0.3", } } - ["airflow", "triggers", "file", "FileTrigger"] => ProviderReplacement::ProviderName { - name: "airflow.providers.standard.triggers.file.FileTrigger", + ["airflow", "triggers", "file", "FileTrigger"] => ProviderReplacement::AutoImport { + module: "airflow.providers.standard.triggers.file", + name: "FileTrigger", provider: "standard", version: "0.0.3", }, @@ -258,11 +270,32 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan } _ => return, }; - checker.report_diagnostic(Diagnostic::new( + + let mut diagnostic = Diagnostic::new( Airflow3SuggestedToMoveToProvider { deprecated: qualified_name.to_string(), - replacement, + replacement: replacement.clone(), }, ranged.range(), - )); + ); + + if let ProviderReplacement::AutoImport { + module, + name, + provider: _, + version: _, + } = replacement + { + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer().get_or_import_symbol( + &ImportRequest::import_from(module, name), + expr.start(), + checker.semantic(), + )?; + let replacement_edit = Edit::range_replacement(binding, ranged.range()); + Ok(Fix::safe_edits(import_edit, [replacement_edit])) + }); + } + + checker.report_diagnostic(diagnostic); } From d2a238dfad2bd5d3572a7a8a5b11fdf91d22b8f8 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 1 May 2025 01:33:21 +0530 Subject: [PATCH 0201/1161] [red-knot] Update call binding to return all matching overloads (#17618) ## Summary This PR updates the existing overload matching methods to return an iterator of all the matched overloads instead. This would be useful once the overload call evaluation algorithm is implemented which should provide an accurate picture of all the matched overloads. The return type would then be picked from either the only matched overload or the first overload from the ones that are matched. In an earlier version of this PR, it tried to check if using an intersection of return types from the matched overload would help reduce the false positives but that's not enough. [This comment](https://github.com/astral-sh/ruff/pull/17618#issuecomment-2842891696) keep the ecosystem analysis for that change for prosperity. > [!NOTE] > > The best way to review this PR is by hiding the whitespace changes because there are two instances where a large match expression is indented to be inside a loop over matching overlods > > Screenshot 2025-04-28 at 15 12 16 ## Test Plan Make sure existing test cases are unaffected and no ecosystem changes. --- crates/red_knot_python_semantic/src/types.rs | 42 +- .../src/types/call/bind.rs | 962 +++++++++--------- .../src/types/infer.rs | 577 +++++------ 3 files changed, 813 insertions(+), 768 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f4d12a06f540aa..a942a4acd942aa 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4527,27 +4527,43 @@ impl<'db> Type<'db> { new_call_outcome @ (None | Some(Ok(_))), init_call_outcome @ (None | Some(Ok(_))), ) => { + fn combine_specializations<'db>( + db: &'db dyn Db, + s1: Option>, + s2: Option>, + ) -> Option> { + match (s1, s2) { + (None, None) => None, + (Some(s), None) | (None, Some(s)) => Some(s), + (Some(s1), Some(s2)) => Some(s1.combine(db, s2)), + } + } + + fn combine_binding_specialization<'db>( + db: &'db dyn Db, + binding: &CallableBinding<'db>, + ) -> Option> { + binding + .matching_overloads() + .map(|(_, binding)| binding.inherited_specialization()) + .reduce(|acc, specialization| { + combine_specializations(db, acc, specialization) + }) + .flatten() + } + let new_specialization = new_call_outcome .and_then(Result::ok) .as_ref() .and_then(Bindings::single_element) - .and_then(CallableBinding::matching_overload) - .and_then(|(_, binding)| binding.inherited_specialization()); + .and_then(|binding| combine_binding_specialization(db, binding)); let init_specialization = init_call_outcome .and_then(Result::ok) .as_ref() .and_then(Bindings::single_element) - .and_then(CallableBinding::matching_overload) - .and_then(|(_, binding)| binding.inherited_specialization()); - let specialization = match (new_specialization, init_specialization) { - (None, None) => None, - (Some(specialization), None) | (None, Some(specialization)) => { - Some(specialization) - } - (Some(new_specialization), Some(init_specialization)) => { - Some(new_specialization.combine(db, init_specialization)) - } - }; + .and_then(|binding| combine_binding_specialization(db, binding)); + let specialization = + combine_specializations(db, new_specialization, init_specialization); let specialized = specialization .map(|specialization| { Type::instance( diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 09bf3b0d34e046..3bac1677555492 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -221,587 +221,605 @@ impl<'db> Bindings<'db> { // Each special case listed here should have a corresponding clause in `Type::signatures`. for (binding, callable_signature) in self.elements.iter_mut().zip(self.signatures.iter()) { let binding_type = binding.callable_type; - let Some((overload_index, overload)) = binding.matching_overload_mut() else { - continue; - }; - - match binding_type { - Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { - if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) { - match overload.parameter_types() { - [_, Some(owner)] => { - overload.set_return_type(Type::BoundMethod(BoundMethodType::new( - db, function, *owner, - ))); - } - [Some(instance), None] => { - overload.set_return_type(Type::BoundMethod(BoundMethodType::new( - db, - function, - instance.to_meta_type(db), - ))); - } - _ => {} - } - } else if let [Some(first), _] = overload.parameter_types() { - if first.is_none(db) { - overload.set_return_type(Type::FunctionLiteral(function)); - } else { - overload.set_return_type(Type::BoundMethod(BoundMethodType::new( - db, function, *first, - ))); - } - } - } - - Type::WrapperDescriptor(WrapperDescriptorKind::FunctionTypeDunderGet) => { - if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] = - overload.parameter_types() - { + for (overload_index, overload) in binding.matching_overloads_mut() { + match binding_type { + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) { match overload.parameter_types() { - [_, _, Some(owner)] => { + [_, Some(owner)] => { overload.set_return_type(Type::BoundMethod( - BoundMethodType::new(db, *function, *owner), + BoundMethodType::new(db, function, *owner), )); } - - [_, Some(instance), None] => { + [Some(instance), None] => { overload.set_return_type(Type::BoundMethod( BoundMethodType::new( db, - *function, + function, instance.to_meta_type(db), ), )); } - _ => {} } - } else { - match overload.parameter_types() { - [_, Some(instance), _] if instance.is_none(db) => { - overload.set_return_type(*function_ty); - } - [_, Some(instance), _] => { - overload.set_return_type(Type::BoundMethod( - BoundMethodType::new(db, *function, *instance), - )); + } else if let [Some(first), _] = overload.parameter_types() { + if first.is_none(db) { + overload.set_return_type(Type::FunctionLiteral(function)); + } else { + overload.set_return_type(Type::BoundMethod(BoundMethodType::new( + db, function, *first, + ))); + } + } + } + + Type::WrapperDescriptor(WrapperDescriptorKind::FunctionTypeDunderGet) => { + if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] = + overload.parameter_types() + { + if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) { + match overload.parameter_types() { + [_, _, Some(owner)] => { + overload.set_return_type(Type::BoundMethod( + BoundMethodType::new(db, *function, *owner), + )); + } + + [_, Some(instance), None] => { + overload.set_return_type(Type::BoundMethod( + BoundMethodType::new( + db, + *function, + instance.to_meta_type(db), + ), + )); + } + + _ => {} } + } else { + match overload.parameter_types() { + [_, Some(instance), _] if instance.is_none(db) => { + overload.set_return_type(*function_ty); + } + [_, Some(instance), _] => { + overload.set_return_type(Type::BoundMethod( + BoundMethodType::new(db, *function, *instance), + )); + } - _ => {} + _ => {} + } } } } - } - Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderGet) => match overload - .parameter_types() - { - [Some(property @ Type::PropertyInstance(_)), Some(instance), ..] - if instance.is_none(db) => - { - overload.set_return_type(*property); - } - [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias))), ..] - if property.getter(db).is_some_and(|getter| { - getter - .into_function_literal() - .is_some_and(|f| f.name(db) == "__name__") - }) => - { - overload.set_return_type(Type::string_literal(db, type_alias.name(db))); - } - [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), ..] => { - match property - .getter(db) - .and_then(Type::into_function_literal) - .map(|f| f.name(db).as_str()) - { - Some("__name__") => { - overload - .set_return_type(Type::string_literal(db, typevar.name(db))); + Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderGet) => { + match overload.parameter_types() { + [Some(property @ Type::PropertyInstance(_)), Some(instance), ..] + if instance.is_none(db) => + { + overload.set_return_type(*property); } - Some("__bound__") => { - overload.set_return_type( - typevar.upper_bound(db).unwrap_or_else(|| Type::none(db)), - ); + [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeAliasType( + type_alias, + ))), ..] + if property.getter(db).is_some_and(|getter| { + getter + .into_function_literal() + .is_some_and(|f| f.name(db) == "__name__") + }) => + { + overload + .set_return_type(Type::string_literal(db, type_alias.name(db))); } - Some("__constraints__") => { - overload.set_return_type(TupleType::from_elements( - db, - typevar.constraints(db).into_iter().flatten(), - )); + [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), ..] => { + match property + .getter(db) + .and_then(Type::into_function_literal) + .map(|f| f.name(db).as_str()) + { + Some("__name__") => { + overload.set_return_type(Type::string_literal( + db, + typevar.name(db), + )); + } + Some("__bound__") => { + overload.set_return_type( + typevar + .upper_bound(db) + .unwrap_or_else(|| Type::none(db)), + ); + } + Some("__constraints__") => { + overload.set_return_type(TupleType::from_elements( + db, + typevar.constraints(db).into_iter().flatten(), + )); + } + Some("__default__") => { + overload.set_return_type( + typevar.default_ty(db).unwrap_or_else(|| { + KnownClass::NoDefaultType.to_instance(db) + }), + ); + } + _ => {} + } } - Some("__default__") => { - overload.set_return_type( - typevar.default_ty(db).unwrap_or_else(|| { - KnownClass::NoDefaultType.to_instance(db) - }), - ); + [Some(Type::PropertyInstance(property)), Some(instance), ..] => { + if let Some(getter) = property.getter(db) { + if let Ok(return_ty) = getter + .try_call( + db, + &mut CallArgumentTypes::positional([*instance]), + ) + .map(|binding| binding.return_type(db)) + { + overload.set_return_type(return_ty); + } else { + overload.errors.push(BindingError::InternalCallError( + "calling the getter failed", + )); + overload.set_return_type(Type::unknown()); + } + } else { + overload.errors.push(BindingError::InternalCallError( + "property has no getter", + )); + overload.set_return_type(Type::Never); + } } _ => {} } } - [Some(Type::PropertyInstance(property)), Some(instance), ..] => { - if let Some(getter) = property.getter(db) { - if let Ok(return_ty) = getter - .try_call(db, &mut CallArgumentTypes::positional([*instance])) - .map(|binding| binding.return_type(db)) - { - overload.set_return_type(return_ty); - } else { - overload.errors.push(BindingError::InternalCallError( - "calling the getter failed", - )); - overload.set_return_type(Type::unknown()); - } - } else { - overload - .errors - .push(BindingError::InternalCallError("property has no getter")); - overload.set_return_type(Type::Never); - } - } - _ => {} - }, - Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => { - match overload.parameter_types() { - [Some(instance), ..] if instance.is_none(db) => { - overload.set_return_type(Type::PropertyInstance(property)); - } - [Some(instance), ..] => { - if let Some(getter) = property.getter(db) { - if let Ok(return_ty) = getter - .try_call(db, &mut CallArgumentTypes::positional([*instance])) - .map(|binding| binding.return_type(db)) - { - overload.set_return_type(return_ty); + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => { + match overload.parameter_types() { + [Some(instance), ..] if instance.is_none(db) => { + overload.set_return_type(Type::PropertyInstance(property)); + } + [Some(instance), ..] => { + if let Some(getter) = property.getter(db) { + if let Ok(return_ty) = getter + .try_call( + db, + &mut CallArgumentTypes::positional([*instance]), + ) + .map(|binding| binding.return_type(db)) + { + overload.set_return_type(return_ty); + } else { + overload.errors.push(BindingError::InternalCallError( + "calling the getter failed", + )); + overload.set_return_type(Type::unknown()); + } } else { + overload.set_return_type(Type::Never); overload.errors.push(BindingError::InternalCallError( - "calling the getter failed", + "property has no getter", )); - overload.set_return_type(Type::unknown()); } - } else { - overload.set_return_type(Type::Never); - overload.errors.push(BindingError::InternalCallError( - "property has no getter", - )); } + _ => {} } - _ => {} } - } - Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderSet) => { - if let [Some(Type::PropertyInstance(property)), Some(instance), Some(value), ..] = - overload.parameter_types() - { - if let Some(setter) = property.setter(db) { - if let Err(_call_error) = setter.try_call( - db, - &mut CallArgumentTypes::positional([*instance, *value]), - ) { + Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderSet) => { + if let [Some(Type::PropertyInstance(property)), Some(instance), Some(value), ..] = + overload.parameter_types() + { + if let Some(setter) = property.setter(db) { + if let Err(_call_error) = setter.try_call( + db, + &mut CallArgumentTypes::positional([*instance, *value]), + ) { + overload.errors.push(BindingError::InternalCallError( + "calling the setter failed", + )); + } + } else { overload.errors.push(BindingError::InternalCallError( - "calling the setter failed", + "property has no setter", )); } - } else { - overload - .errors - .push(BindingError::InternalCallError("property has no setter")); } } - } - Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => { - if let [Some(instance), Some(value), ..] = overload.parameter_types() { - if let Some(setter) = property.setter(db) { - if let Err(_call_error) = setter.try_call( - db, - &mut CallArgumentTypes::positional([*instance, *value]), - ) { + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => { + if let [Some(instance), Some(value), ..] = overload.parameter_types() { + if let Some(setter) = property.setter(db) { + if let Err(_call_error) = setter.try_call( + db, + &mut CallArgumentTypes::positional([*instance, *value]), + ) { + overload.errors.push(BindingError::InternalCallError( + "calling the setter failed", + )); + } + } else { overload.errors.push(BindingError::InternalCallError( - "calling the setter failed", + "property has no setter", )); } - } else { - overload - .errors - .push(BindingError::InternalCallError("property has no setter")); } } - } - Type::MethodWrapper(MethodWrapperKind::StrStartswith(literal)) => { - if let [Some(Type::StringLiteral(prefix)), None, None] = - overload.parameter_types() - { - overload.set_return_type(Type::BooleanLiteral( - literal.value(db).starts_with(&**prefix.value(db)), - )); + Type::MethodWrapper(MethodWrapperKind::StrStartswith(literal)) => { + if let [Some(Type::StringLiteral(prefix)), None, None] = + overload.parameter_types() + { + overload.set_return_type(Type::BooleanLiteral( + literal.value(db).starts_with(&**prefix.value(db)), + )); + } } - } - Type::DataclassTransformer(params) => { - if let [Some(Type::FunctionLiteral(function))] = overload.parameter_types() { - overload.set_return_type(Type::FunctionLiteral( - function.with_dataclass_transformer_params(db, params), - )); + Type::DataclassTransformer(params) => { + if let [Some(Type::FunctionLiteral(function))] = overload.parameter_types() + { + overload.set_return_type(Type::FunctionLiteral( + function.with_dataclass_transformer_params(db, params), + )); + } } - } - Type::BoundMethod(bound_method) - if bound_method.self_instance(db).is_property_instance() => - { - match bound_method.function(db).name(db).as_str() { - "setter" => { - if let [Some(_), Some(setter)] = overload.parameter_types() { - let mut ty_property = bound_method.self_instance(db); - if let Type::PropertyInstance(property) = ty_property { - ty_property = - Type::PropertyInstance(PropertyInstanceType::new( - db, - property.getter(db), - Some(*setter), - )); + Type::BoundMethod(bound_method) + if bound_method.self_instance(db).is_property_instance() => + { + match bound_method.function(db).name(db).as_str() { + "setter" => { + if let [Some(_), Some(setter)] = overload.parameter_types() { + let mut ty_property = bound_method.self_instance(db); + if let Type::PropertyInstance(property) = ty_property { + ty_property = + Type::PropertyInstance(PropertyInstanceType::new( + db, + property.getter(db), + Some(*setter), + )); + } + overload.set_return_type(ty_property); } - overload.set_return_type(ty_property); } - } - "getter" => { - if let [Some(_), Some(getter)] = overload.parameter_types() { - let mut ty_property = bound_method.self_instance(db); - if let Type::PropertyInstance(property) = ty_property { - ty_property = - Type::PropertyInstance(PropertyInstanceType::new( - db, - Some(*getter), - property.setter(db), - )); + "getter" => { + if let [Some(_), Some(getter)] = overload.parameter_types() { + let mut ty_property = bound_method.self_instance(db); + if let Type::PropertyInstance(property) = ty_property { + ty_property = + Type::PropertyInstance(PropertyInstanceType::new( + db, + Some(*getter), + property.setter(db), + )); + } + overload.set_return_type(ty_property); } + } + "deleter" => { + // TODO: we do not store deleters yet + let ty_property = bound_method.self_instance(db); overload.set_return_type(ty_property); } - } - "deleter" => { - // TODO: we do not store deleters yet - let ty_property = bound_method.self_instance(db); - overload.set_return_type(ty_property); - } - _ => { - // Fall back to typeshed stubs for all other methods + _ => { + // Fall back to typeshed stubs for all other methods + } } } - } - Type::FunctionLiteral(function_type) => match function_type.known(db) { - Some(KnownFunction::IsEquivalentTo) => { - if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - overload.set_return_type(Type::BooleanLiteral( - ty_a.is_equivalent_to(db, *ty_b), - )); + Type::FunctionLiteral(function_type) => match function_type.known(db) { + Some(KnownFunction::IsEquivalentTo) => { + if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty_a.is_equivalent_to(db, *ty_b), + )); + } } - } - Some(KnownFunction::IsSubtypeOf) => { - if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - overload.set_return_type(Type::BooleanLiteral( - ty_a.is_subtype_of(db, *ty_b), - )); + Some(KnownFunction::IsSubtypeOf) => { + if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty_a.is_subtype_of(db, *ty_b), + )); + } } - } - Some(KnownFunction::IsAssignableTo) => { - if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - overload.set_return_type(Type::BooleanLiteral( - ty_a.is_assignable_to(db, *ty_b), - )); + Some(KnownFunction::IsAssignableTo) => { + if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty_a.is_assignable_to(db, *ty_b), + )); + } } - } - Some(KnownFunction::IsDisjointFrom) => { - if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - overload.set_return_type(Type::BooleanLiteral( - ty_a.is_disjoint_from(db, *ty_b), - )); + Some(KnownFunction::IsDisjointFrom) => { + if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty_a.is_disjoint_from(db, *ty_b), + )); + } } - } - Some(KnownFunction::IsGradualEquivalentTo) => { - if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - overload.set_return_type(Type::BooleanLiteral( - ty_a.is_gradual_equivalent_to(db, *ty_b), - )); + Some(KnownFunction::IsGradualEquivalentTo) => { + if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty_a.is_gradual_equivalent_to(db, *ty_b), + )); + } } - } - Some(KnownFunction::IsFullyStatic) => { - if let [Some(ty)] = overload.parameter_types() { - overload.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db))); + Some(KnownFunction::IsFullyStatic) => { + if let [Some(ty)] = overload.parameter_types() { + overload + .set_return_type(Type::BooleanLiteral(ty.is_fully_static(db))); + } } - } - Some(KnownFunction::IsSingleton) => { - if let [Some(ty)] = overload.parameter_types() { - overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db))); + Some(KnownFunction::IsSingleton) => { + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db))); + } } - } - Some(KnownFunction::IsSingleValued) => { - if let [Some(ty)] = overload.parameter_types() { - overload.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db))); + Some(KnownFunction::IsSingleValued) => { + if let [Some(ty)] = overload.parameter_types() { + overload + .set_return_type(Type::BooleanLiteral(ty.is_single_valued(db))); + } } - } - Some(KnownFunction::Len) => { - if let [Some(first_arg)] = overload.parameter_types() { - if let Some(len_ty) = first_arg.len(db) { - overload.set_return_type(len_ty); + Some(KnownFunction::Len) => { + if let [Some(first_arg)] = overload.parameter_types() { + if let Some(len_ty) = first_arg.len(db) { + overload.set_return_type(len_ty); + } } } - } - Some(KnownFunction::Repr) => { - if let [Some(first_arg)] = overload.parameter_types() { - overload.set_return_type(first_arg.repr(db)); + Some(KnownFunction::Repr) => { + if let [Some(first_arg)] = overload.parameter_types() { + overload.set_return_type(first_arg.repr(db)); + } } - } - Some(KnownFunction::Cast) => { - if let [Some(casted_ty), Some(_)] = overload.parameter_types() { - overload.set_return_type(*casted_ty); + Some(KnownFunction::Cast) => { + if let [Some(casted_ty), Some(_)] = overload.parameter_types() { + overload.set_return_type(*casted_ty); + } } - } - Some(KnownFunction::IsProtocol) => { - if let [Some(ty)] = overload.parameter_types() { - overload.set_return_type(Type::BooleanLiteral( - ty.into_class_literal() - .is_some_and(|class| class.is_protocol(db)), - )); + Some(KnownFunction::IsProtocol) => { + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(Type::BooleanLiteral( + ty.into_class_literal() + .is_some_and(|class| class.is_protocol(db)), + )); + } } - } - Some(KnownFunction::GetProtocolMembers) => { - if let [Some(Type::ClassLiteral(class))] = overload.parameter_types() { - if let Some(protocol_class) = class.into_protocol_class(db) { - // TODO: actually a frozenset at runtime (requires support for legacy generic classes) - overload.set_return_type(Type::Tuple(TupleType::new( - db, - protocol_class - .protocol_members(db) - .iter() - .map(|member| Type::string_literal(db, member)) - .collect::]>>(), - ))); + Some(KnownFunction::GetProtocolMembers) => { + if let [Some(Type::ClassLiteral(class))] = overload.parameter_types() { + if let Some(protocol_class) = class.into_protocol_class(db) { + // TODO: actually a frozenset at runtime (requires support for legacy generic classes) + overload.set_return_type(Type::Tuple(TupleType::new( + db, + protocol_class + .protocol_members(db) + .iter() + .map(|member| Type::string_literal(db, member)) + .collect::]>>(), + ))); + } } } - } - Some(KnownFunction::Overload) => { - // TODO: This can be removed once we understand legacy generics because the - // typeshed definition for `typing.overload` is an identity function. - if let [Some(ty)] = overload.parameter_types() { - overload.set_return_type(*ty); + Some(KnownFunction::Overload) => { + // TODO: This can be removed once we understand legacy generics because the + // typeshed definition for `typing.overload` is an identity function. + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(*ty); + } } - } - Some(KnownFunction::Override) => { - // TODO: This can be removed once we understand legacy generics because the - // typeshed definition for `typing.overload` is an identity function. - if let [Some(ty)] = overload.parameter_types() { - overload.set_return_type(*ty); + Some(KnownFunction::Override) => { + // TODO: This can be removed once we understand legacy generics because the + // typeshed definition for `typing.overload` is an identity function. + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(*ty); + } } - } - Some(KnownFunction::AbstractMethod) => { - // TODO: This can be removed once we understand legacy generics because the - // typeshed definition for `abc.abstractmethod` is an identity function. - if let [Some(ty)] = overload.parameter_types() { - overload.set_return_type(*ty); + Some(KnownFunction::AbstractMethod) => { + // TODO: This can be removed once we understand legacy generics because the + // typeshed definition for `abc.abstractmethod` is an identity function. + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(*ty); + } } - } - Some(KnownFunction::Final) => { - // TODO: This can be removed once we understand legacy generics because the - // typeshed definition for `abc.abstractmethod` is an identity function. - if let [Some(ty)] = overload.parameter_types() { - overload.set_return_type(*ty); + Some(KnownFunction::Final) => { + // TODO: This can be removed once we understand legacy generics because the + // typeshed definition for `abc.abstractmethod` is an identity function. + if let [Some(ty)] = overload.parameter_types() { + overload.set_return_type(*ty); + } } - } - Some(KnownFunction::GetattrStatic) => { - let [Some(instance_ty), Some(attr_name), default] = - overload.parameter_types() - else { - continue; - }; + Some(KnownFunction::GetattrStatic) => { + let [Some(instance_ty), Some(attr_name), default] = + overload.parameter_types() + else { + continue; + }; - let Some(attr_name) = attr_name.into_string_literal() else { - continue; - }; - - let default = if let Some(default) = default { - *default - } else { - Type::Never - }; - - let union_with_default = |ty| UnionType::from_elements(db, [ty, default]); - - // TODO: we could emit a diagnostic here (if default is not set) - overload.set_return_type( - match instance_ty.static_member(db, attr_name.value(db)) { - Symbol::Type(ty, Boundness::Bound) => { - if instance_ty.is_fully_static(db) { - ty - } else { - // Here, we attempt to model the fact that an attribute lookup on - // a non-fully static type could fail. This is an approximation, - // as there are gradual types like `tuple[Any]`, on which a lookup - // of (e.g. of the `index` method) would always succeed. + let Some(attr_name) = attr_name.into_string_literal() else { + continue; + }; + let default = if let Some(default) = default { + *default + } else { + Type::Never + }; + + let union_with_default = + |ty| UnionType::from_elements(db, [ty, default]); + + // TODO: we could emit a diagnostic here (if default is not set) + overload.set_return_type( + match instance_ty.static_member(db, attr_name.value(db)) { + Symbol::Type(ty, Boundness::Bound) => { + if instance_ty.is_fully_static(db) { + ty + } else { + // Here, we attempt to model the fact that an attribute lookup on + // a non-fully static type could fail. This is an approximation, + // as there are gradual types like `tuple[Any]`, on which a lookup + // of (e.g. of the `index` method) would always succeed. + + union_with_default(ty) + } + } + Symbol::Type(ty, Boundness::PossiblyUnbound) => { union_with_default(ty) } + Symbol::Unbound => default, + }, + ); + } + + Some(KnownFunction::Dataclass) => { + if let [init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot] = + overload.parameter_types() + { + let mut params = DataclassParams::empty(); + + if to_bool(init, true) { + params |= DataclassParams::INIT; } - Symbol::Type(ty, Boundness::PossiblyUnbound) => { - union_with_default(ty) + if to_bool(repr, true) { + params |= DataclassParams::REPR; + } + if to_bool(eq, true) { + params |= DataclassParams::EQ; + } + if to_bool(order, false) { + params |= DataclassParams::ORDER; + } + if to_bool(unsafe_hash, false) { + params |= DataclassParams::UNSAFE_HASH; + } + if to_bool(frozen, false) { + params |= DataclassParams::FROZEN; + } + if to_bool(match_args, true) { + params |= DataclassParams::MATCH_ARGS; + } + if to_bool(kw_only, false) { + params |= DataclassParams::KW_ONLY; + } + if to_bool(slots, false) { + params |= DataclassParams::SLOTS; + } + if to_bool(weakref_slot, false) { + params |= DataclassParams::WEAKREF_SLOT; } - Symbol::Unbound => default, - }, - ); - } - - Some(KnownFunction::Dataclass) => { - if let [init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot] = - overload.parameter_types() - { - let mut params = DataclassParams::empty(); - if to_bool(init, true) { - params |= DataclassParams::INIT; - } - if to_bool(repr, true) { - params |= DataclassParams::REPR; - } - if to_bool(eq, true) { - params |= DataclassParams::EQ; - } - if to_bool(order, false) { - params |= DataclassParams::ORDER; - } - if to_bool(unsafe_hash, false) { - params |= DataclassParams::UNSAFE_HASH; + overload.set_return_type(Type::DataclassDecorator(params)); } - if to_bool(frozen, false) { - params |= DataclassParams::FROZEN; - } - if to_bool(match_args, true) { - params |= DataclassParams::MATCH_ARGS; - } - if to_bool(kw_only, false) { - params |= DataclassParams::KW_ONLY; - } - if to_bool(slots, false) { - params |= DataclassParams::SLOTS; - } - if to_bool(weakref_slot, false) { - params |= DataclassParams::WEAKREF_SLOT; - } - - overload.set_return_type(Type::DataclassDecorator(params)); } - } - Some(KnownFunction::DataclassTransform) => { - if let [eq_default, order_default, kw_only_default, frozen_default, _field_specifiers, _kwargs] = - overload.parameter_types() - { - let mut params = DataclassTransformerParams::empty(); + Some(KnownFunction::DataclassTransform) => { + if let [eq_default, order_default, kw_only_default, frozen_default, _field_specifiers, _kwargs] = + overload.parameter_types() + { + let mut params = DataclassTransformerParams::empty(); - if to_bool(eq_default, true) { - params |= DataclassTransformerParams::EQ_DEFAULT; - } - if to_bool(order_default, false) { - params |= DataclassTransformerParams::ORDER_DEFAULT; - } - if to_bool(kw_only_default, false) { - params |= DataclassTransformerParams::KW_ONLY_DEFAULT; - } - if to_bool(frozen_default, false) { - params |= DataclassTransformerParams::FROZEN_DEFAULT; - } + if to_bool(eq_default, true) { + params |= DataclassTransformerParams::EQ_DEFAULT; + } + if to_bool(order_default, false) { + params |= DataclassTransformerParams::ORDER_DEFAULT; + } + if to_bool(kw_only_default, false) { + params |= DataclassTransformerParams::KW_ONLY_DEFAULT; + } + if to_bool(frozen_default, false) { + params |= DataclassTransformerParams::FROZEN_DEFAULT; + } - overload.set_return_type(Type::DataclassTransformer(params)); + overload.set_return_type(Type::DataclassTransformer(params)); + } } - } - _ => { - if let Some(params) = function_type.dataclass_transformer_params(db) { - // This is a call to a custom function that was decorated with `@dataclass_transformer`. - // If this function was called with a keyword argument like `order=False`, we extract - // the argument type and overwrite the corresponding flag in `dataclass_params` after - // constructing them from the `dataclass_transformer`-parameter defaults. - - let mut dataclass_params = DataclassParams::from(params); - - if let Some(Some(Type::BooleanLiteral(order))) = callable_signature - .iter() - .nth(overload_index) - .and_then(|signature| { - let (idx, _) = - signature.parameters().keyword_by_name("order")?; - overload.parameter_types().get(idx) - }) - { - dataclass_params.set(DataclassParams::ORDER, *order); - } + _ => { + if let Some(params) = function_type.dataclass_transformer_params(db) { + // This is a call to a custom function that was decorated with `@dataclass_transformer`. + // If this function was called with a keyword argument like `order=False`, we extract + // the argument type and overwrite the corresponding flag in `dataclass_params` after + // constructing them from the `dataclass_transformer`-parameter defaults. + + let mut dataclass_params = DataclassParams::from(params); + + if let Some(Some(Type::BooleanLiteral(order))) = callable_signature + .iter() + .nth(overload_index) + .and_then(|signature| { + let (idx, _) = + signature.parameters().keyword_by_name("order")?; + overload.parameter_types().get(idx) + }) + { + dataclass_params.set(DataclassParams::ORDER, *order); + } - overload.set_return_type(Type::DataclassDecorator(dataclass_params)); + overload + .set_return_type(Type::DataclassDecorator(dataclass_params)); + } } - } - }, - - Type::ClassLiteral(class) => match class.known(db) { - Some(KnownClass::Bool) => match overload.parameter_types() { - [Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)), - [None] => overload.set_return_type(Type::BooleanLiteral(false)), - _ => {} }, - Some(KnownClass::Str) if overload_index == 0 => { - match overload.parameter_types() { - [Some(arg)] => overload.set_return_type(arg.str(db)), - [None] => overload.set_return_type(Type::string_literal(db, "")), + Type::ClassLiteral(class) => match class.known(db) { + Some(KnownClass::Bool) => match overload.parameter_types() { + [Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)), + [None] => overload.set_return_type(Type::BooleanLiteral(false)), _ => {} + }, + + Some(KnownClass::Str) if overload_index == 0 => { + match overload.parameter_types() { + [Some(arg)] => overload.set_return_type(arg.str(db)), + [None] => overload.set_return_type(Type::string_literal(db, "")), + _ => {} + } } - } - Some(KnownClass::Type) if overload_index == 0 => { - if let [Some(arg)] = overload.parameter_types() { - overload.set_return_type(arg.to_meta_type(db)); + Some(KnownClass::Type) if overload_index == 0 => { + if let [Some(arg)] = overload.parameter_types() { + overload.set_return_type(arg.to_meta_type(db)); + } } - } - Some(KnownClass::Property) => { - if let [getter, setter, ..] = overload.parameter_types() { - overload.set_return_type(Type::PropertyInstance( - PropertyInstanceType::new(db, *getter, *setter), - )); + Some(KnownClass::Property) => { + if let [getter, setter, ..] = overload.parameter_types() { + overload.set_return_type(Type::PropertyInstance( + PropertyInstanceType::new(db, *getter, *setter), + )); + } } + + _ => {} + }, + + Type::KnownInstance(KnownInstanceType::TypedDict) => { + overload.set_return_type(todo_type!("TypedDict")); } + // Not a special case _ => {} - }, - - Type::KnownInstance(KnownInstanceType::TypedDict) => { - overload.set_return_type(todo_type!("TypedDict")); } - - // Not a special case - _ => {} } } } @@ -868,7 +886,7 @@ impl<'db> CallableBinding<'db> { // the matching overloads. Make sure to implement that as part of separating call binding into // two phases. // - // [1] https://github.com/python/typing/pull/1839 + // [1] https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation let overloads = signature .into_iter() .map(|signature| { @@ -928,35 +946,39 @@ impl<'db> CallableBinding<'db> { /// Returns whether there were any errors binding this call site. If the callable has multiple /// overloads, they must _all_ have errors. pub(crate) fn has_binding_errors(&self) -> bool { - self.matching_overload().is_none() + self.matching_overloads().next().is_none() } - /// Returns the overload that matched for this call binding. Returns `None` if none of the - /// overloads matched. - pub(crate) fn matching_overload(&self) -> Option<(usize, &Binding<'db>)> { + /// Returns an iterator over all the overloads that matched for this call binding. + pub(crate) fn matching_overloads(&self) -> impl Iterator)> { self.overloads .iter() .enumerate() - .find(|(_, overload)| overload.as_result().is_ok()) + .filter(|(_, overload)| overload.as_result().is_ok()) } - /// Returns the overload that matched for this call binding. Returns `None` if none of the - /// overloads matched. - pub(crate) fn matching_overload_mut(&mut self) -> Option<(usize, &mut Binding<'db>)> { + /// Returns an iterator over all the mutable overloads that matched for this call binding. + pub(crate) fn matching_overloads_mut( + &mut self, + ) -> impl Iterator)> { self.overloads .iter_mut() .enumerate() - .find(|(_, overload)| overload.as_result().is_ok()) + .filter(|(_, overload)| overload.as_result().is_ok()) } /// Returns the return type of this call. For a valid call, this is the return type of the - /// overload that the arguments matched against. For an invalid call to a non-overloaded + /// first overload that the arguments matched against. For an invalid call to a non-overloaded /// function, this is the return type of the function. For an invalid call to an overloaded /// function, we return `Type::unknown`, since we cannot make any useful conclusions about /// which overload was intended to be called. pub(crate) fn return_type(&self) -> Type<'db> { - if let Some((_, overload)) = self.matching_overload() { - return overload.return_type(); + // TODO: Implement the overload call evaluation algorithm as mentioned in the spec [1] to + // get the matching overload and use that to get the return type. + // + // [1]: https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation + if let Some((_, first_overload)) = self.matching_overloads().next() { + return first_overload.return_type(); } if let [overload] = self.overloads.as_slice() { return overload.return_type(); diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 22b821bdd392ba..d39b5dc34c419d 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -4627,307 +4627,310 @@ impl<'db> TypeInferenceBuilder<'db> { Ok(mut bindings) => { for binding in &mut bindings { let binding_type = binding.callable_type; - let Some((_, overload)) = binding.matching_overload_mut() else { - continue; - }; - - match binding_type { - Type::FunctionLiteral(function_literal) => { - let Some(known_function) = function_literal.known(self.db()) else { - continue; - }; + for (_, overload) in binding.matching_overloads_mut() { + match binding_type { + Type::FunctionLiteral(function_literal) => { + let Some(known_function) = function_literal.known(self.db()) else { + continue; + }; - match known_function { - KnownFunction::RevealType => { - if let [Some(revealed_type)] = overload.parameter_types() { - if let Some(builder) = self.context.report_diagnostic( - DiagnosticId::RevealedType, - Severity::Info, - ) { - let mut diag = builder.into_diagnostic("Revealed type"); - let span = self.context.span(call_expression); - diag.annotate(Annotation::primary(span).message( - format_args!( - "`{}`", - revealed_type.display(self.db()) - ), - )); + match known_function { + KnownFunction::RevealType => { + if let [Some(revealed_type)] = overload.parameter_types() { + if let Some(builder) = self.context.report_diagnostic( + DiagnosticId::RevealedType, + Severity::Info, + ) { + let mut diag = + builder.into_diagnostic("Revealed type"); + let span = self.context.span(call_expression); + diag.annotate(Annotation::primary(span).message( + format_args!( + "`{}`", + revealed_type.display(self.db()) + ), + )); + } } } - } - KnownFunction::AssertType => { - if let [Some(actual_ty), Some(asserted_ty)] = - overload.parameter_types() - { - if !actual_ty - .is_gradual_equivalent_to(self.db(), *asserted_ty) + KnownFunction::AssertType => { + if let [Some(actual_ty), Some(asserted_ty)] = + overload.parameter_types() { - if let Some(builder) = self.context.report_lint( - &TYPE_ASSERTION_FAILURE, - call_expression, - ) { - builder.into_diagnostic(format_args!( - "Actual type `{}` is not the same \ + if !actual_ty + .is_gradual_equivalent_to(self.db(), *asserted_ty) + { + if let Some(builder) = self.context.report_lint( + &TYPE_ASSERTION_FAILURE, + call_expression, + ) { + builder.into_diagnostic(format_args!( + "Actual type `{}` is not the same \ as asserted type `{}`", - actual_ty.display(self.db()), - asserted_ty.display(self.db()), - )); + actual_ty.display(self.db()), + asserted_ty.display(self.db()), + )); + } } } } - } - KnownFunction::AssertNever => { - if let [Some(actual_ty)] = overload.parameter_types() { - if !actual_ty.is_equivalent_to(self.db(), Type::Never) { - if let Some(builder) = self.context.report_lint( - &TYPE_ASSERTION_FAILURE, - call_expression, - ) { - builder.into_diagnostic(format_args!( - "Expected type `Never`, got `{}` instead", - actual_ty.display(self.db()), - )); + KnownFunction::AssertNever => { + if let [Some(actual_ty)] = overload.parameter_types() { + if !actual_ty.is_equivalent_to(self.db(), Type::Never) { + if let Some(builder) = self.context.report_lint( + &TYPE_ASSERTION_FAILURE, + call_expression, + ) { + builder.into_diagnostic(format_args!( + "Expected type `Never`, got `{}` instead", + actual_ty.display(self.db()), + )); + } } } } - } - KnownFunction::StaticAssert => { - if let [Some(parameter_ty), message] = - overload.parameter_types() - { - let truthiness = match parameter_ty.try_bool(self.db()) { - Ok(truthiness) => truthiness, - Err(err) => { - let condition = arguments - .find_argument("condition", 0) - .map(|argument| match argument { + KnownFunction::StaticAssert => { + if let [Some(parameter_ty), message] = + overload.parameter_types() + { + let truthiness = match parameter_ty.try_bool(self.db()) + { + Ok(truthiness) => truthiness, + Err(err) => { + let condition = arguments + .find_argument("condition", 0) + .map(|argument| { + match argument { ruff_python_ast::ArgOrKeyword::Arg( expr, ) => ast::AnyNodeRef::from(expr), ruff_python_ast::ArgOrKeyword::Keyword( keyword, ) => ast::AnyNodeRef::from(keyword), - }) - .unwrap_or(ast::AnyNodeRef::from( - call_expression, - )); + } + }) + .unwrap_or(ast::AnyNodeRef::from( + call_expression, + )); - err.report_diagnostic(&self.context, condition); + err.report_diagnostic(&self.context, condition); - continue; - } - }; + continue; + } + }; - if let Some(builder) = self - .context - .report_lint(&STATIC_ASSERT_ERROR, call_expression) - { - if !truthiness.is_always_true() { - if let Some(message) = message - .and_then(Type::into_string_literal) - .map(|s| &**s.value(self.db())) - { - builder.into_diagnostic(format_args!( - "Static assertion error: {message}" - )); - } else if *parameter_ty - == Type::BooleanLiteral(false) - { - builder.into_diagnostic( - "Static assertion error: \ + if let Some(builder) = self + .context + .report_lint(&STATIC_ASSERT_ERROR, call_expression) + { + if !truthiness.is_always_true() { + if let Some(message) = message + .and_then(Type::into_string_literal) + .map(|s| &**s.value(self.db())) + { + builder.into_diagnostic(format_args!( + "Static assertion error: {message}" + )); + } else if *parameter_ty + == Type::BooleanLiteral(false) + { + builder.into_diagnostic( + "Static assertion error: \ argument evaluates to `False`", - ); - } else if truthiness.is_always_false() { - builder.into_diagnostic(format_args!( - "Static assertion error: \ + ); + } else if truthiness.is_always_false() { + builder.into_diagnostic(format_args!( + "Static assertion error: \ argument of type `{parameter_ty}` \ is statically known to be falsy", - parameter_ty = - parameter_ty.display(self.db()) - )); - } else { - builder.into_diagnostic(format_args!( - "Static assertion error: \ + parameter_ty = + parameter_ty.display(self.db()) + )); + } else { + builder.into_diagnostic(format_args!( + "Static assertion error: \ argument of type `{parameter_ty}` \ has an ambiguous static truthiness", - parameter_ty = - parameter_ty.display(self.db()) - )); + parameter_ty = + parameter_ty.display(self.db()) + )); + } } } } } - } - KnownFunction::Cast => { - if let [Some(casted_type), Some(source_type)] = - overload.parameter_types() - { - let db = self.db(); - if (source_type.is_equivalent_to(db, *casted_type) - || source_type.normalized(db) - == casted_type.normalized(db)) - && !source_type.contains_todo(db) + KnownFunction::Cast => { + if let [Some(casted_type), Some(source_type)] = + overload.parameter_types() { - if let Some(builder) = self - .context - .report_lint(&REDUNDANT_CAST, call_expression) + let db = self.db(); + if (source_type.is_equivalent_to(db, *casted_type) + || source_type.normalized(db) + == casted_type.normalized(db)) + && !source_type.contains_todo(db) { - builder.into_diagnostic(format_args!( - "Value is already of type `{}`", - casted_type.display(db), - )); + if let Some(builder) = self + .context + .report_lint(&REDUNDANT_CAST, call_expression) + { + builder.into_diagnostic(format_args!( + "Value is already of type `{}`", + casted_type.display(db), + )); + } } } } - } - KnownFunction::GetProtocolMembers => { - if let [Some(Type::ClassLiteral(class))] = - overload.parameter_types() - { - if !class.is_protocol(self.db()) { - report_bad_argument_to_get_protocol_members( - &self.context, - call_expression, - *class, - ); + KnownFunction::GetProtocolMembers => { + if let [Some(Type::ClassLiteral(class))] = + overload.parameter_types() + { + if !class.is_protocol(self.db()) { + report_bad_argument_to_get_protocol_members( + &self.context, + call_expression, + *class, + ); + } } } - } - KnownFunction::IsInstance | KnownFunction::IsSubclass => { - if let [_, Some(Type::ClassLiteral(class))] = - overload.parameter_types() - { - if let Some(protocol_class) = - class.into_protocol_class(self.db()) + KnownFunction::IsInstance | KnownFunction::IsSubclass => { + if let [_, Some(Type::ClassLiteral(class))] = + overload.parameter_types() { - if !protocol_class.is_runtime_checkable(self.db()) { - report_runtime_check_against_non_runtime_checkable_protocol( + if let Some(protocol_class) = + class.into_protocol_class(self.db()) + { + if !protocol_class.is_runtime_checkable(self.db()) { + report_runtime_check_against_non_runtime_checkable_protocol( &self.context, call_expression, protocol_class, known_function ); + } } } } + _ => {} } - _ => {} } - } - Type::ClassLiteral(class) => { - let Some(known_class) = class.known(self.db()) else { - continue; - }; + Type::ClassLiteral(class) => { + let Some(known_class) = class.known(self.db()) else { + continue; + }; - match known_class { - KnownClass::Super => { - // Handle the case where `super()` is called with no arguments. - // In this case, we need to infer the two arguments: - // 1. The nearest enclosing class - // 2. The first parameter of the current function (typically `self` or `cls`) - match overload.parameter_types() { - [] => { - let scope = self.scope(); - - let Some(enclosing_class) = - self.enclosing_class_symbol(scope) - else { - overload.set_return_type(Type::unknown()); - BoundSuperError::UnavailableImplicitArguments - .report_diagnostic( + match known_class { + KnownClass::Super => { + // Handle the case where `super()` is called with no arguments. + // In this case, we need to infer the two arguments: + // 1. The nearest enclosing class + // 2. The first parameter of the current function (typically `self` or `cls`) + match overload.parameter_types() { + [] => { + let scope = self.scope(); + + let Some(enclosing_class) = + self.enclosing_class_symbol(scope) + else { + overload.set_return_type(Type::unknown()); + BoundSuperError::UnavailableImplicitArguments + .report_diagnostic( + &self.context, + call_expression.into(), + ); + continue; + }; + + let Some(first_param) = + self.first_param_type_in_scope(scope) + else { + overload.set_return_type(Type::unknown()); + BoundSuperError::UnavailableImplicitArguments + .report_diagnostic( + &self.context, + call_expression.into(), + ); + continue; + }; + + let bound_super = BoundSuperType::build( + self.db(), + enclosing_class, + first_param, + ) + .unwrap_or_else(|err| { + err.report_diagnostic( &self.context, call_expression.into(), ); - continue; - }; + Type::unknown() + }); - let Some(first_param) = - self.first_param_type_in_scope(scope) - else { - overload.set_return_type(Type::unknown()); - BoundSuperError::UnavailableImplicitArguments - .report_diagnostic( + overload.set_return_type(bound_super); + } + [Some(pivot_class_type), Some(owner_type)] => { + let bound_super = BoundSuperType::build( + self.db(), + *pivot_class_type, + *owner_type, + ) + .unwrap_or_else(|err| { + err.report_diagnostic( &self.context, call_expression.into(), ); - continue; - }; - - let bound_super = BoundSuperType::build( - self.db(), - enclosing_class, - first_param, - ) - .unwrap_or_else(|err| { - err.report_diagnostic( - &self.context, - call_expression.into(), - ); - Type::unknown() - }); - - overload.set_return_type(bound_super); - } - [Some(pivot_class_type), Some(owner_type)] => { - let bound_super = BoundSuperType::build( - self.db(), - *pivot_class_type, - *owner_type, - ) - .unwrap_or_else(|err| { - err.report_diagnostic( - &self.context, - call_expression.into(), - ); - Type::unknown() - }); + Type::unknown() + }); - overload.set_return_type(bound_super); + overload.set_return_type(bound_super); + } + _ => (), } - _ => (), } - } - KnownClass::TypeVar => { - let assigned_to = (self.index) - .try_expression(call_expression_node) - .and_then(|expr| expr.assigned_to(self.db())); + KnownClass::TypeVar => { + let assigned_to = (self.index) + .try_expression(call_expression_node) + .and_then(|expr| expr.assigned_to(self.db())); - let Some(target) = - assigned_to.as_ref().and_then(|assigned_to| { - match assigned_to.node().targets.as_slice() { - [ast::Expr::Name(target)] => Some(target), - _ => None, - } - }) - else { - if let Some(builder) = self.context.report_lint( - &INVALID_LEGACY_TYPE_VARIABLE, - call_expression, - ) { - builder.into_diagnostic(format_args!( + let Some(target) = + assigned_to.as_ref().and_then(|assigned_to| { + match assigned_to.node().targets.as_slice() { + [ast::Expr::Name(target)] => Some(target), + _ => None, + } + }) + else { + if let Some(builder) = self.context.report_lint( + &INVALID_LEGACY_TYPE_VARIABLE, + call_expression, + ) { + builder.into_diagnostic(format_args!( "A legacy `typing.TypeVar` must be immediately assigned to a variable", )); - } - continue; - }; - - let [Some(name_param), constraints, bound, default, _contravariant, _covariant, _infer_variance] = - overload.parameter_types() - else { - continue; - }; - - let name_param = name_param - .into_string_literal() - .map(|name| name.value(self.db()).as_ref()); - if name_param.is_none_or(|name_param| name_param != target.id) { - if let Some(builder) = self.context.report_lint( - &INVALID_LEGACY_TYPE_VARIABLE, - call_expression, - ) { - builder.into_diagnostic(format_args!( + } + continue; + }; + + let [Some(name_param), constraints, bound, default, _contravariant, _covariant, _infer_variance] = + overload.parameter_types() + else { + continue; + }; + + let name_param = name_param + .into_string_literal() + .map(|name| name.value(self.db()).as_ref()); + if name_param + .is_none_or(|name_param| name_param != target.id) + { + if let Some(builder) = self.context.report_lint( + &INVALID_LEGACY_TYPE_VARIABLE, + call_expression, + ) { + builder.into_diagnostic(format_args!( "The name of a legacy `typing.TypeVar`{} must match \ the name of the variable it is assigned to (`{}`)", if let Some(name_param) = name_param { @@ -4937,60 +4940,63 @@ impl<'db> TypeInferenceBuilder<'db> { }, target.id, )); + } + continue; } - continue; - } - let bound_or_constraint = match (bound, constraints) { - (Some(bound), None) => { - Some(TypeVarBoundOrConstraints::UpperBound(*bound)) - } + let bound_or_constraint = match (bound, constraints) { + (Some(bound), None) => { + Some(TypeVarBoundOrConstraints::UpperBound(*bound)) + } + + (None, Some(_constraints)) => { + // We don't use UnionType::from_elements or UnionBuilder here, + // because we don't want to simplify the list of constraints like + // we do with the elements of an actual union type. + // TODO: Consider using a new `OneOfType` connective here instead, + // since that more accurately represents the actual semantics of + // typevar constraints. + let elements = UnionType::new( + self.db(), + overload + .arguments_for_parameter( + &call_argument_types, + 1, + ) + .map(|(_, ty)| ty) + .collect::>(), + ); + Some(TypeVarBoundOrConstraints::Constraints( + elements, + )) + } + + // TODO: Emit a diagnostic that TypeVar cannot be both bounded and + // constrained + (Some(_), Some(_)) => continue, - (None, Some(_constraints)) => { - // We don't use UnionType::from_elements or UnionBuilder here, - // because we don't want to simplify the list of constraints like - // we do with the elements of an actual union type. - // TODO: Consider using a new `OneOfType` connective here instead, - // since that more accurately represents the actual semantics of - // typevar constraints. - let elements = UnionType::new( + (None, None) => None, + }; + + let containing_assignment = + self.index.expect_single_definition(target); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::TypeVar(TypeVarInstance::new( self.db(), - overload - .arguments_for_parameter( - &call_argument_types, - 1, - ) - .map(|(_, ty)| ty) - .collect::>(), - ); - Some(TypeVarBoundOrConstraints::Constraints(elements)) - } + target.id.clone(), + containing_assignment, + bound_or_constraint, + *default, + TypeVarKind::Legacy, + )), + )); + } - // TODO: Emit a diagnostic that TypeVar cannot be both bounded and - // constrained - (Some(_), Some(_)) => continue, - - (None, None) => None, - }; - - let containing_assignment = - self.index.expect_single_definition(target); - overload.set_return_type(Type::KnownInstance( - KnownInstanceType::TypeVar(TypeVarInstance::new( - self.db(), - target.id.clone(), - containing_assignment, - bound_or_constraint, - *default, - TypeVarKind::Legacy, - )), - )); + _ => (), } - - _ => (), } + _ => (), } - _ => (), } } bindings.return_type(self.db()) @@ -6637,7 +6643,8 @@ impl<'db> TypeInferenceBuilder<'db> { .next() .expect("valid bindings should have one callable"); let (_, overload) = callable - .matching_overload() + .matching_overloads() + .next() .expect("valid bindings should have matching overload"); let specialization = generic_context.specialize( self.db(), From 650cbdd296c0f4bf8b744104c18bed2f99ff46df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 22:12:03 +0200 Subject: [PATCH 0202/1161] Update dependency vite to v6.2.7 (#17746) --- playground/package-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index 7effd1c8304ab0..61791af3fd90e8 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -41,10 +41,10 @@ "pyodide": "^0.27.4", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable-panels": "^2.1.9", + "react-resizable-panels": "^2.1.7", "red_knot_wasm": "file:red_knot_wasm", "shared": "0.0.0", - "smol-toml": "^1.3.4" + "smol-toml": "^1.3.1" }, "devDependencies": { "vite-plugin-static-copy": "^2.3.0" @@ -5809,9 +5809,9 @@ } }, "node_modules/vite": { - "version": "6.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", - "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", + "version": "6.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.7.tgz", + "integrity": "sha512-qg3LkeuinTrZoJHHF94coSaTfIPyBYoywp+ys4qu20oSJFbKMYoIJo0FWJT9q6Vp49l6z9IsJRbHdcGtiKbGoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6087,10 +6087,10 @@ "monaco-editor": "^0.52.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable-panels": "^2.1.9", + "react-resizable-panels": "^2.0.0", "ruff_wasm": "file:ruff_wasm", "shared": "0.0.0", - "smol-toml": "^1.3.4" + "smol-toml": "^1.3.0" } }, "ruff/ruff_wasm": { @@ -6103,7 +6103,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.3.2", "react": "^19.0.0", - "react-resizable-panels": "^2.1.9" + "react-resizable-panels": "^2.1.7" } } } From d33a50368644c0bd98d4d66d0b9b5dbbaa5f89db Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Wed, 30 Apr 2025 17:40:16 -0300 Subject: [PATCH 0203/1161] [red-knot] Add tests for classes that have incompatible `__new__` and `__init__` methods (#17747) Closes #17737 --- .../resources/mdtest/call/constructor.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md b/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md index 83ca0093eee360..db20ce4a962a4b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md @@ -343,6 +343,64 @@ reveal_type(Foo(1)) # revealed: Foo reveal_type(Foo(1, 2)) # revealed: Foo ``` +### Incompatible signatures + +```py +import abc + +class Foo: + def __new__(cls) -> "Foo": + return object.__new__(cls) + + def __init__(self, x): + self.x = 42 + +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo()) # revealed: Foo + +# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 0, got 1" +reveal_type(Foo(42)) # revealed: Foo + +class Foo2: + def __new__(cls, x) -> "Foo2": + return object.__new__(cls) + + def __init__(self): + pass + +# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" +reveal_type(Foo2()) # revealed: Foo2 + +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1" +reveal_type(Foo2(42)) # revealed: Foo2 + +class Foo3(metaclass=abc.ABCMeta): + def __new__(cls) -> "Foo3": + return object.__new__(cls) + + def __init__(self, x): + self.x = 42 + +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo3()) # revealed: Foo3 + +# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 0, got 1" +reveal_type(Foo3(42)) # revealed: Foo3 + +class Foo4(metaclass=abc.ABCMeta): + def __new__(cls, x) -> "Foo4": + return object.__new__(cls) + + def __init__(self): + pass + +# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" +reveal_type(Foo4()) # revealed: Foo4 + +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1" +reveal_type(Foo4(42)) # revealed: Foo4 +``` + ### Lookup of `__new__` The `__new__` method is always invoked on the class itself, never on the metaclass. This is From 03d8679adff964ddb4d2945d5b39665bead72755 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 30 Apr 2025 22:52:04 +0200 Subject: [PATCH 0204/1161] [red-knot] Preliminary `NamedTuple` support (#17738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds preliminary support for `NamedTuple`s, including: * No false positives when constructing a `NamedTuple` object * Correct signature for the synthesized `__new__` method, i.e. proper checking of constructor calls * A patched MRO (`NamedTuple` => `tuple`), mainly to make type inference of named attributes possible, but also to better reflect the runtime MRO. All of this works: ```py from typing import NamedTuple class Person(NamedTuple): id: int name: str age: int | None = None alice = Person(1, "Alice", 42) alice = Person(id=1, name="Alice", age=42) reveal_type(alice.id) # revealed: int reveal_type(alice.name) # revealed: str reveal_type(alice.age) # revealed: int | None # error: [missing-argument] Person(3) # error: [too-many-positional-arguments] Person(3, "Eve", 99, "extra") # error: [invalid-argument-type] Person(id="3", name="Eve") ``` Not included: * type inference for index-based access. * support for the functional `MyTuple = NamedTuple("MyTuple", […])` syntax ## Test Plan New Markdown tests ## Ecosystem analysis ``` Diagnostic Analysis Report ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━┓ ┃ Diagnostic ID ┃ Severity ┃ Removed ┃ Added ┃ Net Change ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━┩ │ lint:call-non-callable │ error │ 0 │ 3 │ +3 │ │ lint:call-possibly-unbound-method │ warning │ 0 │ 4 │ +4 │ │ lint:invalid-argument-type │ error │ 0 │ 72 │ +72 │ │ lint:invalid-context-manager │ error │ 0 │ 2 │ +2 │ │ lint:invalid-return-type │ error │ 0 │ 2 │ +2 │ │ lint:missing-argument │ error │ 0 │ 46 │ +46 │ │ lint:no-matching-overload │ error │ 19121 │ 0 │ -19121 │ │ lint:not-iterable │ error │ 0 │ 6 │ +6 │ │ lint:possibly-unbound-attribute │ warning │ 13 │ 32 │ +19 │ │ lint:redundant-cast │ warning │ 0 │ 1 │ +1 │ │ lint:unresolved-attribute │ error │ 0 │ 10 │ +10 │ │ lint:unsupported-operator │ error │ 3 │ 9 │ +6 │ │ lint:unused-ignore-comment │ warning │ 15 │ 4 │ -11 │ ├───────────────────────────────────┼──────────┼─────────┼───────┼────────────┤ │ TOTAL │ │ 19152 │ 191 │ -18961 │ └───────────────────────────────────┴──────────┴─────────┴───────┴────────────┘ Analysis complete. Found 13 unique diagnostic IDs. Total diagnostics removed: 19152 Total diagnostics added: 191 Net change: -18961 ``` I uploaded the ecosystem full diff (ignoring the 19k `no-matching-overload` diagnostics) [here](https://shark.fish/diff-namedtuple.html). * There are some new `missing-argument` false positives which come from the fact that named tuples are often created using unpacking as in `MyNamedTuple(*fields)`, which we do not understand yet. * There are some new `unresolved-attribute` false positives, because methods like `_replace` are not available. * Lots of the `invalid-argument-type` diagnostics look like true positives --------- Co-authored-by: Douglas Creager --- .../resources/mdtest/named_tuple.md | 120 +++++++++ .../src/types/class.rs | 233 +++++++++++------- .../src/types/class_base.rs | 14 +- crates/ruff_benchmark/benches/red_knot.rs | 23 +- 4 files changed, 275 insertions(+), 115 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/named_tuple.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/named_tuple.md b/crates/red_knot_python_semantic/resources/mdtest/named_tuple.md new file mode 100644 index 00000000000000..db3aab7bfd404a --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/named_tuple.md @@ -0,0 +1,120 @@ +# `NamedTuple` + +`NamedTuple` is a type-safe way to define named tuples — a tuple where each field can be accessed by +name, and not just by its numeric position within the tuple: + +## `typing.NamedTuple` + +### Basics + +```py +from typing import NamedTuple + +class Person(NamedTuple): + id: int + name: str + age: int | None = None + +alice = Person(1, "Alice", 42) +alice = Person(id=1, name="Alice", age=42) +bob = Person(2, "Bob") +bob = Person(id=2, name="Bob") + +reveal_type(alice.id) # revealed: int +reveal_type(alice.name) # revealed: str +reveal_type(alice.age) # revealed: int | None + +# TODO: These should reveal the types of the fields +reveal_type(alice[0]) # revealed: Unknown +reveal_type(alice[1]) # revealed: Unknown +reveal_type(alice[2]) # revealed: Unknown + +# error: [missing-argument] +Person(3) + +# error: [too-many-positional-arguments] +Person(3, "Eve", 99, "extra") + +# error: [invalid-argument-type] +Person(id="3", name="Eve") +``` + +Alternative functional syntax: + +```py +Person2 = NamedTuple("Person", [("id", int), ("name", str)]) +alice2 = Person2(1, "Alice") + +# TODO: should be an error +Person2(1) + +reveal_type(alice2.id) # revealed: @Todo(GenericAlias instance) +reveal_type(alice2.name) # revealed: @Todo(GenericAlias instance) +``` + +### Multiple Inheritance + +Multiple inheritance is not supported for `NamedTuple` classes: + +```py +from typing import NamedTuple + +# This should ideally emit a diagnostic +class C(NamedTuple, object): + id: int + name: str +``` + +### Inheriting from a `NamedTuple` + +Inheriting from a `NamedTuple` is supported, but new fields on the subclass will not be part of the +synthesized `__new__` signature: + +```py +from typing import NamedTuple + +class User(NamedTuple): + id: int + name: str + +class SuperUser(User): + level: int + +# This is fine: +alice = SuperUser(1, "Alice") +reveal_type(alice.level) # revealed: int + +# This is an error because `level` is not part of the signature: +# error: [too-many-positional-arguments] +alice = SuperUser(1, "Alice", 3) +``` + +### Generic named tuples + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import NamedTuple + +class Property[T](NamedTuple): + name: str + value: T + +# TODO: this should be supported (no error, revealed type of `Property[float]`) +# error: [invalid-argument-type] +reveal_type(Property("height", 3.4)) # revealed: Property[Unknown] +``` + +## `collections.namedtuple` + +```py +from collections import namedtuple + +Person = namedtuple("Person", ["id", "name", "age"], defaults=[None]) + +alice = Person(1, "Alice", 42) +bob = Person(2, "Bob") +``` diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 912d3946ac2c68..4c322a2031c324 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -101,6 +101,42 @@ fn inheritance_cycle_initial<'db>( None } +/// A category of classes with code generation capabilities (with synthesized methods). +#[derive(Clone, Copy, Debug, PartialEq)] +enum CodeGeneratorKind { + /// Classes decorated with `@dataclass` or similar dataclass-like decorators + DataclassLike, + /// Classes inheriting from `typing.NamedTuple` + NamedTuple, +} + +impl CodeGeneratorKind { + fn from_class(db: &dyn Db, class: ClassLiteral<'_>) -> Option { + if CodeGeneratorKind::DataclassLike.matches(db, class) { + Some(CodeGeneratorKind::DataclassLike) + } else if CodeGeneratorKind::NamedTuple.matches(db, class) { + Some(CodeGeneratorKind::NamedTuple) + } else { + None + } + } + + fn matches<'db>(self, db: &'db dyn Db, class: ClassLiteral<'db>) -> bool { + match self { + Self::DataclassLike => { + class.dataclass_params(db).is_some() + || class + .try_metaclass(db) + .is_ok_and(|(_, transformer_params)| transformer_params.is_some()) + } + Self::NamedTuple => class.explicit_bases(db).iter().any(|base| { + base.into_class_literal() + .is_some_and(|c| c.is_known(db, KnownClass::NamedTuple)) + }), + } + } +} + /// A specialization of a generic class with a particular assignment of types to typevars. #[salsa::interned(debug)] pub struct GenericAlias<'db> { @@ -986,106 +1022,108 @@ impl<'db> ClassLiteral<'db> { }); if symbol.symbol.is_unbound() { - if let Some(dataclass_member) = self.own_dataclass_member(db, specialization, name) { - return Symbol::bound(dataclass_member).into(); + if let Some(synthesized_member) = self.own_synthesized_member(db, specialization, name) + { + return Symbol::bound(synthesized_member).into(); } } symbol } - /// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`. - fn own_dataclass_member( + /// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or + /// a synthesized `__new__` method for a `NamedTuple`. + fn own_synthesized_member( self, db: &'db dyn Db, specialization: Option>, name: &str, ) -> Option> { - let params = self.dataclass_params(db); - let has_dataclass_param = |param| params.is_some_and(|params| params.contains(param)); + let dataclass_params = self.dataclass_params(db); + let has_dataclass_param = + |param| dataclass_params.is_some_and(|params| params.contains(param)); - match name { - "__init__" => { - let has_synthesized_dunder_init = has_dataclass_param(DataclassParams::INIT) - || self - .try_metaclass(db) - .is_ok_and(|(_, transformer_params)| transformer_params.is_some()); + let field_policy = CodeGeneratorKind::from_class(db, self)?; - if !has_synthesized_dunder_init { - return None; - } - - let mut parameters = vec![]; - - for (name, (mut attr_ty, mut default_ty)) in - self.dataclass_fields(db, specialization) - { - // The descriptor handling below is guarded by this fully-static check, because dynamic - // types like `Any` are valid (data) descriptors: since they have all possible attributes, - // they also have a (callable) `__set__` method. The problem is that we can't determine - // the type of the value parameter this way. Instead, we want to use the dynamic type - // itself in this case, so we skip the special descriptor handling. - if attr_ty.is_fully_static(db) { - let dunder_set = attr_ty.class_member(db, "__set__".into()); - if let Some(dunder_set) = dunder_set.symbol.ignore_possibly_unbound() { - // This type of this attribute is a data descriptor. Instead of overwriting the - // descriptor attribute, data-classes will (implicitly) call the `__set__` method - // of the descriptor. This means that the synthesized `__init__` parameter for - // this attribute is determined by possible `value` parameter types with which - // the `__set__` method can be called. We build a union of all possible options - // to account for possible overloads. - let mut value_types = UnionBuilder::new(db); - for signature in &dunder_set.signatures(db) { - for overload in signature { - if let Some(value_param) = - overload.parameters().get_positional(2) - { - value_types = value_types.add( - value_param - .annotated_type() - .unwrap_or_else(Type::unknown), - ); - } else if overload.parameters().is_gradual() { - value_types = value_types.add(Type::unknown()); - } + let signature_from_fields = |mut parameters: Vec<_>| { + for (name, (mut attr_ty, mut default_ty)) in + self.fields(db, specialization, field_policy) + { + // The descriptor handling below is guarded by this fully-static check, because dynamic + // types like `Any` are valid (data) descriptors: since they have all possible attributes, + // they also have a (callable) `__set__` method. The problem is that we can't determine + // the type of the value parameter this way. Instead, we want to use the dynamic type + // itself in this case, so we skip the special descriptor handling. + if attr_ty.is_fully_static(db) { + let dunder_set = attr_ty.class_member(db, "__set__".into()); + if let Some(dunder_set) = dunder_set.symbol.ignore_possibly_unbound() { + // This type of this attribute is a data descriptor. Instead of overwriting the + // descriptor attribute, data-classes will (implicitly) call the `__set__` method + // of the descriptor. This means that the synthesized `__init__` parameter for + // this attribute is determined by possible `value` parameter types with which + // the `__set__` method can be called. We build a union of all possible options + // to account for possible overloads. + let mut value_types = UnionBuilder::new(db); + for signature in &dunder_set.signatures(db) { + for overload in signature { + if let Some(value_param) = overload.parameters().get_positional(2) { + value_types = value_types.add( + value_param.annotated_type().unwrap_or_else(Type::unknown), + ); + } else if overload.parameters().is_gradual() { + value_types = value_types.add(Type::unknown()); } } - attr_ty = value_types.build(); - - // The default value of the attribute is *not* determined by the right hand side - // of the class-body assignment. Instead, the runtime invokes `__get__` on the - // descriptor, as if it had been called on the class itself, i.e. it passes `None` - // for the `instance` argument. - - if let Some(ref mut default_ty) = default_ty { - *default_ty = default_ty - .try_call_dunder_get( - db, - Type::none(db), - Type::ClassLiteral(self), - ) - .map(|(return_ty, _)| return_ty) - .unwrap_or_else(Type::unknown); - } + } + attr_ty = value_types.build(); + + // The default value of the attribute is *not* determined by the right hand side + // of the class-body assignment. Instead, the runtime invokes `__get__` on the + // descriptor, as if it had been called on the class itself, i.e. it passes `None` + // for the `instance` argument. + + if let Some(ref mut default_ty) = default_ty { + *default_ty = default_ty + .try_call_dunder_get(db, Type::none(db), Type::ClassLiteral(self)) + .map(|(return_ty, _)| return_ty) + .unwrap_or_else(Type::unknown); } } + } - let mut parameter = - Parameter::positional_or_keyword(name).with_annotated_type(attr_ty); + let mut parameter = + Parameter::positional_or_keyword(name).with_annotated_type(attr_ty); - if let Some(default_ty) = default_ty { - parameter = parameter.with_default_type(default_ty); - } - - parameters.push(parameter); + if let Some(default_ty) = default_ty { + parameter = parameter.with_default_type(default_ty); } - let init_signature = - Signature::new(Parameters::new(parameters), Some(Type::none(db))); + parameters.push(parameter); + } + + let signature = Signature::new(Parameters::new(parameters), Some(Type::none(db))); + Some(Type::Callable(CallableType::single(db, signature))) + }; + + match (field_policy, name) { + (CodeGeneratorKind::DataclassLike, "__init__") => { + let has_synthesized_dunder_init = has_dataclass_param(DataclassParams::INIT) + || self + .try_metaclass(db) + .is_ok_and(|(_, transformer_params)| transformer_params.is_some()); - Some(Type::Callable(CallableType::single(db, init_signature))) + if !has_synthesized_dunder_init { + return None; + } + + signature_from_fields(vec![]) + } + (CodeGeneratorKind::NamedTuple, "__new__") => { + let cls_parameter = Parameter::positional_or_keyword(Name::new_static("cls")) + .with_annotated_type(KnownClass::Type.to_instance(db)); + signature_from_fields(vec![cls_parameter]) } - "__lt__" | "__le__" | "__gt__" | "__ge__" => { + (CodeGeneratorKind::DataclassLike, "__lt__" | "__le__" | "__gt__" | "__ge__") => { if !has_dataclass_param(DataclassParams::ORDER) { return None; } @@ -1106,27 +1144,27 @@ impl<'db> ClassLiteral<'db> { } } - fn is_dataclass(self, db: &'db dyn Db) -> bool { - self.dataclass_params(db).is_some() - || self - .try_metaclass(db) - .is_ok_and(|(_, transformer_params)| transformer_params.is_some()) - } - /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. /// - /// See [`ClassLiteral::own_dataclass_fields`] for more details. - fn dataclass_fields( + /// See [`ClassLiteral::own_fields`] for more details. + fn fields( self, db: &'db dyn Db, specialization: Option>, + field_policy: CodeGeneratorKind, ) -> FxOrderMap, Option>)> { - let dataclasses_in_mro: Vec<_> = self + if field_policy == CodeGeneratorKind::NamedTuple { + // NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the + // fields of this class only. + return self.own_fields(db); + } + + let matching_classes_in_mro: Vec<_> = self .iter_mro(db, specialization) .filter_map(|superclass| { if let Some(class) = superclass.into_class() { let class_literal = class.class_literal(db).0; - if class_literal.is_dataclass(db) { + if field_policy.matches(db, class_literal) { Some(class_literal) } else { None @@ -1138,10 +1176,10 @@ impl<'db> ClassLiteral<'db> { // We need to collect into a `Vec` here because we iterate the MRO in reverse order .collect(); - dataclasses_in_mro + matching_classes_in_mro .into_iter() .rev() - .flat_map(|class| class.own_dataclass_fields(db)) + .flat_map(|class| class.own_fields(db)) // We collect into a FxOrderMap here to deduplicate attributes .collect() } @@ -1157,10 +1195,7 @@ impl<'db> ClassLiteral<'db> { /// y: str = "a" /// ``` /// we return a map `{"x": (int, None), "y": (str, Some(Literal["a"]))}`. - fn own_dataclass_fields( - self, - db: &'db dyn Db, - ) -> FxOrderMap, Option>)> { + fn own_fields(self, db: &'db dyn Db) -> FxOrderMap, Option>)> { let mut attributes = FxOrderMap::default(); let class_body_scope = self.body_scope(db); @@ -1925,6 +1960,7 @@ pub enum KnownClass { TypeVarTuple, TypeAliasType, NoDefaultType, + NamedTuple, NewType, SupportsIndex, // Collections @@ -2011,6 +2047,8 @@ impl<'db> KnownClass { | Self::Float | Self::Enum | Self::ABCMeta + // Empty tuples are AlwaysFalse; non-empty tuples are AlwaysTrue + | Self::NamedTuple // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 // and raises a `TypeError` in Python >=3.14 // (see https://docs.python.org/3/library/constants.html#NotImplemented) @@ -2071,6 +2109,7 @@ impl<'db> KnownClass { | Self::TypeVarTuple | Self::TypeAliasType | Self::NoDefaultType + | Self::NamedTuple | Self::NewType | Self::ChainMap | Self::Counter @@ -2118,6 +2157,7 @@ impl<'db> KnownClass { Self::UnionType => "UnionType", Self::MethodWrapperType => "MethodWrapperType", Self::WrapperDescriptorType => "WrapperDescriptorType", + Self::NamedTuple => "NamedTuple", Self::NoneType => "NoneType", Self::SpecialForm => "_SpecialForm", Self::TypeVar => "TypeVar", @@ -2305,6 +2345,7 @@ impl<'db> KnownClass { Self::Any | Self::SpecialForm | Self::TypeVar + | Self::NamedTuple | Self::StdlibAlias | Self::SupportsIndex => KnownModule::Typing, Self::TypeAliasType @@ -2397,6 +2438,7 @@ impl<'db> KnownClass { | Self::Enum | Self::ABCMeta | Self::Super + | Self::NamedTuple | Self::NewType => false, } } @@ -2457,6 +2499,7 @@ impl<'db> KnownClass { | Self::ABCMeta | Self::Super | Self::UnionType + | Self::NamedTuple | Self::NewType => false, } } @@ -2498,6 +2541,7 @@ impl<'db> KnownClass { "UnionType" => Self::UnionType, "MethodWrapperType" => Self::MethodWrapperType, "WrapperDescriptorType" => Self::WrapperDescriptorType, + "NamedTuple" => Self::NamedTuple, "NewType" => Self::NewType, "TypeAliasType" => Self::TypeAliasType, "TypeVar" => Self::TypeVar, @@ -2586,6 +2630,7 @@ impl<'db> KnownClass { | Self::ParamSpecArgs | Self::ParamSpecKwargs | Self::TypeVarTuple + | Self::NamedTuple | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), } } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 50ab0a9d78c2a5..03b627233dbd8f 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -72,11 +72,15 @@ impl<'db> ClassBase<'db> { pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { match ty { Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), - Type::ClassLiteral(literal) => Some(if literal.is_known(db, KnownClass::Any) { - Self::Dynamic(DynamicType::Any) - } else { - Self::Class(literal.default_specialization(db)) - }), + Type::ClassLiteral(literal) => { + if literal.is_known(db, KnownClass::Any) { + Some(Self::Dynamic(DynamicType::Any)) + } else if literal.is_known(db, KnownClass::NamedTuple) { + Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db)) + } else { + Some(Self::Class(literal.default_specialization(db))) + } + } Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), Type::NominalInstance(instance) if instance.class().is_known(db, KnownClass::GenericAlias) => diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs index 6dab343b05e1dc..e0b0da246ba744 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -59,22 +59,13 @@ type KeyDiagnosticFields = ( Severity, ); -static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[ - ( - DiagnosticId::lint("no-matching-overload"), - Some("/src/tomllib/_parser.py"), - Some(2329..2358), - "No overload of bound method `__init__` matches arguments", - Severity::Error, - ), - ( - DiagnosticId::lint("unused-ignore-comment"), - Some("/src/tomllib/_parser.py"), - Some(22299..22333), - "Unused blanket `type: ignore` directive", - Severity::Warning, - ), -]; +static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[( + DiagnosticId::lint("unused-ignore-comment"), + Some("/src/tomllib/_parser.py"), + Some(22299..22333), + "Unused blanket `type: ignore` directive", + Severity::Warning, +)]; fn tomllib_path(file: &TestFile) -> SystemPathBuf { SystemPathBuf::from("src").join(file.name()) From e17e1e860b2f075d0aab31754d1a9e1a38d42144 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 07:57:03 +0200 Subject: [PATCH 0205/1161] Sync vendored typeshed stubs (#17753) Co-authored-by: typeshedbot <> --- .../vendor/typeshed/source_commit.txt | 2 +- .../stdlib/_frozen_importlib_external.pyi | 12 +++++++++- .../vendor/typeshed/stdlib/ast.pyi | 4 ++++ .../vendor/typeshed/stdlib/contextlib.pyi | 2 +- .../typeshed/stdlib/curses/__init__.pyi | 5 ----- .../stdlib/email/_header_value_parser.pyi | 4 ++-- .../typeshed/stdlib/email/_policybase.pyi | 6 ++--- .../vendor/typeshed/stdlib/email/errors.pyi | 2 +- .../vendor/typeshed/stdlib/email/policy.pyi | 6 ++--- .../vendor/typeshed/stdlib/email/utils.pyi | 4 ++-- .../vendor/typeshed/stdlib/enum.pyi | 2 ++ .../vendor/typeshed/stdlib/importlib/abc.pyi | 3 +++ .../vendor/typeshed/stdlib/pydoc.pyi | 11 ++++++++-- .../vendor/typeshed/stdlib/sys/__init__.pyi | 6 ++--- .../vendor/typeshed/stdlib/tarfile.pyi | 13 +++++++++-- .../vendor/typeshed/stdlib/threading.pyi | 22 +++++++++++++------ .../typeshed/stdlib/tkinter/__init__.pyi | 1 + .../vendor/typeshed/stdlib/unittest/mock.pyi | 10 ++++----- 18 files changed, 76 insertions(+), 39 deletions(-) diff --git a/crates/red_knot_vendored/vendor/typeshed/source_commit.txt b/crates/red_knot_vendored/vendor/typeshed/source_commit.txt index 1c8127c8978907..b3112953f24d5c 100644 --- a/crates/red_knot_vendored/vendor/typeshed/source_commit.txt +++ b/crates/red_knot_vendored/vendor/typeshed/source_commit.txt @@ -1 +1 @@ -f65bdc1acde54fda93c802459280da74518d2eef +eec809d049d10a5ae9b88780eab15fe36a9768d7 diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi index 386cf20808e4f2..edad50a8d85838 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi @@ -36,7 +36,10 @@ def spec_from_file_location( loader: LoaderProtocol | None = None, submodule_search_locations: list[str] | None = ..., ) -> importlib.machinery.ModuleSpec | None: ... - +@deprecated( + "Deprecated as of Python 3.6: Use site configuration instead. " + "Future versions of Python may not enable this finder by default." +) class WindowsRegistryFinder(importlib.abc.MetaPathFinder): if sys.version_info < (3, 12): @classmethod @@ -118,6 +121,13 @@ class FileLoader: class SourceFileLoader(importlib.abc.FileLoader, FileLoader, importlib.abc.SourceLoader, SourceLoader): # type: ignore[misc] # incompatible method arguments in base classes def set_data(self, path: str, data: ReadableBuffer, *, _mode: int = 0o666) -> None: ... def path_stats(self, path: str) -> Mapping[str, Any]: ... + def source_to_code( # type: ignore[override] # incompatible with InspectLoader.source_to_code + self, + data: ReadableBuffer | str | _ast.Module | _ast.Expression | _ast.Interactive, + path: ReadableBuffer | StrPath, + *, + _optimize: int = -1, + ) -> types.CodeType: ... class SourcelessFileLoader(importlib.abc.FileLoader, FileLoader, _LoaderBasics): def get_code(self, fullname: str) -> types.CodeType | None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ast.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/ast.pyi index 90c6d2ff0e68d8..1a3d3e97d11e2d 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/ast.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/ast.pyi @@ -1893,8 +1893,12 @@ if sys.version_info >= (3, 14): def compare(left: AST, right: AST, /, *, compare_attributes: bool = False) -> bool: ... class NodeVisitor: + # All visit methods below can be overwritten by subclasses and return an + # arbitrary value, which is passed to the caller. def visit(self, node: AST) -> Any: ... def generic_visit(self, node: AST) -> Any: ... + # The following visit methods are not defined on NodeVisitor, but can + # be implemented by subclasses and are called during a visit if defined. def visit_Module(self, node: Module) -> Any: ... def visit_Interactive(self, node: Interactive) -> Any: ... def visit_Expression(self, node: Expression) -> Any: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/contextlib.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/contextlib.pyi index 70d0dbdcb2f106..4663b448c79c89 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/contextlib.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/contextlib.pyi @@ -179,7 +179,7 @@ class AsyncExitStack(_BaseExitStack[_ExitT_co], metaclass=abc.ABCMeta): async def __aenter__(self) -> Self: ... async def __aexit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / - ) -> bool: ... + ) -> _ExitT_co: ... if sys.version_info >= (3, 10): class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/__init__.pyi index edc64a00cd39f4..5c157fd7c2f610 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/__init__.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/__init__.pyi @@ -23,11 +23,6 @@ COLOR_PAIRS: int def wrapper(func: Callable[Concatenate[window, _P], _T], /, *arg: _P.args, **kwds: _P.kwargs) -> _T: ... -# typeshed used the name _CursesWindow for the underlying C class before -# it was mapped to the name 'window' in 3.8. -# Kept here as a legacy alias in case any third-party code is relying on it. -_CursesWindow = window - # At runtime this class is unexposed and calls itself curses.ncurses_version. # That name would conflict with the actual curses.ncurses_version, which is # an instance of this class. diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi index f4e9ca68d6a99e..a8abfead921722 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi @@ -17,9 +17,9 @@ TOKEN_ENDS: Final[set[str]] ASPECIALS: Final[set[str]] ATTRIBUTE_ENDS: Final[set[str]] EXTENDED_ATTRIBUTE_ENDS: Final[set[str]] -# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 NLSET: Final[set[str]] -# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 SPECIALSNL: Final[set[str]] if sys.version_info >= (3, 10): diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_policybase.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_policybase.pyi index f5dbbd96da1479..5266609e597fb1 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_policybase.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_policybase.pyi @@ -23,7 +23,7 @@ class _PolicyBase(Generic[_MessageT]): raise_on_defect: bool mangle_from_: bool message_factory: _MessageFactory[_MessageT] | None - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 verify_generated_headers: bool def __init__( @@ -35,7 +35,7 @@ class _PolicyBase(Generic[_MessageT]): raise_on_defect: bool = False, mangle_from_: bool = ..., # default depends on sub-class message_factory: _MessageFactory[_MessageT] | None = None, - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 verify_generated_headers: bool = True, ) -> None: ... def clone( @@ -47,7 +47,7 @@ class _PolicyBase(Generic[_MessageT]): raise_on_defect: bool = ..., mangle_from_: bool = ..., message_factory: _MessageFactory[_MessageT] | None = ..., - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 verify_generated_headers: bool = ..., ) -> Self: ... def __add__(self, other: Policy) -> Self: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/errors.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/errors.pyi index f105576c5ee49c..b501a58665560c 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/errors.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/errors.pyi @@ -7,7 +7,7 @@ class BoundaryError(MessageParseError): ... class MultipartConversionError(MessageError, TypeError): ... class CharsetError(MessageError): ... -# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 class HeaderWriteError(MessageError): ... class MessageDefect(ValueError): diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/policy.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/policy.pyi index 5b145bcf231804..5005483edf868b 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/policy.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/policy.pyi @@ -24,7 +24,7 @@ class EmailPolicy(Policy[_MessageT]): raise_on_defect: bool = ..., mangle_from_: bool = ..., message_factory: None = None, - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 verify_generated_headers: bool = ..., utf8: bool = ..., refold_source: str = ..., @@ -41,7 +41,7 @@ class EmailPolicy(Policy[_MessageT]): raise_on_defect: bool = ..., mangle_from_: bool = ..., message_factory: _MessageFactory[_MessageT] | None = ..., - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 verify_generated_headers: bool = ..., utf8: bool = ..., refold_source: str = ..., @@ -62,7 +62,7 @@ class EmailPolicy(Policy[_MessageT]): raise_on_defect: bool = ..., mangle_from_: bool = ..., message_factory: _MessageFactory[_MessageT] | None = ..., - # Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 + # Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 verify_generated_headers: bool = ..., utf8: bool = ..., refold_source: str = ..., diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/utils.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/utils.pyi index dc3eecb5ef7fb7..efc32a7abce292 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/utils.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/email/utils.pyi @@ -30,11 +30,11 @@ _PDTZ: TypeAlias = tuple[int, int, int, int, int, int, int, int, int, int | None def quote(str: str) -> str: ... def unquote(str: str) -> str: ... -# `strict` parameter added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# `strict` parameter added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 def parseaddr(addr: str | list[str], *, strict: bool = True) -> tuple[str, str]: ... def formataddr(pair: tuple[str | None, str], charset: str | Charset = "utf-8") -> str: ... -# `strict` parameter added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5 +# `strict` parameter added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5 def getaddresses(fieldvalues: Iterable[str], *, strict: bool = True) -> list[tuple[str, str]]: ... @overload def parsedate(data: None) -> None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/enum.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/enum.pyi index 8c88b26a3a2fa7..26f19886711328 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/enum.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/enum.pyi @@ -299,6 +299,7 @@ if sys.version_info >= (3, 11): def __or__(self, other: int) -> Self: ... def __and__(self, other: int) -> Self: ... def __xor__(self, other: int) -> Self: ... + def __invert__(self) -> Self: ... __ror__ = __or__ __rand__ = __and__ __rxor__ = __xor__ @@ -309,6 +310,7 @@ else: def __or__(self, other: int) -> Self: ... def __and__(self, other: int) -> Self: ... def __xor__(self, other: int) -> Self: ... + def __invert__(self) -> Self: ... __ror__ = __or__ __rand__ = __and__ __rxor__ = __xor__ diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/abc.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/abc.pyi index 8a106b3a64d7ff..3016a3a43b36ad 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/abc.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/abc.pyi @@ -8,6 +8,7 @@ from importlib import _bootstrap_external from importlib.machinery import ModuleSpec from io import BufferedReader from typing import IO, Any, Literal, Protocol, overload, runtime_checkable +from typing_extensions import deprecated if sys.version_info >= (3, 11): __all__ = [ @@ -38,6 +39,7 @@ else: if sys.version_info < (3, 12): class Finder(metaclass=ABCMeta): ... +@deprecated("Deprecated as of Python 3.7: Use importlib.resources.abc.TraversableResources instead.") class ResourceLoader(Loader): @abstractmethod def get_data(self, path: str) -> bytes: ... @@ -58,6 +60,7 @@ class ExecutionLoader(InspectLoader): def get_filename(self, fullname: str) -> str: ... class SourceLoader(_bootstrap_external.SourceLoader, ResourceLoader, ExecutionLoader, metaclass=ABCMeta): # type: ignore[misc] # incompatible definitions of source_to_code in the base classes + @deprecated("Deprecated as of Python 3.3: Use importlib.resources.abc.SourceLoader.path_stats instead.") def path_mtime(self, path: str) -> float: ... def set_data(self, path: str, data: bytes) -> None: ... def get_source(self, fullname: str) -> str | None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc.pyi index 144f782acad575..f14b9d1bb69989 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc.pyi @@ -6,7 +6,7 @@ from collections.abc import Callable, Container, Mapping, MutableMapping from reprlib import Repr from types import MethodType, ModuleType, TracebackType from typing import IO, Any, AnyStr, Final, NoReturn, Protocol, TypeVar -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, deprecated __all__ = ["help"] @@ -31,7 +31,14 @@ def stripid(text: str) -> str: ... def allmethods(cl: type) -> MutableMapping[str, MethodType]: ... def visiblename(name: str, all: Container[str] | None = None, obj: object = None) -> bool: ... def classify_class_attrs(object: object) -> list[tuple[str, str, type, str]]: ... -def ispackage(path: str) -> bool: ... + +if sys.version_info >= (3, 13): + @deprecated("Deprecated in Python 3.13.") + def ispackage(path: str) -> bool: ... + +else: + def ispackage(path: str) -> bool: ... + def source_synopsis(file: IO[AnyStr]) -> AnyStr | None: ... def synopsis(filename: str, cache: MutableMapping[str, tuple[int, str]] = {}) -> str | None: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sys/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/sys/__init__.pyi index a2cca3509a9ce0..2d894674c4af67 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/sys/__init__.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/sys/__init__.pyi @@ -96,7 +96,7 @@ flags: _flags # This can be re-visited when typeshed drops support for 3.10, # at which point all supported versions will include int_max_str_digits # in all patch versions. -# 3.8 and 3.9 are 15 or 16-tuple +# 3.9 is 15 or 16-tuple # 3.10 is 16 or 17-tuple # 3.11+ is an 18-tuple. @final @@ -184,7 +184,7 @@ class _flags(_UninstantiableStructseq, tuple[int, ...]): # Whether or not this exists on lower versions of Python # may depend on which patch release you're using # (it was backported to all Python versions on 3.8+ as a security fix) - # Added in: 3.8.14, 3.9.14, 3.10.7 + # Added in: 3.9.14, 3.10.7 # and present in all versions of 3.11 and later. @property def int_max_str_digits(self) -> int: ... @@ -448,7 +448,7 @@ if sys.platform == "win32": def get_coroutine_origin_tracking_depth() -> int: ... def set_coroutine_origin_tracking_depth(depth: int) -> None: ... -# The following two functions were added in 3.11.0, 3.10.7, 3.9.14, and 3.8.14, +# The following two functions were added in 3.11.0, 3.10.7, and 3.9.14, # as part of the response to CVE-2020-10735 def set_int_max_str_digits(maxdigits: int) -> None: ... def get_int_max_str_digits() -> int: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tarfile.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/tarfile.pyi index cd31b101c886b8..31094f87872dc5 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tarfile.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/tarfile.pyi @@ -7,7 +7,7 @@ from collections.abc import Callable, Iterable, Iterator, Mapping from gzip import _ReadableFileobj as _GzipReadableFileobj, _WritableFileobj as _GzipWritableFileobj from types import TracebackType from typing import IO, ClassVar, Literal, Protocol, overload -from typing_extensions import Self, TypeAlias +from typing_extensions import Self, TypeAlias, deprecated __all__ = [ "TarFile", @@ -622,7 +622,6 @@ class TarInfo: offset: int offset_data: int sparse: bytes | None - tarfile: TarFile | None mode: int type: bytes linkname: str @@ -632,6 +631,16 @@ class TarInfo: gname: str pax_headers: Mapping[str, str] def __init__(self, name: str = "") -> None: ... + if sys.version_info >= (3, 13): + @property + @deprecated("Deprecated in Python 3.13; removal scheduled for Python 3.16") + def tarfile(self) -> TarFile | None: ... + @tarfile.setter + @deprecated("Deprecated in Python 3.13; removal scheduled for Python 3.16") + def tarfile(self, tarfile: TarFile | None) -> None: ... + else: + tarfile: TarFile | None + @classmethod def frombuf(cls, buf: bytes | bytearray, encoding: str, errors: str) -> Self: ... @classmethod diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/threading.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/threading.pyi index e3965fab0e803f..99f5c8d2a516da 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/threading.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/threading.pyi @@ -5,6 +5,7 @@ from _typeshed import ProfileFunction, TraceFunction from collections.abc import Callable, Iterable, Mapping from types import TracebackType from typing import Any, TypeVar, final +from typing_extensions import deprecated _T = TypeVar("_T") @@ -44,9 +45,11 @@ if sys.version_info >= (3, 12): _profile_hook: ProfileFunction | None def active_count() -> int: ... -def activeCount() -> int: ... # deprecated alias for active_count() +@deprecated("Use active_count() instead") +def activeCount() -> int: ... def current_thread() -> Thread: ... -def currentThread() -> Thread: ... # deprecated alias for current_thread() +@deprecated("Use current_thread() instead") +def currentThread() -> Thread: ... def get_ident() -> int: ... def enumerate() -> list[Thread]: ... def main_thread() -> Thread: ... @@ -89,11 +92,14 @@ class Thread: @property def native_id(self) -> int | None: ... # only available on some platforms def is_alive(self) -> bool: ... - # the following methods are all deprecated - def getName(self) -> str: ... - def setName(self, name: str) -> None: ... + @deprecated("Get the daemon attribute instead") def isDaemon(self) -> bool: ... + @deprecated("Set the daemon attribute instead") def setDaemon(self, daemonic: bool) -> None: ... + @deprecated("Use the name attribute instead") + def getName(self) -> str: ... + @deprecated("Use the name attribute instead") + def setName(self, name: str) -> None: ... class _DummyThread(Thread): def __init__(self) -> None: ... @@ -124,7 +130,8 @@ class Condition: def wait_for(self, predicate: Callable[[], _T], timeout: float | None = None) -> _T: ... def notify(self, n: int = 1) -> None: ... def notify_all(self) -> None: ... - def notifyAll(self) -> None: ... # deprecated alias for notify_all() + @deprecated("Use notify_all() instead") + def notifyAll(self) -> None: ... class Semaphore: _value: int @@ -138,7 +145,8 @@ class BoundedSemaphore(Semaphore): ... class Event: def is_set(self) -> bool: ... - def isSet(self) -> bool: ... # deprecated alias for is_set() + @deprecated("Use is_set() instead") + def isSet(self) -> bool: ... def set(self) -> None: ... def clear(self) -> None: ... def wait(self, timeout: float | None = None) -> bool: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi index 291e2fc5108f68..dcac61d77e0a91 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi @@ -977,6 +977,7 @@ class Tk(Misc, Wm): sync: bool = False, use: str | None = None, ) -> None: ... + # Keep this in sync with ttktheme.ThemedTk. See issue #13858 @overload def configure( self, diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/mock.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/mock.pyi index d2664465097f30..9e353900f2d7f4 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/mock.pyi +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/mock.pyi @@ -1,4 +1,5 @@ import sys +from _typeshed import MaybeNone from collections.abc import Awaitable, Callable, Coroutine, Iterable, Mapping, Sequence from contextlib import _GeneratorContextManager from types import TracebackType @@ -69,16 +70,13 @@ _CallValue: TypeAlias = str | tuple[Any, ...] | Mapping[str, Any] | _ArgsKwargs class _Call(tuple[Any, ...]): def __new__( - cls, value: _CallValue = (), name: str | None = "", parent: Any | None = None, two: bool = False, from_kall: bool = True + cls, value: _CallValue = (), name: str | None = "", parent: _Call | None = None, two: bool = False, from_kall: bool = True ) -> Self: ... - name: Any - parent: Any - from_kall: Any def __init__( self, value: _CallValue = (), name: str | None = None, - parent: Any | None = None, + parent: _Call | None = None, two: bool = False, from_kall: bool = True, ) -> None: ... @@ -162,7 +160,7 @@ class NonCallableMock(Base, Any): side_effect: Any called: bool call_count: int - call_args: Any + call_args: _Call | MaybeNone call_args_list: _CallList mock_calls: _CallList def _format_mock_call_signature(self, args: Any, kwargs: Any) -> str: ... From 67ef3707339ec29dc6d10d09a3daae4ea9760f18 Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Thu, 1 May 2025 03:16:28 -0300 Subject: [PATCH 0206/1161] [`flake8-use-pathlib`] Fix `PTH116` false positive when `stat` is passed a file descriptor (#17709) Co-authored-by: Dhruv Manilawala --- .../test/fixtures/flake8_use_pathlib/full_name.py | 14 ++++++++++++++ .../rules/replaceable_by_pathlib.rs | 11 ++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py index 8680212b20c3aa..bd00f5193e3e83 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py @@ -56,6 +56,7 @@ def opener(path, flags): def foo(y: int): open(y) + # https://github.com/astral-sh/ruff/issues/17691 def f() -> int: return 1 @@ -68,3 +69,16 @@ def f() -> int: def bytes_str_func() -> bytes: return b"foo" open(bytes_str_func()) + +# https://github.com/astral-sh/ruff/issues/17693 +os.stat(1) +os.stat(x) + + +def func() -> int: + return 2 +os.stat(func()) + + +def bar(x: int): + os.stat(x) diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index a4bce419397001..dd47bb12d0a27c 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -55,7 +55,16 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { // PTH114 ["os", "path", "islink"] => OsPathIslink.into(), // PTH116 - ["os", "stat"] => OsStat.into(), + ["os", "stat"] => { + if call + .arguments + .find_positional(0) + .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) + { + return; + } + OsStat.into() + } // PTH117 ["os", "path", "isabs"] => OsPathIsabs.into(), // PTH118 From 9c57862262558c08dae8b4e488444cac611f45cf Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 1 May 2025 08:51:53 +0200 Subject: [PATCH 0207/1161] [red-knot] Cache source type during semanic index building (#17756) Co-authored-by: Alex Waygood --- .../red_knot_python_semantic/src/semantic_index/builder.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index a633c2c5cfc5d1..75c7494b4327fe 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -10,7 +10,7 @@ use ruff_db::source::{source_text, SourceText}; use ruff_index::IndexVec; use ruff_python_ast::name::Name; use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor}; -use ruff_python_ast::{self as ast, PythonVersion}; +use ruff_python_ast::{self as ast, PySourceType, PythonVersion}; use ruff_python_parser::semantic_errors::{ SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, }; @@ -75,6 +75,7 @@ pub(super) struct SemanticIndexBuilder<'db> { // Builder state db: &'db dyn Db, file: File, + source_type: PySourceType, module: &'db ParsedModule, scope_stack: Vec, /// The assignments we're currently visiting, with @@ -118,6 +119,7 @@ impl<'db> SemanticIndexBuilder<'db> { let mut builder = Self { db, file, + source_type: file.source_type(db.upcast()), module: parsed, scope_stack: Vec::new(), current_assignments: vec![], @@ -445,7 +447,7 @@ impl<'db> SemanticIndexBuilder<'db> { #[allow(unsafe_code)] // SAFETY: `definition_node` is guaranteed to be a child of `self.module` let kind = unsafe { definition_node.into_owned(self.module.clone()) }; - let category = kind.category(self.file.is_stub(self.db.upcast())); + let category = kind.category(self.source_type.is_stub()); let is_reexported = kind.is_reexported(); let definition = Definition::new( From b7e69ecbfc5d3455e309432cc2d1da34973e739b Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 1 May 2025 09:25:48 +0200 Subject: [PATCH 0208/1161] [red-knot] Increase durability of read-only `File` fields (#17757) --- crates/ruff_db/src/files.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/ruff_db/src/files.rs b/crates/ruff_db/src/files.rs index 74a058f57ddfbc..6e0061b2f33813 100644 --- a/crates/ruff_db/src/files.rs +++ b/crates/ruff_db/src/files.rs @@ -94,7 +94,9 @@ impl Files { .root(db, path) .map_or(Durability::default(), |root| root.durability(db)); - let builder = File::builder(FilePath::System(absolute)).durability(durability); + let builder = File::builder(FilePath::System(absolute)) + .durability(durability) + .path_durability(Durability::HIGH); let builder = match metadata { Ok(metadata) if metadata.file_type().is_file() => builder @@ -159,9 +161,11 @@ impl Files { tracing::trace!("Adding virtual file {}", path); let virtual_file = VirtualFile( File::builder(FilePath::SystemVirtual(path.to_path_buf())) + .path_durability(Durability::HIGH) .status(FileStatus::Exists) .revision(FileRevision::zero()) .permissions(None) + .permissions_durability(Durability::HIGH) .new(db), ); self.inner @@ -272,7 +276,7 @@ impl std::panic::RefUnwindSafe for Files {} /// A file that's either stored on the host system's file system or in the vendored file system. #[salsa::input] pub struct File { - /// The path of the file. + /// The path of the file (immutable). #[return_ref] pub path: FilePath, From 76ec64d5357f2db4371c3759d417b890964d5726 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 1 May 2025 16:18:12 +0800 Subject: [PATCH 0209/1161] [`red-knot`] Allow subclasses of Any to be assignable to Callable types (#17717) ## Summary Fixes #17701. ## Test plan New Markdown test. --------- Co-authored-by: David Peter --- .../resources/mdtest/annotations/any.md | 57 ++++++++++++++----- crates/red_knot_python_semantic/src/types.rs | 6 ++ .../src/types/class.rs | 18 +++--- 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md index e0de156eb0bdd5..731b4de2fd9425 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md @@ -46,30 +46,27 @@ def f(): y: Any = "not an Any" # error: [invalid-assignment] ``` -## Subclass +## Subclasses of `Any` The spec allows you to define subclasses of `Any`. -`Subclass` has an unknown superclass, which might be `int`. The assignment to `x` should not be +`SubclassOfAny` has an unknown superclass, which might be `int`. The assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore assignable to `int`. ```py from typing import Any -class Subclass(Any): ... +class SubclassOfAny(Any): ... -reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]] +reveal_type(SubclassOfAny.__mro__) # revealed: tuple[Literal[SubclassOfAny], Any, Literal[object]] -x: Subclass = 1 # error: [invalid-assignment] -y: int = Subclass() - -def _(s: Subclass): - reveal_type(s) # revealed: Subclass +x: SubclassOfAny = 1 # error: [invalid-assignment] +y: int = SubclassOfAny() ``` -`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly -be a subclass of `FinalClass`: +`SubclassOfAny` should not be assignable to a final class though, because `SubclassOfAny` could not +possibly be a subclass of `FinalClass`: ```py from typing import final @@ -77,11 +74,43 @@ from typing import final @final class FinalClass: ... -f: FinalClass = Subclass() # error: [invalid-assignment] +f: FinalClass = SubclassOfAny() # error: [invalid-assignment] + +@final +class OtherFinalClass: ... + +f: FinalClass | OtherFinalClass = SubclassOfAny() # error: [invalid-assignment] +``` + +A subclass of `Any` can also be assigned to arbitrary `Callable` types: + +```py +from typing import Callable, Any + +def takes_callable1(f: Callable): + f() + +takes_callable1(SubclassOfAny()) + +def takes_callable2(f: Callable[[int], None]): + f(1) + +takes_callable2(SubclassOfAny()) +``` + +A subclass of `Any` cannot be assigned to literal types, since those can not be subclassed: + +```py +from typing import Any, Literal + +class MockAny(Any): + pass + +x: Literal[1] = MockAny() # error: [invalid-assignment] ``` -A use case where this comes up is with mocking libraries, where the mock object should be assignable -to any type: +A use case where subclasses of `Any` come up is in mocking libraries, where the mock object should +be assignable to (almost) any type: ```py from unittest.mock import MagicMock diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index a942a4acd942aa..debb4d14eb1a3b 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1477,6 +1477,12 @@ impl<'db> Type<'db> { self_callable.is_assignable_to(db, target_callable) } + (Type::NominalInstance(instance), Type::Callable(_)) + if instance.class().is_subclass_of_any_or_unknown(db) => + { + true + } + (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => { let call_symbol = self.member(db, "__call__").symbol; match call_symbol { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 4c322a2031c324..67a7d0800d7605 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -231,6 +231,16 @@ impl<'db> ClassType<'db> { class_literal.is_final(db) } + /// Is this class a subclass of `Any` or `Unknown`? + pub(crate) fn is_subclass_of_any_or_unknown(self, db: &'db dyn Db) -> bool { + self.iter_mro(db).any(|base| { + matches!( + base, + ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown) + ) + }) + } + /// If `self` and `other` are generic aliases of the same generic class, returns their /// corresponding specializations. fn compatible_specializations( @@ -310,13 +320,7 @@ impl<'db> ClassType<'db> { } } - if self.iter_mro(db).any(|base| { - matches!( - base, - ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown) - ) - }) && !other.is_final(db) - { + if self.is_subclass_of_any_or_unknown(db) && !other.is_final(db) { return true; } From 41f3f21629c62e6ecf3613b4a27b19fe0a06a458 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 1 May 2025 13:32:45 +0100 Subject: [PATCH 0210/1161] Improve messages outputted by py-fuzzer (#17764) --- python/py-fuzzer/fuzz.py | 43 +++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/python/py-fuzzer/fuzz.py b/python/py-fuzzer/fuzz.py index 22d7e41a40feca..ff084e38ab60b5 100644 --- a/python/py-fuzzer/fuzz.py +++ b/python/py-fuzzer/fuzz.py @@ -120,6 +120,8 @@ class FuzzResult: maybe_bug: MinimizedSourceCode | None # The executable we're testing executable: Executable + _: KW_ONLY + only_new_bugs: bool def print_description(self, index: int, num_seeds: int) -> None: """Describe the results of fuzzing the parser with this seed.""" @@ -131,12 +133,16 @@ def print_description(self, index: int, num_seeds: int) -> None: ) print(f"{msg:<60} {progress:>15}", flush=True) + new = "new " if self.only_new_bugs else "" + if self.maybe_bug: match self.executable: case Executable.RUFF: - panic_message = "The following code triggers a parser bug:" + panic_message = f"The following code triggers a {new}parser bug:" case Executable.RED_KNOT: - panic_message = "The following code triggers a red-knot panic:" + panic_message = ( + f"The following code triggers a {new}red-knot panic:" + ) case _ as unreachable: assert_never(unreachable) @@ -153,6 +159,7 @@ def fuzz_code(seed: Seed, args: ResolvedCliArgs) -> FuzzResult: minimizer_callback: Callable[[str], bool] | None = None if args.baseline_executable_path is None: + only_new_bugs = False if contains_bug( code, executable=args.executable, executable_path=args.test_executable_path ): @@ -162,22 +169,24 @@ def fuzz_code(seed: Seed, args: ResolvedCliArgs) -> FuzzResult: executable=args.executable, executable_path=args.test_executable_path, ) - elif contains_new_bug( - code, - executable=args.executable, - test_executable_path=args.test_executable_path, - baseline_executable_path=args.baseline_executable_path, - ): - bug_found = True - minimizer_callback = partial( - contains_new_bug, + else: + only_new_bugs = True + if contains_new_bug( + code, executable=args.executable, test_executable_path=args.test_executable_path, baseline_executable_path=args.baseline_executable_path, - ) + ): + bug_found = True + minimizer_callback = partial( + contains_new_bug, + executable=args.executable, + test_executable_path=args.test_executable_path, + baseline_executable_path=args.baseline_executable_path, + ) if not bug_found: - return FuzzResult(seed, None, args.executable) + return FuzzResult(seed, None, args.executable, only_new_bugs=only_new_bugs) assert minimizer_callback is not None @@ -194,14 +203,15 @@ def fuzz_code(seed: Seed, args: ResolvedCliArgs) -> FuzzResult: else: maybe_bug = MinimizedSourceCode(code) - return FuzzResult(seed, maybe_bug, args.executable) + return FuzzResult(seed, maybe_bug, args.executable, only_new_bugs=only_new_bugs) def run_fuzzer_concurrently(args: ResolvedCliArgs) -> list[FuzzResult]: num_seeds = len(args.seeds) print( f"Concurrently running the fuzzer on " - f"{num_seeds} randomly generated source-code files..." + f"{num_seeds} randomly generated source-code " + f"file{'s' if num_seeds != 1 else ''}..." ) bugs: list[FuzzResult] = [] with concurrent.futures.ProcessPoolExecutor() as executor: @@ -229,7 +239,8 @@ def run_fuzzer_sequentially(args: ResolvedCliArgs) -> list[FuzzResult]: num_seeds = len(args.seeds) print( f"Sequentially running the fuzzer on " - f"{num_seeds} randomly generated source-code files..." + f"{num_seeds} randomly generated source-code " + f"file{'s' if num_seeds != 1 else ''}..." ) bugs: list[FuzzResult] = [] for i, seed in enumerate(args.seeds, start=1): From 3353d07938da58314abbd00b29e2ed9a6a08aa83 Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Thu, 1 May 2025 11:01:17 -0300 Subject: [PATCH 0211/1161] [`flake8-use-pathlib`] Fix `PTH104`false positive when `rename` is passed a file descriptor (#17712) ## Summary Contains the same changes to the semantic type inference as https://github.com/astral-sh/ruff/pull/17705. Fixes #17694 ## Test Plan Snapshot tests. --------- Co-authored-by: Dhruv Manilawala Co-authored-by: Brent Westbrook --- .../fixtures/flake8_use_pathlib/full_name.py | 5 +++ .../rules/replaceable_by_pathlib.rs | 45 +++++++++---------- ...ake8_use_pathlib__tests__full_name.py.snap | 30 +++++++++++++ 3 files changed, 57 insertions(+), 23 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py index bd00f5193e3e83..0f99bc4b85d032 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py @@ -82,3 +82,8 @@ def func() -> int: def bar(x: int): os.stat(x) + +# https://github.com/astral-sh/ruff/issues/17694 +os.rename("src", "dst", src_dir_fd=3, dst_dir_fd=4) +os.rename("src", "dst", src_dir_fd=3) +os.rename("src", "dst", dst_dir_fd=4) diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index dd47bb12d0a27c..14b724730ffb35 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -32,7 +32,27 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { // PTH103 ["os", "mkdir"] => OsMkdir.into(), // PTH104 - ["os", "rename"] => OsRename.into(), + ["os", "rename"] => { + // `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are + // are set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rename) + // ```text + // 0 1 2 3 + // os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None) + // ``` + if call + .arguments + .find_argument_value("src_dir_fd", 2) + .is_some_and(|expr| !expr.is_none_literal_expr()) + || call + .arguments + .find_argument_value("dst_dir_fd", 3) + .is_some_and(|expr| !expr.is_none_literal_expr()) + { + return; + } + OsRename.into() + } // PTH105 ["os", "replace"] => OsReplace.into(), // PTH106 @@ -135,7 +155,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { || call .arguments .find_positional(0) - .is_some_and(|expr| is_file_descriptor_or_bytes_str(expr, checker.semantic())) + .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) { return; } @@ -174,10 +194,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { } } -fn is_file_descriptor_or_bytes_str(expr: &Expr, semantic: &SemanticModel) -> bool { - is_file_descriptor(expr, semantic) || is_bytes_string(expr, semantic) -} - /// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer. fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool { if matches!( @@ -201,23 +217,6 @@ fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool { typing::is_int(binding, semantic) } -/// Returns `true` if the given expression is a bytes string. -fn is_bytes_string(expr: &Expr, semantic: &SemanticModel) -> bool { - if matches!(expr, Expr::BytesLiteral(_)) { - return true; - } - - let Some(name) = get_name_expr(expr) else { - return false; - }; - - let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { - return false; - }; - - typing::is_bytes(binding, semantic) -} - fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> { match expr { Expr::Name(name) => Some(name), diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap index e0d29b06be0f9d..63d515f0b3ac3e 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap @@ -317,3 +317,33 @@ full_name.py:47:1: PTH123 `open()` should be replaced by `Path.open()` | ^^^^ PTH123 48 | open(p, 'r', - 1, None, None, None, False, opener) | + +full_name.py:65:1: PTH123 `open()` should be replaced by `Path.open()` + | +63 | open(f()) +64 | +65 | open(b"foo") + | ^^^^ PTH123 +66 | byte_str = b"bar" +67 | open(byte_str) + | + +full_name.py:67:1: PTH123 `open()` should be replaced by `Path.open()` + | +65 | open(b"foo") +66 | byte_str = b"bar" +67 | open(byte_str) + | ^^^^ PTH123 +68 | +69 | def bytes_str_func() -> bytes: + | + +full_name.py:71:1: PTH123 `open()` should be replaced by `Path.open()` + | +69 | def bytes_str_func() -> bytes: +70 | return b"foo" +71 | open(bytes_str_func()) + | ^^^^ PTH123 +72 | +73 | # https://github.com/astral-sh/ruff/issues/17693 + | From 75effb8ed7430288648eb616b1499939700edff6 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 1 May 2025 10:19:58 -0400 Subject: [PATCH 0212/1161] Bump 0.11.8 (#17766) --- CHANGELOG.md | 36 +++++++++++++++++++++++++++++++ Cargo.lock | 6 +++--- README.md | 6 +++--- crates/ruff/Cargo.toml | 2 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_wasm/Cargo.toml | 2 +- docs/integrations.md | 8 +++---- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- 10 files changed, 52 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e5ddb546035fb..5c06e1c122f5e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # Changelog +## 0.11.8 + +### Preview features + +- \[`airflow`\] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR302`, `AIR311`) ([#17553](https://github.com/astral-sh/ruff/pull/17553), [#17570](https://github.com/astral-sh/ruff/pull/17570), [#17571](https://github.com/astral-sh/ruff/pull/17571)) +- \[`airflow`\] Extend `AIR301` rule ([#17598](https://github.com/astral-sh/ruff/pull/17598)) +- \[`airflow`\] Update existing `AIR302` rules with better suggestions ([#17542](https://github.com/astral-sh/ruff/pull/17542)) +- \[`refurb`\] Mark fix as safe for `readlines-in-for` (`FURB129`) ([#17644](https://github.com/astral-sh/ruff/pull/17644)) +- [syntax-errors] `nonlocal` declaration at module level ([#17559](https://github.com/astral-sh/ruff/pull/17559)) +- [syntax-errors] Detect single starred expression assignment `x = *y` ([#17624](https://github.com/astral-sh/ruff/pull/17624)) + +### Bug fixes + +- \[`flake8-pyi`\] Ensure `Literal[None,] | Literal[None,]` is not autofixed to `None | None` (`PYI061`) ([#17659](https://github.com/astral-sh/ruff/pull/17659)) +- \[`flake8-use-pathlib`\] Avoid suggesting `Path.iterdir()` for `os.listdir` with file descriptor (`PTH208`) ([#17715](https://github.com/astral-sh/ruff/pull/17715)) +- \[`flake8-use-pathlib`\] Fix `PTH104` false positive when `rename` is passed a file descriptor ([#17712](https://github.com/astral-sh/ruff/pull/17712)) +- \[`flake8-use-pathlib`\] Fix `PTH116` false positive when `stat` is passed a file descriptor ([#17709](https://github.com/astral-sh/ruff/pull/17709)) +- \[`flake8-use-pathlib`\] Fix `PTH123` false positive when `open` is passed a file descriptor from a function call ([#17705](https://github.com/astral-sh/ruff/pull/17705)) +- \[`pycodestyle`\] Fix duplicated diagnostic in `E712` ([#17651](https://github.com/astral-sh/ruff/pull/17651)) +- \[`pylint`\] Detect `global` declarations in module scope (`PLE0118`) ([#17411](https://github.com/astral-sh/ruff/pull/17411)) +- [syntax-errors] Make `async-comprehension-in-sync-comprehension` more specific ([#17460](https://github.com/astral-sh/ruff/pull/17460)) + +### Configuration + +- Add option to disable `typing_extensions` imports ([#17611](https://github.com/astral-sh/ruff/pull/17611)) + +### Documentation + +- Fix example syntax for the `lint.pydocstyle.ignore-var-parameters` option ([#17740](https://github.com/astral-sh/ruff/pull/17740)) +- Add fix safety sections (`ASYNC116`, `FLY002`, `D200`, `RUF005`, `RUF017`, `RUF027`, `RUF028`, `RUF057`) ([#17497](https://github.com/astral-sh/ruff/pull/17497), [#17496](https://github.com/astral-sh/ruff/pull/17496), [#17502](https://github.com/astral-sh/ruff/pull/17502), [#17484](https://github.com/astral-sh/ruff/pull/17484), [#17480](https://github.com/astral-sh/ruff/pull/17480), [#17485](https://github.com/astral-sh/ruff/pull/17485), [#17722](https://github.com/astral-sh/ruff/pull/17722), [#17483](https://github.com/astral-sh/ruff/pull/17483)) + +### Other changes + +- Add Python 3.14 to configuration options ([#17647](https://github.com/astral-sh/ruff/pull/17647)) +- Make syntax error for unparenthesized except tuples version specific to before 3.14 ([#17660](https://github.com/astral-sh/ruff/pull/17660)) + ## 0.11.7 ### Preview features diff --git a/Cargo.lock b/Cargo.lock index f78010f0ec8e10..db2f5d5c1c89fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2758,7 +2758,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.11.7" +version = "0.11.8" dependencies = [ "anyhow", "argfile", @@ -2994,7 +2994,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.11.7" +version = "0.11.8" dependencies = [ "aho-corasick", "anyhow", @@ -3320,7 +3320,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.11.7" +version = "0.11.8" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/README.md b/README.md index 7978b3abfc3c7b..6bb9bd2a213ff6 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" # For a specific version. -curl -LsSf https://astral.sh/ruff/0.11.7/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.11.7/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.11.8/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.11.8/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.7 + rev: v0.11.8 hooks: # Run the linter. - id: ruff diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index ced197e12399c8..135c323df64dd3 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.11.7" +version = "0.11.8" publish = true authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 63c6455ae6e8c2..37be59af5e5073 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.11.7" +version = "0.11.8" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index bfe2823928a0e6..a4ccef555f738e 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.11.7" +version = "0.11.8" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/integrations.md b/docs/integrations.md index 77b379996c456f..81001ea5e3db3e 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma stage: build interruptible: true image: - name: ghcr.io/astral-sh/ruff:0.11.7-alpine + name: ghcr.io/astral-sh/ruff:0.11.8-alpine before_script: - cd $CI_PROJECT_DIR - ruff --version @@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.7 + rev: v0.11.8 hooks: # Run the linter. - id: ruff @@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.7 + rev: v0.11.8 hooks: # Run the linter. - id: ruff @@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.7 + rev: v0.11.8 hooks: # Run the linter. - id: ruff diff --git a/docs/tutorial.md b/docs/tutorial.md index 33538951696666..5d7adbca8f46c9 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.7 + rev: v0.11.8 hooks: # Run the linter. - id: ruff diff --git a/pyproject.toml b/pyproject.toml index 8b8011be234c85..a76aa557042ded 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.11.7" +version = "0.11.8" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 68b6acf4c2e88e..953d56074db3cf 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.11.7" +version = "0.11.8" description = "" authors = ["Charles Marsh "] From 163d52640765b4a8f7d73b5762dce442d63bd690 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 1 May 2025 11:29:52 -0400 Subject: [PATCH 0213/1161] Allow passing a virtual environment to `ruff analyze graph` (#17743) Summary -- Fixes #16598 by adding the `--python` flag to `ruff analyze graph`, which adds a `PythonPath` to the `SearchPathSettings` for module resolution. For the [albatross-virtual-workspace] example from the uv repo, this updates the output from the initial issue: ```shell > ruff analyze graph packages/albatross { "packages/albatross/check_installed_albatross.py": [ "packages/albatross/src/albatross/__init__.py" ], "packages/albatross/src/albatross/__init__.py": [] } ``` To include both the the workspace `bird_feeder` import _and_ the third-party `tqdm` import in the output: ```shell > myruff analyze graph packages/albatross --python .venv { "packages/albatross/check_installed_albatross.py": [ "packages/albatross/src/albatross/__init__.py" ], "packages/albatross/src/albatross/__init__.py": [ ".venv/lib/python3.12/site-packages/tqdm/__init__.py", "packages/bird-feeder/src/bird_feeder/__init__.py" ] } ``` Note the hash in the uv link! I was temporarily very confused why my local tests were showing an `iniconfig` import instead of `tqdm` until I realized that the example has been updated on the uv main branch, which I had locally. Test Plan -- A new integration test with a stripped down venv based on the `albatross` example. [albatross-virtual-workspace]: https://github.com/astral-sh/uv/tree/aa629c4a54c31d6132ab1655b90dd7542c17d120/scripts/workspaces/albatross-virtual-workspace --- crates/ruff/src/args.rs | 5 + crates/ruff/src/commands/analyze_graph.rs | 2 + crates/ruff/tests/analyze_graph.rs | 150 ++++++++++++++++++++++ crates/ruff_graph/src/db.rs | 9 +- 4 files changed, 164 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index e28508b381837b..bd95e59816e23a 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -177,6 +177,9 @@ pub struct AnalyzeGraphCommand { /// The minimum Python version that should be supported. #[arg(long, value_enum)] target_version: Option, + /// Path to a virtual environment to use for resolving additional dependencies + #[arg(long)] + python: Option, } // The `Parser` derive is for ruff_dev, for ruff `Args` would be sufficient @@ -796,6 +799,7 @@ impl AnalyzeGraphCommand { let format_arguments = AnalyzeGraphArgs { files: self.files, direction: self.direction, + python: self.python, }; let cli_overrides = ExplicitConfigOverrides { @@ -1261,6 +1265,7 @@ impl LineColumnParseError { pub struct AnalyzeGraphArgs { pub files: Vec, pub direction: Direction, + pub python: Option, } /// Configuration overrides provided via dedicated CLI flags: diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs index 6069f6d0520978..d0bf72edf78f00 100644 --- a/crates/ruff/src/commands/analyze_graph.rs +++ b/crates/ruff/src/commands/analyze_graph.rs @@ -75,6 +75,8 @@ pub(crate) fn analyze_graph( .target_version .as_tuple() .into(), + args.python + .and_then(|python| SystemPathBuf::from_path_buf(python).ok()), )?; let imports = { diff --git a/crates/ruff/tests/analyze_graph.rs b/crates/ruff/tests/analyze_graph.rs index 62f4a6c3f91587..54da7dd7054695 100644 --- a/crates/ruff/tests/analyze_graph.rs +++ b/crates/ruff/tests/analyze_graph.rs @@ -422,3 +422,153 @@ fn nested_imports() -> Result<()> { Ok(()) } + +/// Test for venv resolution with the `--python` flag. +/// +/// Based on the [albatross-virtual-workspace] example from the uv repo and the report in [#16598]. +/// +/// [albatross-virtual-workspace]: https://github.com/astral-sh/uv/tree/aa629c4a/scripts/workspaces/albatross-virtual-workspace +/// [#16598]: https://github.com/astral-sh/ruff/issues/16598 +#[test] +fn venv() -> Result<()> { + let tempdir = TempDir::new()?; + let root = ChildPath::new(tempdir.path()); + + // packages + // ├── albatross + // │ ├── check_installed_albatross.py + // │ ├── pyproject.toml + // │ └── src + // │ └── albatross + // │ └── __init__.py + // └── bird-feeder + // ├── check_installed_bird_feeder.py + // ├── pyproject.toml + // └── src + // └── bird_feeder + // └── __init__.py + + let packages = root.child("packages"); + + let albatross = packages.child("albatross"); + albatross + .child("check_installed_albatross.py") + .write_str("from albatross import fly")?; + albatross + .child("pyproject.toml") + .write_str(indoc::indoc! {r#" + [project] + name = "albatross" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["bird-feeder", "tqdm>=4,<5"] + + [tool.uv.sources] + bird-feeder = { workspace = true } + "#})?; + albatross + .child("src") + .child("albatross") + .child("__init__.py") + .write_str("import tqdm; from bird_feeder import use")?; + + let bird_feeder = packages.child("bird-feeder"); + bird_feeder + .child("check_installed_bird_feeder.py") + .write_str("from bird_feeder import use; from albatross import fly")?; + bird_feeder + .child("pyproject.toml") + .write_str(indoc::indoc! {r#" + [project] + name = "bird-feeder" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = ["anyio>=4.3.0,<5"] + "#})?; + bird_feeder + .child("src") + .child("bird_feeder") + .child("__init__.py") + .write_str("import anyio")?; + + let venv = root.child(".venv"); + let bin = venv.child("bin"); + bin.child("python").touch()?; + let home = format!("home = {}", bin.to_string_lossy()); + venv.child("pyvenv.cfg").write_str(&home)?; + let site_packages = venv.child("lib").child("python3.12").child("site-packages"); + site_packages + .child("_albatross.pth") + .write_str(&albatross.join("src").to_string_lossy())?; + site_packages + .child("_bird_feeder.pth") + .write_str(&bird_feeder.join("src").to_string_lossy())?; + site_packages.child("tqdm").child("__init__.py").touch()?; + + // without `--python .venv`, the result should only include dependencies within the albatross + // package + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!( + command().arg("packages/albatross").current_dir(&root), + @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "packages/albatross/check_installed_albatross.py": [ + "packages/albatross/src/albatross/__init__.py" + ], + "packages/albatross/src/albatross/__init__.py": [] + } + + ----- stderr ----- + "#); + }); + + // with `--python .venv` both workspace and third-party dependencies are included + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!( + command().args(["--python", ".venv"]).arg("packages/albatross").current_dir(&root), + @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "packages/albatross/check_installed_albatross.py": [ + "packages/albatross/src/albatross/__init__.py" + ], + "packages/albatross/src/albatross/__init__.py": [ + ".venv/lib/python3.12/site-packages/tqdm/__init__.py", + "packages/bird-feeder/src/bird_feeder/__init__.py" + ] + } + + ----- stderr ----- + "#); + }); + + // test the error message for a non-existent venv. it's important that the `ruff analyze graph` + // flag matches the red-knot flag used to generate the error message (`--python`) + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!( + command().args(["--python", "none"]).arg("packages/albatross").current_dir(&root), + @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Invalid search path settings + Cause: Failed to discover the site-packages directory: Invalid `--python` argument: `none` could not be canonicalized + "); + }); + + Ok(()) +} diff --git a/crates/ruff_graph/src/db.rs b/crates/ruff_graph/src/db.rs index 368d571505878a..e339262bff4fc4 100644 --- a/crates/ruff_graph/src/db.rs +++ b/crates/ruff_graph/src/db.rs @@ -4,7 +4,8 @@ use zip::CompressionMethod; use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; use red_knot_python_semantic::{ - default_lint_registry, Db, Program, ProgramSettings, PythonPlatform, SearchPathSettings, + default_lint_registry, Db, Program, ProgramSettings, PythonPath, PythonPlatform, + SearchPathSettings, }; use ruff_db::files::{File, Files}; use ruff_db::system::{OsSystem, System, SystemPathBuf}; @@ -32,8 +33,12 @@ impl ModuleDb { pub fn from_src_roots( src_roots: Vec, python_version: PythonVersion, + venv_path: Option, ) -> Result { - let search_paths = SearchPathSettings::new(src_roots); + let mut search_paths = SearchPathSettings::new(src_roots); + if let Some(venv_path) = venv_path { + search_paths.python_path = PythonPath::from_cli_flag(venv_path); + } let db = Self::default(); Program::from_settings( From b7ce694162bcd82592926afbbbf918ed61c49bad Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 30 Apr 2025 14:06:18 -0400 Subject: [PATCH 0214/1161] red_knot_server: add auto-completion MVP This PR does the wiring necessary to respond to completion requests from LSP clients. As far as the actual completion results go, they are nearly about the dumbest and simplest thing we can do: we simply return a de-duplicated list of all identifiers from the current module. --- crates/red_knot_ide/src/completion.rs | 39 +++++++++++++ crates/red_knot_ide/src/lib.rs | 2 + crates/red_knot_server/src/server.rs | 3 + crates/red_knot_server/src/server/api.rs | 5 ++ .../src/server/api/requests.rs | 2 + .../src/server/api/requests/completion.rs | 58 +++++++++++++++++++ .../src/server/schedule/task.rs | 1 - 7 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 crates/red_knot_ide/src/completion.rs create mode 100644 crates/red_knot_server/src/server/api/requests/completion.rs diff --git a/crates/red_knot_ide/src/completion.rs b/crates/red_knot_ide/src/completion.rs new file mode 100644 index 00000000000000..275b1fcb1ed759 --- /dev/null +++ b/crates/red_knot_ide/src/completion.rs @@ -0,0 +1,39 @@ +use ruff_db::files::File; +use ruff_db::parsed::parsed_module; +use ruff_python_ast::visitor::source_order::SourceOrderVisitor; +use ruff_python_ast::{AnyNodeRef, Identifier}; +use ruff_text_size::TextSize; + +use crate::Db; + +pub struct Completion { + pub label: String, +} + +pub fn completion(db: &dyn Db, file: File, _offset: TextSize) -> Vec { + let parsed = parsed_module(db.upcast(), file); + identifiers(parsed.syntax().into()) + .into_iter() + .map(|label| Completion { label }) + .collect() +} + +fn identifiers(node: AnyNodeRef) -> Vec { + struct Visitor { + identifiers: Vec, + } + + impl<'a> SourceOrderVisitor<'a> for Visitor { + fn visit_identifier(&mut self, id: &'a Identifier) { + self.identifiers.push(id.id.as_str().to_string()); + } + } + + let mut visitor = Visitor { + identifiers: vec![], + }; + node.visit_source_order(&mut visitor); + visitor.identifiers.sort(); + visitor.identifiers.dedup(); + visitor.identifiers +} diff --git a/crates/red_knot_ide/src/lib.rs b/crates/red_knot_ide/src/lib.rs index 48f1145894e303..a85c0cd1fee4bf 100644 --- a/crates/red_knot_ide/src/lib.rs +++ b/crates/red_knot_ide/src/lib.rs @@ -1,3 +1,4 @@ +mod completion; mod db; mod find_node; mod goto; @@ -5,6 +6,7 @@ mod hover; mod inlay_hints; mod markup; +pub use completion::completion; pub use db::Db; pub use goto::goto_type_definition; pub use hover::hover; diff --git a/crates/red_knot_server/src/server.rs b/crates/red_knot_server/src/server.rs index e57d1f7bbf94b7..3556fec9d42c50 100644 --- a/crates/red_knot_server/src/server.rs +++ b/crates/red_knot_server/src/server.rs @@ -227,6 +227,9 @@ impl Server { inlay_hint_provider: Some(lsp_types::OneOf::Right( InlayHintServerCapabilities::Options(InlayHintOptions::default()), )), + completion_provider: Some(lsp_types::CompletionOptions { + ..Default::default() + }), ..Default::default() } } diff --git a/crates/red_knot_server/src/server/api.rs b/crates/red_knot_server/src/server/api.rs index 478666ea61d7be..5bfc835cc55835 100644 --- a/crates/red_knot_server/src/server/api.rs +++ b/crates/red_knot_server/src/server/api.rs @@ -36,6 +36,11 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> { request::InlayHintRequestHandler::METHOD => background_request_task::< request::InlayHintRequestHandler, >(req, BackgroundSchedule::Worker), + request::CompletionRequestHandler::METHOD => background_request_task::< + request::CompletionRequestHandler, + >( + req, BackgroundSchedule::LatencySensitive + ), method => { tracing::warn!("Received request {method} which does not have a handler"); diff --git a/crates/red_knot_server/src/server/api/requests.rs b/crates/red_knot_server/src/server/api/requests.rs index b6e907aa0c4055..bfdec09623f7f8 100644 --- a/crates/red_knot_server/src/server/api/requests.rs +++ b/crates/red_knot_server/src/server/api/requests.rs @@ -1,8 +1,10 @@ +mod completion; mod diagnostic; mod goto_type_definition; mod hover; mod inlay_hints; +pub(super) use completion::CompletionRequestHandler; pub(super) use diagnostic::DocumentDiagnosticRequestHandler; pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; pub(super) use hover::HoverRequestHandler; diff --git a/crates/red_knot_server/src/server/api/requests/completion.rs b/crates/red_knot_server/src/server/api/requests/completion.rs new file mode 100644 index 00000000000000..93a1c9d8ccfe34 --- /dev/null +++ b/crates/red_knot_server/src/server/api/requests/completion.rs @@ -0,0 +1,58 @@ +use std::borrow::Cow; + +use lsp_types::request::Completion; +use lsp_types::{CompletionItem, CompletionParams, CompletionResponse, Url}; +use red_knot_ide::completion; +use red_knot_project::ProjectDatabase; +use ruff_db::source::{line_index, source_text}; + +use crate::document::PositionExt; +use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; +use crate::server::client::Notifier; +use crate::DocumentSnapshot; + +pub(crate) struct CompletionRequestHandler; + +impl RequestHandler for CompletionRequestHandler { + type RequestType = Completion; +} + +impl BackgroundDocumentRequestHandler for CompletionRequestHandler { + fn document_url(params: &CompletionParams) -> Cow { + Cow::Borrowed(¶ms.text_document_position.text_document.uri) + } + + fn run_with_snapshot( + snapshot: DocumentSnapshot, + db: ProjectDatabase, + _notifier: Notifier, + params: CompletionParams, + ) -> crate::server::Result> { + let Some(file) = snapshot.file(&db) else { + tracing::debug!("Failed to resolve file for {:?}", params); + return Ok(None); + }; + + let source = source_text(&db, file); + let line_index = line_index(&db, file); + let offset = params.text_document_position.position.to_text_size( + &source, + &line_index, + snapshot.encoding(), + ); + let completions = completion(&db, file, offset); + if completions.is_empty() { + return Ok(None); + } + + let items: Vec = completions + .into_iter() + .map(|comp| CompletionItem { + label: comp.label, + ..Default::default() + }) + .collect(); + let response = CompletionResponse::Array(items); + Ok(Some(response)) + } +} diff --git a/crates/red_knot_server/src/server/schedule/task.rs b/crates/red_knot_server/src/server/schedule/task.rs index b9ae6f4a59718f..d269c1edb5a3b1 100644 --- a/crates/red_knot_server/src/server/schedule/task.rs +++ b/crates/red_knot_server/src/server/schedule/task.rs @@ -21,7 +21,6 @@ pub(in crate::server) enum BackgroundSchedule { Fmt, /// The task should be run on the general high-priority background /// thread. Reserved for actions caused by the user typing (e.g.syntax highlighting). - #[expect(dead_code)] LatencySensitive, /// The task should be run on a regular-priority background thread. /// The default for any request that isn't in the critical path of the user typing. From 0c80c56afc5b17af6b2c4274689d5d3f32af5ad7 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Bodas <55339528+abhijeetbodas2001@users.noreply.github.com> Date: Thu, 1 May 2025 23:48:35 +0530 Subject: [PATCH 0215/1161] [syntax-errors] Use consistent message for bad starred expression usage. (#17772) --- crates/ruff_python_parser/src/semantic_errors.rs | 2 +- .../invalid_syntax@assign_stmt_starred_expr_value.py.snap | 8 ++++---- ...lid_syntax@expressions__yield__star_expression.py.snap | 2 +- .../invalid_syntax@for_stmt_invalid_iter_expr.py.snap | 2 +- .../invalid_syntax@for_stmt_invalid_target.py.snap | 4 ++-- .../invalid_syntax@return_stmt_invalid_expr.py.snap | 4 ++-- .../snapshots/invalid_syntax@single_star_for.py.snap | 4 ++-- .../snapshots/invalid_syntax@single_star_return.py.snap | 2 +- .../snapshots/invalid_syntax@single_star_yield.py.snap | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 83366b035faf70..b9d443cb591890 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -941,7 +941,7 @@ impl Display for SemanticSyntaxError { write!(f, "name `{name}` is used prior to global declaration") } SemanticSyntaxErrorKind::InvalidStarExpression => { - f.write_str("can't use starred expression here") + f.write_str("Starred expression cannot be used here") } SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(python_version) => { write!( diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap index fc1b6152922b84..220f6e607436a2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_starred_expr_value.py.snap @@ -165,7 +165,7 @@ Module( | 1 | _ = *[42] - | ^^^^^ Syntax Error: can't use starred expression here + | ^^^^^ Syntax Error: Starred expression cannot be used here 2 | _ = *{42} 3 | _ = *list() | @@ -174,7 +174,7 @@ Module( | 1 | _ = *[42] 2 | _ = *{42} - | ^^^^^ Syntax Error: can't use starred expression here + | ^^^^^ Syntax Error: Starred expression cannot be used here 3 | _ = *list() 4 | _ = *(p + q) | @@ -184,7 +184,7 @@ Module( 1 | _ = *[42] 2 | _ = *{42} 3 | _ = *list() - | ^^^^^^^ Syntax Error: can't use starred expression here + | ^^^^^^^ Syntax Error: Starred expression cannot be used here 4 | _ = *(p + q) | @@ -193,5 +193,5 @@ Module( 2 | _ = *{42} 3 | _ = *list() 4 | _ = *(p + q) - | ^^^^^^^^ Syntax Error: can't use starred expression here + | ^^^^^^^^ Syntax Error: Starred expression cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__star_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__star_expression.py.snap index 2a976eb9a7eb18..0045d8615f349d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__star_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__star_expression.py.snap @@ -118,7 +118,7 @@ Module( | 1 | # Cannot use starred expression here 2 | yield (*x) - | ^^ Syntax Error: can't use starred expression here + | ^^ Syntax Error: Starred expression cannot be used here 3 | 4 | yield *x and y, z | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap index cf0600a2cfbbec..f83966eb9282a7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_iter_expr.py.snap @@ -187,7 +187,7 @@ Module( | 1 | for x in *a and b: ... - | ^^^^^^^^ Syntax Error: can't use starred expression here + | ^^^^^^^^ Syntax Error: Starred expression cannot be used here 2 | for x in yield a: ... 3 | for target in x := 1: ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap index 547334181f0ebd..b34450098f0c17 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target.py.snap @@ -469,7 +469,7 @@ Module( 1 | for 1 in x: ... 2 | for "a" in x: ... 3 | for *x and y in z: ... - | ^^^^^^^^ Syntax Error: can't use starred expression here + | ^^^^^^^^ Syntax Error: Starred expression cannot be used here 4 | for *x | y in z: ... 5 | for await x in z: ... | @@ -479,7 +479,7 @@ Module( 2 | for "a" in x: ... 3 | for *x and y in z: ... 4 | for *x | y in z: ... - | ^^^^^^ Syntax Error: can't use starred expression here + | ^^^^^^ Syntax Error: Starred expression cannot be used here 5 | for await x in z: ... 6 | for yield x in y: ... | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@return_stmt_invalid_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@return_stmt_invalid_expr.py.snap index 64c5b6ec7e637a..a6518b0d868c9b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@return_stmt_invalid_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@return_stmt_invalid_expr.py.snap @@ -186,7 +186,7 @@ Module( | 1 | return * - | ^ Syntax Error: can't use starred expression here + | ^ Syntax Error: Starred expression cannot be used here 2 | return yield x 3 | return yield from x | @@ -196,5 +196,5 @@ Module( 3 | return yield from x 4 | return x := 1 5 | return *x and y - | ^^^^^^^^ Syntax Error: can't use starred expression here + | ^^^^^^^^ Syntax Error: Starred expression cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_for.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_for.py.snap index b7d6f53586740b..a5699bbc2b90b4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_for.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_for.py.snap @@ -95,7 +95,7 @@ Module( | 1 | for _ in *x: ... - | ^^ Syntax Error: can't use starred expression here + | ^^ Syntax Error: Starred expression cannot be used here 2 | for *x in xs: ... | @@ -103,5 +103,5 @@ Module( | 1 | for _ in *x: ... 2 | for *x in xs: ... - | ^^ Syntax Error: can't use starred expression here + | ^^ Syntax Error: Starred expression cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_return.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_return.py.snap index 2754123634c6cf..91feb561c855d2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_return.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_return.py.snap @@ -60,5 +60,5 @@ Module( | 1 | def f(): return *x - | ^^ Syntax Error: can't use starred expression here + | ^^ Syntax Error: Starred expression cannot be used here | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_yield.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_yield.py.snap index 6a5e4c01505875..d89940beaa5254 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_yield.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_star_yield.py.snap @@ -65,5 +65,5 @@ Module( | 1 | def f(): yield *x - | ^^ Syntax Error: can't use starred expression here + | ^^ Syntax Error: Starred expression cannot be used here | From e515899141a6004f570509e29dc549f82042631a Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 1 May 2025 20:33:51 +0200 Subject: [PATCH 0216/1161] [red-knot] More informative hover-types for assignments (#17762) ## Summary closes https://github.com/astral-sh/ruff/issues/17122 ## Test Plan * New hover tests * Opened the playground locally and saw that new hover-types are shown as expected. --- crates/red_knot_ide/src/hover.rs | 187 +++++++++++++++++- .../src/types/infer.rs | 37 +++- 2 files changed, 212 insertions(+), 12 deletions(-) diff --git a/crates/red_knot_ide/src/hover.rs b/crates/red_knot_ide/src/hover.rs index 11b1c78c0c1fd4..700f8276e119c4 100644 --- a/crates/red_knot_ide/src/hover.rs +++ b/crates/red_knot_ide/src/hover.rs @@ -444,7 +444,129 @@ mod tests { } #[test] - fn hover_class_member_declaration() { + fn hover_variable_assignment() { + let test = cursor_test( + r#" + value = 1 + "#, + ); + + assert_snapshot!(test.hover(), @r" + Literal[1] + --------------------------------------------- + ```text + Literal[1] + ``` + --------------------------------------------- + info: lint:hover: Hovered content is + --> main.py:2:13 + | + 2 | value = 1 + | ^^^^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_augmented_assignment() { + let test = cursor_test( + r#" + value = 1 + value += 2 + "#, + ); + + // We currently show the *previous* value of the variable (1), not the new one (3). + // Showing the new value might be more intuitive for some users, but the actual 'use' + // of the `value` symbol here in read-context is `1`. This comment mainly exists to + // signal that it might be okay to revisit this in the future and reveal 3 instead. + assert_snapshot!(test.hover(), @r" + Literal[1] + --------------------------------------------- + ```text + Literal[1] + ``` + --------------------------------------------- + info: lint:hover: Hovered content is + --> main.py:3:13 + | + 2 | value = 1 + 3 | value += 2 + | ^^^^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_attribute_assignment() { + let test = cursor_test( + r#" + class C: + attr: int = 1 + + C.attr = 2 + "#, + ); + + assert_snapshot!(test.hover(), @r" + Literal[2] + --------------------------------------------- + ```text + Literal[2] + ``` + --------------------------------------------- + info: lint:hover: Hovered content is + --> main.py:5:13 + | + 3 | attr: int = 1 + 4 | + 5 | C.attr = 2 + | ^^^^^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_augmented_attribute_assignment() { + let test = cursor_test( + r#" + class C: + attr = 1 + + C.attr += 2 + "#, + ); + + // See the comment in the `hover_augmented_assignment` test above. The same + // reasoning applies here. + assert_snapshot!(test.hover(), @r" + Unknown | Literal[1] + --------------------------------------------- + ```text + Unknown | Literal[1] + ``` + --------------------------------------------- + info: lint:hover: Hovered content is + --> main.py:5:13 + | + 3 | attr = 1 + 4 | + 5 | C.attr += 2 + | ^^^^^^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_annotated_assignment() { let test = cursor_test( r#" class Foo: @@ -452,12 +574,11 @@ mod tests { "#, ); - // TODO: This should be int and not `Never`, https://github.com/astral-sh/ruff/issues/17122 assert_snapshot!(test.hover(), @r" - Never + int --------------------------------------------- ```text - Never + int ``` --------------------------------------------- info: lint:hover: Hovered content is @@ -472,6 +593,64 @@ mod tests { "); } + #[test] + fn hover_annotated_assignment_with_rhs() { + let test = cursor_test( + r#" + class Foo: + a: int = 1 + "#, + ); + + assert_snapshot!(test.hover(), @r" + Literal[1] + --------------------------------------------- + ```text + Literal[1] + ``` + --------------------------------------------- + info: lint:hover: Hovered content is + --> main.py:3:13 + | + 2 | class Foo: + 3 | a: int = 1 + | ^- Cursor offset + | | + | source + | + "); + } + + #[test] + fn hover_annotated_attribute_assignment() { + let test = cursor_test( + r#" + class Foo: + def __init__(self, a: int): + self.a: int = a + "#, + ); + + assert_snapshot!(test.hover(), @r" + int + --------------------------------------------- + ```text + int + ``` + --------------------------------------------- + info: lint:hover: Hovered content is + --> main.py:4:17 + | + 2 | class Foo: + 3 | def __init__(self, a: int): + 4 | self.a: int = a + | ^^^^^^- Cursor offset + | | + | source + | + "); + } + #[test] fn hover_type_narrowing() { let test = cursor_test( diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index d39b5dc34c419d..0d2e934ee681d6 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -3143,7 +3143,7 @@ impl<'db> TypeInferenceBuilder<'db> { .. }, ) => { - self.store_expression_type(target, Type::Never); + self.store_expression_type(target, assigned_ty.unwrap_or(Type::unknown())); let object_ty = self.infer_expression(object); @@ -3228,9 +3228,21 @@ impl<'db> TypeInferenceBuilder<'db> { target, simple: _, } = assignment; - self.infer_annotation_expression(annotation, DeferredExpressionState::None); + let annotated = + self.infer_annotation_expression(annotation, DeferredExpressionState::None); self.infer_optional_expression(value.as_deref()); + + // If we have an annotated assignment like `self.attr: int = 1`, we still need to + // do type inference on the `self.attr` target to get types for all sub-expressions. self.infer_expression(target); + + // But here we explicitly overwrite the type for the overall `self.attr` node with + // the annotated type. We do no use `store_expression_type` here, because it checks + // that no type has been stored for the expression before. + let expr_id = target.scoped_expression_id(self.db(), self.scope()); + self.types + .expressions + .insert(expr_id, annotated.inner_type()); } } @@ -3295,6 +3307,11 @@ impl<'db> TypeInferenceBuilder<'db> { } } + // Annotated assignments to non-names are not definitions, so we can only be here + // if the target is a name. In this case, we can simply store types in `target` + // below, instead of calling `infer_expression` (which would return `Never`). + debug_assert!(target.is_name_expr()); + if let Some(value) = value { let inferred_ty = self.infer_expression(value); let inferred_ty = if target @@ -3315,6 +3332,8 @@ impl<'db> TypeInferenceBuilder<'db> { inferred_ty, }, ); + + self.store_expression_type(target, inferred_ty); } else { if self.in_stub() { self.add_declaration_with_binding( @@ -3325,9 +3344,9 @@ impl<'db> TypeInferenceBuilder<'db> { } else { self.add_declaration(target.into(), definition, declared_ty); } - } - self.infer_expression(target); + self.store_expression_type(target, declared_ty.inner_type()); + } } fn infer_augmented_assignment_statement(&mut self, assignment: &ast::StmtAugAssign) { @@ -3416,12 +3435,14 @@ impl<'db> TypeInferenceBuilder<'db> { // Resolve the target type, assuming a load context. let target_type = match &**target { ast::Expr::Name(name) => { - self.store_expression_type(target, Type::Never); - self.infer_name_load(name) + let previous_value = self.infer_name_load(name); + self.store_expression_type(target, previous_value); + previous_value } ast::Expr::Attribute(attr) => { - self.store_expression_type(target, Type::Never); - self.infer_attribute_load(attr) + let previous_value = self.infer_attribute_load(attr); + self.store_expression_type(target, previous_value); + previous_value } _ => self.infer_expression(target), }; From a6dc04f96edb08cb4c1277c077648ce5c75d6baf Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 1 May 2025 23:37:09 +0200 Subject: [PATCH 0217/1161] Fix module name in ASYNC110, 115, and 116 fixes (#17774) --- .../src/rules/flake8_async/helpers.rs | 4 +- ...e8_async__tests__ASYNC110_ASYNC110.py.snap | 6 +- ...e8_async__tests__ASYNC115_ASYNC115.py.snap | 104 ++++------- ...e8_async__tests__ASYNC116_ASYNC116.py.snap | 162 ++++++------------ 4 files changed, 98 insertions(+), 178 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_async/helpers.rs b/crates/ruff_linter/src/rules/flake8_async/helpers.rs index 65cd7bff273add..4faf95c40c8293 100644 --- a/crates/ruff_linter/src/rules/flake8_async/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_async/helpers.rs @@ -24,8 +24,8 @@ impl AsyncModule { impl std::fmt::Display for AsyncModule { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - AsyncModule::AnyIo => write!(f, "asyncio"), - AsyncModule::AsyncIo => write!(f, "anyio"), + AsyncModule::AnyIo => write!(f, "anyio"), + AsyncModule::AsyncIo => write!(f, "asyncio"), AsyncModule::Trio => write!(f, "trio"), } } diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC110_ASYNC110.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC110_ASYNC110.py.snap index b385e812690fe0..13439f55826c22 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC110_ASYNC110.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC110_ASYNC110.py.snap @@ -17,7 +17,7 @@ ASYNC110.py:12:5: ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in | |__________________________________^ ASYNC110 | -ASYNC110.py:22:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop +ASYNC110.py:22:5: ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop | 21 | async def func(): 22 | / while True: @@ -25,7 +25,7 @@ ASYNC110.py:22:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.slee | |_____________________________^ ASYNC110 | -ASYNC110.py:27:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop +ASYNC110.py:27:5: ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop | 26 | async def func(): 27 | / while True: @@ -33,7 +33,7 @@ ASYNC110.py:27:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.slee | |___________________________________^ ASYNC110 | -ASYNC110.py:37:5: ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop +ASYNC110.py:37:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop | 36 | async def func(): 37 | / while True: diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC115_ASYNC115.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC115_ASYNC115.py.snap index ab2185ba56c522..c2ec9f1dbe25bf 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC115_ASYNC115.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC115_ASYNC115.py.snap @@ -133,7 +133,7 @@ ASYNC115.py:59:11: ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `tri 61 61 | 62 62 | def func(): -ASYNC115.py:85:11: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)` +ASYNC115.py:85:11: ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` | 83 | from anyio import sleep 84 | @@ -142,27 +142,19 @@ ASYNC115.py:85:11: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of ` 86 | await anyio.sleep(1) # OK 87 | await anyio.sleep(0, 1) # OK | - = help: Replace with `asyncio.lowlevel.checkpoint()` + = help: Replace with `anyio.lowlevel.checkpoint()` ℹ Safe fix -49 49 | -50 50 | -51 51 | from trio import Event, sleep - 52 |+from asyncio import lowlevel -52 53 | -53 54 | -54 55 | def func(): --------------------------------------------------------------------------------- -82 83 | import anyio -83 84 | from anyio import sleep -84 85 | +82 82 | import anyio +83 83 | from anyio import sleep +84 84 | 85 |- await anyio.sleep(0) # ASYNC115 - 86 |+ await lowlevel.checkpoint() # ASYNC115 -86 87 | await anyio.sleep(1) # OK -87 88 | await anyio.sleep(0, 1) # OK -88 89 | await anyio.sleep(...) # OK + 85 |+ await anyio.lowlevel.checkpoint() # ASYNC115 +86 86 | await anyio.sleep(1) # OK +87 87 | await anyio.sleep(0, 1) # OK +88 88 | await anyio.sleep(...) # OK -ASYNC115.py:91:5: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)` +ASYNC115.py:91:5: ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` | 89 | await anyio.sleep() # OK 90 | @@ -171,27 +163,19 @@ ASYNC115.py:91:5: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `a 92 | foo = 0 93 | anyio.sleep(foo) # OK | - = help: Replace with `asyncio.lowlevel.checkpoint()` + = help: Replace with `anyio.lowlevel.checkpoint()` ℹ Safe fix -49 49 | -50 50 | -51 51 | from trio import Event, sleep - 52 |+from asyncio import lowlevel -52 53 | -53 54 | -54 55 | def func(): --------------------------------------------------------------------------------- -88 89 | await anyio.sleep(...) # OK -89 90 | await anyio.sleep() # OK -90 91 | +88 88 | await anyio.sleep(...) # OK +89 89 | await anyio.sleep() # OK +90 90 | 91 |- anyio.sleep(0) # ASYNC115 - 92 |+ lowlevel.checkpoint() # ASYNC115 -92 93 | foo = 0 -93 94 | anyio.sleep(foo) # OK -94 95 | anyio.sleep(1) # OK + 91 |+ anyio.lowlevel.checkpoint() # ASYNC115 +92 92 | foo = 0 +93 93 | anyio.sleep(foo) # OK +94 94 | anyio.sleep(1) # OK -ASYNC115.py:97:5: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)` +ASYNC115.py:97:5: ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` | 95 | time.sleep(0) # OK 96 | @@ -200,49 +184,33 @@ ASYNC115.py:97:5: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `a 98 | 99 | bar = "bar" | - = help: Replace with `asyncio.lowlevel.checkpoint()` + = help: Replace with `anyio.lowlevel.checkpoint()` ℹ Safe fix -49 49 | -50 50 | -51 51 | from trio import Event, sleep - 52 |+from asyncio import lowlevel -52 53 | -53 54 | -54 55 | def func(): --------------------------------------------------------------------------------- -94 95 | anyio.sleep(1) # OK -95 96 | time.sleep(0) # OK -96 97 | +94 94 | anyio.sleep(1) # OK +95 95 | time.sleep(0) # OK +96 96 | 97 |- sleep(0) # ASYNC115 - 98 |+ lowlevel.checkpoint() # ASYNC115 -98 99 | -99 100 | bar = "bar" -100 101 | anyio.sleep(bar) + 97 |+ anyio.lowlevel.checkpoint() # ASYNC115 +98 98 | +99 99 | bar = "bar" +100 100 | anyio.sleep(bar) -ASYNC115.py:128:15: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)` +ASYNC115.py:128:15: ASYNC115 [*] Use `anyio.lowlevel.checkpoint()` instead of `anyio.sleep(0)` | 126 | import anyio 127 | 128 | anyio.run(anyio.sleep(0)) # ASYNC115 | ^^^^^^^^^^^^^^ ASYNC115 | - = help: Replace with `asyncio.lowlevel.checkpoint()` + = help: Replace with `anyio.lowlevel.checkpoint()` ℹ Safe fix -49 49 | -50 50 | -51 51 | from trio import Event, sleep - 52 |+from asyncio import lowlevel -52 53 | -53 54 | -54 55 | def func(): --------------------------------------------------------------------------------- -125 126 | def func(): -126 127 | import anyio -127 128 | +125 125 | def func(): +126 126 | import anyio +127 127 | 128 |- anyio.run(anyio.sleep(0)) # ASYNC115 - 129 |+ anyio.run(lowlevel.checkpoint()) # ASYNC115 -129 130 | -130 131 | -131 132 | def func(): + 128 |+ anyio.run(anyio.lowlevel.checkpoint()) # ASYNC115 +129 129 | +130 130 | +131 131 | def func(): diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC116_ASYNC116.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC116_ASYNC116.py.snap index c06800468520e3..6904caf6291205 100644 --- a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC116_ASYNC116.py.snap +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC116_ASYNC116.py.snap @@ -147,7 +147,7 @@ ASYNC116.py:57:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usu 59 60 | 60 61 | async def import_anyio(): -ASYNC116.py:64:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:64:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 63 | # These examples are probably not meant to ever wake up: 64 | await anyio.sleep(100000) # error: 116, "async" @@ -155,27 +155,19 @@ ASYNC116.py:64:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should 65 | 66 | # 'inf literal' overflow trick | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -61 62 | import anyio -62 63 | -63 64 | # These examples are probably not meant to ever wake up: +61 61 | import anyio +62 62 | +63 63 | # These examples are probably not meant to ever wake up: 64 |- await anyio.sleep(100000) # error: 116, "async" - 65 |+ await sleep_forever() # error: 116, "async" -65 66 | -66 67 | # 'inf literal' overflow trick -67 68 | await anyio.sleep(1e999) # error: 116, "async" + 64 |+ await anyio.sleep_forever() # error: 116, "async" +65 65 | +66 66 | # 'inf literal' overflow trick +67 67 | await anyio.sleep(1e999) # error: 116, "async" -ASYNC116.py:67:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:67:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 66 | # 'inf literal' overflow trick 67 | await anyio.sleep(1e999) # error: 116, "async" @@ -183,27 +175,19 @@ ASYNC116.py:67:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should 68 | 69 | await anyio.sleep(86399) | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -64 65 | await anyio.sleep(100000) # error: 116, "async" -65 66 | -66 67 | # 'inf literal' overflow trick +64 64 | await anyio.sleep(100000) # error: 116, "async" +65 65 | +66 66 | # 'inf literal' overflow trick 67 |- await anyio.sleep(1e999) # error: 116, "async" - 68 |+ await sleep_forever() # error: 116, "async" -68 69 | -69 70 | await anyio.sleep(86399) -70 71 | await anyio.sleep(86400) + 67 |+ await anyio.sleep_forever() # error: 116, "async" +68 68 | +69 69 | await anyio.sleep(86399) +70 70 | await anyio.sleep(86400) -ASYNC116.py:71:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:71:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 69 | await anyio.sleep(86399) 70 | await anyio.sleep(86400) @@ -211,27 +195,19 @@ ASYNC116.py:71:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should | ^^^^^^^^^^^^^^^^^^^^^ ASYNC116 72 | await anyio.sleep(86401) # error: 116, "async" | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -68 69 | -69 70 | await anyio.sleep(86399) -70 71 | await anyio.sleep(86400) +68 68 | +69 69 | await anyio.sleep(86399) +70 70 | await anyio.sleep(86400) 71 |- await anyio.sleep(86400.01) # error: 116, "async" - 72 |+ await sleep_forever() # error: 116, "async" -72 73 | await anyio.sleep(86401) # error: 116, "async" -73 74 | -74 75 | await anyio.sleep(-1) # will raise a runtime error + 71 |+ await anyio.sleep_forever() # error: 116, "async" +72 72 | await anyio.sleep(86401) # error: 116, "async" +73 73 | +74 74 | await anyio.sleep(-1) # will raise a runtime error -ASYNC116.py:72:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:72:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 70 | await anyio.sleep(86400) 71 | await anyio.sleep(86400.01) # error: 116, "async" @@ -240,27 +216,19 @@ ASYNC116.py:72:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should 73 | 74 | await anyio.sleep(-1) # will raise a runtime error | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -69 70 | await anyio.sleep(86399) -70 71 | await anyio.sleep(86400) -71 72 | await anyio.sleep(86400.01) # error: 116, "async" +69 69 | await anyio.sleep(86399) +70 70 | await anyio.sleep(86400) +71 71 | await anyio.sleep(86400.01) # error: 116, "async" 72 |- await anyio.sleep(86401) # error: 116, "async" - 73 |+ await sleep_forever() # error: 116, "async" -73 74 | -74 75 | await anyio.sleep(-1) # will raise a runtime error -75 76 | await anyio.sleep(0) # handled by different check + 72 |+ await anyio.sleep_forever() # error: 116, "async" +73 73 | +74 74 | await anyio.sleep(-1) # will raise a runtime error +75 75 | await anyio.sleep(0) # handled by different check -ASYNC116.py:101:5: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:101:5: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 100 | # does not require the call to be awaited, nor in an async fun 101 | anyio.sleep(86401) # error: 116, "async" @@ -268,66 +236,50 @@ ASYNC116.py:101:5: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should 102 | # also checks that we don't break visit_Call 103 | anyio.run(anyio.sleep(86401)) # error: 116, "async" | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -98 99 | import anyio -99 100 | -100 101 | # does not require the call to be awaited, nor in an async fun +98 98 | import anyio +99 99 | +100 100 | # does not require the call to be awaited, nor in an async fun 101 |- anyio.sleep(86401) # error: 116, "async" - 102 |+ sleep_forever() # error: 116, "async" -102 103 | # also checks that we don't break visit_Call -103 104 | anyio.run(anyio.sleep(86401)) # error: 116, "async" -104 105 | + 101 |+ anyio.sleep_forever() # error: 116, "async" +102 102 | # also checks that we don't break visit_Call +103 103 | anyio.run(anyio.sleep(86401)) # error: 116, "async" +104 104 | -ASYNC116.py:103:15: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:103:15: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 101 | anyio.sleep(86401) # error: 116, "async" 102 | # also checks that we don't break visit_Call 103 | anyio.run(anyio.sleep(86401)) # error: 116, "async" | ^^^^^^^^^^^^^^^^^^ ASYNC116 | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix -2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. -3 3 | import math -4 4 | from math import inf - 5 |+from asyncio import sleep_forever -5 6 | -6 7 | -7 8 | async def import_trio(): --------------------------------------------------------------------------------- -100 101 | # does not require the call to be awaited, nor in an async fun -101 102 | anyio.sleep(86401) # error: 116, "async" -102 103 | # also checks that we don't break visit_Call +100 100 | # does not require the call to be awaited, nor in an async fun +101 101 | anyio.sleep(86401) # error: 116, "async" +102 102 | # also checks that we don't break visit_Call 103 |- anyio.run(anyio.sleep(86401)) # error: 116, "async" - 104 |+ anyio.run(sleep_forever()) # error: 116, "async" -104 105 | -105 106 | -106 107 | async def import_from_anyio(): + 103 |+ anyio.run(anyio.sleep_forever()) # error: 116, "async" +104 104 | +105 105 | +106 106 | async def import_from_anyio(): -ASYNC116.py:110:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()` +ASYNC116.py:110:11: ASYNC116 [*] `anyio.sleep()` with >24 hour interval should usually be `anyio.sleep_forever()` | 109 | # catch from import 110 | await sleep(86401) # error: 116, "async" | ^^^^^^^^^^^^ ASYNC116 | - = help: Replace with `asyncio.sleep_forever()` + = help: Replace with `anyio.sleep_forever()` ℹ Unsafe fix 2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger. 3 3 | import math 4 4 | from math import inf - 5 |+from asyncio import sleep_forever + 5 |+from anyio import sleep_forever 5 6 | 6 7 | 7 8 | async def import_trio(): From 17050e2ec526423d2879acb28074b4fd3584f0c7 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 2 May 2025 09:25:58 +0200 Subject: [PATCH 0218/1161] doc: Add link to `check-typed-exception` from `S110` and `S112` (#17786) --- .../src/rules/flake8_bandit/rules/try_except_continue.rs | 3 +++ .../src/rules/flake8_bandit/rules/try_except_pass.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs index fa846af54e1f16..b34ad41e9c2174 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_continue.rs @@ -38,6 +38,9 @@ use crate::rules::flake8_bandit::helpers::is_untyped_exception; /// logging.exception("Error occurred") /// ``` /// +/// ## Options +/// - `lint.flake8-bandit.check-typed-exception` +/// /// ## References /// - [Common Weakness Enumeration: CWE-703](https://cwe.mitre.org/data/definitions/703.html) /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs index 693d99ac0a2318..6d52f9f315200f 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/try_except_pass.rs @@ -34,6 +34,9 @@ use crate::rules::flake8_bandit::helpers::is_untyped_exception; /// logging.exception("Exception occurred") /// ``` /// +/// ## Options +/// - `lint.flake8-bandit.check-typed-exception` +/// /// ## References /// - [Common Weakness Enumeration: CWE-703](https://cwe.mitre.org/data/definitions/703.html) /// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) From 3cf44e401a64658c17652cd3a17c897dc50261eb Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 2 May 2025 10:20:37 +0200 Subject: [PATCH 0219/1161] [red-knot] Implicit instance attributes in generic methods (#17769) ## Summary Add the ability to detect instance attribute assignments in class methods that are generic. This does not address the code duplication mentioned in #16928. I can open a ticket for this after this has been merged. closes #16928 ## Test Plan Added regression test. --- ...ion_generic_method_with_nested_function.py | 9 +++++++ .../resources/mdtest/attributes.md | 21 +++++++++++++++ .../src/semantic_index.rs | 27 ++++++++++++++----- .../src/semantic_index/builder.rs | 23 ++++++++++++---- 4 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 crates/red_knot_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py diff --git a/crates/red_knot_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py b/crates/red_knot_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py new file mode 100644 index 00000000000000..055f2c1721f49e --- /dev/null +++ b/crates/red_knot_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py @@ -0,0 +1,9 @@ +# Regression test for an issue that came up while working +# on https://github.com/astral-sh/ruff/pull/17769 + +class C: + def method[T](self, x: T) -> T: + def inner(): + self.attr = 1 + +C().attr diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 5077b0eb8f4f78..17079b86b854f8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1949,6 +1949,27 @@ reveal_type(C.a_type) # revealed: type reveal_type(C.a_none) # revealed: None ``` +### Generic methods + +We also detect implicit instance attributes on methods that are themselves generic. We have an extra +test for this because generic functions have an extra type-params scope in between the function body +scope and the outer scope, so we need to make sure that our implementation can still recognize `f` +as a method of `C` here: + +```toml +[environment] +python-version = "3.12" +``` + +```py +class C: + def f[T](self, t: T) -> T: + self.x: int = 1 + return t + +reveal_type(C().x) # revealed: int +``` + ## Enum classes Enums are not supported yet; attribute access on an enum class is inferred as `Todo`. diff --git a/crates/red_knot_python_semantic/src/semantic_index.rs b/crates/red_knot_python_semantic/src/semantic_index.rs index 619172faef097a..11137f3eebd2e1 100644 --- a/crates/red_knot_python_semantic/src/semantic_index.rs +++ b/crates/red_knot_python_semantic/src/semantic_index.rs @@ -18,7 +18,8 @@ use crate::semantic_index::builder::SemanticIndexBuilder; use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions}; use crate::semantic_index::expression::Expression; use crate::semantic_index::symbol::{ - FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId, SymbolTable, + FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId, + SymbolTable, }; use crate::semantic_index::use_def::{EagerBindingsKey, ScopedEagerBindingsId, UseDefMap}; use crate::Db; @@ -109,12 +110,26 @@ pub(crate) fn attribute_assignments<'db, 's>( let index = semantic_index(db, file); let class_scope_id = class_body_scope.file_scope_id(db); - ChildrenIter::new(index, class_scope_id).filter_map(|(file_scope_id, maybe_method)| { - maybe_method.node().as_function()?; - let attribute_table = index.instance_attribute_table(file_scope_id); + ChildrenIter::new(index, class_scope_id).filter_map(|(child_scope_id, scope)| { + let (function_scope_id, function_scope) = + if scope.node().scope_kind() == ScopeKind::Annotation { + // This could be a generic method with a type-params scope. + // Go one level deeper to find the function scope. The first + // descendant is the (potential) function scope. + let function_scope_id = scope.descendants().start; + (function_scope_id, index.scope(function_scope_id)) + } else { + (child_scope_id, scope) + }; + + function_scope.node().as_function()?; + let attribute_table = index.instance_attribute_table(function_scope_id); let symbol = attribute_table.symbol_id_by_name(name)?; - let use_def = &index.use_def_maps[file_scope_id]; - Some((use_def.instance_attribute_bindings(symbol), file_scope_id)) + let use_def = &index.use_def_maps[function_scope_id]; + Some(( + use_def.instance_attribute_bindings(symbol), + function_scope_id, + )) }) } diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index 75c7494b4327fe..6426e5cc0f89e7 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -183,13 +183,26 @@ impl<'db> SemanticIndexBuilder<'db> { fn is_method_of_class(&self) -> Option { let mut scopes_rev = self.scope_stack.iter().rev(); let current = scopes_rev.next()?; + + if self.scopes[current.file_scope_id].kind() != ScopeKind::Function { + return None; + } + let parent = scopes_rev.next()?; - match ( - self.scopes[current.file_scope_id].kind(), - self.scopes[parent.file_scope_id].kind(), - ) { - (ScopeKind::Function, ScopeKind::Class) => Some(parent.file_scope_id), + match self.scopes[parent.file_scope_id].kind() { + ScopeKind::Class => Some(parent.file_scope_id), + ScopeKind::Annotation => { + // If the function is generic, the parent scope is an annotation scope. + // In this case, we need to go up one level higher to find the class scope. + let grandparent = scopes_rev.next()?; + + if self.scopes[grandparent.file_scope_id].kind() == ScopeKind::Class { + Some(grandparent.file_scope_id) + } else { + None + } + } _ => None, } } From 6d2c10cca2d87147f4122fbb2d2de50be21e40ae Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 2 May 2025 12:11:47 +0200 Subject: [PATCH 0220/1161] [red-knot] Fix panic for `tuple[x[y]]` string annotation (#17787) ## Summary closes #17775 ## Test Plan Added corpus regression test --- .../test/corpus/95_annotation_string_tuple.py | 1 + .../resources/primer/bad.txt | 13 ++++--------- .../resources/primer/good.txt | 5 +++++ .../red_knot_python_semantic/src/types/infer.rs | 15 ++++++++++----- 4 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 crates/red_knot_project/resources/test/corpus/95_annotation_string_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_string_tuple.py b/crates/red_knot_project/resources/test/corpus/95_annotation_string_tuple.py new file mode 100644 index 00000000000000..84681b729e3b17 --- /dev/null +++ b/crates/red_knot_project/resources/test/corpus/95_annotation_string_tuple.py @@ -0,0 +1 @@ +t: "tuple[list[int]]" diff --git a/crates/red_knot_python_semantic/resources/primer/bad.txt b/crates/red_knot_python_semantic/resources/primer/bad.txt index 24a74e3ac94778..bf180717b9519a 100644 --- a/crates/red_knot_python_semantic/resources/primer/bad.txt +++ b/crates/red_knot_python_semantic/resources/primer/bad.txt @@ -1,35 +1,30 @@ Expression # cycle panic (signature_) Tanjun # cycle panic (signature_) -aiohttp # missing expression ID -alerta # missing expression ID altair # cycle panics (try_metaclass_) antidote # hangs / slow artigraph # cycle panics (value_type_) colour # cycle panics (try_metaclass_) core # cycle panics (value_type_) -cpython # missing expression ID, access to field whilst being initialized, too many cycle iterations +cpython # access to field whilst being initialized, too many cycle iterations discord.py # some kind of hang, only when multi-threaded? freqtrade # cycle panics (try_metaclass_) hydpy # cycle panics (try_metaclass_) ibis # cycle panics (try_metaclass_) manticore # stack overflow materialize # stack overflow -meson # missing expression ID mypy # cycle panic (signature_) pandas # slow pandas-stubs # cycle panics (try_metaclass_) pandera # cycle panics (try_metaclass_) prefect # slow -pytest # cycle panics (signature_), missing expression ID +pytest # cycle panics (signature_) pywin32 # bad use-def map (binding with definitely-visible unbound) schemathesis # cycle panics (signature_) scikit-learn # success, but mypy-primer hangs processing the output -scipy # missing expression ID +scipy # missing expression type ("expression should belong to this TypeInference region") spack # success, but mypy-primer hangs processing the output spark # cycle panics (try_metaclass_) -sphinx # missing expression ID -steam.py # missing expression ID +steam.py # cycle panics (try_metaclass_), often hangs when multi-threaded streamlit # cycle panic (signature_) sympy # stack overflow -trio # missing expression ID xarray # cycle panics (try_metaclass_) diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/red_knot_python_semantic/resources/primer/good.txt index f76755770702a7..0455d5c8fe9e6e 100644 --- a/crates/red_knot_python_semantic/resources/primer/good.txt +++ b/crates/red_knot_python_semantic/resources/primer/good.txt @@ -2,10 +2,12 @@ AutoSplit PyGithub PyWinCtl SinbadCogs +aiohttp aiohttp-devtools aioredis aiortc alectryon +alerta anyio apprise arviz @@ -47,6 +49,7 @@ jinja koda-validate kopf kornia +meson mitmproxy mkdocs mkosi @@ -92,11 +95,13 @@ scrapy setuptools sockeye speedrun.com_global_scoreboard_webapp +sphinx starlette static-frame stone strawberry tornado +trio twine typeshed-stats urllib3 diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 0d2e934ee681d6..74fd48d2e7b49e 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -7615,7 +7615,7 @@ impl<'db> TypeInferenceBuilder<'db> { fn element_could_alter_type_of_whole_tuple( element: &ast::Expr, element_ty: Type, - builder: &TypeInferenceBuilder, + builder: &mut TypeInferenceBuilder, ) -> bool { if !element_ty.is_todo() { return false; @@ -7624,10 +7624,15 @@ impl<'db> TypeInferenceBuilder<'db> { match element { ast::Expr::EllipsisLiteral(_) | ast::Expr::Starred(_) => true, ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => { - matches!( - builder.expression_type(value), - Type::KnownInstance(KnownInstanceType::Unpack) - ) + let value_ty = if builder.deferred_state.in_string_annotation() { + // Using `.expression_type` does not work in string annotations, because + // we do not store types for sub-expressions. Re-infer the type here. + builder.infer_expression(value) + } else { + builder.expression_type(value) + }; + + matches!(value_ty, Type::KnownInstance(KnownInstanceType::Unpack)) } _ => false, } From ea3f4ac05996a636c8ba778dc96e0aea5deabd96 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 2 May 2025 13:53:19 +0200 Subject: [PATCH 0221/1161] [red-knot] Refactor: no mutability in call APIs (#17788) ## Summary Remove mutability in parameter types for a few functions such as `with_self` and `try_call`. I tried the `Rc`-approach with cheap cloning [suggest here](https://github.com/astral-sh/ruff/pull/17733#discussion_r2068722860) first, but it turns out we need a whole stack of prepended arguments (there can be [both `self` *and* `cls`](https://github.com/astral-sh/ruff/blob/3cf44e401a64658c17652cd3a17c897dc50261eb/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md?plain=1#L113)), and we would need the same construct not just for `CallArguments` but also for `CallArgumentTypes`. At that point we're cloning `VecDeque`s anyway, so the overhead of cloning the whole `VecDeque` with all arguments didn't seem to justify the additional code complexity. ## Benchmarks Benchmarks on tomllib, black, jinja, isort seem neutral. --- crates/red_knot_python_semantic/src/types.rs | 11 +++---- .../src/types/call/arguments.rs | 33 ++++++++++--------- .../src/types/call/bind.rs | 24 +++++--------- .../src/types/class.rs | 4 +-- .../src/types/infer.rs | 24 +++++++------- 5 files changed, 45 insertions(+), 51 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index debb4d14eb1a3b..a002d0f9ae78ff 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2649,10 +2649,7 @@ impl<'db> Type<'db> { if let Symbol::Type(descr_get, descr_get_boundness) = descr_get { let return_ty = descr_get - .try_call( - db, - &mut CallArgumentTypes::positional([self, instance, owner]), - ) + .try_call(db, &CallArgumentTypes::positional([self, instance, owner])) .map(|bindings| { if descr_get_boundness == Boundness::Bound { bindings.return_type(db) @@ -4207,7 +4204,7 @@ impl<'db> Type<'db> { fn try_call( self, db: &'db dyn Db, - argument_types: &mut CallArgumentTypes<'_, 'db>, + argument_types: &CallArgumentTypes<'_, 'db>, ) -> Result, CallError<'db>> { let signatures = self.signatures(db); Bindings::match_parameters(signatures, argument_types).check_types(db, argument_types) @@ -4420,7 +4417,7 @@ impl<'db> Type<'db> { fn try_call_constructor( self, db: &'db dyn Db, - mut argument_types: CallArgumentTypes<'_, 'db>, + argument_types: CallArgumentTypes<'_, 'db>, ) -> Result, ConstructorCallError<'db>> { debug_assert!(matches!( self, @@ -4486,7 +4483,7 @@ impl<'db> Type<'db> { match new_method { Symbol::Type(new_method, boundness) => { - let result = new_method.try_call(db, argument_types); + let result = new_method.try_call(db, &argument_types); if boundness == Boundness::PossiblyUnbound { return Some(Err(DunderNewCallError::PossiblyUnbound(result.err()))); diff --git a/crates/red_knot_python_semantic/src/types/call/arguments.rs b/crates/red_knot_python_semantic/src/types/call/arguments.rs index cce0c81c0ba37f..df4408121a0388 100644 --- a/crates/red_knot_python_semantic/src/types/call/arguments.rs +++ b/crates/red_knot_python_semantic/src/types/call/arguments.rs @@ -11,16 +11,18 @@ impl<'a> CallArguments<'a> { /// Invoke a function with an optional extra synthetic argument (for a `self` or `cls` /// parameter) prepended to the front of this argument list. (If `bound_self` is none, the /// function is invoked with the unmodified argument list.) - pub(crate) fn with_self(&mut self, bound_self: Option>, f: F) -> R + pub(crate) fn with_self(&self, bound_self: Option>, f: F) -> R where - F: FnOnce(&mut Self) -> R, + F: FnOnce(&Self) -> R, { + let mut call_arguments = self.clone(); + if bound_self.is_some() { - self.0.push_front(Argument::Synthetic); + call_arguments.0.push_front(Argument::Synthetic); } - let result = f(self); + let result = f(&call_arguments); if bound_self.is_some() { - self.0.pop_front(); + call_arguments.0.pop_front(); } result } @@ -55,6 +57,7 @@ pub(crate) enum Argument<'a> { } /// Arguments for a single call, in source order, along with inferred types for each argument. +#[derive(Clone, Debug)] pub(crate) struct CallArgumentTypes<'a, 'db> { arguments: CallArguments<'a>, types: VecDeque>, @@ -93,20 +96,20 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> { /// Invoke a function with an optional extra synthetic argument (for a `self` or `cls` /// parameter) prepended to the front of this argument list. (If `bound_self` is none, the /// function is invoked with the unmodified argument list.) - pub(crate) fn with_self(&mut self, bound_self: Option>, f: F) -> R + pub(crate) fn with_self(&self, bound_self: Option>, f: F) -> R where - F: FnOnce(&mut Self) -> R, + F: FnOnce(Self) -> R, { + let mut call_argument_types = self.clone(); + if let Some(bound_self) = bound_self { - self.arguments.0.push_front(Argument::Synthetic); - self.types.push_front(bound_self); + call_argument_types + .arguments + .0 + .push_front(Argument::Synthetic); + call_argument_types.types.push_front(bound_self); } - let result = f(self); - if bound_self.is_some() { - self.arguments.0.pop_front(); - self.types.pop_front(); - } - result + f(call_argument_types) } pub(crate) fn iter(&self) -> impl Iterator, Type<'db>)> + '_ { diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 3bac1677555492..1af29eb62e13a0 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -56,7 +56,7 @@ impl<'db> Bindings<'db> { /// verify that each argument type is assignable to the corresponding parameter type. pub(crate) fn match_parameters( signatures: Signatures<'db>, - arguments: &mut CallArguments<'_>, + arguments: &CallArguments<'_>, ) -> Self { let mut argument_forms = vec![None; arguments.len()]; let mut conflicting_forms = vec![false; arguments.len()]; @@ -92,7 +92,7 @@ impl<'db> Bindings<'db> { pub(crate) fn check_types( mut self, db: &'db dyn Db, - argument_types: &mut CallArgumentTypes<'_, 'db>, + argument_types: &CallArgumentTypes<'_, 'db>, ) -> Result> { for (signature, element) in self.signatures.iter().zip(&mut self.elements) { element.check_types(db, signature, argument_types); @@ -351,10 +351,7 @@ impl<'db> Bindings<'db> { [Some(Type::PropertyInstance(property)), Some(instance), ..] => { if let Some(getter) = property.getter(db) { if let Ok(return_ty) = getter - .try_call( - db, - &mut CallArgumentTypes::positional([*instance]), - ) + .try_call(db, &CallArgumentTypes::positional([*instance])) .map(|binding| binding.return_type(db)) { overload.set_return_type(return_ty); @@ -383,10 +380,7 @@ impl<'db> Bindings<'db> { [Some(instance), ..] => { if let Some(getter) = property.getter(db) { if let Ok(return_ty) = getter - .try_call( - db, - &mut CallArgumentTypes::positional([*instance]), - ) + .try_call(db, &CallArgumentTypes::positional([*instance])) .map(|binding| binding.return_type(db)) { overload.set_return_type(return_ty); @@ -414,7 +408,7 @@ impl<'db> Bindings<'db> { if let Some(setter) = property.setter(db) { if let Err(_call_error) = setter.try_call( db, - &mut CallArgumentTypes::positional([*instance, *value]), + &CallArgumentTypes::positional([*instance, *value]), ) { overload.errors.push(BindingError::InternalCallError( "calling the setter failed", @@ -433,7 +427,7 @@ impl<'db> Bindings<'db> { if let Some(setter) = property.setter(db) { if let Err(_call_error) = setter.try_call( db, - &mut CallArgumentTypes::positional([*instance, *value]), + &CallArgumentTypes::positional([*instance, *value]), ) { overload.errors.push(BindingError::InternalCallError( "calling the setter failed", @@ -874,7 +868,7 @@ pub(crate) struct CallableBinding<'db> { impl<'db> CallableBinding<'db> { fn match_parameters( signature: &CallableSignature<'db>, - arguments: &mut CallArguments<'_>, + arguments: &CallArguments<'_>, argument_forms: &mut [Option], conflicting_forms: &mut [bool], ) -> Self { @@ -912,13 +906,13 @@ impl<'db> CallableBinding<'db> { &mut self, db: &'db dyn Db, signature: &CallableSignature<'db>, - argument_types: &mut CallArgumentTypes<'_, 'db>, + argument_types: &CallArgumentTypes<'_, 'db>, ) { // If this callable is a bound method, prepend the self instance onto the arguments list // before checking. argument_types.with_self(signature.bound_type, |argument_types| { for (signature, overload) in signature.iter().zip(&mut self.overloads) { - overload.check_types(db, signature, argument_types); + overload.check_types(db, signature, &argument_types); } }); } diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 67a7d0800d7605..d118d28af2ea6d 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -772,9 +772,9 @@ impl<'db> ClassLiteral<'db> { let namespace = KnownClass::Dict.to_instance(db); // TODO: Other keyword arguments? - let mut arguments = CallArgumentTypes::positional([name, bases, namespace]); + let arguments = CallArgumentTypes::positional([name, bases, namespace]); - let return_ty_result = match metaclass.try_call(db, &mut arguments) { + let return_ty_result = match metaclass.try_call(db, &arguments) { Ok(bindings) => Ok(bindings.return_type(db)), Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 74fd48d2e7b49e..991eb960dab75a 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1815,7 +1815,7 @@ impl<'db> TypeInferenceBuilder<'db> { for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { inferred_ty = match decorator_ty - .try_call(self.db(), &mut CallArgumentTypes::positional([inferred_ty])) + .try_call(self.db(), &CallArgumentTypes::positional([inferred_ty])) .map(|bindings| bindings.return_type(self.db())) { Ok(return_ty) => return_ty, @@ -2832,7 +2832,7 @@ impl<'db> TypeInferenceBuilder<'db> { let successful_call = meta_dunder_set .try_call( db, - &mut CallArgumentTypes::positional([ + &CallArgumentTypes::positional([ meta_attr_ty, object_ty, value_ty, @@ -2973,7 +2973,7 @@ impl<'db> TypeInferenceBuilder<'db> { let successful_call = meta_dunder_set .try_call( db, - &mut CallArgumentTypes::positional([ + &CallArgumentTypes::positional([ meta_attr_ty, object_ty, value_ty, @@ -4561,7 +4561,7 @@ impl<'db> TypeInferenceBuilder<'db> { // We don't call `Type::try_call`, because we want to perform type inference on the // arguments after matching them to parameters, but before checking that the argument types // are assignable to any parameter annotations. - let mut call_arguments = Self::parse_arguments(arguments); + let call_arguments = Self::parse_arguments(arguments); let callable_type = self.infer_expression(func); if let Type::FunctionLiteral(function) = callable_type { @@ -4640,11 +4640,11 @@ impl<'db> TypeInferenceBuilder<'db> { } let signatures = callable_type.signatures(self.db()); - let bindings = Bindings::match_parameters(signatures, &mut call_arguments); - let mut call_argument_types = + let bindings = Bindings::match_parameters(signatures, &call_arguments); + let call_argument_types = self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms); - match bindings.check_types(self.db(), &mut call_argument_types) { + match bindings.check_types(self.db(), &call_argument_types) { Ok(mut bindings) => { for binding in &mut bindings { let binding_type = binding.callable_type; @@ -6486,7 +6486,7 @@ impl<'db> TypeInferenceBuilder<'db> { Symbol::Type(contains_dunder, Boundness::Bound) => { // If `__contains__` is available, it is used directly for the membership test. contains_dunder - .try_call(db, &mut CallArgumentTypes::positional([right, left])) + .try_call(db, &CallArgumentTypes::positional([right, left])) .map(|bindings| bindings.return_type(db)) .ok() } @@ -6640,7 +6640,7 @@ impl<'db> TypeInferenceBuilder<'db> { generic_context: GenericContext<'db>, ) -> Type<'db> { let slice_node = subscript.slice.as_ref(); - let mut call_argument_types = match slice_node { + let call_argument_types = match slice_node { ast::Expr::Tuple(tuple) => CallArgumentTypes::positional( tuple.elts.iter().map(|elt| self.infer_type_expression(elt)), ), @@ -6650,8 +6650,8 @@ impl<'db> TypeInferenceBuilder<'db> { value_ty, generic_context.signature(self.db()), )); - let bindings = match Bindings::match_parameters(signatures, &mut call_argument_types) - .check_types(self.db(), &mut call_argument_types) + let bindings = match Bindings::match_parameters(signatures, &call_argument_types) + .check_types(self.db(), &call_argument_types) { Ok(bindings) => bindings, Err(CallError(_, bindings)) => { @@ -6893,7 +6893,7 @@ impl<'db> TypeInferenceBuilder<'db> { match ty.try_call( self.db(), - &mut CallArgumentTypes::positional([value_ty, slice_ty]), + &CallArgumentTypes::positional([value_ty, slice_ty]), ) { Ok(bindings) => return bindings.return_type(self.db()), Err(CallError(_, bindings)) => { From 675a5af89af5b786a6c99870fe0cf7f841454271 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 2 May 2025 12:00:02 -0400 Subject: [PATCH 0222/1161] [red-knot] Use `Vec` in `CallArguments`; reuse `self` when we can (#17793) Quick follow-on to #17788. If there is no bound `self` parameter, we can reuse the existing `CallArgument{,Type}s`, and we can use a straight `Vec` instead of a `VecDeque`. --- crates/red_knot_python_semantic/src/types.rs | 30 ++++---- .../src/types/call/arguments.rs | 71 +++++++++---------- .../src/types/call/bind.rs | 59 ++++++++------- 3 files changed, 74 insertions(+), 86 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index a002d0f9ae78ff..ed5ffef2c8c05a 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4470,26 +4470,22 @@ impl<'db> Type<'db> { // An alternative might be to not skip `object.__new__` but instead mark it such that it's // easy to check if that's the one we found? // Note that `__new__` is a static method, so we must inject the `cls` argument. - let new_call_outcome = argument_types.with_self(Some(self_type), |argument_types| { - let new_method = self_type - .find_name_in_mro_with_policy( - db, - "__new__", - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK - | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, - )? - .symbol - .try_call_dunder_get(db, self_type); - - match new_method { + let new_method = self_type.find_name_in_mro_with_policy( + db, + "__new__", + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ); + let new_call_outcome = new_method.and_then(|new_method| { + match new_method.symbol.try_call_dunder_get(db, self_type) { Symbol::Type(new_method, boundness) => { - let result = new_method.try_call(db, &argument_types); - + let result = + new_method.try_call(db, argument_types.with_self(Some(self_type)).as_ref()); if boundness == Boundness::PossiblyUnbound { - return Some(Err(DunderNewCallError::PossiblyUnbound(result.err()))); + Some(Err(DunderNewCallError::PossiblyUnbound(result.err()))) + } else { + Some(result.map_err(DunderNewCallError::CallError)) } - - Some(result.map_err(DunderNewCallError::CallError)) } Symbol::Unbound => None, } diff --git a/crates/red_knot_python_semantic/src/types/call/arguments.rs b/crates/red_knot_python_semantic/src/types/call/arguments.rs index df4408121a0388..23b80528249af8 100644 --- a/crates/red_knot_python_semantic/src/types/call/arguments.rs +++ b/crates/red_knot_python_semantic/src/types/call/arguments.rs @@ -1,30 +1,25 @@ -use std::collections::VecDeque; +use std::borrow::Cow; use std::ops::{Deref, DerefMut}; use super::Type; /// Arguments for a single call, in source order. #[derive(Clone, Debug, Default)] -pub(crate) struct CallArguments<'a>(VecDeque>); +pub(crate) struct CallArguments<'a>(Vec>); impl<'a> CallArguments<'a> { - /// Invoke a function with an optional extra synthetic argument (for a `self` or `cls` - /// parameter) prepended to the front of this argument list. (If `bound_self` is none, the - /// function is invoked with the unmodified argument list.) - pub(crate) fn with_self(&self, bound_self: Option>, f: F) -> R - where - F: FnOnce(&Self) -> R, - { - let mut call_arguments = self.clone(); - - if bound_self.is_some() { - call_arguments.0.push_front(Argument::Synthetic); - } - let result = f(&call_arguments); + /// Prepend an optional extra synthetic argument (for a `self` or `cls` parameter) to the front + /// of this argument list. (If `bound_self` is none, we return the the argument list + /// unmodified.) + pub(crate) fn with_self(&self, bound_self: Option>) -> Cow { if bound_self.is_some() { - call_arguments.0.pop_front(); + let arguments = std::iter::once(Argument::Synthetic) + .chain(self.0.iter().copied()) + .collect(); + Cow::Owned(CallArguments(arguments)) + } else { + Cow::Borrowed(self) } - result } pub(crate) fn len(&self) -> usize { @@ -57,25 +52,23 @@ pub(crate) enum Argument<'a> { } /// Arguments for a single call, in source order, along with inferred types for each argument. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub(crate) struct CallArgumentTypes<'a, 'db> { arguments: CallArguments<'a>, - types: VecDeque>, + types: Vec>, } impl<'a, 'db> CallArgumentTypes<'a, 'db> { /// Create a [`CallArgumentTypes`] with no arguments. pub(crate) fn none() -> Self { - let arguments = CallArguments::default(); - let types = VecDeque::default(); - Self { arguments, types } + Self::default() } /// Create a [`CallArgumentTypes`] from an iterator over non-variadic positional argument /// types. pub(crate) fn positional(positional_tys: impl IntoIterator>) -> Self { - let types: VecDeque<_> = positional_tys.into_iter().collect(); - let arguments = CallArguments(vec![Argument::Positional; types.len()].into()); + let types: Vec<_> = positional_tys.into_iter().collect(); + let arguments = CallArguments(vec![Argument::Positional; types.len()]); Self { arguments, types } } @@ -93,23 +86,23 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> { Self { arguments, types } } - /// Invoke a function with an optional extra synthetic argument (for a `self` or `cls` - /// parameter) prepended to the front of this argument list. (If `bound_self` is none, the - /// function is invoked with the unmodified argument list.) - pub(crate) fn with_self(&self, bound_self: Option>, f: F) -> R - where - F: FnOnce(Self) -> R, - { - let mut call_argument_types = self.clone(); - + /// Prepend an optional extra synthetic argument (for a `self` or `cls` parameter) to the front + /// of this argument list. (If `bound_self` is none, we return the the argument list + /// unmodified.) + pub(crate) fn with_self(&self, bound_self: Option>) -> Cow { if let Some(bound_self) = bound_self { - call_argument_types - .arguments - .0 - .push_front(Argument::Synthetic); - call_argument_types.types.push_front(bound_self); + let arguments = CallArguments( + std::iter::once(Argument::Synthetic) + .chain(self.arguments.0.iter().copied()) + .collect(), + ); + let types = std::iter::once(bound_self) + .chain(self.types.iter().copied()) + .collect(); + Cow::Owned(CallArgumentTypes { arguments, types }) + } else { + Cow::Borrowed(self) } - f(call_argument_types) } pub(crate) fn iter(&self) -> impl Iterator, Type<'db>)> + '_ { diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 1af29eb62e13a0..9b796109744809 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -874,32 +874,32 @@ impl<'db> CallableBinding<'db> { ) -> Self { // If this callable is a bound method, prepend the self instance onto the arguments list // before checking. - arguments.with_self(signature.bound_type, |arguments| { - // TODO: This checks every overload. In the proposed more detailed call checking spec [1], - // arguments are checked for arity first, and are only checked for type assignability against - // the matching overloads. Make sure to implement that as part of separating call binding into - // two phases. - // - // [1] https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation - let overloads = signature - .into_iter() - .map(|signature| { - Binding::match_parameters( - signature, - arguments, - argument_forms, - conflicting_forms, - ) - }) - .collect(); + let arguments = arguments.with_self(signature.bound_type); - CallableBinding { - callable_type: signature.callable_type, - signature_type: signature.signature_type, - dunder_call_is_possibly_unbound: signature.dunder_call_is_possibly_unbound, - overloads, - } - }) + // TODO: This checks every overload. In the proposed more detailed call checking spec [1], + // arguments are checked for arity first, and are only checked for type assignability against + // the matching overloads. Make sure to implement that as part of separating call binding into + // two phases. + // + // [1] https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation + let overloads = signature + .into_iter() + .map(|signature| { + Binding::match_parameters( + signature, + arguments.as_ref(), + argument_forms, + conflicting_forms, + ) + }) + .collect(); + + CallableBinding { + callable_type: signature.callable_type, + signature_type: signature.signature_type, + dunder_call_is_possibly_unbound: signature.dunder_call_is_possibly_unbound, + overloads, + } } fn check_types( @@ -910,11 +910,10 @@ impl<'db> CallableBinding<'db> { ) { // If this callable is a bound method, prepend the self instance onto the arguments list // before checking. - argument_types.with_self(signature.bound_type, |argument_types| { - for (signature, overload) in signature.iter().zip(&mut self.overloads) { - overload.check_types(db, signature, &argument_types); - } - }); + let argument_types = argument_types.with_self(signature.bound_type); + for (signature, overload) in signature.iter().zip(&mut self.overloads) { + overload.check_types(db, signature, argument_types.as_ref()); + } } fn as_result(&self) -> Result<(), CallErrorKind> { From f7cae4ffb52aa2238ebe90b418069bdb36749e9d Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 2 May 2025 22:19:03 +0200 Subject: [PATCH 0223/1161] [red-knot] Don't panic when `primary-span` is missing while panicking (#17799) --- crates/red_knot_python_semantic/src/types/context.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/red_knot_python_semantic/src/types/context.rs b/crates/red_knot_python_semantic/src/types/context.rs index 36e1e919f59d8b..37d16c9f227f68 100644 --- a/crates/red_knot_python_semantic/src/types/context.rs +++ b/crates/red_knot_python_semantic/src/types/context.rs @@ -468,6 +468,11 @@ impl Drop for DiagnosticGuard<'_, '_> { // once. let diag = self.diag.take().unwrap(); + if std::thread::panicking() { + // Don't submit diagnostics when panicking because they might be incomplete. + return; + } + let Some(ann) = diag.primary_annotation() else { panic!( "All diagnostics reported by `InferContext` must have a \ From 96697c98f34e189f7389cfa9b60dd465bb497242 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 2 May 2025 20:34:20 -0400 Subject: [PATCH 0224/1161] [red-knot] Legacy generic classes (#17721) This adds support for legacy generic classes, which use a `typing.Generic` base class, or which inherit from another generic class that has been specialized with legacy typevars. --------- Co-authored-by: Carl Meyer --- .../annotations/stdlib_typing_aliases.md | 51 +- .../annotations/unsupported_special_forms.md | 5 +- .../resources/mdtest/attributes.md | 5 +- .../resources/mdtest/call/methods.md | 2 +- .../resources/mdtest/decorators.md | 4 +- .../resources/mdtest/directives/cast.md | 2 +- .../resources/mdtest/exception/except_star.md | 15 +- .../resources/mdtest/expression/lambda.md | 5 +- .../resources/mdtest/function/parameters.md | 9 +- .../resources/mdtest/generics/builtins.md | 35 ++ .../mdtest/generics/legacy/classes.md | 443 ++++++++++++++++++ .../mdtest/generics/legacy/functions.md | 272 +++++++++++ .../{legacy.md => legacy/variables.md} | 0 .../mdtest/generics/{ => pep695}/classes.md | 96 ++-- .../mdtest/generics/{ => pep695}/functions.md | 6 +- .../{pep695.md => pep695/variables.md} | 0 .../mdtest/generics/{ => pep695}/variance.md | 2 +- .../mdtest/literal/collections/dictionary.md | 2 +- .../resources/mdtest/protocols.md | 4 +- .../mdtest/scopes/moduletype_attrs.md | 8 +- ...cy_syntax_-_Inferring_a_bound_typevar.snap | 90 ++++ ...tax_-_Inferring_a_constrained_typevar.snap | 105 +++++ ...5_syntax_-_Inferring_a_bound_typevar.snap} | 14 +- ...ax_-_Inferring_a_constrained_typevar.snap} | 14 +- .../resources/mdtest/subscript/tuple.md | 2 +- .../type_properties/is_assignable_to.md | 2 - .../resources/primer/bad.txt | 2 + .../resources/primer/good.txt | 2 - crates/red_knot_python_semantic/src/types.rs | 70 +-- .../src/types/call/bind.rs | 21 + .../src/types/class.rs | 89 +++- .../src/types/class_base.rs | 37 +- .../src/types/diagnostic.rs | 34 +- .../src/types/display.rs | 8 +- .../src/types/generics.rs | 53 ++- .../src/types/infer.rs | 159 +++++-- .../src/types/known_instance.rs | 129 ++--- .../src/types/type_ordering.rs | 15 +- .../knot_extensions/knot_extensions.pyi | 4 + knot.schema.json | 10 + 40 files changed, 1547 insertions(+), 279 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/generics/builtins.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/generics/legacy/classes.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md rename crates/red_knot_python_semantic/resources/mdtest/generics/{legacy.md => legacy/variables.md} (100%) rename crates/red_knot_python_semantic/resources/mdtest/generics/{ => pep695}/classes.md (71%) rename crates/red_knot_python_semantic/resources/mdtest/generics/{ => pep695}/functions.md (95%) rename crates/red_knot_python_semantic/resources/mdtest/generics/{pep695.md => pep695/variables.md} (100%) rename crates/red_knot_python_semantic/resources/mdtest/generics/{ => pep695}/variance.md (99%) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap create mode 100644 crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap rename crates/red_knot_python_semantic/resources/mdtest/snapshots/{functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap => functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap} (67%) rename crates/red_knot_python_semantic/resources/mdtest/snapshots/{functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap => functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap} (71%) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md index 4c54139cc47bbc..fe9c54ddd0d421 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -20,7 +20,7 @@ def f( frozen_set_bare: typing.FrozenSet, frozen_set_parametrized: typing.FrozenSet[str], chain_map_bare: typing.ChainMap, - chain_map_parametrized: typing.ChainMap[int], + chain_map_parametrized: typing.ChainMap[str, int], counter_bare: typing.Counter, counter_parametrized: typing.Counter[int], default_dict_bare: typing.DefaultDict, @@ -30,32 +30,45 @@ def f( ordered_dict_bare: typing.OrderedDict, ordered_dict_parametrized: typing.OrderedDict[int, str], ): + # TODO: revealed: list[Unknown] reveal_type(list_bare) # revealed: list + # TODO: revealed: list[int] reveal_type(list_parametrized) # revealed: list - reveal_type(dict_bare) # revealed: dict - reveal_type(dict_parametrized) # revealed: dict + reveal_type(dict_bare) # revealed: dict[Unknown, Unknown] + # TODO: revealed: dict[int, str] + reveal_type(dict_parametrized) # revealed: dict[Unknown, Unknown] + # TODO: revealed: set[Unknown] reveal_type(set_bare) # revealed: set + # TODO: revealed: set[int] reveal_type(set_parametrized) # revealed: set + # TODO: revealed: frozenset[Unknown] reveal_type(frozen_set_bare) # revealed: frozenset + # TODO: revealed: frozenset[str] reveal_type(frozen_set_parametrized) # revealed: frozenset - reveal_type(chain_map_bare) # revealed: ChainMap - reveal_type(chain_map_parametrized) # revealed: ChainMap + reveal_type(chain_map_bare) # revealed: ChainMap[Unknown, Unknown] + # TODO: revealed: ChainMap[str, int] + reveal_type(chain_map_parametrized) # revealed: ChainMap[Unknown, Unknown] - reveal_type(counter_bare) # revealed: Counter - reveal_type(counter_parametrized) # revealed: Counter + reveal_type(counter_bare) # revealed: Counter[Unknown] + # TODO: revealed: Counter[int] + reveal_type(counter_parametrized) # revealed: Counter[Unknown] - reveal_type(default_dict_bare) # revealed: defaultdict - reveal_type(default_dict_parametrized) # revealed: defaultdict + reveal_type(default_dict_bare) # revealed: defaultdict[Unknown, Unknown] + # TODO: revealed: defaultdict[str, int] + reveal_type(default_dict_parametrized) # revealed: defaultdict[Unknown, Unknown] + # TODO: revealed: deque[Unknown] reveal_type(deque_bare) # revealed: deque + # TODO: revealed: deque[str] reveal_type(deque_parametrized) # revealed: deque - reveal_type(ordered_dict_bare) # revealed: OrderedDict - reveal_type(ordered_dict_parametrized) # revealed: OrderedDict + reveal_type(ordered_dict_bare) # revealed: OrderedDict[Unknown, Unknown] + # TODO: revealed: OrderedDict[int, str] + reveal_type(ordered_dict_parametrized) # revealed: OrderedDict[Unknown, Unknown] ``` ## Inheritance @@ -72,19 +85,19 @@ import typing class ListSubclass(typing.List): ... # TODO: generic protocols -# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] +# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]] reveal_type(ListSubclass.__mro__) class DictSubclass(typing.Dict): ... # TODO: generic protocols -# revealed: tuple[Literal[DictSubclass], Literal[dict], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] +# revealed: tuple[Literal[DictSubclass], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]] reveal_type(DictSubclass.__mro__) class SetSubclass(typing.Set): ... # TODO: generic protocols -# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] +# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]] reveal_type(SetSubclass.__mro__) class FrozenSetSubclass(typing.FrozenSet): ... @@ -100,30 +113,30 @@ reveal_type(FrozenSetSubclass.__mro__) class ChainMapSubclass(typing.ChainMap): ... # TODO: generic protocols -# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] +# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap[Unknown, Unknown]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]] reveal_type(ChainMapSubclass.__mro__) class CounterSubclass(typing.Counter): ... # TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[CounterSubclass], Literal[Counter], @Todo(GenericAlias instance), @Todo(`Generic[]` subscript), Literal[object]] +# revealed: tuple[Literal[CounterSubclass], Literal[Counter[Unknown]], Literal[dict[_T, int]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], typing.Generic[_T], Literal[object]] reveal_type(CounterSubclass.__mro__) class DefaultDictSubclass(typing.DefaultDict): ... # TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], @Todo(GenericAlias instance), Literal[object]] +# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict[Unknown, Unknown]], Literal[dict[_KT, _VT]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]] reveal_type(DefaultDictSubclass.__mro__) class DequeSubclass(typing.Deque): ... # TODO: generic protocols -# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] +# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]] reveal_type(DequeSubclass.__mro__) class OrderedDictSubclass(typing.OrderedDict): ... # TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], @Todo(GenericAlias instance), Literal[object]] +# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict[Unknown, Unknown]], Literal[dict[_KT, _VT]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]] reveal_type(OrderedDictSubclass.__mro__) ``` 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 1c7665d84d197f..3f254ebe4737b6 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 @@ -25,10 +25,7 @@ def h() -> TypeIs[int]: ... def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co: # TODO: should understand the annotation reveal_type(args) # revealed: tuple - - # TODO: should understand the annotation - reveal_type(kwargs) # revealed: dict - + reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)] return callback(42, *args, **kwargs) class Foo: diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 17079b86b854f8..72dca325f931f2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1677,7 +1677,7 @@ functions are instances of that class: def f(): ... reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None -reveal_type(f.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None +reveal_type(f.__kwdefaults__) # revealed: dict[str, Any] | None ``` Some attributes are special-cased, however: @@ -1944,7 +1944,8 @@ reveal_type(C.a_float) # revealed: int | float reveal_type(C.a_complex) # revealed: int | float | complex reveal_type(C.a_tuple) # revealed: tuple[int] reveal_type(C.a_range) # revealed: range -reveal_type(C.a_slice) # revealed: slice +# TODO: revealed: slice[Any, Literal[1], Any] +reveal_type(C.a_slice) # revealed: slice[Any, _StartT_co, _StartT_co | _StopT_co] reveal_type(C.a_type) # revealed: type reveal_type(C.a_none) # revealed: None ``` 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 3abc3013722973..c2646aaccd7272 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -94,7 +94,7 @@ function object. We model this explicitly, which means that we can access `__kwd methods, even though it is not available on `types.MethodType`: ```py -reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None +reveal_type(bound_method.__kwdefaults__) # revealed: dict[str, Any] | None ``` ## Basic method calls on class objects and instances diff --git a/crates/red_knot_python_semantic/resources/mdtest/decorators.md b/crates/red_knot_python_semantic/resources/mdtest/decorators.md index b1233f3d0f2109..923bf2e3eb8233 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/decorators.md +++ b/crates/red_knot_python_semantic/resources/mdtest/decorators.md @@ -145,10 +145,10 @@ def f(x: int) -> int: return x**2 # TODO: Should be `_lru_cache_wrapper[int]` -reveal_type(f) # revealed: @Todo(specialized non-generic class) +reveal_type(f) # revealed: _lru_cache_wrapper[_T] # TODO: Should be `int` -reveal_type(f(1)) # revealed: @Todo(specialized non-generic class) +reveal_type(f(1)) # revealed: Unknown ``` ## Lambdas as decorators diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md index 8ff82e29dfef4e..1cfcf5c1a5ffa0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md @@ -61,7 +61,7 @@ from knot_extensions import Unknown def f(x: Any, y: Unknown, z: Any | str | int): a = cast(dict[str, Any], x) - reveal_type(a) # revealed: @Todo(specialized non-generic class) + reveal_type(a) # revealed: dict[str, Any] b = cast(Any, y) reveal_type(b) # revealed: Any diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md b/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md index da1a3de3f040d6..ef75db64822fd1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md @@ -13,8 +13,7 @@ python-version = "3.11" try: help() except* BaseException as e: - # TODO: should be `BaseExceptionGroup[BaseException]` --Alex - reveal_type(e) # revealed: BaseExceptionGroup + reveal_type(e) # revealed: BaseExceptionGroup[BaseException] ``` ## `except*` with specific exception @@ -25,7 +24,7 @@ try: except* OSError as e: # TODO: more precise would be `ExceptionGroup[OSError]` --Alex # (needs homogeneous tuples + generics) - reveal_type(e) # revealed: BaseExceptionGroup + reveal_type(e) # revealed: BaseExceptionGroup[BaseException] ``` ## `except*` with multiple exceptions @@ -36,7 +35,7 @@ try: except* (TypeError, AttributeError) as e: # TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex # (needs homogeneous tuples + generics) - reveal_type(e) # revealed: BaseExceptionGroup + reveal_type(e) # revealed: BaseExceptionGroup[BaseException] ``` ## `except*` with mix of `Exception`s and `BaseException`s @@ -46,7 +45,7 @@ try: help() except* (KeyboardInterrupt, AttributeError) as e: # TODO: more precise would be `BaseExceptionGroup[KeyboardInterrupt | AttributeError]` --Alex - reveal_type(e) # revealed: BaseExceptionGroup + reveal_type(e) # revealed: BaseExceptionGroup[BaseException] ``` ## Invalid `except*` handlers @@ -55,12 +54,10 @@ except* (KeyboardInterrupt, AttributeError) as e: try: help() except* 3 as e: # error: [invalid-exception-caught] - # TODO: Should be `BaseExceptionGroup[Unknown]` --Alex - reveal_type(e) # revealed: BaseExceptionGroup + reveal_type(e) # revealed: BaseExceptionGroup[BaseException] try: help() except* (AttributeError, 42) as e: # error: [invalid-exception-caught] - # TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex - reveal_type(e) # revealed: BaseExceptionGroup + reveal_type(e) # revealed: BaseExceptionGroup[BaseException] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md b/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md index 9db77532cb8a5c..d7d01ed8f575c7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md @@ -83,11 +83,10 @@ Using a variadic parameter: lambda *args: reveal_type(args) # revealed: tuple ``` -Using a keyword-varidic parameter: +Using a keyword-variadic parameter: ```py -# TODO: should be `dict[str, Unknown]` (needs generics) -lambda **kwargs: reveal_type(kwargs) # revealed: dict +lambda **kwargs: reveal_type(kwargs) # revealed: dict[str, Unknown] ``` ## Nested `lambda` expressions 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 a4efccbd3a393c..b4d7b8cd14bc98 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md +++ b/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md @@ -25,12 +25,9 @@ def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, reveal_type(f) # revealed: Literal[4] reveal_type(g) # revealed: Unknown | Literal[5] reveal_type(h) # revealed: Literal[6] - # TODO: should be `tuple[object, ...]` (needs generics) reveal_type(args) # revealed: tuple - - # TODO: should be `dict[str, str]` (needs generics) - reveal_type(kwargs) # revealed: dict + reveal_type(kwargs) # revealed: dict[str, str] ``` ## Unannotated variadic parameters @@ -41,9 +38,7 @@ def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, def g(*args, **kwargs): # TODO: should be `tuple[Unknown, ...]` (needs generics) reveal_type(args) # revealed: tuple - - # TODO: should be `dict[str, Unknown]` (needs generics) - reveal_type(kwargs) # revealed: dict + reveal_type(kwargs) # revealed: dict[str, Unknown] ``` ## Annotation is present but not a fully static type diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/generics/builtins.md new file mode 100644 index 00000000000000..70b9c91902069c --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/builtins.md @@ -0,0 +1,35 @@ +# Generic builtins + +## Variadic keyword arguments with a custom `dict` + +When we define `dict` in a custom typeshed, we must take care to define it as a generic class in the +same way as in the real typeshed. + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +class object: ... +class int: ... +class dict[K, V, Extra]: ... +``` + +`/typeshed/stdlib/typing_extensions.pyi`: + +```pyi +def reveal_type(obj, /): ... +``` + +If we don't, then we won't be able to infer the types of variadic keyword arguments correctly. + +```py +def f(**kwargs): + reveal_type(kwargs) # revealed: Unknown + +def g(**kwargs: int): + reveal_type(kwargs) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/classes.md new file mode 100644 index 00000000000000..935aa2e7d9549d --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -0,0 +1,443 @@ +# Generic classes: Legacy syntax + +## Defining a generic class + +At its simplest, to define a generic class using the legacy syntax, you inherit from the +`typing.Generic` special form, which is "specialized" with the generic class's type variables. + +```py +from knot_extensions import generic_context +from typing import Generic, TypeVar + +T = TypeVar("T") +S = TypeVar("S") + +class SingleTypevar(Generic[T]): ... +class MultipleTypevars(Generic[T, S]): ... + +reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T] +reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S] +``` + +You cannot use the same typevar more than once. + +```py +# TODO: error +class RepeatedTypevar(Generic[T, T]): ... +``` + +You can only specialize `typing.Generic` with typevars (TODO: or param specs or typevar tuples). + +```py +# error: [invalid-argument-type] "`Literal[int]` is not a valid argument to `typing.Generic`" +class GenericOfType(Generic[int]): ... +``` + +You can also define a generic class by inheriting from some _other_ generic class, and specializing +it with typevars. + +```py +class InheritedGeneric(MultipleTypevars[T, S]): ... +class InheritedGenericPartiallySpecialized(MultipleTypevars[T, int]): ... +class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ... + +reveal_type(generic_context(InheritedGeneric)) # revealed: tuple[T, S] +reveal_type(generic_context(InheritedGenericPartiallySpecialized)) # revealed: tuple[T] +reveal_type(generic_context(InheritedGenericFullySpecialized)) # revealed: None +``` + +If you don't specialize a generic base class, we use the default specialization, which maps each +typevar to its default value or `Any`. Since that base class is fully specialized, it does not make +the inheriting class generic. + +```py +class InheritedGenericDefaultSpecialization(MultipleTypevars): ... + +reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None +``` + +When inheriting from a generic class, you can optionally inherit from `typing.Generic` as well. But +if you do, you have to mention all of the typevars that you use in your other base classes. + +```py +class ExplicitInheritedGeneric(MultipleTypevars[T, S], Generic[T, S]): ... + +# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes" +class ExplicitInheritedGenericMissingTypevar(MultipleTypevars[T, S], Generic[T]): ... +class ExplicitInheritedGenericPartiallySpecialized(MultipleTypevars[T, int], Generic[T]): ... +class ExplicitInheritedGenericPartiallySpecializedExtraTypevar(MultipleTypevars[T, int], Generic[T, S]): ... + +# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes" +class ExplicitInheritedGenericPartiallySpecializedMissingTypevar(MultipleTypevars[T, int], Generic[S]): ... + +reveal_type(generic_context(ExplicitInheritedGeneric)) # revealed: tuple[T, S] +reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized)) # revealed: tuple[T] +reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar)) # revealed: tuple[T, S] +``` + +## Specializing generic classes explicitly + +The type parameter can be specified explicitly: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + x: T + +reveal_type(C[int]()) # revealed: C[int] +``` + +The specialization must match the generic types: + +```py +# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2" +reveal_type(C[int, int]()) # revealed: Unknown +``` + +If the type variable has an upper bound, the specialized type must satisfy that bound: + +```py +from typing import Union + +BoundedT = TypeVar("BoundedT", bound=int) +BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str]) + +class Bounded(Generic[BoundedT]): ... +class BoundedByUnion(Generic[BoundedByUnionT]): ... +class IntSubclass(int): ... + +reveal_type(Bounded[int]()) # revealed: Bounded[int] +reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass] + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `str`" +reveal_type(Bounded[str]()) # revealed: Unknown + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`" +reveal_type(Bounded[int | str]()) # revealed: Unknown + +reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int] +reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass] +reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str] +reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str] +``` + +If the type variable is constrained, the specialized type must satisfy those constraints: + +```py +ConstrainedT = TypeVar("ConstrainedT", int, str) + +class Constrained(Generic[ConstrainedT]): ... + +reveal_type(Constrained[int]()) # revealed: Constrained[int] + +# TODO: error: [invalid-argument-type] +# TODO: revealed: Constrained[Unknown] +reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass] + +reveal_type(Constrained[str]()) # revealed: Constrained[str] + +# TODO: error: [invalid-argument-type] +# TODO: revealed: Unknown +reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str] + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int | str`, found `object`" +reveal_type(Constrained[object]()) # revealed: Unknown +``` + +## Inferring generic class parameters + +We can infer the type parameter from a type context: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + x: T + +c: C[int] = C() +# TODO: revealed: C[int] +reveal_type(c) # revealed: C[Unknown] +``` + +The typevars of a fully specialized generic class should no longer be visible: + +```py +# TODO: revealed: int +reveal_type(c.x) # revealed: Unknown +``` + +If the type parameter is not specified explicitly, and there are no constraints that let us infer a +specific type, we infer the typevar's default type: + +```py +DefaultT = TypeVar("DefaultT", default=int) + +class D(Generic[DefaultT]): ... + +reveal_type(D()) # revealed: D[int] +``` + +If a typevar does not provide a default, we use `Unknown`: + +```py +reveal_type(C()) # revealed: C[Unknown] +``` + +## Inferring generic class parameters from constructors + +If the type of a constructor parameter is a class typevar, we can use that to infer the type +parameter. The types inferred from a type context and from a constructor parameter must be +consistent with each other. + +### `__new__` only + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + def __new__(cls, x: T) -> "C[T]": + return object.__new__(cls) + +reveal_type(C(1)) # revealed: C[Literal[1]] + +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") +``` + +### `__init__` only + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[Literal[1]] + +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") +``` + +### Identical `__new__` and `__init__` signatures + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + def __new__(cls, x: T) -> "C[T]": + return object.__new__(cls) + + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[Literal[1]] + +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") +``` + +### Compatible `__new__` and `__init__` signatures + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + def __new__(cls, *args, **kwargs) -> "C[T]": + return object.__new__(cls) + + def __init__(self, x: T) -> None: ... + +reveal_type(C(1)) # revealed: C[Literal[1]] + +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five") + +class D(Generic[T]): + def __new__(cls, x: T) -> "D[T]": + return object.__new__(cls) + + def __init__(self, *args, **kwargs) -> None: ... + +reveal_type(D(1)) # revealed: D[Literal[1]] + +# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`" +wrong_innards: D[int] = D("five") +``` + +### Both present, `__new__` inherited from a generic base class + +If either method comes from a generic base class, we don't currently use its inferred specialization +to specialize the class. + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") +V = TypeVar("V") + +class C(Generic[T, U]): + def __new__(cls, *args, **kwargs) -> "C[T, U]": + return object.__new__(cls) + +class D(C[V, int]): + def __init__(self, x: V) -> None: ... + +reveal_type(D(1)) # revealed: D[Literal[1]] +``` + +### `__init__` is itself generic + +```py +from typing import Generic, TypeVar + +S = TypeVar("S") +T = TypeVar("T") + +class C(Generic[T]): + def __init__(self, x: T, y: S) -> None: ... + +reveal_type(C(1, 1)) # revealed: C[Literal[1]] +reveal_type(C(1, "string")) # revealed: C[Literal[1]] +reveal_type(C(1, True)) # revealed: C[Literal[1]] + +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" +wrong_innards: C[int] = C("five", 1) +``` + +## Generic subclass + +When a generic subclass fills its superclass's type parameter with one of its own, the actual types +propagate through: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): + x: T | None = None + +class ExplicitlyGenericSub(Base[T], Generic[T]): ... +class ImplicitlyGenericSub(Base[T]): ... + +reveal_type(Base[int].x) # revealed: int | None +reveal_type(ExplicitlyGenericSub[int].x) # revealed: int | None +reveal_type(ImplicitlyGenericSub[int].x) # revealed: int | None +``` + +## Generic methods + +Generic classes can contain methods that are themselves generic. The generic methods can refer to +the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in +scope for the method. + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class C(Generic[T]): + def method(self, u: U) -> U: + return u + +c: C[int] = C[int]() +reveal_type(c.method("string")) # revealed: Literal["string"] +``` + +## Cyclic class definitions + +### F-bounded quantification + +A class can use itself as the type parameter of one of its superclasses. (This is also known as the +[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].) + +#### In a stub file + +Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). + +```pyi +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): ... +class Sub(Base[Sub]): ... + +reveal_type(Sub) # revealed: Literal[Sub] +``` + +#### With string forward references + +A similar case can work in a non-stub file, if forward references are stringified: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): ... +class Sub(Base["Sub"]): ... + +reveal_type(Sub) # revealed: Literal[Sub] +``` + +#### Without string forward references + +In a non-stub file, without stringified forward references, this raises a `NameError`: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): ... + +# error: [unresolved-reference] +class Sub(Base[Sub]): ... +``` + +### Cyclic inheritance as a generic parameter + +```pyi +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Derived(list[Derived[T]], Generic[T]): ... +``` + +### Direct cyclic inheritance + +Inheritance that would result in a cyclic MRO is detected as an error. + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +# error: [unresolved-reference] +class C(C, Generic[T]): ... + +# error: [unresolved-reference] +class D(D[int], Generic[T]): ... +``` + +[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern +[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md new file mode 100644 index 00000000000000..9f20d754cb9d83 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -0,0 +1,272 @@ +# Generic functions: Legacy syntax + +## 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 +`object` (or the typevar's upper bound): + +```py +from typing import TypeVar + +T = TypeVar("T") + +# TODO: error, should be (x: object) +def typevar_not_needed(x: T) -> None: + pass + +BoundedT = TypeVar("BoundedT", bound=int) + +# TODO: error, should be (x: int) +def bounded_typevar_not_needed(x: BoundedT) -> None: + pass +``` + +Typevars are only needed if you use them more than once. For instance, to specify that two +parameters must both have the same type: + +```py +def two_params(x: T, y: T) -> T: + return x +``` + +or to specify that a return value is the same as a parameter: + +```py +def return_value(x: T) -> T: + return x +``` + +Each typevar must also appear _somewhere_ in the parameter list: + +```py +def absurd() -> T: + # There's no way to construct a T! + raise ValueError("absurd") +``` + +## Inferring generic function parameter types + +If the type of a generic function parameter is a typevar, then we can infer what type that typevar +is bound to at each call site. + +```py +from typing import TypeVar + +T = TypeVar("T") + +def f(x: T) -> T: + return x + +reveal_type(f(1)) # revealed: Literal[1] +reveal_type(f(1.0)) # revealed: float +reveal_type(f(True)) # revealed: Literal[True] +reveal_type(f("string")) # revealed: Literal["string"] +``` + +## Inferring “deep” generic parameter types + +The matching up of call arguments and discovery of constraints on typevars can be a recursive +process for arbitrarily-nested generic types in parameters. + +```py +from typing import TypeVar + +T = TypeVar("T") + +def f(x: list[T]) -> T: + return x[0] + +# TODO: revealed: float +reveal_type(f([1.0, 2.0])) # revealed: Unknown +``` + +## Inferring a bound typevar + + + +```py +from typing import TypeVar +from typing_extensions import reveal_type + +T = TypeVar("T", bound=int) + +def f(x: T) -> T: + return x + +reveal_type(f(1)) # revealed: Literal[1] +reveal_type(f(True)) # revealed: Literal[True] +# error: [invalid-argument-type] +reveal_type(f("string")) # revealed: Unknown +``` + +## Inferring a constrained typevar + + + +```py +from typing import TypeVar +from typing_extensions import reveal_type + +T = TypeVar("T", int, None) + +def f(x: T) -> T: + return x + +reveal_type(f(1)) # revealed: int +reveal_type(f(True)) # revealed: int +reveal_type(f(None)) # revealed: None +# error: [invalid-argument-type] +reveal_type(f("string")) # revealed: Unknown +``` + +## Typevar constraints + +If a type parameter has an upper bound, that upper bound constrains which types can be used for that +typevar. This effectively adds the upper bound as an intersection to every appearance of the typevar +in the function. + +```py +from typing import TypeVar + +T = TypeVar("T", bound=int) + +def good_param(x: T) -> None: + reveal_type(x) # revealed: T +``` + +If the function is annotated as returning the typevar, this means that the upper bound is _not_ +assignable to that typevar, since return types are contravariant. In `bad`, we can infer that +`x + 1` has type `int`. But `T` might be instantiated with a narrower type than `int`, and so the +return value is not guaranteed to be compatible for all `T: int`. + +```py +def good_return(x: T) -> T: + return x + +def bad_return(x: T) -> T: + # error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `int`" + return x + 1 +``` + +## All occurrences of the same typevar have the same type + +If a typevar appears multiple times in a function signature, all occurrences have the same type. + +```py +from typing import TypeVar + +T = TypeVar("T") +S = TypeVar("S") + +def different_types(cond: bool, t: T, s: S) -> T: + if cond: + return t + else: + # error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `S`" + return s + +def same_types(cond: bool, t1: T, t2: T) -> T: + if cond: + return t1 + else: + return t2 +``` + +## All occurrences of the same constrained typevar have the same type + +The above is true even when the typevars are constrained. Here, both `int` and `str` have `__add__` +methods that are compatible with the return type, so the `return` expression is always well-typed: + +```py +from typing import TypeVar + +T = TypeVar("T", int, str) + +def same_constrained_types(t1: T, t2: T) -> T: + # TODO: no error + # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`" + return t1 + t2 +``` + +This is _not_ the same as a union type, because of this additional constraint that the two +occurrences have the same type. In `unions_are_different`, `t1` and `t2` might have different types, +and an `int` and a `str` cannot be added together: + +```py +def unions_are_different(t1: int | str, t2: int | str) -> int | str: + # error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`" + return t1 + t2 +``` + +## Typevar inference is a unification problem + +When inferring typevar assignments in a generic function call, we cannot simply solve constraints +eagerly for each parameter in turn. We must solve a unification problem involving all of the +parameters simultaneously. + +```py +from typing import TypeVar + +T = TypeVar("T") + +def two_params(x: T, y: T) -> T: + return x + +reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"] +reveal_type(two_params("a", 1)) # revealed: Literal["a", 1] +``` + +When one of the parameters is a union, we attempt to find the smallest specialization that satisfies +all of the constraints. + +```py +def union_param(x: T | None) -> T: + if x is None: + raise ValueError + return x + +reveal_type(union_param("a")) # revealed: Literal["a"] +reveal_type(union_param(1)) # revealed: Literal[1] +reveal_type(union_param(None)) # revealed: Unknown +``` + +```py +def union_and_nonunion_params(x: T | int, y: T) -> T: + return y + +reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"] +reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"] +reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1] +reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1] +reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1] +``` + +```py +S = TypeVar("S") + +def tuple_param(x: T | S, y: tuple[T, S]) -> tuple[T, S]: + return y + +reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] +reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]] +``` + +## Inferring nested generic function calls + +We can infer type assignments in nested calls to multiple generic functions. If they use the same +type variable, we do not confuse the two; `T@f` and `T@g` have separate types in each example below. + +```py +from typing import TypeVar + +T = TypeVar("T") + +def f(x: T) -> tuple[T, int]: + return (x, 1) + +def g(x: T) -> T | None: + return x + +reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] +reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/variables.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md rename to crates/red_knot_python_semantic/resources/mdtest/generics/legacy/variables.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/classes.md similarity index 71% rename from crates/red_knot_python_semantic/resources/mdtest/generics/classes.md rename to crates/red_knot_python_semantic/resources/mdtest/generics/pep695/classes.md index 82b2b8f7eb5b21..400f4dc8d4ce50 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -1,60 +1,73 @@ -# Generic classes +# Generic classes: PEP 695 syntax ```toml [environment] python-version = "3.13" ``` -## PEP 695 syntax +## Defining a generic class -TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic. - -This is a generic class defined using PEP 695 syntax: +At its simplest, to define a generic class using PEP 695 syntax, you add a list of typevars after +the class name. ```py -class C[T]: ... +from knot_extensions import generic_context + +class SingleTypevar[T]: ... +class MultipleTypevars[T, S]: ... + +reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T] +reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S] ``` -A class that inherits from a generic class, and fills its type parameters with typevars, is generic: +You cannot use the same typevar more than once. ```py -class D[U](C[U]): ... +# error: [invalid-syntax] "duplicate type parameter" +class RepeatedTypevar[T, T]: ... ``` -A class that inherits from a generic class, but fills its type parameters with concrete types, is -_not_ generic: +You can only use typevars (TODO: or param specs or typevar tuples) in the class's generic context. ```py -class E(C[int]): ... +# TODO: error +class GenericOfType[int]: ... ``` -A class that inherits from a generic class, and doesn't fill its type parameters at all, implicitly -uses the default value for the typevar. In this case, that default type is `Unknown`, so `F` -inherits from `C[Unknown]` and is not itself generic. +You can also define a generic class by inheriting from some _other_ generic class, and specializing +it with typevars. With PEP 695 syntax, you must explicitly list all of the typevars that you use in +your base classes. ```py -class F(C): ... -``` +class InheritedGeneric[U, V](MultipleTypevars[U, V]): ... +class InheritedGenericPartiallySpecialized[U](MultipleTypevars[U, int]): ... +class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ... -## Legacy syntax +reveal_type(generic_context(InheritedGeneric)) # revealed: tuple[U, V] +reveal_type(generic_context(InheritedGenericPartiallySpecialized)) # revealed: tuple[U] +reveal_type(generic_context(InheritedGenericFullySpecialized)) # revealed: None +``` -This is a generic class defined using the legacy syntax: +If you don't specialize a generic base class, we use the default specialization, which maps each +typevar to its default value or `Any`. Since that base class is fully specialized, it does not make +the inheriting class generic. ```py -from typing import Generic, TypeVar +class InheritedGenericDefaultSpecialization(MultipleTypevars): ... -T = TypeVar("T") - -class C(Generic[T]): ... +reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None ``` -A class that inherits from a generic class, and fills its type parameters with typevars, is generic. +You cannot use PEP-695 syntax and the legacy syntax in the same class definition. ```py -class D(C[T]): ... -``` +from typing import Generic, TypeVar -(Examples `E` and `F` from above do not have analogues in the legacy syntax.) +T = TypeVar("T") + +# error: [invalid-generic-class] "Cannot both inherit from `Generic` and use PEP 695 type variables" +class BothGenericSyntaxes[U](Generic[T]): ... +``` ## Specializing generic classes explicitly @@ -84,10 +97,12 @@ class IntSubclass(int): ... reveal_type(Bounded[int]()) # revealed: Bounded[int] reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass] +# TODO: update this diagnostic to talk about type parameters and specializations # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `str`" reveal_type(Bounded[str]()) # revealed: Unknown -# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`" +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`" reveal_type(Bounded[int | str]()) # revealed: Unknown reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int] @@ -113,6 +128,7 @@ reveal_type(Constrained[str]()) # revealed: Constrained[str] # TODO: revealed: Unknown reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str] +# TODO: update this diagnostic to talk about type parameters and specializations # error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int | str`, found `object`" reveal_type(Constrained[object]()) # revealed: Unknown ``` @@ -158,7 +174,7 @@ If the type of a constructor parameter is a class typevar, we can use that to in parameter. The types inferred from a type context and from a constructor parameter must be consistent with each other. -## `__new__` only +### `__new__` only ```py class C[T]: @@ -171,7 +187,7 @@ reveal_type(C(1)) # revealed: C[Literal[1]] wrong_innards: C[int] = C("five") ``` -## `__init__` only +### `__init__` only ```py class C[T]: @@ -183,7 +199,7 @@ reveal_type(C(1)) # revealed: C[Literal[1]] wrong_innards: C[int] = C("five") ``` -## Identical `__new__` and `__init__` signatures +### Identical `__new__` and `__init__` signatures ```py class C[T]: @@ -198,7 +214,7 @@ reveal_type(C(1)) # revealed: C[Literal[1]] wrong_innards: C[int] = C("five") ``` -## Compatible `__new__` and `__init__` signatures +### Compatible `__new__` and `__init__` signatures ```py class C[T]: @@ -224,9 +240,23 @@ reveal_type(D(1)) # revealed: D[Literal[1]] wrong_innards: D[int] = D("five") ``` -## `__init__` is itself generic +### Both present, `__new__` inherited from a generic base class + +If either method comes from a generic base class, we don't currently use its inferred specialization +to specialize the class. + +```py +class C[T, U]: + def __new__(cls, *args, **kwargs) -> "C[T, U]": + return object.__new__(cls) + +class D[V](C[V, int]): + def __init__(self, x: V) -> None: ... + +reveal_type(D(1)) # revealed: D[Literal[1]] +``` -TODO: These do not currently work yet, because we don't correctly model the nested generic contexts. +### `__init__` is itself generic ```py class C[T]: diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/generics/functions.md rename to crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md index 22c62b1d35f398..ee5ed72c98b9b4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -1,4 +1,4 @@ -# Generic functions +# Generic functions: PEP 695 syntax ```toml [environment] @@ -83,7 +83,7 @@ def f[T: int](x: T) -> T: reveal_type(f(1)) # revealed: Literal[1] reveal_type(f(True)) # revealed: Literal[True] -# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`" +# error: [invalid-argument-type] reveal_type(f("string")) # revealed: Unknown ``` @@ -100,7 +100,7 @@ def f[T: (int, None)](x: T) -> T: reveal_type(f(1)) # revealed: int reveal_type(f(True)) # revealed: int reveal_type(f(None)) # revealed: None -# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`" +# error: [invalid-argument-type] reveal_type(f("string")) # revealed: Unknown ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variables.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md rename to crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variables.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/variance.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variance.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/generics/variance.md rename to crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variance.md index 1541a46fbca738..489e74a77e8168 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/variance.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -1,4 +1,4 @@ -# Variance +# Variance: PEP 695 syntax ```toml [environment] diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md index 9a57135f2a3434..37abd2b98bb201 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md @@ -3,5 +3,5 @@ ## Empty dictionary ```py -reveal_type({}) # revealed: dict +reveal_type({}) # revealed: dict[Unknown, Unknown] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 30afd66e7c4df4..7f681a23c450d6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -70,8 +70,8 @@ simultaneously: class DuplicateBases(Protocol, Protocol[T]): x: T -# TODO: should not have `Generic` multiple times and `Protocol` multiple times -# revealed: tuple[Literal[DuplicateBases], typing.Protocol, typing.Generic, @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] +# TODO: should not have `Protocol` multiple times +# revealed: tuple[Literal[DuplicateBases], typing.Protocol, @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]] reveal_type(DuplicateBases.__mro__) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md index 8cbe44952647b3..f4fde3a2bbe612 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -58,8 +58,7 @@ reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: ob reveal_type(typing.__class__) # revealed: Literal[ModuleType] -# TODO: needs support generics; should be `dict[str, Any]`: -reveal_type(typing.__dict__) # revealed: @Todo(specialized non-generic class) +reveal_type(typing.__dict__) # revealed: dict[str, Any] ``` Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with @@ -91,9 +90,8 @@ reveal_type(__dict__) # revealed: Literal["foo"] import foo from foo import __dict__ as foo_dict -# TODO: needs support generics; should be `dict[str, Any]` for both of these: -reveal_type(foo.__dict__) # revealed: @Todo(specialized non-generic class) -reveal_type(foo_dict) # revealed: @Todo(specialized non-generic class) +reveal_type(foo.__dict__) # revealed: dict[str, Any] +reveal_type(foo_dict) # revealed: dict[str, Any] ``` ## Conditionally global or `ModuleType` attribute diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap new file mode 100644 index 00000000000000..902b25a6e7acb5 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap @@ -0,0 +1,90 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: functions.md - Generic functions: Legacy syntax - Inferring a bound typevar +mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar + 2 | from typing_extensions import reveal_type + 3 | + 4 | T = TypeVar("T", bound=int) + 5 | + 6 | def f(x: T) -> T: + 7 | return x + 8 | + 9 | reveal_type(f(1)) # revealed: Literal[1] +10 | reveal_type(f(True)) # revealed: Literal[True] +11 | # error: [invalid-argument-type] +12 | reveal_type(f("string")) # revealed: Unknown +``` + +# Diagnostics + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:9:1 + | + 7 | return x + 8 | + 9 | reveal_type(f(1)) # revealed: Literal[1] + | ^^^^^^^^^^^^^^^^^ `Literal[1]` +10 | reveal_type(f(True)) # revealed: Literal[True] +11 | # error: [invalid-argument-type] + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:10:1 + | + 9 | reveal_type(f(1)) # revealed: Literal[1] +10 | reveal_type(f(True)) # revealed: Literal[True] + | ^^^^^^^^^^^^^^^^^^^^ `Literal[True]` +11 | # error: [invalid-argument-type] +12 | reveal_type(f("string")) # revealed: Unknown + | + +``` + +``` +error: lint:invalid-argument-type: Argument to this function is incorrect + --> src/mdtest_snippet.py:12:15 + | +10 | reveal_type(f(True)) # revealed: Literal[True] +11 | # error: [invalid-argument-type] +12 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound of type variable `T` + | +info: Type variable defined here + --> src/mdtest_snippet.py:4:1 + | +2 | from typing_extensions import reveal_type +3 | +4 | T = TypeVar("T", bound=int) + | ^ +5 | +6 | def f(x: T) -> T: + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:12:1 + | +10 | reveal_type(f(True)) # revealed: Literal[True] +11 | # error: [invalid-argument-type] +12 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap new file mode 100644 index 00000000000000..f84cf8c7831869 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap @@ -0,0 +1,105 @@ +--- +source: crates/red_knot_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: functions.md - Generic functions: Legacy syntax - Inferring a constrained typevar +mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import TypeVar + 2 | from typing_extensions import reveal_type + 3 | + 4 | T = TypeVar("T", int, None) + 5 | + 6 | def f(x: T) -> T: + 7 | return x + 8 | + 9 | reveal_type(f(1)) # revealed: int +10 | reveal_type(f(True)) # revealed: int +11 | reveal_type(f(None)) # revealed: None +12 | # error: [invalid-argument-type] +13 | reveal_type(f("string")) # revealed: Unknown +``` + +# Diagnostics + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:9:1 + | + 7 | return x + 8 | + 9 | reveal_type(f(1)) # revealed: int + | ^^^^^^^^^^^^^^^^^ `int` +10 | reveal_type(f(True)) # revealed: int +11 | reveal_type(f(None)) # revealed: None + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:10:1 + | + 9 | reveal_type(f(1)) # revealed: int +10 | reveal_type(f(True)) # revealed: int + | ^^^^^^^^^^^^^^^^^^^^ `int` +11 | reveal_type(f(None)) # revealed: None +12 | # error: [invalid-argument-type] + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:11:1 + | + 9 | reveal_type(f(1)) # revealed: int +10 | reveal_type(f(True)) # revealed: int +11 | reveal_type(f(None)) # revealed: None + | ^^^^^^^^^^^^^^^^^^^^ `None` +12 | # error: [invalid-argument-type] +13 | reveal_type(f("string")) # revealed: Unknown + | + +``` + +``` +error: lint:invalid-argument-type: Argument to this function is incorrect + --> src/mdtest_snippet.py:13:15 + | +11 | reveal_type(f(None)) # revealed: None +12 | # error: [invalid-argument-type] +13 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints of type variable `T` + | +info: Type variable defined here + --> src/mdtest_snippet.py:4:1 + | +2 | from typing_extensions import reveal_type +3 | +4 | T = TypeVar("T", int, None) + | ^ +5 | +6 | def f(x: T) -> T: + | + +``` + +``` +info: revealed-type: Revealed type + --> src/mdtest_snippet.py:13:1 + | +11 | reveal_type(f(None)) # revealed: None +12 | # error: [invalid-argument-type] +13 | reveal_type(f("string")) # revealed: Unknown + | ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap similarity index 67% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap rename to crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap index 1fcfed30588f79..5c03b2551b6a60 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap @@ -3,8 +3,8 @@ source: crates/red_knot_test/src/lib.rs expression: snapshot --- --- -mdtest name: functions.md - Generic functions - Inferring a bound typevar -mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +mdtest name: functions.md - Generic functions: PEP 695 syntax - Inferring a bound typevar +mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md --- # Python source files @@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions 5 | 6 | reveal_type(f(1)) # revealed: Literal[1] 7 | reveal_type(f(True)) # revealed: Literal[True] -8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`" +8 | # error: [invalid-argument-type] 9 | reveal_type(f("string")) # revealed: Unknown ``` @@ -34,7 +34,7 @@ info: revealed-type: Revealed type 6 | reveal_type(f(1)) # revealed: Literal[1] | ^^^^^^^^^^^^^^^^^ `Literal[1]` 7 | reveal_type(f(True)) # revealed: Literal[True] -8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bo... +8 | # error: [invalid-argument-type] | ``` @@ -46,7 +46,7 @@ info: revealed-type: Revealed type 6 | reveal_type(f(1)) # revealed: Literal[1] 7 | reveal_type(f(True)) # revealed: Literal[True] | ^^^^^^^^^^^^^^^^^^^^ `Literal[True]` -8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b... +8 | # error: [invalid-argument-type] 9 | reveal_type(f("string")) # revealed: Unknown | @@ -57,7 +57,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect --> src/mdtest_snippet.py:9:15 | 7 | reveal_type(f(True)) # revealed: Literal[True] -8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b... +8 | # error: [invalid-argument-type] 9 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound of type variable `T` | @@ -78,7 +78,7 @@ info: revealed-type: Revealed type --> src/mdtest_snippet.py:9:1 | 7 | reveal_type(f(True)) # revealed: Literal[True] -8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b... +8 | # error: [invalid-argument-type] 9 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown` | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap similarity index 71% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap rename to crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap index 2e2802007b5b10..d1b259b7303546 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap @@ -3,8 +3,8 @@ source: crates/red_knot_test/src/lib.rs expression: snapshot --- --- -mdtest name: functions.md - Generic functions - Inferring a constrained typevar -mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +mdtest name: functions.md - Generic functions: PEP 695 syntax - Inferring a constrained typevar +mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md --- # Python source files @@ -20,7 +20,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions 6 | reveal_type(f(1)) # revealed: int 7 | reveal_type(f(True)) # revealed: int 8 | reveal_type(f(None)) # revealed: None - 9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`" + 9 | # error: [invalid-argument-type] 10 | reveal_type(f("string")) # revealed: Unknown ``` @@ -48,7 +48,7 @@ info: revealed-type: Revealed type 7 | reveal_type(f(True)) # revealed: int | ^^^^^^^^^^^^^^^^^^^^ `int` 8 | reveal_type(f(None)) # revealed: None -9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra... +9 | # error: [invalid-argument-type] | ``` @@ -61,7 +61,7 @@ info: revealed-type: Revealed type 7 | reveal_type(f(True)) # revealed: int 8 | reveal_type(f(None)) # revealed: None | ^^^^^^^^^^^^^^^^^^^^ `None` - 9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra... + 9 | # error: [invalid-argument-type] 10 | reveal_type(f("string")) # revealed: Unknown | @@ -72,7 +72,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect --> src/mdtest_snippet.py:10:15 | 8 | reveal_type(f(None)) # revealed: None - 9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra... + 9 | # error: [invalid-argument-type] 10 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints of type variable `T` | @@ -93,7 +93,7 @@ info: revealed-type: Revealed type --> src/mdtest_snippet.py:10:1 | 8 | reveal_type(f(None)) # revealed: None - 9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra... + 9 | # error: [invalid-argument-type] 10 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown` | diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index 04b39d1f504ec1..f07efe0e2fca35 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -116,6 +116,6 @@ from typing import Tuple class C(Tuple): ... # TODO: generic protocols -# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] +# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]] reveal_type(C.__mro__) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 68c7e8ce1a6ab8..840c4f48e343c1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -595,8 +595,6 @@ from functools import partial def f(x: int, y: str) -> None: ... -# TODO: no error -# error: [invalid-assignment] "Object of type `partial` is not assignable to `(int, /) -> None`" c1: Callable[[int], None] = partial(f, y="a") ``` diff --git a/crates/red_knot_python_semantic/resources/primer/bad.txt b/crates/red_knot_python_semantic/resources/primer/bad.txt index bf180717b9519a..4d2f648ab280e9 100644 --- a/crates/red_knot_python_semantic/resources/primer/bad.txt +++ b/crates/red_knot_python_semantic/resources/primer/bad.txt @@ -17,6 +17,7 @@ pandas # slow pandas-stubs # cycle panics (try_metaclass_) pandera # cycle panics (try_metaclass_) prefect # slow +pylint # cycle panics (self-recursive type alias) pytest # cycle panics (signature_) pywin32 # bad use-def map (binding with definitely-visible unbound) schemathesis # cycle panics (signature_) @@ -27,4 +28,5 @@ spark # cycle panics (try_metaclass_) steam.py # cycle panics (try_metaclass_), often hangs when multi-threaded streamlit # cycle panic (signature_) sympy # stack overflow +trio # cycle panics (deferred annotatation resolving in wrong scope) xarray # cycle panics (try_metaclass_) diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/red_knot_python_semantic/resources/primer/good.txt index 0455d5c8fe9e6e..219abfbf11503d 100644 --- a/crates/red_knot_python_semantic/resources/primer/good.txt +++ b/crates/red_knot_python_semantic/resources/primer/good.txt @@ -78,7 +78,6 @@ pycryptodome pydantic pyinstrument pyjwt -pylint pylox pyodide pyp @@ -101,7 +100,6 @@ static-frame stone strawberry tornado -trio twine typeshed-stats urllib3 diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ed5ffef2c8c05a..a519c44fc30b95 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -589,11 +589,7 @@ impl<'db> Type<'db> { pub fn contains_todo(&self, db: &'db dyn Db) -> bool { match self { - Self::Dynamic( - DynamicType::Todo(_) - | DynamicType::SubscriptedProtocol - | DynamicType::SubscriptedGeneric, - ) => true, + Self::Dynamic(DynamicType::Todo(_) | DynamicType::SubscriptedProtocol) => true, Self::AlwaysFalsy | Self::AlwaysTruthy @@ -636,9 +632,7 @@ impl<'db> Type<'db> { Self::SubclassOf(subclass_of) => match subclass_of.subclass_of() { SubclassOfInner::Dynamic( - DynamicType::Todo(_) - | DynamicType::SubscriptedProtocol - | DynamicType::SubscriptedGeneric, + DynamicType::Todo(_) | DynamicType::SubscriptedProtocol, ) => true, SubclassOfInner::Dynamic(DynamicType::Unknown | DynamicType::Any) => false, SubclassOfInner::Class(_) => false, @@ -656,17 +650,11 @@ impl<'db> Type<'db> { Self::BoundSuper(bound_super) => { matches!( bound_super.pivot_class(db), - ClassBase::Dynamic( - DynamicType::Todo(_) - | DynamicType::SubscriptedGeneric - | DynamicType::SubscriptedProtocol - ) + ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::SubscriptedProtocol) ) || matches!( bound_super.owner(db), SuperOwnerKind::Dynamic( - DynamicType::Todo(_) - | DynamicType::SubscriptedGeneric - | DynamicType::SubscriptedProtocol + DynamicType::Todo(_) | DynamicType::SubscriptedProtocol ) ) } @@ -4432,18 +4420,19 @@ impl<'db> Type<'db> { // have the class's typevars still in the method signature when we attempt to call it. To // do this, we instead use the _identity_ specialization, which maps each of the class's // generic typevars to itself. - let (generic_origin, self_type) = match self { + let (generic_origin, generic_context, self_type) = match self { Type::ClassLiteral(class) => match class.generic_context(db) { Some(generic_context) => { let specialization = generic_context.identity_specialization(db); ( Some(class), + Some(generic_context), Type::GenericAlias(GenericAlias::new(db, class, specialization)), ) } - _ => (None, self), + _ => (None, None, self), }, - _ => (None, self), + _ => (None, None, self), }; // As of now we do not model custom `__call__` on meta-classes, so the code below @@ -4555,12 +4544,18 @@ impl<'db> Type<'db> { .and_then(Result::ok) .as_ref() .and_then(Bindings::single_element) - .and_then(|binding| combine_binding_specialization(db, binding)); + .and_then(|binding| combine_binding_specialization(db, binding)) + .filter(|specialization| { + Some(specialization.generic_context(db)) == generic_context + }); let init_specialization = init_call_outcome .and_then(Result::ok) .as_ref() .and_then(Bindings::single_element) - .and_then(|binding| combine_binding_specialization(db, binding)); + .and_then(|binding| combine_binding_specialization(db, binding)) + .filter(|specialization| { + Some(specialization.generic_context(db)) == generic_context + }); let specialization = combine_specializations(db, new_specialization, init_specialization); let specialized = specialization @@ -4741,7 +4736,7 @@ impl<'db> Type<'db> { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol], fallback_type: Type::unknown(), }), - KnownInstanceType::Generic => Err(InvalidTypeExpressionError { + KnownInstanceType::Generic(_) => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Generic], fallback_type: Type::unknown(), }), @@ -5141,6 +5136,10 @@ impl<'db> Type<'db> { } } + Type::GenericAlias(alias) => { + alias.specialization(db).find_legacy_typevars(db, typevars); + } + Type::Dynamic(_) | Type::Never | Type::AlwaysTruthy @@ -5151,7 +5150,6 @@ impl<'db> Type<'db> { | Type::DataclassTransformer(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) - | Type::GenericAlias(_) | Type::SubclassOf(_) | Type::IntLiteral(_) | Type::BooleanLiteral(_) @@ -5176,7 +5174,10 @@ impl<'db> Type<'db> { match self { Type::IntLiteral(_) | Type::BooleanLiteral(_) => self.repr(db), Type::StringLiteral(_) | Type::LiteralString => *self, - Type::KnownInstance(known_instance) => Type::string_literal(db, known_instance.repr()), + Type::KnownInstance(known_instance) => Type::StringLiteral(StringLiteralType::new( + db, + known_instance.repr(db).to_string().into_boxed_str(), + )), // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), } @@ -5194,7 +5195,10 @@ impl<'db> Type<'db> { Type::string_literal(db, &format!("'{}'", literal.value(db).escape_default())) } Type::LiteralString => Type::LiteralString, - Type::KnownInstance(known_instance) => Type::string_literal(db, known_instance.repr()), + Type::KnownInstance(known_instance) => Type::StringLiteral(StringLiteralType::new( + db, + known_instance.repr(db).to_string().into_boxed_str(), + )), // TODO: handle more complex types _ => KnownClass::Str.to_instance(db), } @@ -5390,9 +5394,6 @@ pub enum DynamicType { /// Temporary type until we support generic protocols. /// We use a separate variant (instead of `Todo(…)`) in order to be able to match on them explicitly. SubscriptedProtocol, - /// Temporary type until we support old-style generics. - /// We use a separate variant (instead of `Todo(…)`) in order to be able to match on them explicitly. - SubscriptedGeneric, } impl std::fmt::Display for DynamicType { @@ -5408,11 +5409,6 @@ impl std::fmt::Display for DynamicType { } else { "@Todo" }), - DynamicType::SubscriptedGeneric => f.write_str(if cfg!(debug_assertions) { - "@Todo(`Generic[]` subscript)" - } else { - "@Todo" - }), } } } @@ -5568,12 +5564,12 @@ impl<'db> InvalidTypeExpression<'db> { InvalidTypeExpression::TypeQualifier(qualifier) => write!( f, "Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions)", - q = qualifier.repr() + q = qualifier.repr(self.db) ), InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) => write!( f, "Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)", - q = qualifier.repr() + q = qualifier.repr(self.db) ), InvalidTypeExpression::InvalidType(ty) => write!( f, @@ -6932,6 +6928,8 @@ pub enum KnownFunction { IsSingleton, /// `knot_extensions.is_single_valued` IsSingleValued, + /// `knot_extensions.generic_context` + GenericContext, } impl KnownFunction { @@ -6987,6 +6985,7 @@ impl KnownFunction { | Self::IsSingleValued | Self::IsSingleton | Self::IsSubtypeOf + | Self::GenericContext | Self::StaticAssert => module.is_knot_extensions(), } } @@ -8383,6 +8382,7 @@ pub(crate) mod tests { KnownFunction::IsSingleton | KnownFunction::IsSubtypeOf + | KnownFunction::GenericContext | KnownFunction::StaticAssert | KnownFunction::IsFullyStatic | KnownFunction::IsDisjointFrom diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 9b796109744809..3921ae1e679170 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -564,6 +564,27 @@ impl<'db> Bindings<'db> { } } + Some(KnownFunction::GenericContext) => { + if let [Some(ty)] = overload.parameter_types() { + // TODO: Handle generic functions, and unions/intersections of + // generic types + overload.set_return_type(match ty { + Type::ClassLiteral(class) => match class.generic_context(db) { + Some(generic_context) => TupleType::from_elements( + db, + generic_context + .variables(db) + .iter() + .map(|typevar| Type::TypeVar(*typevar)), + ), + None => Type::none(db), + }, + + _ => Type::none(db), + }); + } + } + Some(KnownFunction::Len) => { if let [Some(first_arg)] = overload.parameter_types() { if let Some(len_ty) = first_arg.len(db) { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index d118d28af2ea6d..cc77fdd4d3be12 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -454,8 +454,26 @@ impl<'db> ClassLiteral<'db> { self.known(db) == Some(known_class) } - #[salsa::tracked] pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + // Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code + // the knowledge that this class is not generic. + if self.is_known(db, KnownClass::VersionInfo) { + return None; + } + + // We've already verified that the class literal does not contain both a PEP-695 generic + // scope and a `typing.Generic` base class. + // + // Note that if a class has an explicit legacy generic context (by inheriting from + // `typing.Generic`), and also an implicit one (by inheriting from other generic classes, + // specialized by typevars), the explicit one takes precedence. + self.pep695_generic_context(db) + .or_else(|| self.legacy_generic_context(db)) + .or_else(|| self.inherited_legacy_generic_context(db)) + } + + #[salsa::tracked] + pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option> { let scope = self.body_scope(db); let class_def_node = scope.node(db).expect_class(); class_def_node.type_params.as_ref().map(|type_params| { @@ -464,6 +482,26 @@ impl<'db> ClassLiteral<'db> { }) } + pub(crate) fn legacy_generic_context(self, db: &'db dyn Db) -> Option> { + self.explicit_bases(db).iter().find_map(|base| match base { + Type::KnownInstance(KnownInstanceType::Generic(generic_context)) => *generic_context, + _ => None, + }) + } + + pub(crate) fn inherited_legacy_generic_context( + self, + db: &'db dyn Db, + ) -> Option> { + GenericContext::from_base_classes( + db, + self.explicit_bases(db) + .iter() + .copied() + .filter(|ty| matches!(ty, Type::GenericAlias(_))), + ) + } + /// Return `true` if this class represents the builtin class `object` pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { self.is_known(db, KnownClass::Object) @@ -919,10 +957,8 @@ impl<'db> ClassLiteral<'db> { for superclass in mro_iter { match superclass { - ClassBase::Dynamic( - DynamicType::SubscriptedGeneric | DynamicType::SubscriptedProtocol, - ) - | ClassBase::Generic + ClassBase::Dynamic(DynamicType::SubscriptedProtocol) + | ClassBase::Generic(_) | ClassBase::Protocol => { // TODO: We currently skip `Protocol` when looking up class members, in order to // avoid creating many dynamic types in our test suite that would otherwise @@ -1264,10 +1300,8 @@ impl<'db> ClassLiteral<'db> { for superclass in self.iter_mro(db, specialization) { match superclass { - ClassBase::Dynamic( - DynamicType::SubscriptedProtocol | DynamicType::SubscriptedGeneric, - ) - | ClassBase::Generic + ClassBase::Dynamic(DynamicType::SubscriptedProtocol) + | ClassBase::Generic(_) | ClassBase::Protocol => { // TODO: We currently skip these when looking up instance members, in order to // avoid creating many dynamic types in our test suite that would otherwise @@ -2237,6 +2271,43 @@ impl<'db> KnownClass { .unwrap_or_else(Type::unknown) } + /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] + /// representing all possible instances of the generic class with a specialization. + /// + /// If the class cannot be found in typeshed, or if you provide a specialization with the wrong + /// number of types, a debug-level log message will be emitted stating this. + pub(crate) fn to_specialized_instance( + self, + db: &'db dyn Db, + specialization: impl IntoIterator>, + ) -> Type<'db> { + let class_literal = self.to_class_literal(db).expect_class_literal(); + let Some(generic_context) = class_literal.generic_context(db) else { + return Type::unknown(); + }; + + let types = specialization.into_iter().collect::>(); + if types.len() != generic_context.len(db) { + // a cache of the `KnownClass`es that we have already seen mismatched-arity + // specializations for (and therefore that we've already logged a warning for) + static MESSAGES: LazyLock>> = LazyLock::new(Mutex::default); + if MESSAGES.lock().unwrap().insert(self) { + tracing::info!( + "Wrong number of types when specializing {}. \ + Falling back to `Unknown` for the symbol instead.", + self.display(db) + ); + } + return Type::unknown(); + } + + let specialization = generic_context.specialize(db, types); + Type::instance( + db, + ClassType::Generic(GenericAlias::new(db, class_literal, specialization)), + ) + } + /// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. /// /// Return an error if the symbol cannot be found in the expected typeshed module, diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 03b627233dbd8f..28f1484d31a28d 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -1,3 +1,4 @@ +use crate::types::generics::GenericContext; use crate::types::{ todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, MroIterator, Type, }; @@ -21,7 +22,7 @@ pub enum ClassBase<'db> { /// Bare `Generic` cannot be subclassed directly in user code, /// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`, /// `Protocol[T]`, or bare `Protocol`. - Generic, + Generic(Option>), } impl<'db> ClassBase<'db> { @@ -50,7 +51,13 @@ impl<'db> ClassBase<'db> { write!(f, "", alias.display(self.db)) } ClassBase::Protocol => f.write_str("typing.Protocol"), - ClassBase::Generic => f.write_str("typing.Generic"), + ClassBase::Generic(generic_context) => { + f.write_str("typing.Generic")?; + if let Some(generic_context) = generic_context { + write!(f, "{}", generic_context.display(self.db))?; + } + Ok(()) + } } } } @@ -181,7 +188,9 @@ impl<'db> ClassBase<'db> { Self::try_from_type(db, todo_type!("Support for Callable as a base class")) } KnownInstanceType::Protocol => Some(ClassBase::Protocol), - KnownInstanceType::Generic => Some(ClassBase::Generic), + KnownInstanceType::Generic(generic_context) => { + Some(ClassBase::Generic(generic_context)) + } }, } } @@ -189,20 +198,22 @@ impl<'db> ClassBase<'db> { pub(super) fn into_class(self) -> Option> { match self { Self::Class(class) => Some(class), - Self::Dynamic(_) | Self::Generic | Self::Protocol => None, + Self::Dynamic(_) | Self::Generic(_) | Self::Protocol => None, } } /// Iterate over the MRO of this base pub(super) fn mro(self, db: &'db dyn Db) -> impl Iterator> { match self { - ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic), - ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => ClassBaseMroIterator::length_3( - db, - self, - ClassBase::Dynamic(DynamicType::SubscriptedGeneric), - ), - ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self), + ClassBase::Protocol => { + ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(None)) + } + ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => { + ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(None)) + } + ClassBase::Dynamic(_) | ClassBase::Generic(_) => { + ClassBaseMroIterator::length_2(db, self) + } ClassBase::Class(class) => ClassBaseMroIterator::from_class(db, class), } } @@ -220,7 +231,9 @@ impl<'db> From> for Type<'db> { ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic), ClassBase::Class(class) => class.into(), ClassBase::Protocol => Type::KnownInstance(KnownInstanceType::Protocol), - ClassBase::Generic => Type::KnownInstance(KnownInstanceType::Generic), + ClassBase::Generic(generic_context) => { + Type::KnownInstance(KnownInstanceType::Generic(generic_context)) + } } } } diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index f56300489085d4..c199c220e46521 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -9,6 +9,7 @@ use crate::types::string_annotation::{ RAW_STRING_TYPE_ANNOTATION, }; use crate::types::{class::ProtocolClassLiteral, KnownFunction, KnownInstanceType, Type}; +use crate::Db; use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic}; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::Ranged; @@ -35,6 +36,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_CONTEXT_MANAGER); registry.register_lint(&INVALID_DECLARATION); registry.register_lint(&INVALID_EXCEPTION_CAUGHT); + registry.register_lint(&INVALID_GENERIC_CLASS); registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE); registry.register_lint(&INVALID_METACLASS); registry.register_lint(&INVALID_OVERLOAD); @@ -393,6 +395,32 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for the creation of invalid generic classes + /// + /// ## Why is this bad? + /// There are several requirements that you must follow when defining a generic class. + /// + /// ## Examples + /// ```python + /// from typing import Generic, TypeVar + /// + /// T = TypeVar("T") # okay + /// + /// # error: class uses both PEP-695 syntax and legacy syntax + /// class C[U](Generic[T]): ... + /// ``` + /// + /// ## References + /// - [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction) + pub(crate) static INVALID_GENERIC_CLASS = { + summary: "detects invalid generic classes", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for the creation of invalid legacy `TypeVar`s @@ -1378,6 +1406,7 @@ pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node: } pub(crate) fn report_invalid_arguments_to_annotated( + db: &dyn Db, context: &InferContext, subscript: &ast::ExprSubscript, ) { @@ -1387,7 +1416,7 @@ pub(crate) fn report_invalid_arguments_to_annotated( builder.into_diagnostic(format_args!( "Special form `{}` expected at least 2 arguments \ (one type and at least one metadata element)", - KnownInstanceType::Annotated.repr() + KnownInstanceType::Annotated.repr(db) )); } @@ -1427,6 +1456,7 @@ pub(crate) fn report_bad_argument_to_get_protocol_members( } pub(crate) fn report_invalid_arguments_to_callable( + db: &dyn Db, context: &InferContext, subscript: &ast::ExprSubscript, ) { @@ -1435,7 +1465,7 @@ pub(crate) fn report_invalid_arguments_to_callable( }; builder.into_diagnostic(format_args!( "Special form `{}` expected exactly two arguments (parameter types and return type)", - KnownInstanceType::Callable.repr() + KnownInstanceType::Callable.repr(db) )); } diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index c878a81539fc30..fc30a4f4654d75 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -14,7 +14,7 @@ use crate::types::{ StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType, WrapperDescriptorKind, }; -use crate::Db; +use crate::{Db, FxOrderSet}; use rustc_hash::FxHashMap; impl<'db> Type<'db> { @@ -113,7 +113,7 @@ impl Display for DisplayRepresentation<'_> { SubclassOfInner::Class(class) => write!(f, "type[{}]", class.name(self.db)), SubclassOfInner::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), }, - Type::KnownInstance(known_instance) => f.write_str(known_instance.repr()), + Type::KnownInstance(known_instance) => write!(f, "{}", known_instance.repr(self.db)), Type::FunctionLiteral(function) => { let signature = function.signature(self.db); @@ -317,7 +317,7 @@ impl<'db> GenericContext<'db> { } pub struct DisplayGenericContext<'db> { - typevars: &'db [TypeVarInstance<'db>], + typevars: &'db FxOrderSet>, db: &'db dyn Db, } @@ -376,7 +376,7 @@ impl<'db> Specialization<'db> { } pub struct DisplaySpecialization<'db> { - typevars: &'db [TypeVarInstance<'db>], + typevars: &'db FxOrderSet>, types: &'db [Type<'db>], db: &'db dyn Db, full: bool, diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 8589f14a5cb5a0..0210cd2062b9d8 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -16,7 +16,7 @@ use crate::{Db, FxOrderSet}; #[salsa::interned(debug)] pub struct GenericContext<'db> { #[return_ref] - pub(crate) variables: Box<[TypeVarInstance<'db>]>, + pub(crate) variables: FxOrderSet>, } impl<'db> GenericContext<'db> { @@ -26,7 +26,7 @@ impl<'db> GenericContext<'db> { index: &'db SemanticIndex<'db>, type_params_node: &ast::TypeParams, ) -> Self { - let variables: Box<[_]> = type_params_node + let variables: FxOrderSet<_> = type_params_node .iter() .filter_map(|type_param| Self::variable_from_type_param(db, index, type_param)) .collect(); @@ -54,7 +54,7 @@ impl<'db> GenericContext<'db> { } } - /// Creates a generic context from the legecy `TypeVar`s that appear in a function parameter + /// Creates a generic context from the legacy `TypeVar`s that appear in a function parameter /// list. pub(crate) fn from_function_params( db: &'db dyn Db, @@ -76,10 +76,29 @@ impl<'db> GenericContext<'db> { if variables.is_empty() { return None; } - let variables: Box<[_]> = variables.into_iter().collect(); Some(Self::new(db, variables)) } + /// Creates a generic context from the legacy `TypeVar`s that appear in class's base class + /// list. + pub(crate) fn from_base_classes( + db: &'db dyn Db, + bases: impl Iterator>, + ) -> Option { + let mut variables = FxOrderSet::default(); + for base in bases { + base.find_legacy_typevars(db, &mut variables); + } + if variables.is_empty() { + return None; + } + Some(Self::new(db, variables)) + } + + pub(crate) fn len(self, db: &'db dyn Db) -> usize { + self.variables(db).len() + } + pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { let parameters = Parameters::new( self.variables(db) @@ -130,11 +149,18 @@ impl<'db> GenericContext<'db> { self.specialize(db, types.into()) } + pub(crate) fn is_subset_of(self, db: &'db dyn Db, other: GenericContext<'db>) -> bool { + self.variables(db).is_subset(other.variables(db)) + } + + /// Creates a specialization of this generic context. Panics if the length of `types` does not + /// match the number of typevars in the generic context. pub(crate) fn specialize( self, db: &'db dyn Db, types: Box<[Type<'db>]>, ) -> Specialization<'db> { + assert!(self.variables(db).len() == types.len()); Specialization::new(db, self, types) } } @@ -205,12 +231,11 @@ impl<'db> Specialization<'db> { /// Returns the type that a typevar is specialized to, or None if the typevar isn't part of /// this specialization. pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option> { - self.generic_context(db) + let index = self + .generic_context(db) .variables(db) - .into_iter() - .zip(self.types(db)) - .find(|(var, _)| **var == typevar) - .map(|(_, ty)| *ty) + .get_index_of(&typevar)?; + Some(self.types(db)[index]) } pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Specialization<'db>) -> bool { @@ -324,6 +349,16 @@ impl<'db> Specialization<'db> { true } + + pub(crate) fn find_legacy_typevars( + self, + db: &'db dyn Db, + typevars: &mut FxOrderSet>, + ) { + for ty in self.types(db) { + ty.find_legacy_typevars(db, typevars); + } + } } /// Performs type inference between parameter annotations and argument types, producing a diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 991eb960dab75a..0734bcf7f82347 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -72,10 +72,11 @@ use crate::types::diagnostic::{ report_possibly_unbound_attribute, TypeCheckDiagnostics, CALL_NON_CALLABLE, CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, INCONSISTENT_MRO, - INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, - INVALID_LEGACY_TYPE_VARIABLE, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, - INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, - UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR, + INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, + INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE, + INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS, + POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, + UNSUPPORTED_OPERATOR, }; use crate::types::generics::GenericContext; use crate::types::mro::MroErrorKind; @@ -92,7 +93,7 @@ use crate::types::{ }; use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; -use crate::Db; +use crate::{Db, FxOrderSet}; use super::context::{InNoTypeCheck, InferContext}; use super::diagnostic::{ @@ -767,7 +768,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let DefinitionKind::Class(class) = definition.kind(self.db()) { ty.inner_type() .into_class_literal() - .map(|ty| (ty, class.node())) + .map(|class_literal| (class_literal, class.node())) } else { None } @@ -801,7 +802,7 @@ impl<'db> TypeInferenceBuilder<'db> { // - If the class is a protocol class: check for inheritance from a non-protocol class for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() { let base_class = match base_class { - Type::KnownInstance(KnownInstanceType::Generic) => { + Type::KnownInstance(KnownInstanceType::Generic(None)) => { if let Some(builder) = self .context .report_lint(&INVALID_BASE, &class_node.bases()[i]) @@ -976,6 +977,35 @@ impl<'db> TypeInferenceBuilder<'db> { } } } + + // (5) Check that a generic class does not have invalid or conflicting generic + // contexts. + if class.pep695_generic_context(self.db()).is_some() + && class.legacy_generic_context(self.db()).is_some() + { + if let Some(builder) = self.context.report_lint(&INVALID_GENERIC_CLASS, class_node) + { + builder.into_diagnostic( + "Cannot both inherit from `Generic` and use PEP 695 type variables", + ); + } + } + + if let (Some(legacy), Some(inherited)) = ( + class.legacy_generic_context(self.db()), + class.inherited_legacy_generic_context(self.db()), + ) { + if !inherited.is_subset_of(self.db(), legacy) { + if let Some(builder) = + self.context.report_lint(&INVALID_GENERIC_CLASS, class_node) + { + builder.into_diagnostic( + "`Generic` base class must include all type \ + variables used in other base classes", + ); + } + } + } } } @@ -2011,9 +2041,11 @@ impl<'db> TypeInferenceBuilder<'db> { definition: Definition<'db>, ) { if let Some(annotation) = parameter.annotation() { - let _annotated_ty = self.file_expression_type(annotation); - // TODO `dict[str, annotated_type]` - let ty = KnownClass::Dict.to_instance(self.db()); + let annotated_ty = self.file_expression_type(annotation); + let ty = KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), annotated_ty], + ); self.add_declaration_with_binding( parameter.into(), definition, @@ -2023,8 +2055,10 @@ impl<'db> TypeInferenceBuilder<'db> { self.add_binding( parameter.into(), definition, - // TODO `dict[str, Unknown]` - KnownClass::Dict.to_instance(self.db()), + KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), Type::unknown()], + ), ); } } @@ -5547,8 +5581,6 @@ impl<'db> TypeInferenceBuilder<'db> { | (_, todo @ Type::Dynamic(DynamicType::Todo(_)), _) => Some(todo), (todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _, _) | (_, todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _) => Some(todo), - (todo @ Type::Dynamic(DynamicType::SubscriptedGeneric), _, _) - | (_, todo @ Type::Dynamic(DynamicType::SubscriptedGeneric), _) => Some(todo), (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( @@ -6641,9 +6673,16 @@ impl<'db> TypeInferenceBuilder<'db> { ) -> Type<'db> { let slice_node = subscript.slice.as_ref(); let call_argument_types = match slice_node { - ast::Expr::Tuple(tuple) => CallArgumentTypes::positional( - tuple.elts.iter().map(|elt| self.infer_type_expression(elt)), - ), + ast::Expr::Tuple(tuple) => { + let arguments = CallArgumentTypes::positional( + tuple.elts.iter().map(|elt| self.infer_type_expression(elt)), + ); + self.store_expression_type( + slice_node, + TupleType::from_elements(self.db(), arguments.iter().map(|(_, ty)| ty)), + ); + arguments + } _ => CallArgumentTypes::positional([self.infer_type_expression(slice_node)]), }; let signatures = Signatures::single(CallableSignature::single( @@ -6812,8 +6851,14 @@ impl<'db> TypeInferenceBuilder<'db> { (Type::KnownInstance(KnownInstanceType::Protocol), _) => { Type::Dynamic(DynamicType::SubscriptedProtocol) } - (Type::KnownInstance(KnownInstanceType::Generic), _) => { - Type::Dynamic(DynamicType::SubscriptedGeneric) + (Type::KnownInstance(KnownInstanceType::Generic(None)), Type::Tuple(typevars)) => { + self.infer_subscript_legacy_generic_class(value_node, typevars.elements(self.db())) + } + (Type::KnownInstance(KnownInstanceType::Generic(None)), typevar) => self + .infer_subscript_legacy_generic_class(value_node, std::slice::from_ref(&typevar)), + (Type::KnownInstance(KnownInstanceType::Generic(Some(_))), _) => { + // TODO: emit a diagnostic + todo_type!("doubly-specialized typing.Generic") } (Type::KnownInstance(known_instance), _) if known_instance.class().is_special_form() => @@ -6934,7 +6979,7 @@ impl<'db> TypeInferenceBuilder<'db> { if !value_ty.into_class_literal().is_some_and(|class| { class .iter_mro(self.db(), None) - .contains(&ClassBase::Dynamic(DynamicType::SubscriptedGeneric)) + .any(|base| matches!(base, ClassBase::Generic(_))) }) { report_non_subscriptable( &self.context, @@ -6969,6 +7014,35 @@ impl<'db> TypeInferenceBuilder<'db> { } } + fn infer_subscript_legacy_generic_class( + &mut self, + value_node: &ast::Expr, + typevars: &[Type<'db>], + ) -> Type<'db> { + let typevars: Option> = typevars + .iter() + .map(|typevar| match typevar { + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Some(*typevar), + _ => { + if let Some(builder) = + self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node) + { + builder.into_diagnostic(format_args!( + "`{}` is not a valid argument to `typing.Generic`", + typevar.display(self.db()), + )); + } + None + } + }) + .collect(); + let Some(typevars) = typevars else { + return Type::unknown(); + }; + let generic_context = GenericContext::new(self.db(), typevars); + Type::KnownInstance(KnownInstanceType::Generic(Some(generic_context))) + } + fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { enum SliceArg { Arg(Option), @@ -7137,7 +7211,11 @@ impl<'db> TypeInferenceBuilder<'db> { }) = slice { if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); + report_invalid_arguments_to_annotated( + self.db(), + &self.context, + subscript, + ); } if let [inner_annotation, metadata @ ..] = &arguments[..] { @@ -7155,7 +7233,11 @@ impl<'db> TypeInferenceBuilder<'db> { TypeAndQualifiers::unknown() } } else { - report_invalid_arguments_to_annotated(&self.context, subscript); + report_invalid_arguments_to_annotated( + self.db(), + &self.context, + subscript, + ); self.infer_annotation_expression_impl(slice) } } @@ -7169,7 +7251,7 @@ impl<'db> TypeInferenceBuilder<'db> { builder.into_diagnostic(format_args!( "Type qualifier `{type_qualifier}` \ expects exactly one type parameter", - type_qualifier = known_instance.repr(), + type_qualifier = known_instance.repr(self.db()), )); } Type::unknown().into() @@ -7829,7 +7911,7 @@ impl<'db> TypeInferenceBuilder<'db> { elts: arguments, .. }) = arguments_slice else { - report_invalid_arguments_to_annotated(&self.context, subscript); + report_invalid_arguments_to_annotated(self.db(), &self.context, subscript); // `Annotated[]` with less than two arguments is an error at runtime. // However, we still treat `Annotated[T]` as `T` here for the purpose of @@ -7839,7 +7921,7 @@ impl<'db> TypeInferenceBuilder<'db> { }; if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); + report_invalid_arguments_to_annotated(self.db(), &self.context, subscript); } let [type_expr, metadata @ ..] = &arguments[..] else { @@ -7923,7 +8005,7 @@ impl<'db> TypeInferenceBuilder<'db> { }; if !correct_argument_number { - report_invalid_arguments_to_callable(&self.context, subscript); + report_invalid_arguments_to_callable(self.db(), &self.context, subscript); } let callable_type = if let (Some(parameters), Some(return_type), true) = @@ -7952,7 +8034,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected exactly one type parameter", - known_instance.repr() + known_instance.repr(self.db()) )); } Type::unknown() @@ -7979,7 +8061,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected exactly one type parameter", - known_instance.repr() + known_instance.repr(self.db()) )); } Type::unknown() @@ -7995,7 +8077,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected exactly one type parameter", - known_instance.repr() + known_instance.repr(self.db()) )); } Type::unknown() @@ -8028,7 +8110,7 @@ impl<'db> TypeInferenceBuilder<'db> { "Expected the first argument to `{}` \ to be a callable object, \ but got an object of type `{}`", - known_instance.repr(), + known_instance.repr(self.db()), argument_type.display(db) )); } @@ -8093,7 +8175,7 @@ impl<'db> TypeInferenceBuilder<'db> { builder.into_diagnostic(format_args!( "Type qualifier `{}` is not allowed in type expressions \ (only in annotation expressions)", - known_instance.repr() + known_instance.repr(self.db()) )); } self.infer_type_expression(arguments_slice) @@ -8122,9 +8204,14 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_type_expression(arguments_slice); Type::Dynamic(DynamicType::SubscriptedProtocol) } - KnownInstanceType::Generic => { + KnownInstanceType::Generic(_) => { self.infer_type_expression(arguments_slice); - Type::Dynamic(DynamicType::SubscriptedGeneric) + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "`typing.Generic` is not allowed in type expressions", + )); + } + Type::unknown() } KnownInstanceType::NoReturn | KnownInstanceType::Never @@ -8136,7 +8223,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", - known_instance.repr() + known_instance.repr(self.db()) )); } Type::unknown() @@ -8150,7 +8237,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected no type parameter", - known_instance.repr() + known_instance.repr(self.db()) )); } Type::unknown() @@ -8161,7 +8248,7 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let mut diag = builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", - known_instance.repr() + known_instance.repr(self.db()) )); diag.info("Did you mean to use `Literal[...]` instead?"); } diff --git a/crates/red_knot_python_semantic/src/types/known_instance.rs b/crates/red_knot_python_semantic/src/types/known_instance.rs index 7a8cb2e4642ed9..b1cce6d9798c73 100644 --- a/crates/red_knot_python_semantic/src/types/known_instance.rs +++ b/crates/red_knot_python_semantic/src/types/known_instance.rs @@ -8,6 +8,9 @@ //! variant can only be inhabited by one or two specific objects at runtime with //! locations that are known in advance. +use std::fmt::Display; + +use super::generics::GenericContext; use super::{class::KnownClass, ClassType, Truthiness, Type, TypeAliasType, TypeVarInstance}; use crate::db::Db; use crate::module_resolver::{file_to_module, KnownModule}; @@ -59,7 +62,7 @@ pub enum KnownInstanceType<'db> { /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) Protocol, /// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`) - Generic, + Generic(Option>), /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) Type, /// A single instance of `typing.TypeVar` @@ -142,7 +145,7 @@ impl<'db> KnownInstanceType<'db> { | Self::ChainMap | Self::OrderedDict | Self::Protocol - | Self::Generic + | Self::Generic(_) | Self::ReadOnly | Self::TypeAliasType(_) | Self::Unknown @@ -156,54 +159,10 @@ impl<'db> KnownInstanceType<'db> { } /// Return the repr of the symbol at runtime - pub(crate) fn repr(self) -> &'db str { - match self { - Self::Annotated => "typing.Annotated", - Self::Literal => "typing.Literal", - Self::LiteralString => "typing.LiteralString", - Self::Optional => "typing.Optional", - Self::Union => "typing.Union", - Self::NoReturn => "typing.NoReturn", - Self::Never => "typing.Never", - Self::Any => "typing.Any", - Self::Tuple => "typing.Tuple", - Self::Type => "typing.Type", - Self::TypingSelf => "typing.Self", - Self::Final => "typing.Final", - Self::ClassVar => "typing.ClassVar", - Self::Callable => "typing.Callable", - Self::Concatenate => "typing.Concatenate", - Self::Unpack => "typing.Unpack", - Self::Required => "typing.Required", - Self::NotRequired => "typing.NotRequired", - Self::TypeAlias => "typing.TypeAlias", - Self::TypeGuard => "typing.TypeGuard", - Self::TypedDict => "typing.TypedDict", - Self::TypeIs => "typing.TypeIs", - Self::List => "typing.List", - Self::Dict => "typing.Dict", - Self::DefaultDict => "typing.DefaultDict", - Self::Set => "typing.Set", - Self::FrozenSet => "typing.FrozenSet", - Self::Counter => "typing.Counter", - Self::Deque => "typing.Deque", - Self::ChainMap => "typing.ChainMap", - Self::OrderedDict => "typing.OrderedDict", - Self::Protocol => "typing.Protocol", - Self::Generic => "typing.Generic", - Self::ReadOnly => "typing.ReadOnly", - // This is a legacy `TypeVar` _outside_ of any generic class or function, so we render - // it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll - // have a `Type::TypeVar(_)`, which is rendered as the typevar's name. - Self::TypeVar(_) => "typing.TypeVar", - Self::TypeAliasType(_) => "typing.TypeAliasType", - Self::Unknown => "knot_extensions.Unknown", - Self::AlwaysTruthy => "knot_extensions.AlwaysTruthy", - Self::AlwaysFalsy => "knot_extensions.AlwaysFalsy", - Self::Not => "knot_extensions.Not", - Self::Intersection => "knot_extensions.Intersection", - Self::TypeOf => "knot_extensions.TypeOf", - Self::CallableTypeOf => "knot_extensions.CallableTypeOf", + pub(crate) fn repr(self, db: &'db dyn Db) -> impl Display + 'db { + KnownInstanceRepr { + known_instance: self, + db, } } @@ -243,7 +202,7 @@ impl<'db> KnownInstanceType<'db> { Self::ChainMap => KnownClass::StdlibAlias, Self::OrderedDict => KnownClass::StdlibAlias, Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says - Self::Generic => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says + Self::Generic(_) => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says Self::TypeVar(_) => KnownClass::TypeVar, Self::TypeAliasType(_) => KnownClass::TypeAliasType, Self::TypeOf => KnownClass::SpecialForm, @@ -287,7 +246,7 @@ impl<'db> KnownInstanceType<'db> { "Counter" => Self::Counter, "ChainMap" => Self::ChainMap, "OrderedDict" => Self::OrderedDict, - "Generic" => Self::Generic, + "Generic" => Self::Generic(None), "Protocol" => Self::Protocol, "Optional" => Self::Optional, "Union" => Self::Union, @@ -347,7 +306,7 @@ impl<'db> KnownInstanceType<'db> { | Self::NoReturn | Self::Tuple | Self::Type - | Self::Generic + | Self::Generic(_) | Self::Callable => module.is_typing(), Self::Annotated | Self::Protocol @@ -383,3 +342,67 @@ impl<'db> KnownInstanceType<'db> { self.class().to_class_literal(db) } } + +struct KnownInstanceRepr<'db> { + known_instance: KnownInstanceType<'db>, + db: &'db dyn Db, +} + +impl Display for KnownInstanceRepr<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.known_instance { + KnownInstanceType::Annotated => f.write_str("typing.Annotated"), + KnownInstanceType::Literal => f.write_str("typing.Literal"), + KnownInstanceType::LiteralString => f.write_str("typing.LiteralString"), + KnownInstanceType::Optional => f.write_str("typing.Optional"), + KnownInstanceType::Union => f.write_str("typing.Union"), + KnownInstanceType::NoReturn => f.write_str("typing.NoReturn"), + KnownInstanceType::Never => f.write_str("typing.Never"), + KnownInstanceType::Any => f.write_str("typing.Any"), + KnownInstanceType::Tuple => f.write_str("typing.Tuple"), + KnownInstanceType::Type => f.write_str("typing.Type"), + KnownInstanceType::TypingSelf => f.write_str("typing.Self"), + KnownInstanceType::Final => f.write_str("typing.Final"), + KnownInstanceType::ClassVar => f.write_str("typing.ClassVar"), + KnownInstanceType::Callable => f.write_str("typing.Callable"), + KnownInstanceType::Concatenate => f.write_str("typing.Concatenate"), + KnownInstanceType::Unpack => f.write_str("typing.Unpack"), + KnownInstanceType::Required => f.write_str("typing.Required"), + KnownInstanceType::NotRequired => f.write_str("typing.NotRequired"), + KnownInstanceType::TypeAlias => f.write_str("typing.TypeAlias"), + KnownInstanceType::TypeGuard => f.write_str("typing.TypeGuard"), + KnownInstanceType::TypedDict => f.write_str("typing.TypedDict"), + KnownInstanceType::TypeIs => f.write_str("typing.TypeIs"), + KnownInstanceType::List => f.write_str("typing.List"), + KnownInstanceType::Dict => f.write_str("typing.Dict"), + KnownInstanceType::DefaultDict => f.write_str("typing.DefaultDict"), + KnownInstanceType::Set => f.write_str("typing.Set"), + KnownInstanceType::FrozenSet => f.write_str("typing.FrozenSet"), + KnownInstanceType::Counter => f.write_str("typing.Counter"), + KnownInstanceType::Deque => f.write_str("typing.Deque"), + KnownInstanceType::ChainMap => f.write_str("typing.ChainMap"), + KnownInstanceType::OrderedDict => f.write_str("typing.OrderedDict"), + KnownInstanceType::Protocol => f.write_str("typing.Protocol"), + KnownInstanceType::Generic(generic_context) => { + f.write_str("typing.Generic")?; + if let Some(generic_context) = generic_context { + write!(f, "{}", generic_context.display(self.db))?; + } + Ok(()) + } + KnownInstanceType::ReadOnly => f.write_str("typing.ReadOnly"), + // This is a legacy `TypeVar` _outside_ of any generic class or function, so we render + // it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll + // have a `Type::TypeVar(_)`, which is rendered as the typevar's name. + KnownInstanceType::TypeVar(_) => f.write_str("typing.TypeVar"), + KnownInstanceType::TypeAliasType(_) => f.write_str("typing.TypeAliasType"), + KnownInstanceType::Unknown => f.write_str("knot_extensions.Unknown"), + KnownInstanceType::AlwaysTruthy => f.write_str("knot_extensions.AlwaysTruthy"), + KnownInstanceType::AlwaysFalsy => f.write_str("knot_extensions.AlwaysFalsy"), + KnownInstanceType::Not => f.write_str("knot_extensions.Not"), + KnownInstanceType::Intersection => f.write_str("knot_extensions.Intersection"), + KnownInstanceType::TypeOf => f.write_str("knot_extensions.TypeOf"), + KnownInstanceType::CallableTypeOf => f.write_str("knot_extensions.CallableTypeOf"), + } + } +} diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index d62baf9b90b3ac..bd390aff6ee554 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -159,8 +159,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (_, ClassBase::Class(_)) => Ordering::Greater, (ClassBase::Protocol, _) => Ordering::Less, (_, ClassBase::Protocol) => Ordering::Greater, - (ClassBase::Generic, _) => Ordering::Less, - (_, ClassBase::Generic) => Ordering::Greater, + (ClassBase::Generic(left), ClassBase::Generic(right)) => left.cmp(right), + (ClassBase::Generic(_), _) => Ordering::Less, + (_, ClassBase::Generic(_)) => Ordering::Greater, (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { dynamic_elements_ordering(*left, *right) } @@ -250,8 +251,11 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (KnownInstanceType::OrderedDict, _) => Ordering::Less, (_, KnownInstanceType::OrderedDict) => Ordering::Greater, - (KnownInstanceType::Generic, _) => Ordering::Less, - (_, KnownInstanceType::Generic) => Ordering::Greater, + (KnownInstanceType::Generic(left), KnownInstanceType::Generic(right)) => { + left.cmp(right) + } + (KnownInstanceType::Generic(_), _) => Ordering::Less, + (_, KnownInstanceType::Generic(_)) => Ordering::Greater, (KnownInstanceType::Protocol, _) => Ordering::Less, (_, KnownInstanceType::Protocol) => Ordering::Greater, @@ -380,9 +384,6 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering #[cfg(not(debug_assertions))] (DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal, - (DynamicType::SubscriptedGeneric, _) => Ordering::Less, - (_, DynamicType::SubscriptedGeneric) => Ordering::Greater, - (DynamicType::SubscriptedProtocol, _) => Ordering::Less, (_, DynamicType::SubscriptedProtocol) => Ordering::Greater, } diff --git a/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi b/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi index d7e69093d8ac0a..163a1d1cc61904 100644 --- a/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi +++ b/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi @@ -26,3 +26,7 @@ def is_gradual_equivalent_to(type_a: Any, type_b: Any) -> bool: ... def is_fully_static(type: Any) -> bool: ... def is_singleton(type: Any) -> bool: ... def is_single_valued(type: Any) -> bool: ... + +# Returns the generic context of a type as a tuple of typevars, or `None` if the +# type is not generic. +def generic_context(type: Any) -> Any: ... diff --git a/knot.schema.json b/knot.schema.json index aeda31fd530bc7..49bc35855bb6da 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -440,6 +440,16 @@ } ] }, + "invalid-generic-class": { + "title": "detects invalid generic classes", + "description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\n\n## Examples\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\") # okay\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "invalid-ignore-comment": { "title": "detects ignore comments that use invalid syntax", "description": "## What it does\nChecks for `type: ignore` and `knot: ignore` comments that are syntactically incorrect.\n\n## Why is this bad?\nA syntactically incorrect ignore comment is probably a mistake and is useless.\n\n## Examples\n```py\na = 20 / 0 # type: ignoree\n```\n\nUse instead:\n\n```py\na = 20 / 0 # type: ignore\n```", From 78d435630187c36e5f0a94517a70e0a101963f36 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 3 May 2025 00:00:11 -0700 Subject: [PATCH 0225/1161] [red-knot] add tracing of salsa events in mdtests (#17803) --- Cargo.lock | 1 + crates/red_knot_test/Cargo.toml | 1 + crates/red_knot_test/src/db.rs | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index db2f5d5c1c89fb..c775732fd41489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2647,6 +2647,7 @@ dependencies = [ "tempfile", "thiserror 2.0.12", "toml", + "tracing", ] [[package]] diff --git a/crates/red_knot_test/Cargo.toml b/crates/red_knot_test/Cargo.toml index 06727647c9cd1b..73aeb8daa48275 100644 --- a/crates/red_knot_test/Cargo.toml +++ b/crates/red_knot_test/Cargo.toml @@ -33,6 +33,7 @@ smallvec = { workspace = true } serde = { workspace = true } tempfile = { workspace = true } toml = { workspace = true } +tracing = { workspace = true } thiserror = { workspace = true } [lints] diff --git a/crates/red_knot_test/src/db.rs b/crates/red_knot_test/src/db.rs index b4fcccb9276aaf..c26206e66a3b95 100644 --- a/crates/red_knot_test/src/db.rs +++ b/crates/red_knot_test/src/db.rs @@ -96,7 +96,10 @@ impl SemanticDb for Db { #[salsa::db] impl salsa::Database for Db { - fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {} + fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) { + let event = event(); + tracing::trace!("event: {:?}", event); + } } impl DbWithWritableSystem for Db { From 084352f72c3cacf5cbed7f8ac63ef1f46d9c4241 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 3 May 2025 11:12:23 +0100 Subject: [PATCH 0226/1161] [red-knot] Distinguish fully static protocols from non-fully-static protocols (#17795) --- .../mdtest/generics/pep695/functions.md | 55 ++++ .../resources/mdtest/protocols.md | 35 ++- crates/red_knot_python_semantic/src/types.rs | 18 +- .../src/types/call/bind.rs | 6 +- .../src/types/class.rs | 148 +---------- .../src/types/diagnostic.rs | 4 +- .../src/types/display.rs | 6 +- .../src/types/instance.rs | 67 +++-- .../src/types/protocol_class.rs | 242 ++++++++++++++++++ 9 files changed, 377 insertions(+), 204 deletions(-) create mode 100644 crates/red_knot_python_semantic/src/types/protocol_class.rs diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md index ee5ed72c98b9b4..d09049544f947b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -232,3 +232,58 @@ def g[T](x: T) -> T | None: reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None ``` + +## Protocols as TypeVar bounds + +Protocol types can be used as TypeVar bounds, just like nominal types. + +```py +from typing import Any, Protocol +from knot_extensions import static_assert, is_assignable_to + +class SupportsClose(Protocol): + def close(self) -> None: ... + +class ClosableFullyStaticProtocol(Protocol): + x: int + def close(self) -> None: ... + +class ClosableNonFullyStaticProtocol(Protocol): + x: Any + def close(self) -> None: ... + +class ClosableFullyStaticNominal: + x: int + def close(self) -> None: ... + +class ClosableNonFullyStaticNominal: + x: int + def close(self) -> None: ... + +class NotClosableProtocol(Protocol): ... +class NotClosableNominal: ... + +def close_and_return[T: SupportsClose](x: T) -> T: + x.close() + return x + +def f( + a: SupportsClose, + b: ClosableFullyStaticProtocol, + c: ClosableNonFullyStaticProtocol, + d: ClosableFullyStaticNominal, + e: ClosableNonFullyStaticNominal, + f: NotClosableProtocol, + g: NotClosableNominal, +): + reveal_type(close_and_return(a)) # revealed: SupportsClose + reveal_type(close_and_return(b)) # revealed: ClosableFullyStaticProtocol + reveal_type(close_and_return(c)) # revealed: ClosableNonFullyStaticProtocol + reveal_type(close_and_return(d)) # revealed: ClosableFullyStaticNominal + reveal_type(close_and_return(e)) # revealed: ClosableNonFullyStaticNominal + + # error: [invalid-argument-type] "does not satisfy upper bound" + reveal_type(close_and_return(f)) # revealed: Unknown + # error: [invalid-argument-type] "does not satisfy upper bound" + reveal_type(close_and_return(g)) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 7f681a23c450d6..b55a41a17c1bcf 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -1087,7 +1087,8 @@ from knot_extensions import is_equivalent_to class HasMutableXAttr(Protocol): x: int -static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) +# TODO: should pass +static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error: [static-assert-error] static_assert(is_subtype_of(HasMutableXAttr, HasXProperty)) static_assert(is_assignable_to(HasMutableXAttr, HasXProperty)) @@ -1350,25 +1351,43 @@ class NotFullyStatic(Protocol): x: Any static_assert(is_fully_static(FullyStatic)) - -# TODO: should pass -static_assert(not is_fully_static(NotFullyStatic)) # error: [static-assert-error] +static_assert(not is_fully_static(NotFullyStatic)) ``` -Non-fully-static protocols do not participate in subtyping, only assignability: +Non-fully-static protocols do not participate in subtyping or equivalence, only assignability and +gradual equivalence: ```py -from knot_extensions import is_subtype_of, is_assignable_to +from knot_extensions import is_subtype_of, is_assignable_to, is_equivalent_to, is_gradual_equivalent_to class NominalWithX: x: int = 42 static_assert(is_assignable_to(NominalWithX, FullyStatic)) static_assert(is_assignable_to(NominalWithX, NotFullyStatic)) + +static_assert(not is_subtype_of(FullyStatic, NotFullyStatic)) +static_assert(is_assignable_to(FullyStatic, NotFullyStatic)) + +static_assert(not is_subtype_of(NotFullyStatic, FullyStatic)) +static_assert(is_assignable_to(NotFullyStatic, FullyStatic)) + +static_assert(not is_subtype_of(NominalWithX, NotFullyStatic)) +static_assert(is_assignable_to(NominalWithX, NotFullyStatic)) + static_assert(is_subtype_of(NominalWithX, FullyStatic)) -# TODO: this should pass -static_assert(not is_subtype_of(NominalWithX, NotFullyStatic)) # error: [static-assert-error] +static_assert(is_equivalent_to(FullyStatic, FullyStatic)) +static_assert(not is_equivalent_to(NotFullyStatic, NotFullyStatic)) + +static_assert(is_gradual_equivalent_to(FullyStatic, FullyStatic)) +static_assert(is_gradual_equivalent_to(NotFullyStatic, NotFullyStatic)) + +class AlsoNotFullyStatic(Protocol): + x: Any + +static_assert(not is_equivalent_to(NotFullyStatic, AlsoNotFullyStatic)) +static_assert(is_gradual_equivalent_to(NotFullyStatic, AlsoNotFullyStatic)) ``` Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index a519c44fc30b95..404b59738e35ed 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -68,6 +68,7 @@ mod instance; mod known_instance; mod mro; mod narrow; +mod protocol_class; mod signatures; mod slots; mod string_annotation; @@ -674,7 +675,7 @@ impl<'db> Type<'db> { .any(|ty| ty.contains_todo(db)) } - Self::ProtocolInstance(protocol) => protocol.contains_todo(), + Self::ProtocolInstance(protocol) => protocol.contains_todo(db), } } @@ -2061,7 +2062,7 @@ impl<'db> Type<'db> { | Type::AlwaysTruthy | Type::PropertyInstance(_) => true, - Type::ProtocolInstance(protocol) => protocol.is_fully_static(), + Type::ProtocolInstance(protocol) => protocol.is_fully_static(db), Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { None => true, @@ -2515,16 +2516,7 @@ impl<'db> Type<'db> { Type::NominalInstance(instance) => instance.class().instance_member(db, name), - Type::ProtocolInstance(protocol) => match protocol.inner() { - Protocol::FromClass(class) => class.instance_member(db, name), - Protocol::Synthesized(synthesized) => { - if synthesized.members(db).contains(name) { - SymbolAndQualifiers::todo("Capture type of synthesized protocol members") - } else { - Symbol::Unbound.into() - } - } - }, + Type::ProtocolInstance(protocol) => protocol.instance_member(db, name), Type::FunctionLiteral(_) => KnownClass::FunctionType .to_instance(db) @@ -5415,7 +5407,7 @@ impl std::fmt::Display for DynamicType { bitflags! { /// Type qualifiers that appear in an annotation expression. - #[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] + #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, salsa::Update, Hash)] pub(crate) struct TypeQualifiers: u8 { /// `typing.ClassVar` const CLASS_VAR = 1 << 0; diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 3921ae1e679170..d6068e8f55790b 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -621,9 +621,9 @@ impl<'db> Bindings<'db> { overload.set_return_type(Type::Tuple(TupleType::new( db, protocol_class - .protocol_members(db) - .iter() - .map(|member| Type::string_literal(db, member)) + .interface(db) + .members() + .map(|member| Type::string_literal(db, member.name())) .collect::]>>(), ))); } diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index cc77fdd4d3be12..664fa0706eed5a 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -1,5 +1,4 @@ use std::hash::BuildHasherDefault; -use std::ops::Deref; use std::sync::{LazyLock, Mutex}; use super::{ @@ -14,7 +13,6 @@ use crate::types::signatures::{Parameter, Parameters}; use crate::types::{ CallableType, DataclassParams, DataclassTransformerParams, KnownInstanceType, Signature, }; -use crate::FxOrderSet; use crate::{ module_resolver::file_to_module, semantic_index::{ @@ -669,7 +667,7 @@ impl<'db> ClassLiteral<'db> { .collect() } - fn known_function_decorators( + pub(super) fn known_function_decorators( self, db: &'db dyn Db, ) -> impl Iterator + 'db { @@ -1750,11 +1748,6 @@ impl<'db> ClassLiteral<'db> { } } - /// Returns `Some` if this is a protocol class, `None` otherwise. - pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option> { - self.is_protocol(db).then_some(ProtocolClassLiteral(self)) - } - /// Returns the [`Span`] of the class's "header": the class name /// and any arguments passed to the `class` statement. E.g. /// @@ -1784,145 +1777,6 @@ impl<'db> From> for Type<'db> { } } -/// Representation of a single `Protocol` class definition. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(super) struct ProtocolClassLiteral<'db>(ClassLiteral<'db>); - -impl<'db> ProtocolClassLiteral<'db> { - /// Returns the protocol members of this class. - /// - /// A protocol's members define the interface declared by the protocol. - /// They therefore determine how the protocol should behave with regards to - /// assignability and subtyping. - /// - /// The list of members consists of all bindings and declarations that take place - /// in the protocol's class body, except for a list of excluded attributes which should - /// not be taken into account. (This list includes `__init__` and `__new__`, which can - /// legally be defined on protocol classes but do not constitute protocol members.) - /// - /// It is illegal for a protocol class to have any instance attributes that are not declared - /// in the protocol's class body. If any are assigned to, they are not taken into account in - /// the protocol's list of members. - pub(super) fn protocol_members(self, db: &'db dyn Db) -> &'db FxOrderSet { - /// The list of excluded members is subject to change between Python versions, - /// especially for dunders, but it probably doesn't matter *too* much if this - /// list goes out of date. It's up to date as of Python commit 87b1ea016b1454b1e83b9113fa9435849b7743aa - /// () - fn excluded_from_proto_members(member: &str) -> bool { - matches!( - member, - "_is_protocol" - | "__non_callable_proto_members__" - | "__static_attributes__" - | "__orig_class__" - | "__match_args__" - | "__weakref__" - | "__doc__" - | "__parameters__" - | "__module__" - | "_MutableMapping__marker" - | "__slots__" - | "__dict__" - | "__new__" - | "__protocol_attrs__" - | "__init__" - | "__class_getitem__" - | "__firstlineno__" - | "__abstractmethods__" - | "__orig_bases__" - | "_is_runtime_protocol" - | "__subclasshook__" - | "__type_params__" - | "__annotations__" - | "__annotate__" - | "__annotate_func__" - | "__annotations_cache__" - ) - } - - #[salsa::tracked(return_ref, cycle_fn=proto_members_cycle_recover, cycle_initial=proto_members_cycle_initial)] - fn cached_protocol_members<'db>( - db: &'db dyn Db, - class: ClassLiteral<'db>, - ) -> FxOrderSet { - let mut members = FxOrderSet::default(); - - for parent_protocol in class - .iter_mro(db, None) - .filter_map(ClassBase::into_class) - .filter_map(|class| class.class_literal(db).0.into_protocol_class(db)) - { - let parent_scope = parent_protocol.body_scope(db); - let use_def_map = use_def_map(db, parent_scope); - let symbol_table = symbol_table(db, parent_scope); - - members.extend( - use_def_map - .all_public_declarations() - .flat_map(|(symbol_id, declarations)| { - symbol_from_declarations(db, declarations) - .map(|symbol| (symbol_id, symbol)) - }) - .filter_map(|(symbol_id, symbol)| { - symbol.symbol.ignore_possibly_unbound().map(|_| symbol_id) - }) - // Bindings in the class body that are not declared in the class body - // are not valid protocol members, and we plan to emit diagnostics for them - // elsewhere. Invalid or not, however, it's important that we still consider - // them to be protocol members. The implementation of `issubclass()` and - // `isinstance()` for runtime-checkable protocols considers them to be protocol - // members at runtime, and it's important that we accurately understand - // type narrowing that uses `isinstance()` or `issubclass()` with - // runtime-checkable protocols. - .chain(use_def_map.all_public_bindings().filter_map( - |(symbol_id, bindings)| { - symbol_from_bindings(db, bindings) - .ignore_possibly_unbound() - .map(|_| symbol_id) - }, - )) - .map(|symbol_id| symbol_table.symbol(symbol_id).name()) - .filter(|name| !excluded_from_proto_members(name)) - .cloned(), - ); - } - - members.sort(); - members.shrink_to_fit(); - members - } - - fn proto_members_cycle_recover( - _db: &dyn Db, - _value: &FxOrderSet, - _count: u32, - _class: ClassLiteral, - ) -> salsa::CycleRecoveryAction> { - salsa::CycleRecoveryAction::Iterate - } - - fn proto_members_cycle_initial(_db: &dyn Db, _class: ClassLiteral) -> FxOrderSet { - FxOrderSet::default() - } - - let _span = tracing::trace_span!("protocol_members", "class='{}'", self.name(db)).entered(); - cached_protocol_members(db, *self) - } - - pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool { - self.known_function_decorators(db) - .contains(&KnownFunction::RuntimeCheckable) - } -} - -impl<'db> Deref for ProtocolClassLiteral<'db> { - type Target = ClassLiteral<'db>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(super) enum InheritanceCycle { /// The class is cyclically defined and is a participant in the cycle. diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index c199c220e46521..6b0b9fb4ad43d3 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -1,5 +1,6 @@ use super::context::InferContext; use super::ClassLiteral; +use crate::db::Db; use crate::declare_lint; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; use crate::suppression::FileSuppressionId; @@ -8,8 +9,7 @@ use crate::types::string_annotation::{ IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, RAW_STRING_TYPE_ANNOTATION, }; -use crate::types::{class::ProtocolClassLiteral, KnownFunction, KnownInstanceType, Type}; -use crate::Db; +use crate::types::{protocol_class::ProtocolClassLiteral, KnownFunction, KnownInstanceType, Type}; use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic}; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::Ranged; diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index fc30a4f4654d75..b6e3681f2d4bc9 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -86,11 +86,11 @@ impl Display for DisplayRepresentation<'_> { Protocol::FromClass(ClassType::Generic(alias)) => alias.display(self.db).fmt(f), Protocol::Synthesized(synthetic) => { f.write_str(" Type<'db> { pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { @@ -34,9 +34,9 @@ impl<'db> Type<'db> { // as well as whether each member *exists* on `self`. protocol .0 - .protocol_members(db) - .iter() - .all(|member| !self.member(db, member).symbol.is_unbound()) + .interface(db) + .members() + .all(|member| !self.member(db, member.name()).symbol.is_unbound()) } } @@ -164,54 +164,51 @@ impl<'db> ProtocolInstanceType<'db> { } match self.0 { Protocol::FromClass(_) => Type::ProtocolInstance(Self(Protocol::Synthesized( - SynthesizedProtocolType::new(db, self.0.protocol_members(db)), + SynthesizedProtocolType::new(db, self.0.interface(db)), ))), Protocol::Synthesized(_) => Type::ProtocolInstance(self), } } - /// TODO: this should return `true` if any of the members of this protocol type contain any `Todo` types. - #[expect(clippy::unused_self)] - pub(super) fn contains_todo(self) -> bool { - false + /// Return `true` if any of the members of this protocol type contain any `Todo` types. + pub(super) fn contains_todo(self, db: &'db dyn Db) -> bool { + self.0.interface(db).contains_todo(db) } /// Return `true` if this protocol type is fully static. - /// - /// TODO: should not be considered fully static if any members do not have fully static types - #[expect(clippy::unused_self)] - pub(super) fn is_fully_static(self) -> bool { - true + pub(super) fn is_fully_static(self, db: &'db dyn Db) -> bool { + self.0.interface(db).is_fully_static(db) } /// Return `true` if this protocol type is a subtype of the protocol `other`. - /// - /// TODO: consider the types of the members as well as their existence pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { - self.0 - .protocol_members(db) - .is_superset(other.0.protocol_members(db)) + self.is_fully_static(db) && other.is_fully_static(db) && self.is_assignable_to(db, other) } /// Return `true` if this protocol type is assignable to the protocol `other`. /// /// TODO: consider the types of the members as well as their existence pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { - self.is_subtype_of(db, other) + other + .0 + .interface(db) + .is_sub_interface_of(self.0.interface(db)) } /// Return `true` if this protocol type is equivalent to the protocol `other`. /// /// TODO: consider the types of the members as well as their existence pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - self.normalized(db) == other.normalized(db) + self.is_fully_static(db) + && other.is_fully_static(db) + && self.normalized(db) == other.normalized(db) } /// Return `true` if this protocol type is gradually equivalent to the protocol `other`. /// /// TODO: consider the types of the members as well as their existence pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - self.is_equivalent_to(db, other) + self.normalized(db) == other.normalized(db) } /// Return `true` if this protocol type is disjoint from the protocol `other`. @@ -222,6 +219,20 @@ impl<'db> ProtocolInstanceType<'db> { pub(super) fn is_disjoint_from(self, _db: &'db dyn Db, _other: Self) -> bool { false } + + pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + match self.inner() { + Protocol::FromClass(class) => class.instance_member(db, name), + Protocol::Synthesized(synthesized) => synthesized + .interface(db) + .member_by_name(name) + .map(|member| SymbolAndQualifiers { + symbol: Symbol::bound(member.ty()), + qualifiers: member.qualifiers(), + }) + .unwrap_or_else(|| KnownClass::Object.to_instance(db).instance_member(db, name)), + } + } } /// An enumeration of the two kinds of protocol types: those that originate from a class @@ -236,15 +247,15 @@ pub(super) enum Protocol<'db> { impl<'db> Protocol<'db> { /// Return the members of this protocol type - fn protocol_members(self, db: &'db dyn Db) -> &'db FxOrderSet { + fn interface(self, db: &'db dyn Db) -> &'db ProtocolInterface<'db> { match self { Self::FromClass(class) => class .class_literal(db) .0 .into_protocol_class(db) .expect("Protocol class literal should be a protocol class") - .protocol_members(db), - Self::Synthesized(synthesized) => synthesized.members(db), + .interface(db), + Self::Synthesized(synthesized) => synthesized.interface(db), } } } @@ -258,5 +269,5 @@ impl<'db> Protocol<'db> { #[salsa::interned(debug)] pub(super) struct SynthesizedProtocolType<'db> { #[return_ref] - pub(super) members: FxOrderSet, + pub(super) interface: ProtocolInterface<'db>, } diff --git a/crates/red_knot_python_semantic/src/types/protocol_class.rs b/crates/red_knot_python_semantic/src/types/protocol_class.rs new file mode 100644 index 00000000000000..6706dea53c1434 --- /dev/null +++ b/crates/red_knot_python_semantic/src/types/protocol_class.rs @@ -0,0 +1,242 @@ +use std::{collections::BTreeMap, ops::Deref}; + +use itertools::Itertools; + +use ruff_python_ast::name::Name; + +use crate::{ + db::Db, + semantic_index::{symbol_table, use_def_map}, + symbol::{symbol_from_bindings, symbol_from_declarations}, + types::{ClassBase, ClassLiteral, KnownFunction, Type, TypeQualifiers}, +}; + +impl<'db> ClassLiteral<'db> { + /// Returns `Some` if this is a protocol class, `None` otherwise. + pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option> { + self.is_protocol(db).then_some(ProtocolClassLiteral(self)) + } +} + +/// Representation of a single `Protocol` class definition. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) struct ProtocolClassLiteral<'db>(ClassLiteral<'db>); + +impl<'db> ProtocolClassLiteral<'db> { + /// Returns the protocol members of this class. + /// + /// A protocol's members define the interface declared by the protocol. + /// They therefore determine how the protocol should behave with regards to + /// assignability and subtyping. + /// + /// The list of members consists of all bindings and declarations that take place + /// in the protocol's class body, except for a list of excluded attributes which should + /// not be taken into account. (This list includes `__init__` and `__new__`, which can + /// legally be defined on protocol classes but do not constitute protocol members.) + /// + /// It is illegal for a protocol class to have any instance attributes that are not declared + /// in the protocol's class body. If any are assigned to, they are not taken into account in + /// the protocol's list of members. + pub(super) fn interface(self, db: &'db dyn Db) -> &'db ProtocolInterface<'db> { + let _span = tracing::trace_span!("protocol_members", "class='{}'", self.name(db)).entered(); + cached_protocol_interface(db, *self) + } + + pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool { + self.known_function_decorators(db) + .contains(&KnownFunction::RuntimeCheckable) + } +} + +impl<'db> Deref for ProtocolClassLiteral<'db> { + type Target = ClassLiteral<'db>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// The interface of a protocol: the members of that protocol, and the types of those members. +#[derive(Debug, PartialEq, Eq, salsa::Update, Default, Clone, Hash)] +pub(super) struct ProtocolInterface<'db>(BTreeMap>); + +impl<'db> ProtocolInterface<'db> { + /// Iterate over the members of this protocol. + pub(super) fn members<'a>(&'a self) -> impl ExactSizeIterator> { + self.0.iter().map(|(name, data)| ProtocolMember { + name, + ty: data.ty, + qualifiers: data.qualifiers, + }) + } + + pub(super) fn member_by_name<'a>(&self, name: &'a str) -> Option> { + self.0.get(name).map(|data| ProtocolMember { + name, + ty: data.ty, + qualifiers: data.qualifiers, + }) + } + + /// Return `true` if all members of this protocol are fully static. + pub(super) fn is_fully_static(&self, db: &'db dyn Db) -> bool { + self.members().all(|member| member.ty.is_fully_static(db)) + } + + /// Return `true` if if all members on `self` are also members of `other`. + /// + /// TODO: this method should consider the types of the members as well as their names. + pub(super) fn is_sub_interface_of(&self, other: &Self) -> bool { + self.0 + .keys() + .all(|member_name| other.0.contains_key(member_name)) + } + + /// Return `true` if any of the members of this protocol type contain any `Todo` types. + pub(super) fn contains_todo(&self, db: &'db dyn Db) -> bool { + self.members().any(|member| member.ty.contains_todo(db)) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)] +struct ProtocolMemberData<'db> { + ty: Type<'db>, + qualifiers: TypeQualifiers, +} + +/// A single member of a protocol interface. +#[derive(Debug, PartialEq, Eq)] +pub(super) struct ProtocolMember<'a, 'db> { + name: &'a str, + ty: Type<'db>, + qualifiers: TypeQualifiers, +} + +impl<'a, 'db> ProtocolMember<'a, 'db> { + pub(super) fn name(&self) -> &'a str { + self.name + } + + pub(super) fn ty(&self) -> Type<'db> { + self.ty + } + + pub(super) fn qualifiers(&self) -> TypeQualifiers { + self.qualifiers + } +} + +/// Returns `true` if a declaration or binding to a given name in a protocol class body +/// should be excluded from the list of protocol members of that class. +/// +/// The list of excluded members is subject to change between Python versions, +/// especially for dunders, but it probably doesn't matter *too* much if this +/// list goes out of date. It's up to date as of Python commit 87b1ea016b1454b1e83b9113fa9435849b7743aa +/// () +fn excluded_from_proto_members(member: &str) -> bool { + matches!( + member, + "_is_protocol" + | "__non_callable_proto_members__" + | "__static_attributes__" + | "__orig_class__" + | "__match_args__" + | "__weakref__" + | "__doc__" + | "__parameters__" + | "__module__" + | "_MutableMapping__marker" + | "__slots__" + | "__dict__" + | "__new__" + | "__protocol_attrs__" + | "__init__" + | "__class_getitem__" + | "__firstlineno__" + | "__abstractmethods__" + | "__orig_bases__" + | "_is_runtime_protocol" + | "__subclasshook__" + | "__type_params__" + | "__annotations__" + | "__annotate__" + | "__annotate_func__" + | "__annotations_cache__" + ) +} + +/// Inner Salsa query for [`ProtocolClassLiteral::interface`]. +#[salsa::tracked(return_ref, cycle_fn=proto_interface_cycle_recover, cycle_initial=proto_interface_cycle_initial)] +fn cached_protocol_interface<'db>( + db: &'db dyn Db, + class: ClassLiteral<'db>, +) -> ProtocolInterface<'db> { + let mut members = BTreeMap::default(); + + for parent_protocol in class + .iter_mro(db, None) + .filter_map(ClassBase::into_class) + .filter_map(|class| class.class_literal(db).0.into_protocol_class(db)) + { + let parent_scope = parent_protocol.body_scope(db); + let use_def_map = use_def_map(db, parent_scope); + let symbol_table = symbol_table(db, parent_scope); + + members.extend( + use_def_map + .all_public_declarations() + .flat_map(|(symbol_id, declarations)| { + symbol_from_declarations(db, declarations).map(|symbol| (symbol_id, symbol)) + }) + .filter_map(|(symbol_id, symbol)| { + symbol + .symbol + .ignore_possibly_unbound() + .map(|ty| (symbol_id, ty, symbol.qualifiers)) + }) + // Bindings in the class body that are not declared in the class body + // are not valid protocol members, and we plan to emit diagnostics for them + // elsewhere. Invalid or not, however, it's important that we still consider + // them to be protocol members. The implementation of `issubclass()` and + // `isinstance()` for runtime-checkable protocols considers them to be protocol + // members at runtime, and it's important that we accurately understand + // type narrowing that uses `isinstance()` or `issubclass()` with + // runtime-checkable protocols. + .chain( + use_def_map + .all_public_bindings() + .filter_map(|(symbol_id, bindings)| { + symbol_from_bindings(db, bindings) + .ignore_possibly_unbound() + .map(|ty| (symbol_id, ty, TypeQualifiers::default())) + }), + ) + .map(|(symbol_id, member, qualifiers)| { + (symbol_table.symbol(symbol_id).name(), member, qualifiers) + }) + .filter(|(name, _, _)| !excluded_from_proto_members(name)) + .map(|(name, ty, qualifiers)| { + let member = ProtocolMemberData { ty, qualifiers }; + (name.clone(), member) + }), + ); + } + + ProtocolInterface(members) +} + +fn proto_interface_cycle_recover<'db>( + _db: &dyn Db, + _value: &ProtocolInterface<'db>, + _count: u32, + _class: ClassLiteral<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn proto_interface_cycle_initial<'db>( + _db: &dyn Db, + _class: ClassLiteral<'db>, +) -> ProtocolInterface<'db> { + ProtocolInterface::default() +} From b7d0b3f9e5251d3b43bbb322d6aa3d0ae8b865c9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 3 May 2025 13:41:55 +0100 Subject: [PATCH 0227/1161] [red-knot] Add tests asserting that subclasses of `Any` are assignable to arbitrary protocol types (#17810) --- .../resources/mdtest/annotations/any.md | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md index 731b4de2fd9425..14468e3d93e0e7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md @@ -82,10 +82,10 @@ class OtherFinalClass: ... f: FinalClass | OtherFinalClass = SubclassOfAny() # error: [invalid-assignment] ``` -A subclass of `Any` can also be assigned to arbitrary `Callable` types: +A subclass of `Any` can also be assigned to arbitrary `Callable` and `Protocol` types: ```py -from typing import Callable, Any +from typing import Callable, Any, Protocol def takes_callable1(f: Callable): f() @@ -96,6 +96,25 @@ def takes_callable2(f: Callable[[int], None]): f(1) takes_callable2(SubclassOfAny()) + +class CallbackProtocol(Protocol): + def __call__(self, x: int, /) -> None: ... + +def takes_callback_proto(f: CallbackProtocol): + f(1) + +takes_callback_proto(SubclassOfAny()) + +class OtherProtocol(Protocol): + x: int + @property + def foo(self) -> bytes: ... + @foo.setter + def foo(self, x: str) -> None: ... + +def takes_other_protocol(f: OtherProtocol): ... + +takes_other_protocol(SubclassOfAny()) ``` A subclass of `Any` cannot be assigned to literal types, since those can not be subclassed: From 097af060c95f434900fe74dcbd34a1c3291ba1ab Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Sat, 3 May 2025 10:59:46 -0300 Subject: [PATCH 0228/1161] [`refurb`] Fix false positive for float and complex numbers in `FURB116` (#17661) --- .../resources/test/fixtures/refurb/FURB116.py | 5 +++++ .../src/rules/refurb/rules/fstring_number_format.rs | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB116.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB116.py index 9c968e32aa8880..b95f9147e77acc 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB116.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB116.py @@ -17,3 +17,8 @@ def return_num() -> int: ## invalid print(oct(0o1337)[1:]) print(hex(0x1337)[3:]) + +# https://github.com/astral-sh/ruff/issues/16472 +# float and complex numbers should be ignored +print(bin(1.0)[2:]) +print(bin(3.14j)[2:]) diff --git a/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs b/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs index d71772e6ded692..0d18e8d06fb120 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/fstring_number_format.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{self as ast, Expr, ExprCall}; +use ruff_python_ast::{self as ast, Expr, ExprCall, Number}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -110,6 +110,17 @@ pub(crate) fn fstring_number_format(checker: &Checker, subscript: &ast::ExprSubs return; }; + // float and complex numbers are false positives, ignore them. + if matches!( + arg, + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: Number::Float(_) | Number::Complex { .. }, + .. + }) + ) { + return; + } + // Generate a replacement, if possible. let replacement = if matches!( arg, From 91481a8be75446c7b404211fdeccd65c68a371ff Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 3 May 2025 15:29:05 +0100 Subject: [PATCH 0229/1161] [red-knot] Minor simplifications to `types/display.rs` (#17813) --- .../src/types/display.rs | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index b6e3681f2d4bc9..b4f9a103cb08f9 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -76,7 +76,7 @@ impl Display for DisplayRepresentation<'_> { (_, Some(KnownClass::NoneType)) => f.write_str("None"), (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)), - (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)), + (ClassType::Generic(alias), _) => alias.display(self.db).fmt(f), } } Type::ProtocolInstance(protocol) => match protocol.inner() { @@ -104,16 +104,14 @@ impl Display for DisplayRepresentation<'_> { } // TODO functions and classes should display using a fully qualified name Type::ClassLiteral(class) => f.write_str(class.name(self.db)), - Type::GenericAlias(generic) => { - write!(f, "{}", generic.display(self.db)) - } + Type::GenericAlias(generic) => generic.display(self.db).fmt(f), Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { // Only show the bare class name here; ClassBase::display would render this as // type[] instead of type[Foo]. SubclassOfInner::Class(class) => write!(f, "type[{}]", class.name(self.db)), SubclassOfInner::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), }, - Type::KnownInstance(known_instance) => write!(f, "{}", known_instance.repr(self.db)), + Type::KnownInstance(known_instance) => known_instance.repr(self.db).fmt(f), Type::FunctionLiteral(function) => { let signature = function.signature(self.db); @@ -263,9 +261,7 @@ impl Display for DisplayRepresentation<'_> { } f.write_str("]") } - Type::TypeVar(typevar) => { - write!(f, "{}", typevar.name(self.db)) - } + Type::TypeVar(typevar) => f.write_str(typevar.name(self.db)), Type::AlwaysTruthy => f.write_str("AlwaysTruthy"), Type::AlwaysFalsy => f.write_str("AlwaysFalsy"), Type::BoundSuper(bound_super) => { @@ -328,7 +324,7 @@ impl Display for DisplayGenericContext<'_> { if idx > 0 { f.write_str(", ")?; } - write!(f, "{}", var.name(self.db))?; + f.write_str(var.name(self.db))?; match var.bound_or_constraints(self.db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { write!(f, ": {}", bound.display(self.db))?; @@ -339,7 +335,7 @@ impl Display for DisplayGenericContext<'_> { if idx > 0 { f.write_str(", ")?; } - write!(f, "{}", constraint.display(self.db))?; + constraint.display(self.db).fmt(f)?; } f.write_char(')')?; } @@ -399,7 +395,7 @@ impl Display for DisplaySpecialization<'_> { if idx > 0 { f.write_str(", ")?; } - write!(f, "{}", ty.display(self.db))?; + ty.display(self.db).fmt(f)?; } f.write_char(']') } @@ -423,7 +419,7 @@ pub(crate) struct DisplayCallableType<'db> { impl Display for DisplayCallableType<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self.signatures { - [signature] => write!(f, "{}", signature.display(self.db)), + [signature] => signature.display(self.db).fmt(f), signatures => { // TODO: How to display overloads? f.write_str("Overload[")?; @@ -490,9 +486,7 @@ impl Display for DisplaySignature<'_> { f, ") -> {}", self.return_ty.unwrap_or(Type::unknown()).display(self.db) - )?; - - Ok(()) + ) } } @@ -510,7 +504,7 @@ struct DisplayParameter<'db> { impl Display for DisplayParameter<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if let Some(name) = self.param.display_name() { - write!(f, "{name}")?; + f.write_str(&name)?; if let Some(annotated_type) = self.param.annotated_type() { write!(f, ": {}", annotated_type.display(self.db))?; } @@ -767,9 +761,9 @@ impl Display for DisplayStringLiteralType<'_> { match ch { // `escape_debug` will escape even single quotes, which is not necessary for our // use case as we are already using double quotes to wrap the string. - '\'' => f.write_char('\'')?, - _ => write!(f, "{}", ch.escape_debug())?, - } + '\'' => f.write_char('\''), + _ => ch.escape_debug().fmt(f), + }?; } f.write_char('"') } From c4a08782ccfd1658782f341f27717e3688f4708f Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Sat, 3 May 2025 11:38:31 -0400 Subject: [PATCH 0230/1161] Add regression test for parent `noqa` (#17783) Summary -- Adds a regression test for #2253 after I tried to delete the fix from #2464. --- crates/ruff/tests/lint.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index 77dcc222e7ed1c..cb701bf1402b81 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -1900,6 +1900,40 @@ def first_square(): Ok(()) } +/// Regression test for +#[test] +fn add_noqa_parent() -> Result<()> { + let tempdir = TempDir::new()?; + let test_path = tempdir.path().join("noqa.py"); + fs::write( + &test_path, + r#" +from foo import ( # noqa: F401 + bar +) + "#, + )?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--add-noqa") + .arg("--select=F401") + .arg("noqa.py") + .current_dir(&tempdir), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + }); + + Ok(()) +} + /// Infer `3.11` from `requires-python` in `pyproject.toml`. #[test] fn requires_python() -> Result<()> { From 52b04708701a9ee622a3ffb568561d4972db3cd9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 3 May 2025 16:43:18 +0100 Subject: [PATCH 0231/1161] [red-knot] Synthesize a `__call__` attribute for Callable types (#17809) ## Summary Currently this assertion fails on `main`, because we do not synthesize a `__call__` attribute for Callable types: ```py from typing import Protocol, Callable from knot_extensions import static_assert, is_assignable_to class Foo(Protocol): def __call__(self, x: int, /) -> str: ... static_assert(is_assignable_to(Callable[[int], str], Foo)) ``` This PR fixes that. See previous discussion about this in https://github.com/astral-sh/ruff/pull/16493#discussion_r1985098508 and https://github.com/astral-sh/ruff/pull/17682#issuecomment-2839527750 ## Test Plan Existing mdtests updated; a couple of new ones added. --- .../resources/mdtest/annotations/callable.md | 7 +--- .../resources/mdtest/protocols.md | 32 +++++++++++-------- crates/red_knot_python_semantic/src/types.rs | 4 +++ 3 files changed, 24 insertions(+), 19 deletions(-) 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 29cef6cae348ca..9bd7ae87424326 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md @@ -300,12 +300,7 @@ from typing import Callable def _(c: Callable[[int], int]): reveal_type(c.__init__) # revealed: def __init__(self) -> None reveal_type(c.__class__) # revealed: type - - # TODO: The member lookup for `Callable` uses `object` which does not have a `__call__` - # attribute. We could special case `__call__` in this context. Refer to - # https://github.com/astral-sh/ruff/pull/16493#discussion_r1985098508 for more details. - # error: [unresolved-attribute] "Type `(int, /) -> int` has no attribute `__call__`" - reveal_type(c.__call__) # revealed: Unknown + reveal_type(c.__call__) # revealed: (int, /) -> int ``` [gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index b55a41a17c1bcf..1b33205e479528 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -1476,26 +1476,32 @@ signature implied by the `Callable` type is assignable to the signature of the ` specified by the protocol: ```py +from knot_extensions import TypeOf + class Foo(Protocol): def __call__(self, x: int, /) -> str: ... -# TODO: these fail because we don't yet understand that all `Callable` types have a `__call__` method, -# and we therefore don't think that the `Callable` type is assignable to `Foo`. They should pass. -static_assert(is_subtype_of(Callable[[int], str], Foo)) # error: [static-assert-error] -static_assert(is_assignable_to(Callable[[int], str], Foo)) # error: [static-assert-error] +static_assert(is_subtype_of(Callable[[int], str], Foo)) +static_assert(is_assignable_to(Callable[[int], str], Foo)) -static_assert(not is_subtype_of(Callable[[str], str], Foo)) -static_assert(not is_assignable_to(Callable[[str], str], Foo)) -static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo)) -static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo)) +# TODO: these should pass +static_assert(not is_subtype_of(Callable[[str], str], Foo)) # error: [static-assert-error] +static_assert(not is_assignable_to(Callable[[str], str], Foo)) # error: [static-assert-error] +static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo)) # error: [static-assert-error] +static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo)) # error: [static-assert-error] def h(obj: Callable[[int], str], obj2: Foo, obj3: Callable[[str], str]): - # TODO: this fails because we don't yet understand that all `Callable` types have a `__call__` method, - # and we therefore don't think that the `Callable` type is assignable to `Foo`. It should pass. - obj2 = obj # error: [invalid-assignment] + obj2 = obj + + # TODO: we should emit [invalid-assignment] here because the signature of `obj3` is not assignable + # to the declared type of `obj2` + obj2 = obj3 + +def satisfies_foo(x: int) -> str: + return "foo" - # This diagnostic is correct, however. - obj2 = obj3 # error: [invalid-assignment] +static_assert(is_subtype_of(TypeOf[satisfies_foo], Foo)) +static_assert(is_assignable_to(TypeOf[satisfies_foo], Foo)) ``` ## Protocols are never singleton types, and are never single-valued types diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 404b59738e35ed..049f2de3be06f0 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2953,6 +2953,10 @@ impl<'db> Type<'db> { Type::DataclassDecorator(_) => KnownClass::FunctionType .to_instance(db) .member_lookup_with_policy(db, name, policy), + + Type::Callable(_) | Type::DataclassTransformer(_) if name_str == "__call__" => { + Symbol::bound(self).into() + } Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Object .to_instance(db) .member_lookup_with_policy(db, name, policy), From e6a798b9625b9456dfd5b1d109830981b33ad954 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 3 May 2025 16:43:37 +0100 Subject: [PATCH 0232/1161] [red-knot] Recurse into the types of protocol members when normalizing a protocol's interface (#17808) ## Summary Currently red-knot does not understand `Foo` and `Bar` here as being equivalent: ```py from typing import Protocol class A: ... class B: ... class C: ... class Foo(Protocol): x: A | B | C class Bar(Protocol): x: B | A | C ``` Nor does it understand `A | B | Foo` as being equivalent to `Bar | B | A`. This PR fixes that. ## Test Plan new mdtest assertions added that fail on `main` --- .../resources/mdtest/protocols.md | 16 ++++++ .../src/types/instance.rs | 52 ++++++++++++++----- .../src/types/protocol_class.rs | 18 +++++++ 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 1b33205e479528..cc3f8143f8e964 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -816,6 +816,22 @@ class B: ... static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A)) ``` +Protocols are considered equivalent if their members are equivalent, even if those members are +differently ordered unions: + +```py +class C: ... + +class UnionProto1(Protocol): + x: A | B | C + +class UnionProto2(Protocol): + x: C | A | B + +static_assert(is_equivalent_to(UnionProto1, UnionProto2)) +static_assert(is_equivalent_to(UnionProto1 | A | B, B | UnionProto2 | A)) +``` + ## Intersections of protocols An intersection of two protocol types `X` and `Y` is equivalent to a protocol type `Z` that inherits diff --git a/crates/red_knot_python_semantic/src/types/instance.rs b/crates/red_knot_python_semantic/src/types/instance.rs index a6e3f805d1cd4b..762dc9a34149f7 100644 --- a/crates/red_knot_python_semantic/src/types/instance.rs +++ b/crates/red_knot_python_semantic/src/types/instance.rs @@ -5,6 +5,8 @@ use super::{ClassType, KnownClass, SubclassOfType, Type}; use crate::symbol::{Symbol, SymbolAndQualifiers}; use crate::Db; +pub(super) use synthesized_protocol::SynthesizedProtocolType; + impl<'db> Type<'db> { pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { if class.class_literal(db).0.is_protocol(db) { @@ -164,7 +166,7 @@ impl<'db> ProtocolInstanceType<'db> { } match self.0 { Protocol::FromClass(_) => Type::ProtocolInstance(Self(Protocol::Synthesized( - SynthesizedProtocolType::new(db, self.0.interface(db)), + SynthesizedProtocolType::new(db, self.0.interface(db).clone()), ))), Protocol::Synthesized(_) => Type::ProtocolInstance(self), } @@ -237,9 +239,7 @@ impl<'db> ProtocolInstanceType<'db> { /// An enumeration of the two kinds of protocol types: those that originate from a class /// definition in source code, and those that are synthesized from a set of members. -#[derive( - Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, salsa::Supertype, PartialOrd, Ord, -)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, PartialOrd, Ord)] pub(super) enum Protocol<'db> { FromClass(ClassType<'db>), Synthesized(SynthesizedProtocolType<'db>), @@ -260,14 +260,38 @@ impl<'db> Protocol<'db> { } } -/// A "synthesized" protocol type that is dissociated from a class definition in source code. -/// -/// Two synthesized protocol types with the same members will share the same Salsa ID, -/// making them easy to compare for equivalence. A synthesized protocol type is therefore -/// returned by [`ProtocolInstanceType::normalized`] so that two protocols with the same members -/// will be understood as equivalent even in the context of differently ordered unions or intersections. -#[salsa::interned(debug)] -pub(super) struct SynthesizedProtocolType<'db> { - #[return_ref] - pub(super) interface: ProtocolInterface<'db>, +mod synthesized_protocol { + use crate::db::Db; + use crate::types::protocol_class::ProtocolInterface; + + /// A "synthesized" protocol type that is dissociated from a class definition in source code. + /// + /// Two synthesized protocol types with the same members will share the same Salsa ID, + /// making them easy to compare for equivalence. A synthesized protocol type is therefore + /// returned by [`super::ProtocolInstanceType::normalized`] so that two protocols with the same members + /// will be understood as equivalent even in the context of differently ordered unions or intersections. + /// + /// The constructor method of this type maintains the invariant that a synthesized protocol type + /// is always constructed from a *normalized* protocol interface. + #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, PartialOrd, Ord)] + pub(in crate::types) struct SynthesizedProtocolType<'db>(SynthesizedProtocolTypeInner<'db>); + + impl<'db> SynthesizedProtocolType<'db> { + pub(super) fn new(db: &'db dyn Db, interface: ProtocolInterface<'db>) -> Self { + Self(SynthesizedProtocolTypeInner::new( + db, + interface.normalized(db), + )) + } + + pub(in crate::types) fn interface(self, db: &'db dyn Db) -> &'db ProtocolInterface<'db> { + self.0.interface(db) + } + } + + #[salsa::interned(debug)] + struct SynthesizedProtocolTypeInner<'db> { + #[return_ref] + interface: ProtocolInterface<'db>, + } } diff --git a/crates/red_knot_python_semantic/src/types/protocol_class.rs b/crates/red_knot_python_semantic/src/types/protocol_class.rs index 6706dea53c1434..743ca2f7a79910 100644 --- a/crates/red_knot_python_semantic/src/types/protocol_class.rs +++ b/crates/red_knot_python_semantic/src/types/protocol_class.rs @@ -96,6 +96,15 @@ impl<'db> ProtocolInterface<'db> { pub(super) fn contains_todo(&self, db: &'db dyn Db) -> bool { self.members().any(|member| member.ty.contains_todo(db)) } + + pub(super) fn normalized(self, db: &'db dyn Db) -> Self { + Self( + self.0 + .into_iter() + .map(|(name, data)| (name, data.normalized(db))) + .collect(), + ) + } } #[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)] @@ -104,6 +113,15 @@ struct ProtocolMemberData<'db> { qualifiers: TypeQualifiers, } +impl<'db> ProtocolMemberData<'db> { + fn normalized(self, db: &'db dyn Db) -> Self { + Self { + ty: self.ty.normalized(db), + qualifiers: self.qualifiers, + } + } +} + /// A single member of a protocol interface. #[derive(Debug, PartialEq, Eq)] pub(super) struct ProtocolMember<'a, 'db> { From b51c4f82ea6b6e40dcf3622dd009d463bb7e2247 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 3 May 2025 19:49:15 +0200 Subject: [PATCH 0233/1161] Rename Red Knot (#17820) --- .github/CODEOWNERS | 10 +- .github/workflows/ci.yaml | 58 +-- .github/workflows/daily_property_tests.yaml | 10 +- .github/workflows/mypy_primer.yaml | 8 +- ...ayground.yml => publish-ty-playground.yml} | 14 +- .github/workflows/sync_typeshed.yaml | 16 +- .pre-commit-config.yaml | 4 +- Cargo.lock | 426 +++++++++--------- Cargo.toml | 12 +- _typos.toml | 2 +- crates/red_knot/README.md | 25 - .../mdtest/suppressions/knot_ignore.md | 191 -------- .../knot_extensions/README.md | 3 - crates/ruff/tests/analyze_graph.rs | 2 +- crates/ruff_benchmark/Cargo.toml | 4 +- .../benches/{red_knot.rs => ty.rs} | 16 +- crates/ruff_db/src/diagnostic/mod.rs | 10 +- crates/ruff_db/src/diagnostic/render.rs | 2 +- crates/ruff_db/src/testing.rs | 10 +- crates/ruff_dev/Cargo.toml | 2 +- crates/ruff_dev/src/generate_all.rs | 4 +- ...e_knot_schema.rs => generate_ty_schema.rs} | 8 +- crates/ruff_dev/src/main.rs | 8 +- crates/ruff_graph/Cargo.toml | 2 +- crates/ruff_graph/src/collector.rs | 2 +- crates/ruff_graph/src/db.rs | 10 +- crates/ruff_graph/src/resolver.rs | 2 +- .../test/fixtures/flake8_bandit/S704.py | 2 +- crates/ruff_linter/src/logging.rs | 2 +- ...s__flake8_bandit__tests__S704_S704.py.snap | 2 +- crates/ruff_macros/src/lib.rs | 4 +- crates/{red_knot => ty}/Cargo.toml | 8 +- crates/ty/README.md | 25 + crates/{red_knot => ty}/build.rs | 6 +- crates/{red_knot => ty}/docs/mypy_primer.md | 8 +- .../docs/tracing-flamegraph.png | Bin crates/{red_knot => ty}/docs/tracing.md | 28 +- crates/{red_knot => ty}/src/args.rs | 20 +- crates/{red_knot => ty}/src/logging.rs | 26 +- crates/{red_knot => ty}/src/main.rs | 20 +- crates/{red_knot => ty}/src/python_version.rs | 0 crates/{red_knot => ty}/src/version.rs | 25 +- crates/{red_knot => ty}/tests/cli.rs | 30 +- .../{red_knot => ty}/tests/file_watching.rs | 40 +- crates/{red_knot_ide => ty_ide}/Cargo.toml | 6 +- .../src/completion.rs | 0 crates/{red_knot_ide => ty_ide}/src/db.rs | 8 +- .../{red_knot_ide => ty_ide}/src/find_node.rs | 0 crates/{red_knot_ide => ty_ide}/src/goto.rs | 4 +- crates/{red_knot_ide => ty_ide}/src/hover.rs | 4 +- .../src/inlay_hints.rs | 10 +- crates/{red_knot_ide => ty_ide}/src/lib.rs | 8 +- crates/{red_knot_ide => ty_ide}/src/markup.rs | 0 .../Cargo.toml | 14 +- .../resources/test/corpus/00_const.py | 0 .../resources/test/corpus/00_empty.py | 0 .../resources/test/corpus/00_expr_discard.py | 0 .../resources/test/corpus/00_expr_var1.py | 0 .../resources/test/corpus/01_expr_unary.py | 0 .../resources/test/corpus/02_expr_attr.py | 0 .../test/corpus/02_expr_attr_multiline.py | 0 .../corpus/02_expr_attr_multiline_assign.py | 0 .../resources/test/corpus/02_expr_bin_bool.py | 0 .../resources/test/corpus/02_expr_binary.py | 0 .../test/corpus/02_expr_bool_op_multiline.py | 0 .../test/corpus/02_expr_bool_op_multiline2.py | 0 .../resources/test/corpus/02_expr_rel.py | 0 .../test/corpus/02_expr_rel_multiple.py | 0 .../resources/test/corpus/02_expr_subscr.py | 0 .../resources/test/corpus/03_dict.py | 0 .../resources/test/corpus/03_dict_ex.py | 0 .../test/corpus/03_dict_literal_large.py | 0 .../test/corpus/03_dict_unpack_huge.py | 0 .../resources/test/corpus/03_list.py | 0 .../resources/test/corpus/03_list_ex.py | 0 .../resources/test/corpus/03_list_large.py | 0 .../resources/test/corpus/03_set.py | 0 .../resources/test/corpus/03_set_multi.py | 0 .../resources/test/corpus/03_slice.py | 0 .../resources/test/corpus/03_slice_ext.py | 0 .../resources/test/corpus/03_tuple.py | 0 .../resources/test/corpus/03_tuple_ex.py | 0 .../resources/test/corpus/04_assign.py | 0 .../resources/test/corpus/04_assign_attr.py | 0 .../test/corpus/04_assign_attr_func.py | 0 .../test/corpus/04_assign_invalid_target.py | 0 .../test/corpus/04_assign_named_expr.py | 0 .../04_assign_named_expr_invalid_target.py | 0 .../resources/test/corpus/04_assign_subscr.py | 0 .../resources/test/corpus/04_assign_unpack.py | 0 .../test/corpus/04_assign_unpack_ex.py | 0 .../test/corpus/04_assign_unpack_tuple.py | 0 .../resources/test/corpus/04_aug_assign.py | 0 .../corpus/04_aug_assign_attr_multiline.py | 0 .../test/corpus/04_aug_assign_attr_sub.py | 0 .../corpus/04_aug_assign_invalid_target.py | 0 .../resources/test/corpus/05_funcall.py | 0 .../resources/test/corpus/05_funcall_1.py | 0 .../resources/test/corpus/05_funcall_2.py | 0 .../corpus/05_funcall_in_multiline_tuple.py | 0 .../resources/test/corpus/05_funcall_kw.py | 0 .../test/corpus/05_funcall_kw_many.py | 0 .../test/corpus/05_funcall_kw_pos.py | 0 .../corpus/05_funcall_method_multiline.py | 0 .../test/corpus/06_funcall_kwargs.py | 0 .../test/corpus/06_funcall_many_args.py | 0 .../test/corpus/06_funcall_starargs_ex.py | 0 .../test/corpus/06_funcall_varargs.py | 0 .../test/corpus/06_funcall_varargs_kwargs.py | 0 .../corpus/06_funcall_varargs_kwargs_mixed.py | 0 .../resources/test/corpus/07_ifexpr.py | 0 .../test/corpus/07_ifexpr_multiline.py | 0 .../test/corpus/07_ifexpr_multiline2.py | 0 .../resources/test/corpus/08_del.py | 0 .../resources/test/corpus/08_del_multi.py | 0 .../resources/test/corpus/09_pass.py | 0 .../resources/test/corpus/10_if.py | 0 .../test/corpus/10_if_chained_compare.py | 0 .../resources/test/corpus/10_if_false.py | 0 .../resources/test/corpus/10_if_invalid.py | 0 .../resources/test/corpus/10_if_true.py | 0 .../test/corpus/10_if_with_named_expr.py | 0 .../resources/test/corpus/11_if_else.py | 0 .../corpus/11_if_else_deeply_nested_for.py | 0 .../resources/test/corpus/11_if_else_false.py | 0 .../resources/test/corpus/11_if_else_true.py | 0 .../resources/test/corpus/12_if_elif.py | 0 .../resources/test/corpus/12_if_elif_else.py | 0 .../test/corpus/13_ifelse_complex1.py | 0 .../resources/test/corpus/13_ifelse_many.py | 0 .../resources/test/corpus/15_while.py | 0 .../resources/test/corpus/15_while_break.py | 0 .../test/corpus/15_while_break_in_finally.py | 0 .../corpus/15_while_break_invalid_in_class.py | 0 .../corpus/15_while_break_invalid_in_func.py | 0 .../test/corpus/15_while_break_non_empty.py | 0 .../test/corpus/15_while_break_non_exit.py | 0 .../test/corpus/15_while_continue.py | 0 .../resources/test/corpus/15_while_false.py | 0 .../test/corpus/15_while_infinite.py | 0 .../resources/test/corpus/15_while_true.py | 0 .../resources/test/corpus/16_for.py | 0 .../resources/test/corpus/16_for_break.py | 0 .../corpus/16_for_break_invalid_in_class.py | 0 .../corpus/16_for_break_invalid_in_func.py | 0 .../resources/test/corpus/16_for_continue.py | 0 .../resources/test/corpus/16_for_else.py | 0 .../resources/test/corpus/16_for_invalid.py | 0 .../test/corpus/16_for_list_literal.py | 0 .../test/corpus/16_for_nested_ifs.py | 0 .../resources/test/corpus/20_lambda.py | 0 .../resources/test/corpus/20_lambda_const.py | 0 .../test/corpus/20_lambda_default_arg.py | 0 .../resources/test/corpus/20_lambda_ifelse.py | 0 .../resources/test/corpus/21_func1.py | 0 .../resources/test/corpus/21_func1_ret.py | 0 .../resources/test/corpus/21_func_assign.py | 0 .../resources/test/corpus/21_func_assign2.py | 0 .../resources/test/corpus/22_func_arg.py | 0 .../resources/test/corpus/22_func_vararg.py | 0 .../resources/test/corpus/23_func_ret.py | 0 .../resources/test/corpus/23_func_ret_val.py | 0 .../resources/test/corpus/24_func_if_ret.py | 0 .../test/corpus/24_func_ifelse_ret.py | 0 .../test/corpus/24_func_ifnot_ret.py | 0 .../test/corpus/25_func_annotations.py | 0 .../test/corpus/25_func_annotations_nested.py | 0 .../corpus/25_func_annotations_same_name.py | 0 .../test/corpus/25_func_annotations_scope.py | 0 .../corpus/25_func_annotations_starred.py | 0 .../test/corpus/26_func_const_defaults.py | 0 .../test/corpus/26_func_defaults_same_name.py | 0 .../resources/test/corpus/27_func_generic.py | 0 .../test/corpus/27_func_generic_bound.py | 0 .../test/corpus/27_func_generic_constraint.py | 0 .../test/corpus/27_func_generic_default.py | 0 .../test/corpus/27_func_generic_paramspec.py | 0 .../27_func_generic_paramspec_default.py | 0 .../test/corpus/27_func_generic_tuple.py | 0 .../corpus/27_func_generic_tuple_default.py | 0 .../resources/test/corpus/30_func_enclosed.py | 0 .../test/corpus/30_func_enclosed_many.py | 0 .../resources/test/corpus/31_func_global.py | 0 .../corpus/31_func_global_annotated_later.py | 0 .../resources/test/corpus/31_func_nonlocal.py | 0 .../test/corpus/32_func_global_nested.py | 0 ..._docstring_optimizable_tuple_and_return.py | 0 .../resources/test/corpus/40_import.py | 0 .../resources/test/corpus/41_from_import.py | 0 .../test/corpus/42_import_from_dot.py | 0 .../resources/test/corpus/50_yield.py | 0 .../resources/test/corpus/51_gen_comp.py | 0 .../resources/test/corpus/51_gen_comp2.py | 0 .../resources/test/corpus/52_gen_comp_if.py | 0 .../resources/test/corpus/53_dict_comp.py | 0 .../resources/test/corpus/53_list_comp.py | 0 .../test/corpus/53_list_comp_method.py | 0 .../resources/test/corpus/53_set_comp.py | 0 .../test/corpus/54_list_comp_func.py | 0 .../test/corpus/54_list_comp_lambda.py | 0 .../corpus/54_list_comp_lambda_listcomp.py | 0 .../test/corpus/54_list_comp_recur_func.py | 0 .../test/corpus/55_list_comp_nested.py | 0 .../resources/test/corpus/56_yield_from.py | 0 .../resources/test/corpus/57_await.py | 0 .../resources/test/corpus/58_async_for.py | 0 .../test/corpus/58_async_for_break.py | 0 .../test/corpus/58_async_for_continue.py | 0 .../test/corpus/58_async_for_dict_comp.py | 0 .../test/corpus/58_async_for_else.py | 0 .../test/corpus/58_async_for_gen_comp.py | 0 .../test/corpus/58_async_for_list_comp.py | 0 .../test/corpus/58_async_for_set_comp.py | 0 .../resources/test/corpus/59_async_with.py | 0 .../test/corpus/59_async_with_nested_with.py | 0 .../resources/test/corpus/60_try_except.py | 0 .../resources/test/corpus/60_try_except2.py | 0 .../test/corpus/60_try_except_bare.py | 0 .../resources/test/corpus/60_try_finally.py | 0 .../test/corpus/60_try_finally_codeobj.py | 0 .../test/corpus/60_try_finally_cond.py | 0 .../test/corpus/60_try_finally_for.py | 0 .../test/corpus/60_try_finally_ret.py | 0 .../test/corpus/61_try_except_finally.py | 0 .../resources/test/corpus/62_try_except_as.py | 0 .../test/corpus/62_try_except_break.py | 0 .../test/corpus/62_try_except_cond.py | 0 ...try_except_double_nested_inside_if_else.py | 0 .../test/corpus/62_try_except_return.py | 0 .../resources/test/corpus/63_raise.py | 0 .../resources/test/corpus/63_raise_func.py | 0 .../resources/test/corpus/63_raise_x.py | 0 .../test/corpus/63_raise_x_from_y.py | 0 .../resources/test/corpus/64_assert.py | 0 .../resources/test/corpus/67_with.py | 0 .../resources/test/corpus/67_with_as.py | 0 .../resources/test/corpus/67_with_as_func.py | 0 .../test/corpus/67_with_cond_return.py | 0 ...side_try_finally_multiple_terminal_elif.py | 0 ...e_try_finally_preceding_terminal_except.py | 0 .../test/corpus/67_with_multi_exit.py | 0 .../test/corpus/67_with_non_name_target.py | 0 .../resources/test/corpus/67_with_return.py | 0 .../resources/test/corpus/68_with2.py | 0 .../corpus/69_for_try_except_continue1.py | 0 .../corpus/69_for_try_except_continue2.py | 0 .../corpus/69_for_try_except_continue3.py | 0 .../resources/test/corpus/70_class.py | 0 .../resources/test/corpus/70_class_base.py | 0 .../resources/test/corpus/70_class_doc_str.py | 0 .../resources/test/corpus/71_class_meth.py | 0 .../resources/test/corpus/71_class_var.py | 0 .../resources/test/corpus/72_class_mix.py | 0 .../resources/test/corpus/73_class_generic.py | 0 .../test/corpus/73_class_generic_bounds.py | 0 .../corpus/73_class_generic_constraints.py | 0 .../test/corpus/73_class_generic_defaults.py | 0 .../test/corpus/73_class_generic_paramspec.py | 0 .../73_class_generic_paramspec_default.py | 0 .../test/corpus/73_class_generic_tuple.py | 0 .../corpus/73_class_generic_tuple_default.py | 0 .../resources/test/corpus/74_class_kwargs.py | 0 .../test/corpus/74_class_kwargs_2.py | 0 .../resources/test/corpus/74_class_super.py | 0 .../test/corpus/74_class_super_nested.py | 0 .../resources/test/corpus/74_just_super.py | 0 .../resources/test/corpus/75_classderef.py | 0 .../resources/test/corpus/75_classderef_no.py | 0 .../test/corpus/76_class_nonlocal1.py | 0 .../test/corpus/76_class_nonlocal2.py | 0 .../test/corpus/76_class_nonlocal3.py | 0 .../test/corpus/76_class_nonlocal4.py | 0 .../test/corpus/76_class_nonlocal5.py | 0 .../test/corpus/77_class__class__.py | 0 .../test/corpus/77_class__class__nested.py | 0 .../test/corpus/77_class__class__no_class.py | 0 .../test/corpus/77_class__class__nonlocals.py | 0 .../corpus/77_class__class__nonlocals_2.py | 0 .../test/corpus/77_class__class__param.py | 0 .../corpus/77_class__class__param_lambda.py | 0 .../test/corpus/78_class_body_cond.py | 0 .../resources/test/corpus/78_class_dec.py | 0 .../test/corpus/78_class_dec_member.py | 0 .../test/corpus/78_class_dec_member_func.py | 0 .../resources/test/corpus/79_metaclass.py | 0 .../test/corpus/80_func_kwonlyargs1.py | 0 .../test/corpus/80_func_kwonlyargs2.py | 0 .../test/corpus/80_func_kwonlyargs3.py | 0 .../corpus/81_func_kwonlyargs_defaults.py | 0 .../83_jupyter_notebook_ipython_magic.ipynb | 0 .../resources/test/corpus/85_match.py | 0 .../resources/test/corpus/85_match_as.py | 0 .../resources/test/corpus/85_match_attr.py | 0 .../resources/test/corpus/85_match_class.py | 0 .../resources/test/corpus/85_match_default.py | 0 .../resources/test/corpus/85_match_guard.py | 0 .../corpus/85_match_guard_with_named_expr.py | 0 .../resources/test/corpus/85_match_in_func.py | 0 .../test/corpus/85_match_in_func_with_rest.py | 0 .../test/corpus/85_match_in_func_with_star.py | 0 .../resources/test/corpus/85_match_invalid.py | 0 .../resources/test/corpus/85_match_mapping.py | 0 .../corpus/85_match_mapping_subpattern.py | 0 .../resources/test/corpus/85_match_or.py | 0 .../test/corpus/85_match_sequence.py | 0 .../test/corpus/85_match_sequence_wildcard.py | 0 .../test/corpus/85_match_singleton.py | 0 ...ion_generic_method_with_nested_function.py | 0 .../88_regression_tuple_type_short_circuit.py | 0 .../resources/test/corpus/89_type_alias.py | 0 .../corpus/89_type_alias_invalid_bound.py | 0 .../test/corpus/90_docstring_class.py | 0 .../test/corpus/90_docstring_func.py | 0 .../resources/test/corpus/90_docstring_mod.py | 0 .../resources/test/corpus/91_line_numbers1.py | 0 .../resources/test/corpus/91_line_numbers2.py | 0 .../test/corpus/91_line_numbers2_comp.py | 0 .../resources/test/corpus/91_line_numbers3.py | 0 .../resources/test/corpus/91_line_numbers4.py | 0 .../test/corpus/91_line_numbers_dict.py | 0 .../test/corpus/91_line_numbers_dict_comp.py | 0 .../test/corpus/92_qual_class_in_class.py | 0 .../test/corpus/92_qual_class_in_func.py | 0 .../resources/test/corpus/93_deadcode.py | 0 .../resources/test/corpus/94_strformat.py | 0 .../test/corpus/94_strformat_complex.py | 0 .../test/corpus/94_strformat_conv.py | 0 .../test/corpus/94_strformat_conversion.py | 0 .../test/corpus/94_strformat_spec.py | 0 .../95_annotation_assign_subscript_no_rhs.py | 0 .../test/corpus/95_annotation_assign_tuple.py | 0 .../test/corpus/95_annotation_class.py | 0 .../corpus/95_annotation_class_multiline.py | 0 .../corpus/95_annotation_class_no_value.py | 0 .../corpus/95_annotation_fstring_invalid.py | 0 .../test/corpus/95_annotation_func.py | 0 .../test/corpus/95_annotation_func_future.py | 0 .../test/corpus/95_annotation_global.py | 0 .../corpus/95_annotation_global_simple.py | 0 .../test/corpus/95_annotation_local_attr.py | 0 .../test/corpus/95_annotation_module.py | 0 .../test/corpus/95_annotation_string_tuple.py | 0 .../test/corpus/95_annotation_union.py | 0 .../resources/test/corpus/96_debug.py | 0 .../test/corpus/97_global_nonlocal_store.py | 0 ...nn_assign_annotation_future_annotations.py | 0 .../98_ann_assign_annotation_wrong_future.py | 0 .../corpus/98_ann_assign_invalid_target.py | 0 .../corpus/98_ann_assign_simple_annotation.py | 0 .../test/corpus/99_empty_jump_target_insts.py | 0 .../corpus/cycle_narrowing_constraints.py | 0 .../cycle_negative_narrowing_constraints.py | 0 .../self_referential_function_annotation.py | 0 .../src/combine.rs | 6 +- .../src/db.rs | 16 +- .../src/db/changes.rs | 4 +- .../src/files.rs | 0 .../src/lib.rs | 20 +- .../src/metadata.rs | 73 ++- .../src/metadata/configuration_file.rs | 26 +- .../src/metadata/options.rs | 24 +- .../src/metadata/pyproject.rs | 6 +- .../src/metadata/settings.rs | 4 +- .../src/metadata/value.rs | 0 .../src/walk.rs | 2 +- .../src/watch.rs | 0 .../src/watch/project_watcher.rs | 2 +- .../src/watch/watcher.rs | 0 .../tests/check.rs | 6 +- .../Cargo.toml | 10 +- .../build.rs | 0 .../mdtest.py | 12 +- .../mdtest.py.lock | 0 .../resources/README.md | 2 +- .../resources/mdtest/.mdformat.toml | 0 .../resources/mdtest/annotations/annotated.md | 0 .../resources/mdtest/annotations/any.md | 0 .../resources/mdtest/annotations/callable.md | 2 +- .../resources/mdtest/annotations/deferred.md | 0 .../mdtest/annotations/int_float_complex.md | 0 .../resources/mdtest/annotations/invalid.md | 2 +- .../resources/mdtest/annotations/literal.md | 0 .../mdtest/annotations/literal_string.md | 0 .../resources/mdtest/annotations/never.md | 0 .../resources/mdtest/annotations/new_types.md | 2 +- .../resources/mdtest/annotations/optional.md | 0 .../resources/mdtest/annotations/starred.md | 0 .../annotations/stdlib_typing_aliases.md | 0 .../resources/mdtest/annotations/string.md | 0 .../resources/mdtest/annotations/union.md | 0 .../annotations/unsupported_special_forms.md | 4 +- .../unsupported_type_qualifiers.md | 4 +- .../mdtest/assignment/annotations.md | 0 .../resources/mdtest/assignment/augmented.md | 0 .../mdtest/assignment/multi_target.md | 0 .../resources/mdtest/assignment/unbound.md | 0 .../resources/mdtest/assignment/walrus.md | 0 .../resources/mdtest/attributes.md | 8 +- .../resources/mdtest/binary/booleans.md | 0 .../resources/mdtest/binary/classes.md | 0 .../resources/mdtest/binary/custom.md | 0 .../resources/mdtest/binary/instances.md | 0 .../resources/mdtest/binary/integers.md | 0 .../resources/mdtest/binary/tuples.md | 0 .../resources/mdtest/binary/unions.md | 0 .../resources/mdtest/boolean/short_circuit.md | 0 .../mdtest/boundness_declaredness/public.md | 0 .../resources/mdtest/call/annotation.md | 0 .../resources/mdtest/call/builtins.md | 4 +- .../mdtest/call/callable_instance.md | 0 .../resources/mdtest/call/constructor.md | 0 .../resources/mdtest/call/dunder.md | 0 .../resources/mdtest/call/function.md | 4 +- .../resources/mdtest/call/getattr_static.md | 0 .../resources/mdtest/call/invalid_syntax.md | 0 .../resources/mdtest/call/methods.md | 0 .../resources/mdtest/call/never.md | 0 .../resources/mdtest/call/str_startswith.md | 0 .../resources/mdtest/call/subclass_of.md | 2 +- .../resources/mdtest/call/union.md | 6 +- .../resources/mdtest/class/super.md | 2 +- .../mdtest/comparison/byte_literals.md | 0 .../resources/mdtest/comparison/identity.md | 0 .../comparison/instances/membership_test.md | 0 .../comparison/instances/rich_comparison.md | 0 .../resources/mdtest/comparison/integers.md | 0 .../mdtest/comparison/intersections.md | 0 .../mdtest/comparison/non_bool_returns.md | 0 .../resources/mdtest/comparison/strings.md | 0 .../resources/mdtest/comparison/tuples.md | 0 .../resources/mdtest/comparison/unions.md | 0 .../mdtest/comparison/unsupported.md | 0 .../resources/mdtest/comprehensions/basic.md | 0 .../mdtest/comprehensions/invalid_syntax.md | 0 .../mdtest/conditional/if_expression.md | 0 .../mdtest/conditional/if_statement.md | 0 .../resources/mdtest/conditional/match.md | 0 .../resources/mdtest/dataclass_transform.md | 0 .../resources/mdtest/dataclasses.md | 2 +- .../resources/mdtest/declaration/error.md | 0 .../resources/mdtest/decorators.md | 0 .../resources/mdtest/descriptor_protocol.md | 0 .../diagnostics/attribute_assignment.md | 0 .../diagnostics/invalid_argument_type.md | 0 .../diagnostics/no_matching_overload.md | 0 .../diagnostics/semantic_syntax_errors.md | 0 .../resources/mdtest/diagnostics/shadowing.md | 0 .../resources/mdtest/diagnostics/unpacking.md | 0 .../mdtest/diagnostics/unresolved_import.md | 4 +- .../unsupported_bool_conversion.md | 0 .../version_related_syntax_errors.md | 0 .../mdtest/directives/assert_never.md | 2 +- .../mdtest/directives/assert_type.md | 6 +- .../resources/mdtest/directives/cast.md | 10 +- .../resources/mdtest/doc/README.md | 0 .../doc/public_type_undeclared_symbols.md | 0 .../resources/mdtest/exception/basic.md | 0 .../mdtest/exception/control_flow.md | 0 .../resources/mdtest/exception/except_star.md | 0 .../mdtest/exception/invalid_syntax.md | 0 .../resources/mdtest/expression/assert.md | 0 .../resources/mdtest/expression/attribute.md | 0 .../resources/mdtest/expression/boolean.md | 0 .../resources/mdtest/expression/if.md | 0 .../resources/mdtest/expression/lambda.md | 0 .../resources/mdtest/expression/len.md | 0 .../resources/mdtest/final.md | 0 .../resources/mdtest/function/parameters.md | 0 .../resources/mdtest/function/return_type.md | 0 .../resources/mdtest/generics/builtins.md | 0 .../mdtest/generics/legacy/classes.md | 2 +- .../mdtest/generics/legacy/functions.md | 0 .../mdtest/generics/legacy/variables.md | 0 .../mdtest/generics/pep695/classes.md | 2 +- .../mdtest/generics/pep695/functions.md | 2 +- .../mdtest/generics/pep695/variables.md | 14 +- .../mdtest/generics/pep695/variance.md | 8 +- .../resources/mdtest/generics/scoping.md | 0 .../resources/mdtest/import/basic.md | 4 +- .../resources/mdtest/import/builtins.md | 0 .../resources/mdtest/import/case_sensitive.md | 0 .../resources/mdtest/import/conditional.md | 0 .../resources/mdtest/import/conflicts.md | 0 .../resources/mdtest/import/conventions.md | 7 +- .../resources/mdtest/import/errors.md | 0 .../resources/mdtest/import/invalid_syntax.md | 0 .../resources/mdtest/import/relative.md | 4 +- .../resources/mdtest/import/star.md | 5 +- .../resources/mdtest/import/stub_packages.md | 0 .../resources/mdtest/import/stubs.md | 0 .../resources/mdtest/import/tracking.md | 0 .../resources/mdtest/intersection_types.md | 62 +-- .../resources/mdtest/invalid_syntax.md | 2 +- .../resources/mdtest/known_constants.md | 0 .../resources/mdtest/literal/boolean.md | 0 .../resources/mdtest/literal/bytes.md | 2 +- .../mdtest/literal/collections/dictionary.md | 0 .../mdtest/literal/collections/list.md | 0 .../mdtest/literal/collections/set.md | 0 .../mdtest/literal/collections/tuple.md | 0 .../resources/mdtest/literal/complex.md | 0 .../resources/mdtest/literal/ellipsis.md | 0 .../resources/mdtest/literal/f_string.md | 0 .../resources/mdtest/literal/float.md | 0 .../resources/mdtest/literal/integer.md | 0 .../resources/mdtest/literal/string.md | 0 .../resources/mdtest/loops/async_for.md | 0 .../resources/mdtest/loops/for.md | 0 .../resources/mdtest/loops/iterators.md | 0 .../resources/mdtest/loops/while_loop.md | 0 .../resources/mdtest/mdtest_config.md | 4 +- .../mdtest/mdtest_custom_typeshed.md | 0 .../resources/mdtest/metaclass.md | 0 .../resources/mdtest/mro.md | 0 .../resources/mdtest/named_tuple.md | 0 .../resources/mdtest/narrow/assert.md | 0 .../resources/mdtest/narrow/bool-call.md | 0 .../resources/mdtest/narrow/boolean.md | 0 .../mdtest/narrow/conditionals/boolean.md | 0 .../mdtest/narrow/conditionals/elif_else.md | 0 .../mdtest/narrow/conditionals/eq.md | 0 .../mdtest/narrow/conditionals/in.md | 0 .../mdtest/narrow/conditionals/is.md | 0 .../mdtest/narrow/conditionals/is_not.md | 0 .../mdtest/narrow/conditionals/nested.md | 0 .../mdtest/narrow/conditionals/not.md | 0 .../resources/mdtest/narrow/isinstance.md | 2 +- .../resources/mdtest/narrow/issubclass.md | 0 .../resources/mdtest/narrow/match.md | 0 .../mdtest/narrow/post_if_statement.md | 0 .../resources/mdtest/narrow/truthiness.md | 0 .../resources/mdtest/narrow/type.md | 0 .../resources/mdtest/narrow/while.md | 0 .../resources/mdtest/overloads.md | 0 .../resources/mdtest/pep695_type_aliases.md | 2 +- .../resources/mdtest/properties.md | 0 .../resources/mdtest/protocols.md | 34 +- .../14334_diagnostics_in_wrong_file.md | 0 .../resources/mdtest/scopes/builtin.md | 0 .../resources/mdtest/scopes/eager.md | 0 .../resources/mdtest/scopes/global.md | 0 .../mdtest/scopes/moduletype_attrs.md | 0 .../resources/mdtest/scopes/nonlocal.md | 0 .../resources/mdtest/scopes/unbound.md | 0 .../resources/mdtest/shadowing/class.md | 0 .../resources/mdtest/shadowing/function.md | 0 .../mdtest/shadowing/variable_declaration.md | 0 .../resources/mdtest/slots.md | 0 ..._-_Invalid_`__set__`_method_signature.snap | 4 +- ...a_descriptors_-_Invalid_argument_type.snap | 4 +- ..._attributes_with_class-level_defaults.snap | 4 +- ...ignment_-_Possibly-unbound_attributes.snap | 4 +- ...assignment_-_Pure_instance_attributes.snap | 4 +- ...t_-_Setting_attributes_on_union_types.snap | 4 +- ...ibute_assignment_-_Unknown_attributes.snap | 4 +- ..._-_Attribute_assignment_-_`ClassVar`s.snap | 4 +- ...ructures_-_Unresolvable_module_import.snap | 4 +- ...ures_-_Unresolvable_submodule_imports.snap | 4 +- ..._For_loops_-_Bad_`__getitem__`_method.snap | 4 +- ...for.md_-_For_loops_-_Invalid_iterable.snap | 4 +- ...New_over_old_style_iteration_protocol.snap | 4 +- ...hod_and_`__getitem__`_is_not_callable.snap | 4 +- ...bly-not-callable_`__getitem__`_method.snap | 4 +- ...ossibly_invalid_`__getitem__`_methods.snap | 4 +- ...-_Possibly_invalid_`__iter__`_methods.snap | 4 +- ..._-_Possibly_invalid_`__next__`_method.snap | 4 +- ..._iter__`_and_bad_`__getitem__`_method.snap | 4 +- ..._`_and_possibly_invalid_`__getitem__`.snap | 4 +- ..._`_and_possibly_unbound_`__getitem__`.snap | 4 +- ...element_has_invalid_`__iter__`_method.snap | 4 +- ...nion_element_has_no_`__iter__`_method.snap | 4 +- ...or_loops_-_With_non-callable_iterator.snap | 4 +- ...__iter__`_does_not_return_an_iterator.snap | 4 +- ...__iter__`_method_with_a_bad_signature.snap | 4 +- ...tor_with_an_invalid_`__next__`_method.snap | 4 +- ...cy_syntax_-_Inferring_a_bound_typevar.snap | 4 +- ...tax_-_Inferring_a_constrained_typevar.snap | 4 +- ...95_syntax_-_Inferring_a_bound_typevar.snap | 4 +- ...tax_-_Inferring_a_constrained_typevar.snap | 4 +- ...types_with_invalid_`__bool__`_methods.snap | 4 +- ...lid_argument_type_diagnostics_-_Basic.snap | 4 +- ...t_type_diagnostics_-_Calls_to_methods.snap | 4 +- ...nt_type_diagnostics_-_Different_files.snap | 4 +- ..._diagnostics_-_Different_source_order.snap | 4 +- ...nt_type_diagnostics_-_Many_parameters.snap | 4 +- ...Many_parameters_across_multiple_lines.snap | 4 +- ...eters_with_multiple_invalid_arguments.snap | 4 +- ...hose_type_is_vendored_from_`typeshed`.snap | 4 +- ...gument_types_-_Keyword_only_arguments.snap | 4 +- ..._of_argument_types_-_Mix_of_arguments.snap | 4 +- ...argument_types_-_One_keyword_argument.snap | 4 +- ...y_of_argument_types_-_Only_positional.snap | 4 +- ..._argument_types_-_Synthetic_arguments.snap | 4 +- ...f_argument_types_-_Variadic_arguments.snap | 4 +- ...nt_types_-_Variadic_keyword_arguments.snap | 4 +- ...oesn't_implement_`__bool__`_correctly.snap | 4 +- ...stics_-_Calls_to_overloaded_functions.snap | 4 +- ...hat_implements_`__bool__`_incorrectly.snap | 4 +- ...ds_-_Invalid_-_At_least_two_overloads.snap | 4 +- ...onsistent_decorators_-_`@classmethod`.snap | 4 +- ..._-_Inconsistent_decorators_-_`@final`.snap | 4 +- ...Inconsistent_decorators_-_`@override`.snap | 4 +- ...t_an_implementation_-_Regular_modules.snap | 4 +- ...Protocols_-_Calls_to_protocol_classes.snap | 4 +- ...lid_calls_to_`get_protocol_members()`.snap | 4 +- ..._-_Protocols_-_Narrowing_of_protocols.snap | 4 +- ...ype_-_Invalid_conditional_return_type.snap | 4 +- ...n_type_-_Invalid_implicit_return_type.snap | 4 +- ...ion_return_type_-_Invalid_return_type.snap | 4 +- ...pe_-_Invalid_return_type_in_stub_file.snap | 4 +- ..._don't_implement_`__bool__`_correctly.snap | 4 +- ...chronous_comprehensions_-_Python_3.10.snap | 4 +- ..._Shadowing_-_Implicit_class_shadowing.snap | 4 +- ...adowing_-_Implicit_function_shadowing.snap | 4 +- ...that_incorrectly_implement_`__bool__`.snap | 4 +- ...that_incorrectly_implement_`__bool__`.snap | 4 +- ...ng_-_Exactly_too_few_values_to_unpack.snap | 4 +- ...g_-_Exactly_too_many_values_to_unpack.snap | 4 +- ...acking_-_Right_hand_side_not_iterable.snap | 4 +- ..._Unpacking_-_Too_few_values_to_unpack.snap | 4 +- ...vable_import_that_does_not_use_`from`.snap | 4 +- ...solvable_module_but_unresolvable_item.snap | 4 +- ...`from`_with_an_unknown_current_module.snap | 4 +- ..._`from`_with_an_unknown_nested_module.snap | 4 +- ...ng_`from`_with_an_unresolvable_module.snap | 4 +- ...ing_`from`_with_too_many_leading_dots.snap | 4 +- ...l__`_attribute,_but_it's_not_callable.snap | 4 +- ...hod,_but_has_an_incorrect_return_type.snap | 4 +- ..._method,_but_has_incorrect_parameters.snap | 4 +- ...ember_has_incorrect_`__bool__`_method.snap | 4 +- ...ics_-_`match`_statement_-_Before_3.10.snap | 4 +- .../mdtest/statically_known_branches.md | 0 .../resources/mdtest/stubs/class.md | 0 .../resources/mdtest/stubs/ellipsis.md | 0 .../resources/mdtest/stubs/locals.md | 0 .../resources/mdtest/subscript/bytes.md | 0 .../resources/mdtest/subscript/class.md | 0 .../resources/mdtest/subscript/instance.md | 0 .../resources/mdtest/subscript/lists.md | 0 .../mdtest/subscript/stepsize_zero.md | 0 .../resources/mdtest/subscript/string.md | 0 .../resources/mdtest/subscript/tuple.md | 0 .../mdtest/suppressions/no_type_check.md | 8 +- .../mdtest/suppressions/ty_ignore.md | 191 ++++++++ .../mdtest/suppressions/type_ignore.md | 6 +- .../resources/mdtest/sys_platform.md | 0 .../resources/mdtest/sys_version_info.md | 0 .../resources/mdtest/terminal_statements.md | 0 .../resources/mdtest/type_api.md | 64 +-- .../resources/mdtest/type_of/basic.md | 0 .../resources/mdtest/type_of/dynamic.md | 0 .../mdtest/type_of/typing_dot_Type.md | 0 .../type_properties/is_assignable_to.md | 32 +- .../type_properties/is_disjoint_from.md | 28 +- .../type_properties/is_equivalent_to.md | 22 +- .../mdtest/type_properties/is_fully_static.md | 10 +- .../is_gradual_equivalent_to.md | 8 +- .../type_properties/is_single_valued.md | 2 +- .../mdtest/type_properties/is_singleton.md | 16 +- .../mdtest/type_properties/is_subtype_of.md | 84 ++-- .../mdtest/type_properties/str_repr.md | 0 .../mdtest/type_properties/truthiness.md | 8 +- .../tuples_containing_never.md | 2 +- .../mdtest/type_qualifiers/classvar.md | 0 .../resources/mdtest/type_qualifiers/final.md | 0 .../resources/mdtest/typed_dict.md | 0 .../resources/mdtest/unary/custom.md | 0 .../resources/mdtest/unary/integers.md | 0 .../resources/mdtest/unary/invert_add_usub.md | 0 .../resources/mdtest/unary/not.md | 0 .../resources/mdtest/union_types.md | 10 +- .../resources/mdtest/unpacking.md | 0 .../resources/mdtest/unreachable.md | 0 .../resources/mdtest/with/async.md | 0 .../resources/mdtest/with/sync.md | 0 .../resources/primer/bad.txt | 0 .../resources/primer/good.txt | 0 .../src/ast_node_ref.rs | 0 .../src/db.rs | 2 +- .../src/lib.rs | 0 .../src/lint.rs | 4 +- .../src/list.rs | 0 .../src/module_name.rs | 14 +- .../src/module_resolver/mod.rs | 0 .../src/module_resolver/module.rs | 8 +- .../src/module_resolver/path.rs | 0 .../src/module_resolver/resolver.rs | 0 .../src/module_resolver/testing.rs | 0 .../src/module_resolver/typeshed.rs | 2 +- .../src/node_key.rs | 0 .../src/program.rs | 2 +- .../src/python_platform.rs | 0 .../src/semantic_index.rs | 0 .../src/semantic_index/ast_ids.rs | 0 .../src/semantic_index/builder.rs | 0 .../semantic_index/builder/except_handlers.rs | 0 .../src/semantic_index/definition.rs | 0 .../src/semantic_index/expression.rs | 0 .../semantic_index/narrowing_constraints.rs | 0 .../src/semantic_index/predicate.rs | 0 .../src/semantic_index/re_exports.rs | 0 .../src/semantic_index/symbol.rs | 0 .../src/semantic_index/use_def.rs | 0 .../semantic_index/use_def/symbol_state.rs | 0 .../semantic_index/visibility_constraints.rs | 0 .../src/semantic_model.rs | 0 .../src/site_packages.rs | 2 +- .../src/suppression.rs | 46 +- .../src/symbol.rs | 0 .../src/types.rs | 30 +- .../src/types/builder.rs | 0 .../src/types/call.rs | 0 .../src/types/call/arguments.rs | 0 .../src/types/call/bind.rs | 0 .../src/types/class.rs | 2 +- .../src/types/class_base.rs | 0 .../src/types/context.rs | 0 .../src/types/definition.rs | 0 .../src/types/diagnostic.rs | 2 +- .../src/types/display.rs | 0 .../src/types/generics.rs | 0 .../src/types/infer.rs | 0 .../src/types/instance.rs | 0 .../src/types/known_instance.rs | 30 +- .../src/types/mro.rs | 0 .../src/types/narrow.rs | 0 .../src/types/property_tests.rs | 6 +- .../src/types/property_tests/setup.rs | 0 .../types/property_tests/type_generation.rs | 0 .../src/types/protocol_class.rs | 0 .../src/types/signatures.rs | 0 .../src/types/slots.rs | 0 .../src/types/string_annotation.rs | 8 +- .../src/types/subclass_of.rs | 0 .../src/types/type_ordering.rs | 0 .../src/types/unpacker.rs | 0 .../src/unpack.rs | 0 .../src/util/mod.rs | 0 .../src/util/subscript.rs | 0 .../tests/mdtest.rs | 6 +- .../{red_knot_server => ty_server}/Cargo.toml | 10 +- .../src/document.rs | 0 .../src/document/location.rs | 10 +- .../src/document/notebook.rs | 0 .../src/document/range.rs | 2 +- .../src/document/text_document.rs | 0 .../{red_knot_server => ty_server}/src/lib.rs | 4 +- .../src/logging.rs | 4 +- .../src/message.rs | 0 .../src/server.rs | 0 .../src/server/api.rs | 0 .../src/server/api/diagnostics.rs | 0 .../src/server/api/notifications.rs | 0 .../server/api/notifications/did_change.rs | 2 +- .../src/server/api/notifications/did_close.rs | 2 +- .../api/notifications/did_close_notebook.rs | 2 +- .../src/server/api/notifications/did_open.rs | 2 +- .../api/notifications/did_open_notebook.rs | 2 +- .../src/server/api/requests.rs | 0 .../src/server/api/requests/completion.rs | 4 +- .../src/server/api/requests/diagnostic.rs | 4 +- .../api/requests/goto_type_definition.rs | 4 +- .../src/server/api/requests/hover.rs | 4 +- .../src/server/api/requests/inlay_hints.rs | 4 +- .../src/server/api/traits.rs | 2 +- .../src/server/client.rs | 0 .../src/server/connection.rs | 0 .../src/server/schedule.rs | 0 .../src/server/schedule/task.rs | 0 .../src/server/schedule/thread.rs | 0 .../src/server/schedule/thread/pool.rs | 0 .../src/server/schedule/thread/priority.rs | 0 .../src/session.rs | 6 +- .../src/session/capabilities.rs | 0 .../src/session/index.rs | 0 .../src/session/settings.rs | 0 .../src/system.rs | 2 +- crates/{red_knot_test => ty_test}/Cargo.toml | 6 +- crates/{red_knot_test => ty_test}/README.md | 30 +- .../src/assertion.rs | 2 +- .../{red_knot_test => ty_test}/src/config.rs | 10 +- crates/{red_knot_test => ty_test}/src/db.rs | 6 +- .../src/diagnostic.rs | 0 crates/{red_knot_test => ty_test}/src/lib.rs | 10 +- .../{red_knot_test => ty_test}/src/matcher.rs | 2 +- .../{red_knot_test => ty_test}/src/parser.rs | 0 .../.gitignore | 0 .../Cargo.toml | 2 +- .../README.md | 2 +- .../build.rs | 18 +- .../src/lib.rs | 0 crates/ty_vendored/ty_extensions/README.md | 2 + .../ty_extensions/ty_extensions.pyi} | 0 .../vendor/typeshed/LICENSE | 0 .../vendor/typeshed/README.md | 0 .../vendor/typeshed/source_commit.txt | 0 .../vendor/typeshed/stdlib/VERSIONS | 0 .../vendor/typeshed/stdlib/__future__.pyi | 0 .../vendor/typeshed/stdlib/__main__.pyi | 0 .../vendor/typeshed/stdlib/_ast.pyi | 0 .../vendor/typeshed/stdlib/_asyncio.pyi | 0 .../vendor/typeshed/stdlib/_bisect.pyi | 0 .../vendor/typeshed/stdlib/_blake2.pyi | 0 .../vendor/typeshed/stdlib/_bootlocale.pyi | 0 .../vendor/typeshed/stdlib/_bz2.pyi | 0 .../vendor/typeshed/stdlib/_codecs.pyi | 0 .../typeshed/stdlib/_collections_abc.pyi | 0 .../vendor/typeshed/stdlib/_compat_pickle.pyi | 0 .../vendor/typeshed/stdlib/_compression.pyi | 0 .../vendor/typeshed/stdlib/_contextvars.pyi | 0 .../vendor/typeshed/stdlib/_csv.pyi | 0 .../vendor/typeshed/stdlib/_ctypes.pyi | 0 .../vendor/typeshed/stdlib/_curses.pyi | 0 .../vendor/typeshed/stdlib/_curses_panel.pyi | 0 .../vendor/typeshed/stdlib/_dbm.pyi | 0 .../vendor/typeshed/stdlib/_decimal.pyi | 0 .../typeshed/stdlib/_frozen_importlib.pyi | 0 .../stdlib/_frozen_importlib_external.pyi | 0 .../vendor/typeshed/stdlib/_gdbm.pyi | 0 .../vendor/typeshed/stdlib/_hashlib.pyi | 0 .../vendor/typeshed/stdlib/_heapq.pyi | 0 .../vendor/typeshed/stdlib/_imp.pyi | 0 .../typeshed/stdlib/_interpchannels.pyi | 0 .../vendor/typeshed/stdlib/_interpqueues.pyi | 0 .../vendor/typeshed/stdlib/_interpreters.pyi | 0 .../vendor/typeshed/stdlib/_io.pyi | 0 .../vendor/typeshed/stdlib/_json.pyi | 0 .../vendor/typeshed/stdlib/_locale.pyi | 0 .../vendor/typeshed/stdlib/_lsprof.pyi | 0 .../vendor/typeshed/stdlib/_lzma.pyi | 0 .../vendor/typeshed/stdlib/_markupbase.pyi | 0 .../vendor/typeshed/stdlib/_msi.pyi | 0 .../typeshed/stdlib/_multibytecodec.pyi | 0 .../vendor/typeshed/stdlib/_operator.pyi | 0 .../vendor/typeshed/stdlib/_osx_support.pyi | 0 .../vendor/typeshed/stdlib/_pickle.pyi | 0 .../typeshed/stdlib/_posixsubprocess.pyi | 0 .../vendor/typeshed/stdlib/_py_abc.pyi | 0 .../vendor/typeshed/stdlib/_pydecimal.pyi | 0 .../vendor/typeshed/stdlib/_queue.pyi | 0 .../vendor/typeshed/stdlib/_random.pyi | 0 .../vendor/typeshed/stdlib/_sitebuiltins.pyi | 0 .../vendor/typeshed/stdlib/_socket.pyi | 0 .../vendor/typeshed/stdlib/_sqlite3.pyi | 0 .../vendor/typeshed/stdlib/_ssl.pyi | 0 .../vendor/typeshed/stdlib/_stat.pyi | 0 .../vendor/typeshed/stdlib/_struct.pyi | 0 .../vendor/typeshed/stdlib/_thread.pyi | 0 .../typeshed/stdlib/_threading_local.pyi | 0 .../vendor/typeshed/stdlib/_tkinter.pyi | 0 .../vendor/typeshed/stdlib/_tracemalloc.pyi | 0 .../typeshed/stdlib/_typeshed/README.md | 0 .../typeshed/stdlib/_typeshed/__init__.pyi | 0 .../typeshed/stdlib/_typeshed/dbapi.pyi | 0 .../typeshed/stdlib/_typeshed/importlib.pyi | 0 .../vendor/typeshed/stdlib/_typeshed/wsgi.pyi | 0 .../vendor/typeshed/stdlib/_typeshed/xml.pyi | 0 .../vendor/typeshed/stdlib/_warnings.pyi | 0 .../vendor/typeshed/stdlib/_weakref.pyi | 0 .../vendor/typeshed/stdlib/_weakrefset.pyi | 0 .../vendor/typeshed/stdlib/_winapi.pyi | 0 .../vendor/typeshed/stdlib/abc.pyi | 0 .../vendor/typeshed/stdlib/aifc.pyi | 0 .../vendor/typeshed/stdlib/antigravity.pyi | 0 .../vendor/typeshed/stdlib/argparse.pyi | 0 .../vendor/typeshed/stdlib/array.pyi | 0 .../vendor/typeshed/stdlib/ast.pyi | 0 .../vendor/typeshed/stdlib/asynchat.pyi | 0 .../typeshed/stdlib/asyncio/__init__.pyi | 0 .../typeshed/stdlib/asyncio/base_events.pyi | 0 .../typeshed/stdlib/asyncio/base_futures.pyi | 0 .../stdlib/asyncio/base_subprocess.pyi | 0 .../typeshed/stdlib/asyncio/base_tasks.pyi | 0 .../typeshed/stdlib/asyncio/constants.pyi | 0 .../typeshed/stdlib/asyncio/coroutines.pyi | 0 .../vendor/typeshed/stdlib/asyncio/events.pyi | 0 .../typeshed/stdlib/asyncio/exceptions.pyi | 0 .../stdlib/asyncio/format_helpers.pyi | 0 .../typeshed/stdlib/asyncio/futures.pyi | 0 .../vendor/typeshed/stdlib/asyncio/locks.pyi | 0 .../vendor/typeshed/stdlib/asyncio/log.pyi | 0 .../vendor/typeshed/stdlib/asyncio/mixins.pyi | 0 .../stdlib/asyncio/proactor_events.pyi | 0 .../typeshed/stdlib/asyncio/protocols.pyi | 0 .../vendor/typeshed/stdlib/asyncio/queues.pyi | 0 .../typeshed/stdlib/asyncio/runners.pyi | 0 .../stdlib/asyncio/selector_events.pyi | 0 .../typeshed/stdlib/asyncio/sslproto.pyi | 0 .../typeshed/stdlib/asyncio/staggered.pyi | 0 .../typeshed/stdlib/asyncio/streams.pyi | 0 .../typeshed/stdlib/asyncio/subprocess.pyi | 0 .../typeshed/stdlib/asyncio/taskgroups.pyi | 0 .../vendor/typeshed/stdlib/asyncio/tasks.pyi | 0 .../typeshed/stdlib/asyncio/threads.pyi | 0 .../typeshed/stdlib/asyncio/timeouts.pyi | 0 .../typeshed/stdlib/asyncio/transports.pyi | 0 .../vendor/typeshed/stdlib/asyncio/trsock.pyi | 0 .../typeshed/stdlib/asyncio/unix_events.pyi | 0 .../stdlib/asyncio/windows_events.pyi | 0 .../typeshed/stdlib/asyncio/windows_utils.pyi | 0 .../vendor/typeshed/stdlib/asyncore.pyi | 0 .../vendor/typeshed/stdlib/atexit.pyi | 0 .../vendor/typeshed/stdlib/audioop.pyi | 0 .../vendor/typeshed/stdlib/base64.pyi | 0 .../vendor/typeshed/stdlib/bdb.pyi | 0 .../vendor/typeshed/stdlib/binascii.pyi | 0 .../vendor/typeshed/stdlib/binhex.pyi | 0 .../vendor/typeshed/stdlib/bisect.pyi | 0 .../vendor/typeshed/stdlib/builtins.pyi | 0 .../vendor/typeshed/stdlib/bz2.pyi | 0 .../vendor/typeshed/stdlib/cProfile.pyi | 0 .../vendor/typeshed/stdlib/calendar.pyi | 0 .../vendor/typeshed/stdlib/cgi.pyi | 0 .../vendor/typeshed/stdlib/cgitb.pyi | 0 .../vendor/typeshed/stdlib/chunk.pyi | 0 .../vendor/typeshed/stdlib/cmath.pyi | 0 .../vendor/typeshed/stdlib/cmd.pyi | 0 .../vendor/typeshed/stdlib/code.pyi | 0 .../vendor/typeshed/stdlib/codecs.pyi | 0 .../vendor/typeshed/stdlib/codeop.pyi | 0 .../typeshed/stdlib/collections/__init__.pyi | 0 .../typeshed/stdlib/collections/abc.pyi | 0 .../vendor/typeshed/stdlib/colorsys.pyi | 0 .../vendor/typeshed/stdlib/compileall.pyi | 0 .../typeshed/stdlib/concurrent/__init__.pyi | 0 .../stdlib/concurrent/futures/__init__.pyi | 0 .../stdlib/concurrent/futures/_base.pyi | 0 .../stdlib/concurrent/futures/process.pyi | 0 .../stdlib/concurrent/futures/thread.pyi | 0 .../vendor/typeshed/stdlib/configparser.pyi | 0 .../vendor/typeshed/stdlib/contextlib.pyi | 0 .../vendor/typeshed/stdlib/contextvars.pyi | 0 .../vendor/typeshed/stdlib/copy.pyi | 0 .../vendor/typeshed/stdlib/copyreg.pyi | 0 .../vendor/typeshed/stdlib/crypt.pyi | 0 .../vendor/typeshed/stdlib/csv.pyi | 0 .../typeshed/stdlib/ctypes/__init__.pyi | 0 .../vendor/typeshed/stdlib/ctypes/_endian.pyi | 0 .../stdlib/ctypes/macholib/__init__.pyi | 0 .../typeshed/stdlib/ctypes/macholib/dyld.pyi | 0 .../typeshed/stdlib/ctypes/macholib/dylib.pyi | 0 .../stdlib/ctypes/macholib/framework.pyi | 0 .../vendor/typeshed/stdlib/ctypes/util.pyi | 0 .../typeshed/stdlib/ctypes/wintypes.pyi | 0 .../typeshed/stdlib/curses/__init__.pyi | 0 .../vendor/typeshed/stdlib/curses/ascii.pyi | 0 .../vendor/typeshed/stdlib/curses/has_key.pyi | 0 .../vendor/typeshed/stdlib/curses/panel.pyi | 0 .../vendor/typeshed/stdlib/curses/textpad.pyi | 0 .../vendor/typeshed/stdlib/dataclasses.pyi | 0 .../vendor/typeshed/stdlib/datetime.pyi | 0 .../vendor/typeshed/stdlib/dbm/__init__.pyi | 0 .../vendor/typeshed/stdlib/dbm/dumb.pyi | 0 .../vendor/typeshed/stdlib/dbm/gnu.pyi | 0 .../vendor/typeshed/stdlib/dbm/ndbm.pyi | 0 .../vendor/typeshed/stdlib/dbm/sqlite3.pyi | 0 .../vendor/typeshed/stdlib/decimal.pyi | 0 .../vendor/typeshed/stdlib/difflib.pyi | 0 .../vendor/typeshed/stdlib/dis.pyi | 0 .../typeshed/stdlib/distutils/__init__.pyi | 0 .../stdlib/distutils/_msvccompiler.pyi | 0 .../stdlib/distutils/archive_util.pyi | 0 .../stdlib/distutils/bcppcompiler.pyi | 0 .../typeshed/stdlib/distutils/ccompiler.pyi | 0 .../vendor/typeshed/stdlib/distutils/cmd.pyi | 0 .../stdlib/distutils/command/__init__.pyi | 0 .../stdlib/distutils/command/bdist.pyi | 0 .../stdlib/distutils/command/bdist_dumb.pyi | 0 .../stdlib/distutils/command/bdist_msi.pyi | 0 .../distutils/command/bdist_packager.pyi | 0 .../stdlib/distutils/command/bdist_rpm.pyi | 0 .../distutils/command/bdist_wininst.pyi | 0 .../stdlib/distutils/command/build.pyi | 0 .../stdlib/distutils/command/build_clib.pyi | 0 .../stdlib/distutils/command/build_ext.pyi | 0 .../stdlib/distutils/command/build_py.pyi | 0 .../distutils/command/build_scripts.pyi | 0 .../stdlib/distutils/command/check.pyi | 0 .../stdlib/distutils/command/clean.pyi | 0 .../stdlib/distutils/command/config.pyi | 0 .../stdlib/distutils/command/install.pyi | 0 .../stdlib/distutils/command/install_data.pyi | 0 .../distutils/command/install_egg_info.pyi | 0 .../distutils/command/install_headers.pyi | 0 .../stdlib/distutils/command/install_lib.pyi | 0 .../distutils/command/install_scripts.pyi | 0 .../stdlib/distutils/command/register.pyi | 0 .../stdlib/distutils/command/sdist.pyi | 0 .../stdlib/distutils/command/upload.pyi | 0 .../typeshed/stdlib/distutils/config.pyi | 0 .../vendor/typeshed/stdlib/distutils/core.pyi | 0 .../stdlib/distutils/cygwinccompiler.pyi | 0 .../typeshed/stdlib/distutils/debug.pyi | 0 .../typeshed/stdlib/distutils/dep_util.pyi | 0 .../typeshed/stdlib/distutils/dir_util.pyi | 0 .../vendor/typeshed/stdlib/distutils/dist.pyi | 0 .../typeshed/stdlib/distutils/errors.pyi | 0 .../typeshed/stdlib/distutils/extension.pyi | 0 .../stdlib/distutils/fancy_getopt.pyi | 0 .../typeshed/stdlib/distutils/file_util.pyi | 0 .../typeshed/stdlib/distutils/filelist.pyi | 0 .../vendor/typeshed/stdlib/distutils/log.pyi | 0 .../stdlib/distutils/msvccompiler.pyi | 0 .../typeshed/stdlib/distutils/spawn.pyi | 0 .../typeshed/stdlib/distutils/sysconfig.pyi | 0 .../typeshed/stdlib/distutils/text_file.pyi | 0 .../stdlib/distutils/unixccompiler.pyi | 0 .../vendor/typeshed/stdlib/distutils/util.pyi | 0 .../typeshed/stdlib/distutils/version.pyi | 0 .../vendor/typeshed/stdlib/doctest.pyi | 0 .../vendor/typeshed/stdlib/email/__init__.pyi | 0 .../stdlib/email/_header_value_parser.pyi | 0 .../typeshed/stdlib/email/_policybase.pyi | 0 .../typeshed/stdlib/email/base64mime.pyi | 0 .../vendor/typeshed/stdlib/email/charset.pyi | 0 .../typeshed/stdlib/email/contentmanager.pyi | 0 .../vendor/typeshed/stdlib/email/encoders.pyi | 0 .../vendor/typeshed/stdlib/email/errors.pyi | 0 .../typeshed/stdlib/email/feedparser.pyi | 0 .../typeshed/stdlib/email/generator.pyi | 0 .../vendor/typeshed/stdlib/email/header.pyi | 0 .../typeshed/stdlib/email/headerregistry.pyi | 0 .../typeshed/stdlib/email/iterators.pyi | 0 .../vendor/typeshed/stdlib/email/message.pyi | 0 .../typeshed/stdlib/email/mime/__init__.pyi | 0 .../stdlib/email/mime/application.pyi | 0 .../typeshed/stdlib/email/mime/audio.pyi | 0 .../typeshed/stdlib/email/mime/base.pyi | 0 .../typeshed/stdlib/email/mime/image.pyi | 0 .../typeshed/stdlib/email/mime/message.pyi | 0 .../typeshed/stdlib/email/mime/multipart.pyi | 0 .../stdlib/email/mime/nonmultipart.pyi | 0 .../typeshed/stdlib/email/mime/text.pyi | 0 .../vendor/typeshed/stdlib/email/parser.pyi | 0 .../vendor/typeshed/stdlib/email/policy.pyi | 0 .../typeshed/stdlib/email/quoprimime.pyi | 0 .../vendor/typeshed/stdlib/email/utils.pyi | 0 .../typeshed/stdlib/encodings/__init__.pyi | 0 .../typeshed/stdlib/encodings/aliases.pyi | 0 .../typeshed/stdlib/encodings/ascii.pyi | 0 .../stdlib/encodings/base64_codec.pyi | 0 .../vendor/typeshed/stdlib/encodings/big5.pyi | 0 .../typeshed/stdlib/encodings/big5hkscs.pyi | 0 .../typeshed/stdlib/encodings/bz2_codec.pyi | 0 .../typeshed/stdlib/encodings/charmap.pyi | 0 .../typeshed/stdlib/encodings/cp037.pyi | 0 .../typeshed/stdlib/encodings/cp1006.pyi | 0 .../typeshed/stdlib/encodings/cp1026.pyi | 0 .../typeshed/stdlib/encodings/cp1125.pyi | 0 .../typeshed/stdlib/encodings/cp1140.pyi | 0 .../typeshed/stdlib/encodings/cp1250.pyi | 0 .../typeshed/stdlib/encodings/cp1251.pyi | 0 .../typeshed/stdlib/encodings/cp1252.pyi | 0 .../typeshed/stdlib/encodings/cp1253.pyi | 0 .../typeshed/stdlib/encodings/cp1254.pyi | 0 .../typeshed/stdlib/encodings/cp1255.pyi | 0 .../typeshed/stdlib/encodings/cp1256.pyi | 0 .../typeshed/stdlib/encodings/cp1257.pyi | 0 .../typeshed/stdlib/encodings/cp1258.pyi | 0 .../typeshed/stdlib/encodings/cp273.pyi | 0 .../typeshed/stdlib/encodings/cp424.pyi | 0 .../typeshed/stdlib/encodings/cp437.pyi | 0 .../typeshed/stdlib/encodings/cp500.pyi | 0 .../typeshed/stdlib/encodings/cp720.pyi | 0 .../typeshed/stdlib/encodings/cp737.pyi | 0 .../typeshed/stdlib/encodings/cp775.pyi | 0 .../typeshed/stdlib/encodings/cp850.pyi | 0 .../typeshed/stdlib/encodings/cp852.pyi | 0 .../typeshed/stdlib/encodings/cp855.pyi | 0 .../typeshed/stdlib/encodings/cp856.pyi | 0 .../typeshed/stdlib/encodings/cp857.pyi | 0 .../typeshed/stdlib/encodings/cp858.pyi | 0 .../typeshed/stdlib/encodings/cp860.pyi | 0 .../typeshed/stdlib/encodings/cp861.pyi | 0 .../typeshed/stdlib/encodings/cp862.pyi | 0 .../typeshed/stdlib/encodings/cp863.pyi | 0 .../typeshed/stdlib/encodings/cp864.pyi | 0 .../typeshed/stdlib/encodings/cp865.pyi | 0 .../typeshed/stdlib/encodings/cp866.pyi | 0 .../typeshed/stdlib/encodings/cp869.pyi | 0 .../typeshed/stdlib/encodings/cp874.pyi | 0 .../typeshed/stdlib/encodings/cp875.pyi | 0 .../typeshed/stdlib/encodings/cp932.pyi | 0 .../typeshed/stdlib/encodings/cp949.pyi | 0 .../typeshed/stdlib/encodings/cp950.pyi | 0 .../stdlib/encodings/euc_jis_2004.pyi | 0 .../stdlib/encodings/euc_jisx0213.pyi | 0 .../typeshed/stdlib/encodings/euc_jp.pyi | 0 .../typeshed/stdlib/encodings/euc_kr.pyi | 0 .../typeshed/stdlib/encodings/gb18030.pyi | 0 .../typeshed/stdlib/encodings/gb2312.pyi | 0 .../vendor/typeshed/stdlib/encodings/gbk.pyi | 0 .../typeshed/stdlib/encodings/hex_codec.pyi | 0 .../typeshed/stdlib/encodings/hp_roman8.pyi | 0 .../vendor/typeshed/stdlib/encodings/hz.pyi | 0 .../vendor/typeshed/stdlib/encodings/idna.pyi | 0 .../typeshed/stdlib/encodings/iso2022_jp.pyi | 0 .../stdlib/encodings/iso2022_jp_1.pyi | 0 .../stdlib/encodings/iso2022_jp_2.pyi | 0 .../stdlib/encodings/iso2022_jp_2004.pyi | 0 .../stdlib/encodings/iso2022_jp_3.pyi | 0 .../stdlib/encodings/iso2022_jp_ext.pyi | 0 .../typeshed/stdlib/encodings/iso2022_kr.pyi | 0 .../typeshed/stdlib/encodings/iso8859_1.pyi | 0 .../typeshed/stdlib/encodings/iso8859_10.pyi | 0 .../typeshed/stdlib/encodings/iso8859_11.pyi | 0 .../typeshed/stdlib/encodings/iso8859_13.pyi | 0 .../typeshed/stdlib/encodings/iso8859_14.pyi | 0 .../typeshed/stdlib/encodings/iso8859_15.pyi | 0 .../typeshed/stdlib/encodings/iso8859_16.pyi | 0 .../typeshed/stdlib/encodings/iso8859_2.pyi | 0 .../typeshed/stdlib/encodings/iso8859_3.pyi | 0 .../typeshed/stdlib/encodings/iso8859_4.pyi | 0 .../typeshed/stdlib/encodings/iso8859_5.pyi | 0 .../typeshed/stdlib/encodings/iso8859_6.pyi | 0 .../typeshed/stdlib/encodings/iso8859_7.pyi | 0 .../typeshed/stdlib/encodings/iso8859_8.pyi | 0 .../typeshed/stdlib/encodings/iso8859_9.pyi | 0 .../typeshed/stdlib/encodings/johab.pyi | 0 .../typeshed/stdlib/encodings/koi8_r.pyi | 0 .../typeshed/stdlib/encodings/koi8_t.pyi | 0 .../typeshed/stdlib/encodings/koi8_u.pyi | 0 .../typeshed/stdlib/encodings/kz1048.pyi | 0 .../typeshed/stdlib/encodings/latin_1.pyi | 0 .../typeshed/stdlib/encodings/mac_arabic.pyi | 0 .../stdlib/encodings/mac_croatian.pyi | 0 .../stdlib/encodings/mac_cyrillic.pyi | 0 .../typeshed/stdlib/encodings/mac_farsi.pyi | 0 .../typeshed/stdlib/encodings/mac_greek.pyi | 0 .../typeshed/stdlib/encodings/mac_iceland.pyi | 0 .../typeshed/stdlib/encodings/mac_latin2.pyi | 0 .../typeshed/stdlib/encodings/mac_roman.pyi | 0 .../stdlib/encodings/mac_romanian.pyi | 0 .../typeshed/stdlib/encodings/mac_turkish.pyi | 0 .../vendor/typeshed/stdlib/encodings/mbcs.pyi | 0 .../vendor/typeshed/stdlib/encodings/oem.pyi | 0 .../typeshed/stdlib/encodings/palmos.pyi | 0 .../typeshed/stdlib/encodings/ptcp154.pyi | 0 .../typeshed/stdlib/encodings/punycode.pyi | 0 .../stdlib/encodings/quopri_codec.pyi | 0 .../stdlib/encodings/raw_unicode_escape.pyi | 0 .../typeshed/stdlib/encodings/rot_13.pyi | 0 .../typeshed/stdlib/encodings/shift_jis.pyi | 0 .../stdlib/encodings/shift_jis_2004.pyi | 0 .../stdlib/encodings/shift_jisx0213.pyi | 0 .../typeshed/stdlib/encodings/tis_620.pyi | 0 .../typeshed/stdlib/encodings/undefined.pyi | 0 .../stdlib/encodings/unicode_escape.pyi | 0 .../typeshed/stdlib/encodings/utf_16.pyi | 0 .../typeshed/stdlib/encodings/utf_16_be.pyi | 0 .../typeshed/stdlib/encodings/utf_16_le.pyi | 0 .../typeshed/stdlib/encodings/utf_32.pyi | 0 .../typeshed/stdlib/encodings/utf_32_be.pyi | 0 .../typeshed/stdlib/encodings/utf_32_le.pyi | 0 .../typeshed/stdlib/encodings/utf_7.pyi | 0 .../typeshed/stdlib/encodings/utf_8.pyi | 0 .../typeshed/stdlib/encodings/utf_8_sig.pyi | 0 .../typeshed/stdlib/encodings/uu_codec.pyi | 0 .../typeshed/stdlib/encodings/zlib_codec.pyi | 0 .../typeshed/stdlib/ensurepip/__init__.pyi | 0 .../vendor/typeshed/stdlib/enum.pyi | 0 .../vendor/typeshed/stdlib/errno.pyi | 0 .../vendor/typeshed/stdlib/faulthandler.pyi | 0 .../vendor/typeshed/stdlib/fcntl.pyi | 0 .../vendor/typeshed/stdlib/filecmp.pyi | 0 .../vendor/typeshed/stdlib/fileinput.pyi | 0 .../vendor/typeshed/stdlib/fnmatch.pyi | 0 .../vendor/typeshed/stdlib/formatter.pyi | 0 .../vendor/typeshed/stdlib/fractions.pyi | 0 .../vendor/typeshed/stdlib/ftplib.pyi | 0 .../vendor/typeshed/stdlib/functools.pyi | 0 .../vendor/typeshed/stdlib/gc.pyi | 0 .../vendor/typeshed/stdlib/genericpath.pyi | 0 .../vendor/typeshed/stdlib/getopt.pyi | 0 .../vendor/typeshed/stdlib/getpass.pyi | 0 .../vendor/typeshed/stdlib/gettext.pyi | 0 .../vendor/typeshed/stdlib/glob.pyi | 0 .../vendor/typeshed/stdlib/graphlib.pyi | 0 .../vendor/typeshed/stdlib/grp.pyi | 0 .../vendor/typeshed/stdlib/gzip.pyi | 0 .../vendor/typeshed/stdlib/hashlib.pyi | 0 .../vendor/typeshed/stdlib/heapq.pyi | 0 .../vendor/typeshed/stdlib/hmac.pyi | 0 .../vendor/typeshed/stdlib/html/__init__.pyi | 0 .../vendor/typeshed/stdlib/html/entities.pyi | 0 .../vendor/typeshed/stdlib/html/parser.pyi | 0 .../vendor/typeshed/stdlib/http/__init__.pyi | 0 .../vendor/typeshed/stdlib/http/client.pyi | 0 .../vendor/typeshed/stdlib/http/cookiejar.pyi | 0 .../vendor/typeshed/stdlib/http/cookies.pyi | 0 .../vendor/typeshed/stdlib/http/server.pyi | 0 .../vendor/typeshed/stdlib/imaplib.pyi | 0 .../vendor/typeshed/stdlib/imghdr.pyi | 0 .../vendor/typeshed/stdlib/imp.pyi | 0 .../typeshed/stdlib/importlib/__init__.pyi | 0 .../vendor/typeshed/stdlib/importlib/_abc.pyi | 0 .../typeshed/stdlib/importlib/_bootstrap.pyi | 0 .../stdlib/importlib/_bootstrap_external.pyi | 0 .../vendor/typeshed/stdlib/importlib/abc.pyi | 0 .../typeshed/stdlib/importlib/machinery.pyi | 0 .../stdlib/importlib/metadata/__init__.pyi | 0 .../stdlib/importlib/metadata/_meta.pyi | 0 .../stdlib/importlib/metadata/diagnose.pyi | 0 .../typeshed/stdlib/importlib/readers.pyi | 0 .../stdlib/importlib/resources/__init__.pyi | 0 .../stdlib/importlib/resources/_common.pyi | 0 .../importlib/resources/_functional.pyi | 0 .../stdlib/importlib/resources/abc.pyi | 0 .../stdlib/importlib/resources/readers.pyi | 0 .../stdlib/importlib/resources/simple.pyi | 0 .../typeshed/stdlib/importlib/simple.pyi | 0 .../vendor/typeshed/stdlib/importlib/util.pyi | 0 .../vendor/typeshed/stdlib/inspect.pyi | 0 .../vendor/typeshed/stdlib/io.pyi | 0 .../vendor/typeshed/stdlib/ipaddress.pyi | 0 .../vendor/typeshed/stdlib/itertools.pyi | 0 .../vendor/typeshed/stdlib/json/__init__.pyi | 0 .../vendor/typeshed/stdlib/json/decoder.pyi | 0 .../vendor/typeshed/stdlib/json/encoder.pyi | 0 .../vendor/typeshed/stdlib/json/scanner.pyi | 0 .../vendor/typeshed/stdlib/json/tool.pyi | 0 .../vendor/typeshed/stdlib/keyword.pyi | 0 .../typeshed/stdlib/lib2to3/__init__.pyi | 0 .../typeshed/stdlib/lib2to3/btm_matcher.pyi | 0 .../typeshed/stdlib/lib2to3/fixer_base.pyi | 0 .../stdlib/lib2to3/fixes/__init__.pyi | 0 .../stdlib/lib2to3/fixes/fix_apply.pyi | 0 .../stdlib/lib2to3/fixes/fix_asserts.pyi | 0 .../stdlib/lib2to3/fixes/fix_basestring.pyi | 0 .../stdlib/lib2to3/fixes/fix_buffer.pyi | 0 .../stdlib/lib2to3/fixes/fix_dict.pyi | 0 .../stdlib/lib2to3/fixes/fix_except.pyi | 0 .../stdlib/lib2to3/fixes/fix_exec.pyi | 0 .../stdlib/lib2to3/fixes/fix_execfile.pyi | 0 .../stdlib/lib2to3/fixes/fix_exitfunc.pyi | 0 .../stdlib/lib2to3/fixes/fix_filter.pyi | 0 .../stdlib/lib2to3/fixes/fix_funcattrs.pyi | 0 .../stdlib/lib2to3/fixes/fix_future.pyi | 0 .../stdlib/lib2to3/fixes/fix_getcwdu.pyi | 0 .../stdlib/lib2to3/fixes/fix_has_key.pyi | 0 .../stdlib/lib2to3/fixes/fix_idioms.pyi | 0 .../stdlib/lib2to3/fixes/fix_import.pyi | 0 .../stdlib/lib2to3/fixes/fix_imports.pyi | 0 .../stdlib/lib2to3/fixes/fix_imports2.pyi | 0 .../stdlib/lib2to3/fixes/fix_input.pyi | 0 .../stdlib/lib2to3/fixes/fix_intern.pyi | 0 .../stdlib/lib2to3/fixes/fix_isinstance.pyi | 0 .../stdlib/lib2to3/fixes/fix_itertools.pyi | 0 .../lib2to3/fixes/fix_itertools_imports.pyi | 0 .../stdlib/lib2to3/fixes/fix_long.pyi | 0 .../typeshed/stdlib/lib2to3/fixes/fix_map.pyi | 0 .../stdlib/lib2to3/fixes/fix_metaclass.pyi | 0 .../stdlib/lib2to3/fixes/fix_methodattrs.pyi | 0 .../typeshed/stdlib/lib2to3/fixes/fix_ne.pyi | 0 .../stdlib/lib2to3/fixes/fix_next.pyi | 0 .../stdlib/lib2to3/fixes/fix_nonzero.pyi | 0 .../stdlib/lib2to3/fixes/fix_numliterals.pyi | 0 .../stdlib/lib2to3/fixes/fix_operator.pyi | 0 .../stdlib/lib2to3/fixes/fix_paren.pyi | 0 .../stdlib/lib2to3/fixes/fix_print.pyi | 0 .../stdlib/lib2to3/fixes/fix_raise.pyi | 0 .../stdlib/lib2to3/fixes/fix_raw_input.pyi | 0 .../stdlib/lib2to3/fixes/fix_reduce.pyi | 0 .../stdlib/lib2to3/fixes/fix_reload.pyi | 0 .../stdlib/lib2to3/fixes/fix_renames.pyi | 0 .../stdlib/lib2to3/fixes/fix_repr.pyi | 0 .../stdlib/lib2to3/fixes/fix_set_literal.pyi | 0 .../lib2to3/fixes/fix_standarderror.pyi | 0 .../stdlib/lib2to3/fixes/fix_sys_exc.pyi | 0 .../stdlib/lib2to3/fixes/fix_throw.pyi | 0 .../stdlib/lib2to3/fixes/fix_tuple_params.pyi | 0 .../stdlib/lib2to3/fixes/fix_types.pyi | 0 .../stdlib/lib2to3/fixes/fix_unicode.pyi | 0 .../stdlib/lib2to3/fixes/fix_urllib.pyi | 0 .../stdlib/lib2to3/fixes/fix_ws_comma.pyi | 0 .../stdlib/lib2to3/fixes/fix_xrange.pyi | 0 .../stdlib/lib2to3/fixes/fix_xreadlines.pyi | 0 .../typeshed/stdlib/lib2to3/fixes/fix_zip.pyi | 0 .../vendor/typeshed/stdlib/lib2to3/main.pyi | 0 .../stdlib/lib2to3/pgen2/__init__.pyi | 0 .../typeshed/stdlib/lib2to3/pgen2/driver.pyi | 0 .../typeshed/stdlib/lib2to3/pgen2/grammar.pyi | 0 .../stdlib/lib2to3/pgen2/literals.pyi | 0 .../typeshed/stdlib/lib2to3/pgen2/parse.pyi | 0 .../typeshed/stdlib/lib2to3/pgen2/pgen.pyi | 0 .../typeshed/stdlib/lib2to3/pgen2/token.pyi | 0 .../stdlib/lib2to3/pgen2/tokenize.pyi | 0 .../vendor/typeshed/stdlib/lib2to3/pygram.pyi | 0 .../vendor/typeshed/stdlib/lib2to3/pytree.pyi | 0 .../typeshed/stdlib/lib2to3/refactor.pyi | 0 .../vendor/typeshed/stdlib/linecache.pyi | 0 .../vendor/typeshed/stdlib/locale.pyi | 0 .../typeshed/stdlib/logging/__init__.pyi | 0 .../vendor/typeshed/stdlib/logging/config.pyi | 0 .../typeshed/stdlib/logging/handlers.pyi | 0 .../vendor/typeshed/stdlib/lzma.pyi | 0 .../vendor/typeshed/stdlib/mailbox.pyi | 0 .../vendor/typeshed/stdlib/mailcap.pyi | 0 .../vendor/typeshed/stdlib/marshal.pyi | 0 .../vendor/typeshed/stdlib/math.pyi | 0 .../vendor/typeshed/stdlib/mimetypes.pyi | 0 .../vendor/typeshed/stdlib/mmap.pyi | 0 .../vendor/typeshed/stdlib/modulefinder.pyi | 0 .../typeshed/stdlib/msilib/__init__.pyi | 0 .../vendor/typeshed/stdlib/msilib/schema.pyi | 0 .../typeshed/stdlib/msilib/sequence.pyi | 0 .../vendor/typeshed/stdlib/msilib/text.pyi | 0 .../vendor/typeshed/stdlib/msvcrt.pyi | 0 .../stdlib/multiprocessing/__init__.pyi | 0 .../stdlib/multiprocessing/connection.pyi | 0 .../stdlib/multiprocessing/context.pyi | 0 .../stdlib/multiprocessing/dummy/__init__.pyi | 0 .../multiprocessing/dummy/connection.pyi | 0 .../stdlib/multiprocessing/forkserver.pyi | 0 .../typeshed/stdlib/multiprocessing/heap.pyi | 0 .../stdlib/multiprocessing/managers.pyi | 0 .../typeshed/stdlib/multiprocessing/pool.pyi | 0 .../stdlib/multiprocessing/popen_fork.pyi | 0 .../multiprocessing/popen_forkserver.pyi | 0 .../multiprocessing/popen_spawn_posix.pyi | 0 .../multiprocessing/popen_spawn_win32.pyi | 0 .../stdlib/multiprocessing/process.pyi | 0 .../stdlib/multiprocessing/queues.pyi | 0 .../stdlib/multiprocessing/reduction.pyi | 0 .../multiprocessing/resource_sharer.pyi | 0 .../multiprocessing/resource_tracker.pyi | 0 .../stdlib/multiprocessing/shared_memory.pyi | 0 .../stdlib/multiprocessing/sharedctypes.pyi | 0 .../typeshed/stdlib/multiprocessing/spawn.pyi | 0 .../stdlib/multiprocessing/synchronize.pyi | 0 .../typeshed/stdlib/multiprocessing/util.pyi | 0 .../vendor/typeshed/stdlib/netrc.pyi | 0 .../vendor/typeshed/stdlib/nis.pyi | 0 .../vendor/typeshed/stdlib/nntplib.pyi | 0 .../vendor/typeshed/stdlib/nt.pyi | 0 .../vendor/typeshed/stdlib/ntpath.pyi | 0 .../vendor/typeshed/stdlib/nturl2path.pyi | 0 .../vendor/typeshed/stdlib/numbers.pyi | 0 .../vendor/typeshed/stdlib/opcode.pyi | 0 .../vendor/typeshed/stdlib/operator.pyi | 0 .../vendor/typeshed/stdlib/optparse.pyi | 0 .../vendor/typeshed/stdlib/os/__init__.pyi | 0 .../vendor/typeshed/stdlib/os/path.pyi | 0 .../vendor/typeshed/stdlib/ossaudiodev.pyi | 0 .../vendor/typeshed/stdlib/parser.pyi | 0 .../vendor/typeshed/stdlib/pathlib.pyi | 0 .../vendor/typeshed/stdlib/pdb.pyi | 0 .../vendor/typeshed/stdlib/pickle.pyi | 0 .../vendor/typeshed/stdlib/pickletools.pyi | 0 .../vendor/typeshed/stdlib/pipes.pyi | 0 .../vendor/typeshed/stdlib/pkgutil.pyi | 0 .../vendor/typeshed/stdlib/platform.pyi | 0 .../vendor/typeshed/stdlib/plistlib.pyi | 0 .../vendor/typeshed/stdlib/poplib.pyi | 0 .../vendor/typeshed/stdlib/posix.pyi | 0 .../vendor/typeshed/stdlib/posixpath.pyi | 0 .../vendor/typeshed/stdlib/pprint.pyi | 0 .../vendor/typeshed/stdlib/profile.pyi | 0 .../vendor/typeshed/stdlib/pstats.pyi | 0 .../vendor/typeshed/stdlib/pty.pyi | 0 .../vendor/typeshed/stdlib/pwd.pyi | 0 .../vendor/typeshed/stdlib/py_compile.pyi | 0 .../vendor/typeshed/stdlib/pyclbr.pyi | 0 .../vendor/typeshed/stdlib/pydoc.pyi | 0 .../typeshed/stdlib/pydoc_data/__init__.pyi | 0 .../typeshed/stdlib/pydoc_data/topics.pyi | 0 .../typeshed/stdlib/pyexpat/__init__.pyi | 0 .../vendor/typeshed/stdlib/pyexpat/errors.pyi | 0 .../vendor/typeshed/stdlib/pyexpat/model.pyi | 0 .../vendor/typeshed/stdlib/queue.pyi | 0 .../vendor/typeshed/stdlib/quopri.pyi | 0 .../vendor/typeshed/stdlib/random.pyi | 0 .../vendor/typeshed/stdlib/re.pyi | 0 .../vendor/typeshed/stdlib/readline.pyi | 0 .../vendor/typeshed/stdlib/reprlib.pyi | 0 .../vendor/typeshed/stdlib/resource.pyi | 0 .../vendor/typeshed/stdlib/rlcompleter.pyi | 0 .../vendor/typeshed/stdlib/runpy.pyi | 0 .../vendor/typeshed/stdlib/sched.pyi | 0 .../vendor/typeshed/stdlib/secrets.pyi | 0 .../vendor/typeshed/stdlib/select.pyi | 0 .../vendor/typeshed/stdlib/selectors.pyi | 0 .../vendor/typeshed/stdlib/shelve.pyi | 0 .../vendor/typeshed/stdlib/shlex.pyi | 0 .../vendor/typeshed/stdlib/shutil.pyi | 0 .../vendor/typeshed/stdlib/signal.pyi | 0 .../vendor/typeshed/stdlib/site.pyi | 0 .../vendor/typeshed/stdlib/smtpd.pyi | 0 .../vendor/typeshed/stdlib/smtplib.pyi | 0 .../vendor/typeshed/stdlib/sndhdr.pyi | 0 .../vendor/typeshed/stdlib/socket.pyi | 0 .../vendor/typeshed/stdlib/socketserver.pyi | 0 .../vendor/typeshed/stdlib/spwd.pyi | 0 .../typeshed/stdlib/sqlite3/__init__.pyi | 0 .../vendor/typeshed/stdlib/sqlite3/dbapi2.pyi | 0 .../vendor/typeshed/stdlib/sqlite3/dump.pyi | 0 .../vendor/typeshed/stdlib/sre_compile.pyi | 0 .../vendor/typeshed/stdlib/sre_constants.pyi | 0 .../vendor/typeshed/stdlib/sre_parse.pyi | 0 .../vendor/typeshed/stdlib/ssl.pyi | 0 .../vendor/typeshed/stdlib/stat.pyi | 0 .../vendor/typeshed/stdlib/statistics.pyi | 0 .../vendor/typeshed/stdlib/string.pyi | 0 .../vendor/typeshed/stdlib/stringprep.pyi | 0 .../vendor/typeshed/stdlib/struct.pyi | 0 .../vendor/typeshed/stdlib/subprocess.pyi | 0 .../vendor/typeshed/stdlib/sunau.pyi | 0 .../vendor/typeshed/stdlib/symbol.pyi | 0 .../vendor/typeshed/stdlib/symtable.pyi | 0 .../vendor/typeshed/stdlib/sys/__init__.pyi | 0 .../typeshed/stdlib/sys/_monitoring.pyi | 0 .../vendor/typeshed/stdlib/sysconfig.pyi | 0 .../vendor/typeshed/stdlib/syslog.pyi | 0 .../vendor/typeshed/stdlib/tabnanny.pyi | 0 .../vendor/typeshed/stdlib/tarfile.pyi | 0 .../vendor/typeshed/stdlib/telnetlib.pyi | 0 .../vendor/typeshed/stdlib/tempfile.pyi | 0 .../vendor/typeshed/stdlib/termios.pyi | 0 .../vendor/typeshed/stdlib/textwrap.pyi | 0 .../vendor/typeshed/stdlib/this.pyi | 0 .../vendor/typeshed/stdlib/threading.pyi | 0 .../vendor/typeshed/stdlib/time.pyi | 0 .../vendor/typeshed/stdlib/timeit.pyi | 0 .../typeshed/stdlib/tkinter/__init__.pyi | 0 .../typeshed/stdlib/tkinter/colorchooser.pyi | 0 .../typeshed/stdlib/tkinter/commondialog.pyi | 0 .../typeshed/stdlib/tkinter/constants.pyi | 0 .../vendor/typeshed/stdlib/tkinter/dialog.pyi | 0 .../vendor/typeshed/stdlib/tkinter/dnd.pyi | 0 .../typeshed/stdlib/tkinter/filedialog.pyi | 0 .../vendor/typeshed/stdlib/tkinter/font.pyi | 0 .../typeshed/stdlib/tkinter/messagebox.pyi | 0 .../typeshed/stdlib/tkinter/scrolledtext.pyi | 0 .../typeshed/stdlib/tkinter/simpledialog.pyi | 0 .../vendor/typeshed/stdlib/tkinter/tix.pyi | 0 .../vendor/typeshed/stdlib/tkinter/ttk.pyi | 0 .../vendor/typeshed/stdlib/token.pyi | 0 .../vendor/typeshed/stdlib/tokenize.pyi | 0 .../vendor/typeshed/stdlib/tomllib.pyi | 0 .../vendor/typeshed/stdlib/trace.pyi | 0 .../vendor/typeshed/stdlib/traceback.pyi | 0 .../vendor/typeshed/stdlib/tracemalloc.pyi | 0 .../vendor/typeshed/stdlib/tty.pyi | 0 .../vendor/typeshed/stdlib/turtle.pyi | 0 .../vendor/typeshed/stdlib/types.pyi | 0 .../vendor/typeshed/stdlib/typing.pyi | 0 .../typeshed/stdlib/typing_extensions.pyi | 0 .../vendor/typeshed/stdlib/unicodedata.pyi | 0 .../typeshed/stdlib/unittest/__init__.pyi | 0 .../vendor/typeshed/stdlib/unittest/_log.pyi | 0 .../typeshed/stdlib/unittest/async_case.pyi | 0 .../vendor/typeshed/stdlib/unittest/case.pyi | 0 .../typeshed/stdlib/unittest/loader.pyi | 0 .../vendor/typeshed/stdlib/unittest/main.pyi | 0 .../vendor/typeshed/stdlib/unittest/mock.pyi | 0 .../typeshed/stdlib/unittest/result.pyi | 0 .../typeshed/stdlib/unittest/runner.pyi | 0 .../typeshed/stdlib/unittest/signals.pyi | 0 .../vendor/typeshed/stdlib/unittest/suite.pyi | 0 .../vendor/typeshed/stdlib/unittest/util.pyi | 0 .../typeshed/stdlib/urllib/__init__.pyi | 0 .../vendor/typeshed/stdlib/urllib/error.pyi | 0 .../vendor/typeshed/stdlib/urllib/parse.pyi | 0 .../vendor/typeshed/stdlib/urllib/request.pyi | 0 .../typeshed/stdlib/urllib/response.pyi | 0 .../typeshed/stdlib/urllib/robotparser.pyi | 0 .../vendor/typeshed/stdlib/uu.pyi | 0 .../vendor/typeshed/stdlib/uuid.pyi | 0 .../vendor/typeshed/stdlib/venv/__init__.pyi | 0 .../vendor/typeshed/stdlib/warnings.pyi | 0 .../vendor/typeshed/stdlib/wave.pyi | 0 .../vendor/typeshed/stdlib/weakref.pyi | 0 .../vendor/typeshed/stdlib/webbrowser.pyi | 0 .../vendor/typeshed/stdlib/winreg.pyi | 0 .../vendor/typeshed/stdlib/winsound.pyi | 0 .../typeshed/stdlib/wsgiref/__init__.pyi | 0 .../typeshed/stdlib/wsgiref/handlers.pyi | 0 .../typeshed/stdlib/wsgiref/headers.pyi | 0 .../typeshed/stdlib/wsgiref/simple_server.pyi | 0 .../vendor/typeshed/stdlib/wsgiref/types.pyi | 0 .../vendor/typeshed/stdlib/wsgiref/util.pyi | 0 .../typeshed/stdlib/wsgiref/validate.pyi | 0 .../vendor/typeshed/stdlib/xdrlib.pyi | 0 .../vendor/typeshed/stdlib/xml/__init__.pyi | 0 .../typeshed/stdlib/xml/dom/NodeFilter.pyi | 0 .../typeshed/stdlib/xml/dom/__init__.pyi | 0 .../vendor/typeshed/stdlib/xml/dom/domreg.pyi | 0 .../typeshed/stdlib/xml/dom/expatbuilder.pyi | 0 .../typeshed/stdlib/xml/dom/minicompat.pyi | 0 .../typeshed/stdlib/xml/dom/minidom.pyi | 0 .../typeshed/stdlib/xml/dom/pulldom.pyi | 0 .../typeshed/stdlib/xml/dom/xmlbuilder.pyi | 0 .../stdlib/xml/etree/ElementInclude.pyi | 0 .../typeshed/stdlib/xml/etree/ElementPath.pyi | 0 .../typeshed/stdlib/xml/etree/ElementTree.pyi | 0 .../typeshed/stdlib/xml/etree/__init__.pyi | 0 .../stdlib/xml/etree/cElementTree.pyi | 0 .../typeshed/stdlib/xml/parsers/__init__.pyi | 0 .../stdlib/xml/parsers/expat/__init__.pyi | 0 .../stdlib/xml/parsers/expat/errors.pyi | 0 .../stdlib/xml/parsers/expat/model.pyi | 0 .../typeshed/stdlib/xml/sax/__init__.pyi | 0 .../typeshed/stdlib/xml/sax/_exceptions.pyi | 0 .../typeshed/stdlib/xml/sax/expatreader.pyi | 0 .../typeshed/stdlib/xml/sax/handler.pyi | 0 .../typeshed/stdlib/xml/sax/saxutils.pyi | 0 .../typeshed/stdlib/xml/sax/xmlreader.pyi | 0 .../typeshed/stdlib/xmlrpc/__init__.pyi | 0 .../vendor/typeshed/stdlib/xmlrpc/client.pyi | 0 .../vendor/typeshed/stdlib/xmlrpc/server.pyi | 0 .../vendor/typeshed/stdlib/xxlimited.pyi | 0 .../vendor/typeshed/stdlib/zipapp.pyi | 0 .../typeshed/stdlib/zipfile/__init__.pyi | 0 .../stdlib/zipfile/_path/__init__.pyi | 0 .../typeshed/stdlib/zipfile/_path/glob.pyi | 0 .../vendor/typeshed/stdlib/zipimport.pyi | 0 .../vendor/typeshed/stdlib/zlib.pyi | 0 .../typeshed/stdlib/zoneinfo/__init__.pyi | 0 .../typeshed/stdlib/zoneinfo/_common.pyi | 0 .../typeshed/stdlib/zoneinfo/_tzpath.pyi | 0 crates/{red_knot_wasm => ty_wasm}/Cargo.toml | 10 +- crates/{red_knot_wasm => ty_wasm}/src/lib.rs | 14 +- .../{red_knot_wasm => ty_wasm}/tests/api.rs | 2 +- fuzz/Cargo.toml | 9 +- fuzz/README.md | 4 +- ...d_syntax.rs => ty_check_invalid_syntax.rs} | 14 +- fuzz/init-fuzzer.sh | 2 +- playground/.prettierignore | 4 +- playground/README.md | 6 +- playground/package-lock.json | 67 ++- playground/package.json | 8 +- playground/tsconfig.json | 8 +- playground/tsconfig.node.json | 2 +- playground/{knot => ty}/index.html | 8 +- playground/{knot => ty}/package.json | 10 +- playground/{knot => ty}/public/Astral.png | Bin .../{knot => ty}/public/apple-touch-icon.png | Bin .../{knot => ty}/public/favicon-16x16.png | Bin .../{knot => ty}/public/favicon-32x32.png | Bin playground/{knot => ty}/public/favicon.ico | Bin playground/{knot => ty}/src/Editor/Chrome.tsx | 2 +- .../{knot => ty}/src/Editor/Diagnostics.tsx | 2 +- playground/{knot => ty}/src/Editor/Editor.tsx | 30 +- playground/{knot => ty}/src/Editor/Files.tsx | 2 +- .../src/Editor/SecondaryPanel.tsx | 0 .../src/Editor/SecondarySideBar.tsx | 0 playground/{knot => ty}/src/Editor/api.ts | 0 playground/{knot => ty}/src/Editor/persist.ts | 0 playground/{knot => ty}/src/Playground.tsx | 40 +- playground/{knot => ty}/src/index.css | 0 playground/{knot => ty}/src/main.tsx | 0 playground/{knot => ty}/src/third-party.d.ts | 0 playground/{knot => ty}/src/vite-env.d.ts | 0 playground/{knot => ty}/vite.config.ts | 0 pyproject.toml | 4 +- python/py-fuzzer/fuzz.py | 16 +- .../README.md | 8 +- .../pyproject.toml | 4 +- .../src/benchmark/__init__.py | 0 .../src/benchmark/cases.py | 10 +- .../src/benchmark/projects.py | 0 .../src/benchmark/run.py | 22 +- .../{knot_benchmark => ty_benchmark}/uv.lock | 61 +-- knot.schema.json => ty.schema.json | 24 +- 1564 files changed, 1577 insertions(+), 1557 deletions(-) rename .github/workflows/{publish-knot-playground.yml => publish-ty-playground.yml} (80%) delete mode 100644 crates/red_knot/README.md delete mode 100644 crates/red_knot_python_semantic/resources/mdtest/suppressions/knot_ignore.md delete mode 100644 crates/red_knot_vendored/knot_extensions/README.md rename crates/ruff_benchmark/benches/{red_knot.rs => ty.rs} (94%) rename crates/ruff_dev/src/{generate_knot_schema.rs => generate_ty_schema.rs} (87%) rename crates/{red_knot => ty}/Cargo.toml (89%) create mode 100644 crates/ty/README.md rename crates/{red_knot => ty}/build.rs (94%) rename crates/{red_knot => ty}/docs/mypy_primer.md (89%) rename crates/{red_knot => ty}/docs/tracing-flamegraph.png (100%) rename crates/{red_knot => ty}/docs/tracing.md (78%) rename crates/{red_knot => ty}/src/args.rs (94%) rename crates/{red_knot => ty}/src/logging.rs (89%) rename crates/{red_knot => ty}/src/main.rs (96%) rename crates/{red_knot => ty}/src/python_version.rs (100%) rename crates/{red_knot => ty}/src/version.rs (79%) rename crates/{red_knot => ty}/tests/cli.rs (97%) rename crates/{red_knot => ty}/tests/file_watching.rs (98%) rename crates/{red_knot_ide => ty_ide}/Cargo.toml (85%) rename crates/{red_knot_ide => ty_ide}/src/completion.rs (100%) rename crates/{red_knot_ide => ty_ide}/src/db.rs (92%) rename crates/{red_knot_ide => ty_ide}/src/find_node.rs (100%) rename crates/{red_knot_ide => ty_ide}/src/goto.rs (99%) rename crates/{red_knot_ide => ty_ide}/src/hover.rs (99%) rename crates/{red_knot_ide => ty_ide}/src/inlay_hints.rs (98%) rename crates/{red_knot_ide => ty_ide}/src/lib.rs (98%) rename crates/{red_knot_ide => ty_ide}/src/markup.rs (100%) rename crates/{red_knot_project => ty_project}/Cargo.toml (82%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/00_const.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/00_empty.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/00_expr_discard.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/00_expr_var1.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/01_expr_unary.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/02_expr_attr.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/02_expr_attr_multiline.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/02_expr_attr_multiline_assign.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/02_expr_bin_bool.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/02_expr_binary.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/02_expr_bool_op_multiline.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/02_expr_bool_op_multiline2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/02_expr_rel.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/02_expr_rel_multiple.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/02_expr_subscr.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_dict.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_dict_ex.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_dict_literal_large.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_dict_unpack_huge.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_list.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_list_ex.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_list_large.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_set.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_set_multi.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_slice.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_slice_ext.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_tuple.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/03_tuple_ex.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_assign.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_assign_attr.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_assign_attr_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_assign_invalid_target.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_assign_named_expr.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_assign_named_expr_invalid_target.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_assign_subscr.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_assign_unpack.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_assign_unpack_ex.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_assign_unpack_tuple.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_aug_assign.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_aug_assign_attr_multiline.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_aug_assign_attr_sub.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/04_aug_assign_invalid_target.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/05_funcall.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/05_funcall_1.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/05_funcall_2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/05_funcall_in_multiline_tuple.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/05_funcall_kw.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/05_funcall_kw_many.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/05_funcall_kw_pos.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/05_funcall_method_multiline.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/06_funcall_kwargs.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/06_funcall_many_args.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/06_funcall_starargs_ex.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/06_funcall_varargs.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/06_funcall_varargs_kwargs.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/06_funcall_varargs_kwargs_mixed.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/07_ifexpr.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/07_ifexpr_multiline.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/07_ifexpr_multiline2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/08_del.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/08_del_multi.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/09_pass.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/10_if.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/10_if_chained_compare.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/10_if_false.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/10_if_invalid.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/10_if_true.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/10_if_with_named_expr.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/11_if_else.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/11_if_else_deeply_nested_for.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/11_if_else_false.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/11_if_else_true.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/12_if_elif.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/12_if_elif_else.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/13_ifelse_complex1.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/13_ifelse_many.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while_break.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while_break_in_finally.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while_break_invalid_in_class.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while_break_invalid_in_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while_break_non_empty.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while_break_non_exit.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while_continue.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while_false.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while_infinite.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/15_while_true.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/16_for.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/16_for_break.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/16_for_break_invalid_in_class.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/16_for_break_invalid_in_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/16_for_continue.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/16_for_else.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/16_for_invalid.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/16_for_list_literal.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/16_for_nested_ifs.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/20_lambda.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/20_lambda_const.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/20_lambda_default_arg.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/20_lambda_ifelse.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/21_func1.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/21_func1_ret.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/21_func_assign.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/21_func_assign2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/22_func_arg.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/22_func_vararg.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/23_func_ret.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/23_func_ret_val.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/24_func_if_ret.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/24_func_ifelse_ret.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/24_func_ifnot_ret.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/25_func_annotations.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/25_func_annotations_nested.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/25_func_annotations_same_name.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/25_func_annotations_scope.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/25_func_annotations_starred.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/26_func_const_defaults.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/26_func_defaults_same_name.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/27_func_generic.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/27_func_generic_bound.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/27_func_generic_constraint.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/27_func_generic_default.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/27_func_generic_paramspec.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/27_func_generic_paramspec_default.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/27_func_generic_tuple.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/27_func_generic_tuple_default.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/30_func_enclosed.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/30_func_enclosed_many.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/31_func_global.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/31_func_global_annotated_later.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/31_func_nonlocal.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/32_func_global_nested.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/33_func_with_docstring_optimizable_tuple_and_return.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/40_import.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/41_from_import.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/42_import_from_dot.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/50_yield.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/51_gen_comp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/51_gen_comp2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/52_gen_comp_if.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/53_dict_comp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/53_list_comp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/53_list_comp_method.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/53_set_comp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/54_list_comp_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/54_list_comp_lambda.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/54_list_comp_lambda_listcomp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/54_list_comp_recur_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/55_list_comp_nested.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/56_yield_from.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/57_await.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/58_async_for.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/58_async_for_break.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/58_async_for_continue.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/58_async_for_dict_comp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/58_async_for_else.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/58_async_for_gen_comp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/58_async_for_list_comp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/58_async_for_set_comp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/59_async_with.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/59_async_with_nested_with.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/60_try_except.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/60_try_except2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/60_try_except_bare.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/60_try_finally.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/60_try_finally_codeobj.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/60_try_finally_cond.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/60_try_finally_for.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/60_try_finally_ret.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/61_try_except_finally.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/62_try_except_as.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/62_try_except_break.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/62_try_except_cond.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/62_try_except_double_nested_inside_if_else.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/62_try_except_return.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/63_raise.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/63_raise_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/63_raise_x.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/63_raise_x_from_y.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/64_assert.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/67_with.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/67_with_as.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/67_with_as_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/67_with_cond_return.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/67_with_inside_try_finally_multiple_terminal_elif.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/67_with_inside_try_finally_preceding_terminal_except.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/67_with_multi_exit.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/67_with_non_name_target.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/67_with_return.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/68_with2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/69_for_try_except_continue1.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/69_for_try_except_continue2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/69_for_try_except_continue3.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/70_class.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/70_class_base.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/70_class_doc_str.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/71_class_meth.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/71_class_var.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/72_class_mix.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/73_class_generic.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/73_class_generic_bounds.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/73_class_generic_constraints.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/73_class_generic_defaults.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/73_class_generic_paramspec.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/73_class_generic_paramspec_default.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/73_class_generic_tuple.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/73_class_generic_tuple_default.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/74_class_kwargs.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/74_class_kwargs_2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/74_class_super.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/74_class_super_nested.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/74_just_super.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/75_classderef.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/75_classderef_no.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/76_class_nonlocal1.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/76_class_nonlocal2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/76_class_nonlocal3.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/76_class_nonlocal4.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/76_class_nonlocal5.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/77_class__class__.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/77_class__class__nested.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/77_class__class__no_class.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/77_class__class__nonlocals.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/77_class__class__nonlocals_2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/77_class__class__param.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/77_class__class__param_lambda.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/78_class_body_cond.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/78_class_dec.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/78_class_dec_member.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/78_class_dec_member_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/79_metaclass.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/80_func_kwonlyargs1.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/80_func_kwonlyargs2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/80_func_kwonlyargs3.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/81_func_kwonlyargs_defaults.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/83_jupyter_notebook_ipython_magic.ipynb (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_as.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_attr.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_class.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_default.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_guard.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_guard_with_named_expr.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_in_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_in_func_with_rest.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_in_func_with_star.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_invalid.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_mapping.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_mapping_subpattern.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_or.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_sequence.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_sequence_wildcard.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/85_match_singleton.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/88_regression_generic_method_with_nested_function.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/88_regression_tuple_type_short_circuit.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/89_type_alias.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/89_type_alias_invalid_bound.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/90_docstring_class.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/90_docstring_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/90_docstring_mod.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/91_line_numbers1.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/91_line_numbers2.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/91_line_numbers2_comp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/91_line_numbers3.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/91_line_numbers4.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/91_line_numbers_dict.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/91_line_numbers_dict_comp.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/92_qual_class_in_class.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/92_qual_class_in_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/93_deadcode.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/94_strformat.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/94_strformat_complex.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/94_strformat_conv.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/94_strformat_conversion.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/94_strformat_spec.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_assign_subscript_no_rhs.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_assign_tuple.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_class.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_class_multiline.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_class_no_value.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_fstring_invalid.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_func.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_func_future.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_global.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_global_simple.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_local_attr.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_module.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_string_tuple.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/95_annotation_union.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/96_debug.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/97_global_nonlocal_store.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/98_ann_assign_annotation_future_annotations.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/98_ann_assign_annotation_wrong_future.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/98_ann_assign_invalid_target.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/98_ann_assign_simple_annotation.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/99_empty_jump_target_insts.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/cycle_narrowing_constraints.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/cycle_negative_narrowing_constraints.py (100%) rename crates/{red_knot_project => ty_project}/resources/test/corpus/self_referential_function_annotation.py (100%) rename crates/{red_knot_project => ty_project}/src/combine.rs (97%) rename crates/{red_knot_project => ty_project}/src/db.rs (95%) rename crates/{red_knot_project => ty_project}/src/db/changes.rs (98%) rename crates/{red_knot_project => ty_project}/src/files.rs (100%) rename crates/{red_knot_project => ty_project}/src/lib.rs (97%) rename crates/{red_knot_project => ty_project}/src/metadata.rs (93%) rename crates/{red_knot_project => ty_project}/src/metadata/configuration_file.rs (66%) rename crates/{red_knot_project => ty_project}/src/metadata/options.rs (94%) rename crates/{red_knot_project => ty_project}/src/metadata/pyproject.rs (98%) rename crates/{red_knot_project => ty_project}/src/metadata/settings.rs (92%) rename crates/{red_knot_project => ty_project}/src/metadata/value.rs (100%) rename crates/{red_knot_project => ty_project}/src/walk.rs (99%) rename crates/{red_knot_project => ty_project}/src/watch.rs (100%) rename crates/{red_knot_project => ty_project}/src/watch/project_watcher.rs (98%) rename crates/{red_knot_project => ty_project}/src/watch/watcher.rs (100%) rename crates/{red_knot_project => ty_project}/tests/check.rs (98%) rename crates/{red_knot_python_semantic => ty_python_semantic}/Cargo.toml (91%) rename crates/{red_knot_python_semantic => ty_python_semantic}/build.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/mdtest.py (96%) rename crates/{red_knot_python_semantic => ty_python_semantic}/mdtest.py.lock (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/README.md (66%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/.mdformat.toml (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/annotated.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/any.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/callable.md (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/deferred.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/int_float_complex.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/invalid.md (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/literal.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/literal_string.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/never.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/new_types.md (89%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/optional.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/starred.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/stdlib_typing_aliases.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/string.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/union.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/unsupported_special_forms.md (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/annotations/unsupported_type_qualifiers.md (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/assignment/annotations.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/assignment/augmented.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/assignment/multi_target.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/assignment/unbound.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/assignment/walrus.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/attributes.md (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/binary/booleans.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/binary/classes.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/binary/custom.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/binary/instances.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/binary/integers.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/binary/tuples.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/binary/unions.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/boolean/short_circuit.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/boundness_declaredness/public.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/annotation.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/builtins.md (94%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/callable_instance.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/constructor.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/dunder.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/function.md (98%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/getattr_static.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/invalid_syntax.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/methods.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/never.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/str_startswith.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/subclass_of.md (97%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/call/union.md (97%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/class/super.md (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/byte_literals.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/identity.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/instances/membership_test.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/instances/rich_comparison.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/integers.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/intersections.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/non_bool_returns.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/strings.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/tuples.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/unions.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comparison/unsupported.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comprehensions/basic.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/comprehensions/invalid_syntax.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/conditional/if_expression.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/conditional/if_statement.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/conditional/match.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/dataclass_transform.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/dataclasses.md (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/declaration/error.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/decorators.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/descriptor_protocol.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/diagnostics/attribute_assignment.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/diagnostics/invalid_argument_type.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/diagnostics/no_matching_overload.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/diagnostics/semantic_syntax_errors.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/diagnostics/shadowing.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/diagnostics/unpacking.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/diagnostics/unresolved_import.md (91%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/diagnostics/unsupported_bool_conversion.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/diagnostics/version_related_syntax_errors.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/directives/assert_never.md (98%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/directives/assert_type.md (96%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/directives/cast.md (81%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/doc/README.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/doc/public_type_undeclared_symbols.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/exception/basic.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/exception/control_flow.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/exception/except_star.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/exception/invalid_syntax.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/expression/assert.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/expression/attribute.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/expression/boolean.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/expression/if.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/expression/lambda.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/expression/len.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/final.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/function/parameters.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/function/return_type.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/generics/builtins.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/generics/legacy/classes.md (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/generics/legacy/functions.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/generics/legacy/variables.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/generics/pep695/classes.md (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/generics/pep695/functions.md (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/generics/pep695/variables.md (98%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/generics/pep695/variance.md (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/generics/scoping.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/basic.md (97%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/builtins.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/case_sensitive.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/conditional.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/conflicts.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/conventions.md (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/errors.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/invalid_syntax.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/relative.md (98%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/star.md (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/stub_packages.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/stubs.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/import/tracking.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/intersection_types.md (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/invalid_syntax.md (96%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/known_constants.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/boolean.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/bytes.md (77%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/collections/dictionary.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/collections/list.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/collections/set.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/collections/tuple.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/complex.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/ellipsis.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/f_string.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/float.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/integer.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/literal/string.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/loops/async_for.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/loops/for.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/loops/iterators.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/loops/while_loop.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/mdtest_config.md (90%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/mdtest_custom_typeshed.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/metaclass.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/mro.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/named_tuple.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/assert.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/bool-call.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/boolean.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/conditionals/boolean.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/conditionals/elif_else.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/conditionals/eq.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/conditionals/in.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/conditionals/is.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/conditionals/is_not.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/conditionals/nested.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/conditionals/not.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/isinstance.md (98%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/issubclass.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/match.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/post_if_statement.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/truthiness.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/type.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/narrow/while.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/overloads.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/pep695_type_aliases.md (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/properties.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/protocols.md (97%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/regression/14334_diagnostics_in_wrong_file.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/scopes/builtin.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/scopes/eager.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/scopes/global.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/scopes/moduletype_attrs.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/scopes/nonlocal.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/scopes/unbound.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/shadowing/class.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/shadowing/function.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/shadowing/variable_declaration.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/slots.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap (87%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap (87%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap (88%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap (87%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap (88%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap (88%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap (86%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap (87%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap (81%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap (89%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap (91%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap (82%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap (85%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap (88%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap (96%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap (92%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap (91%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap (92%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap (88%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap (90%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap (94%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap (94%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap (94%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap (84%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap (83%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap (85%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap (84%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap (85%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap (84%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap (85%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap (92%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap (86%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap (86%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap (86%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap (86%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap (85%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap (85%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap (85%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap (86%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap (89%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap (77%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap (83%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap (90%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap (97%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap (96%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap (92%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap (97%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap (98%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap (94%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap (91%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap (91%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap (90%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap (78%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap (79%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap (88%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap (85%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap (76%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap (77%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap (78%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap (78%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap (80%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap (82%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap (80%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap (81%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap (80%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap (83%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap (82%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap (86%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap (86%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap (87%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/snapshots/version_related_syntax_errors.md_-_Version-related_syntax_error_diagnostics_-_`match`_statement_-_Before_3.10.snap (83%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/statically_known_branches.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/stubs/class.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/stubs/ellipsis.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/stubs/locals.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/subscript/bytes.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/subscript/class.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/subscript/instance.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/subscript/lists.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/subscript/stepsize_zero.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/subscript/string.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/subscript/tuple.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/suppressions/no_type_check.md (90%) create mode 100644 crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/suppressions/type_ignore.md (95%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/sys_platform.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/sys_version_info.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/terminal_statements.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_api.md (83%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_of/basic.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_of/dynamic.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_of/typing_dot_Type.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/is_assignable_to.md (94%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/is_disjoint_from.md (92%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/is_equivalent_to.md (91%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/is_fully_static.md (89%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/is_gradual_equivalent_to.md (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/is_single_valued.md (94%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/is_singleton.md (88%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/is_subtype_of.md (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/str_repr.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/truthiness.md (93%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_properties/tuples_containing_never.md (96%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_qualifiers/classvar.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/type_qualifiers/final.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/typed_dict.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/unary/custom.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/unary/integers.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/unary/invert_add_usub.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/unary/not.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/union_types.md (96%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/unpacking.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/unreachable.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/with/async.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/mdtest/with/sync.md (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/primer/bad.txt (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/resources/primer/good.txt (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/ast_node_ref.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/db.rs (98%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/lib.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/lint.rs (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/list.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/module_name.rs (96%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/module_resolver/mod.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/module_resolver/module.rs (96%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/module_resolver/path.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/module_resolver/resolver.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/module_resolver/testing.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/module_resolver/typeshed.rs (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/node_key.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/program.rs (98%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/python_platform.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/ast_ids.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/builder.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/builder/except_handlers.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/definition.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/expression.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/narrowing_constraints.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/predicate.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/re_exports.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/symbol.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/use_def.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/use_def/symbol_state.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_index/visibility_constraints.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/semantic_model.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/site_packages.rs (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/suppression.rs (96%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/symbol.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types.rs (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/builder.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/call.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/call/arguments.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/call/bind.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/class.rs (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/class_base.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/context.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/definition.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/diagnostic.rs (99%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/display.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/generics.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/infer.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/instance.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/known_instance.rs (94%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/mro.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/narrow.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/property_tests.rs (97%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/property_tests/setup.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/property_tests/type_generation.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/protocol_class.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/signatures.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/slots.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/string_annotation.rs (92%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/subclass_of.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/type_ordering.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/types/unpacker.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/unpack.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/util/mod.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/src/util/subscript.rs (100%) rename crates/{red_knot_python_semantic => ty_python_semantic}/tests/mdtest.rs (94%) rename crates/{red_knot_server => ty_server}/Cargo.toml (86%) rename crates/{red_knot_server => ty_server}/src/document.rs (100%) rename crates/{red_knot_server => ty_server}/src/document/location.rs (88%) rename crates/{red_knot_server => ty_server}/src/document/notebook.rs (100%) rename crates/{red_knot_server => ty_server}/src/document/range.rs (99%) rename crates/{red_knot_server => ty_server}/src/document/text_document.rs (100%) rename crates/{red_knot_server => ty_server}/src/lib.rs (90%) rename crates/{red_knot_server => ty_server}/src/logging.rs (96%) rename crates/{red_knot_server => ty_server}/src/message.rs (100%) rename crates/{red_knot_server => ty_server}/src/server.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/api.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/api/diagnostics.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/api/notifications.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/api/notifications/did_change.rs (97%) rename crates/{red_knot_server => ty_server}/src/server/api/notifications/did_close.rs (97%) rename crates/{red_knot_server => ty_server}/src/server/api/notifications/did_close_notebook.rs (96%) rename crates/{red_knot_server => ty_server}/src/server/api/notifications/did_open.rs (97%) rename crates/{red_knot_server => ty_server}/src/server/api/notifications/did_open_notebook.rs (97%) rename crates/{red_knot_server => ty_server}/src/server/api/requests.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/api/requests/completion.rs (96%) rename crates/{red_knot_server => ty_server}/src/server/api/requests/diagnostic.rs (97%) rename crates/{red_knot_server => ty_server}/src/server/api/requests/goto_type_definition.rs (96%) rename crates/{red_knot_server => ty_server}/src/server/api/requests/hover.rs (96%) rename crates/{red_knot_server => ty_server}/src/server/api/requests/inlay_hints.rs (96%) rename crates/{red_knot_server => ty_server}/src/server/api/traits.rs (98%) rename crates/{red_knot_server => ty_server}/src/server/client.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/connection.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/schedule.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/schedule/task.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/schedule/thread.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/schedule/thread/pool.rs (100%) rename crates/{red_knot_server => ty_server}/src/server/schedule/thread/priority.rs (100%) rename crates/{red_knot_server => ty_server}/src/session.rs (98%) rename crates/{red_knot_server => ty_server}/src/session/capabilities.rs (100%) rename crates/{red_knot_server => ty_server}/src/session/index.rs (100%) rename crates/{red_knot_server => ty_server}/src/session/settings.rs (100%) rename crates/{red_knot_server => ty_server}/src/system.rs (99%) rename crates/{red_knot_test => ty_test}/Cargo.toml (88%) rename crates/{red_knot_test => ty_test}/README.md (93%) rename crates/{red_knot_test => ty_test}/src/assertion.rs (99%) rename crates/{red_knot_test => ty_test}/src/config.rs (89%) rename crates/{red_knot_test => ty_test}/src/db.rs (97%) rename crates/{red_knot_test => ty_test}/src/diagnostic.rs (100%) rename crates/{red_knot_test => ty_test}/src/lib.rs (99%) rename crates/{red_knot_test => ty_test}/src/matcher.rs (99%) rename crates/{red_knot_test => ty_test}/src/parser.rs (100%) rename crates/{red_knot_vendored => ty_vendored}/.gitignore (100%) rename crates/{red_knot_vendored => ty_vendored}/Cargo.toml (95%) rename crates/{red_knot_vendored => ty_vendored}/README.md (69%) rename crates/{red_knot_vendored => ty_vendored}/build.rs (86%) rename crates/{red_knot_vendored => ty_vendored}/src/lib.rs (100%) create mode 100644 crates/ty_vendored/ty_extensions/README.md rename crates/{red_knot_vendored/knot_extensions/knot_extensions.pyi => ty_vendored/ty_extensions/ty_extensions.pyi} (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/LICENSE (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/README.md (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/source_commit.txt (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/VERSIONS (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/__future__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/__main__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_ast.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_asyncio.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_bisect.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_blake2.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_bootlocale.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_bz2.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_codecs.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_collections_abc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_compat_pickle.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_compression.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_contextvars.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_csv.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_ctypes.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_curses.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_curses_panel.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_dbm.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_decimal.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_frozen_importlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_frozen_importlib_external.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_gdbm.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_hashlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_heapq.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_imp.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_interpchannels.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_interpqueues.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_interpreters.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_io.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_json.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_locale.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_lsprof.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_lzma.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_markupbase.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_msi.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_multibytecodec.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_operator.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_osx_support.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_pickle.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_posixsubprocess.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_py_abc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_pydecimal.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_queue.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_random.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_sitebuiltins.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_socket.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_sqlite3.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_ssl.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_stat.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_struct.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_thread.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_threading_local.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_tkinter.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_tracemalloc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_typeshed/README.md (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_typeshed/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_typeshed/dbapi.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_typeshed/importlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_typeshed/wsgi.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_typeshed/xml.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_warnings.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_weakref.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_weakrefset.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/_winapi.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/abc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/aifc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/antigravity.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/argparse.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/array.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ast.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asynchat.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/base_events.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/base_futures.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/base_subprocess.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/base_tasks.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/constants.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/coroutines.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/events.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/exceptions.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/format_helpers.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/futures.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/locks.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/log.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/mixins.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/proactor_events.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/protocols.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/queues.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/runners.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/selector_events.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/sslproto.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/staggered.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/streams.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/subprocess.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/taskgroups.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/tasks.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/threads.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/timeouts.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/transports.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/trsock.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/unix_events.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/windows_events.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncio/windows_utils.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/asyncore.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/atexit.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/audioop.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/base64.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/bdb.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/binascii.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/binhex.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/bisect.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/builtins.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/bz2.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/cProfile.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/calendar.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/cgi.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/cgitb.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/chunk.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/cmath.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/cmd.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/code.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/codecs.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/codeop.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/collections/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/collections/abc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/colorsys.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/compileall.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/concurrent/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/concurrent/futures/_base.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/concurrent/futures/process.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/concurrent/futures/thread.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/configparser.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/contextlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/contextvars.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/copy.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/copyreg.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/crypt.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/csv.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ctypes/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ctypes/_endian.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ctypes/macholib/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ctypes/macholib/dyld.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ctypes/macholib/dylib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ctypes/macholib/framework.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ctypes/util.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ctypes/wintypes.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/curses/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/curses/ascii.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/curses/has_key.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/curses/panel.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/curses/textpad.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/dataclasses.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/datetime.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/dbm/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/dbm/dumb.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/dbm/gnu.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/dbm/ndbm.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/dbm/sqlite3.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/decimal.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/difflib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/dis.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/_msvccompiler.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/archive_util.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/bcppcompiler.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/ccompiler.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/cmd.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/bdist.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/bdist_dumb.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/bdist_msi.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/bdist_packager.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/bdist_rpm.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/bdist_wininst.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/build.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/build_clib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/build_ext.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/build_py.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/build_scripts.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/check.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/clean.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/config.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/install.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/install_data.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/install_egg_info.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/install_headers.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/install_lib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/install_scripts.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/register.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/sdist.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/command/upload.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/config.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/core.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/cygwinccompiler.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/debug.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/dep_util.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/dir_util.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/dist.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/errors.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/extension.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/fancy_getopt.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/file_util.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/filelist.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/log.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/msvccompiler.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/spawn.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/sysconfig.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/text_file.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/unixccompiler.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/util.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/distutils/version.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/doctest.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/_header_value_parser.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/_policybase.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/base64mime.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/charset.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/contentmanager.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/encoders.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/errors.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/feedparser.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/generator.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/header.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/headerregistry.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/iterators.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/message.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/mime/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/mime/application.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/mime/audio.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/mime/base.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/mime/image.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/mime/message.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/mime/multipart.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/mime/nonmultipart.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/mime/text.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/parser.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/policy.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/quoprimime.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/email/utils.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/aliases.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/ascii.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/base64_codec.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/big5.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/big5hkscs.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/bz2_codec.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/charmap.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp037.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1006.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1026.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1125.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1140.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1250.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1251.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1252.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1253.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1254.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1255.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1256.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1257.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp1258.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp273.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp424.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp437.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp500.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp720.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp737.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp775.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp850.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp852.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp855.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp856.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp857.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp858.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp860.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp861.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp862.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp863.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp864.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp865.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp866.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp869.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp874.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp875.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp932.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp949.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/cp950.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/euc_jis_2004.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/euc_jisx0213.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/euc_jp.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/euc_kr.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/gb18030.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/gb2312.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/gbk.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/hex_codec.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/hp_roman8.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/hz.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/idna.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso2022_jp.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso2022_jp_1.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso2022_jp_2.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso2022_jp_2004.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso2022_jp_3.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso2022_jp_ext.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso2022_kr.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_1.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_10.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_11.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_13.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_14.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_15.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_16.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_2.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_3.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_4.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_5.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_6.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_7.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_8.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/iso8859_9.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/johab.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/koi8_r.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/koi8_t.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/koi8_u.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/kz1048.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/latin_1.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mac_arabic.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mac_croatian.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mac_cyrillic.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mac_farsi.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mac_greek.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mac_iceland.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mac_latin2.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mac_roman.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mac_romanian.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mac_turkish.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/mbcs.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/oem.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/palmos.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/ptcp154.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/punycode.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/quopri_codec.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/raw_unicode_escape.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/rot_13.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/shift_jis.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/shift_jis_2004.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/shift_jisx0213.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/tis_620.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/undefined.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/unicode_escape.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/utf_16.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/utf_16_be.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/utf_16_le.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/utf_32.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/utf_32_be.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/utf_32_le.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/utf_7.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/utf_8.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/utf_8_sig.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/uu_codec.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/encodings/zlib_codec.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ensurepip/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/enum.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/errno.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/faulthandler.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/fcntl.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/filecmp.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/fileinput.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/fnmatch.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/formatter.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/fractions.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ftplib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/functools.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/gc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/genericpath.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/getopt.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/getpass.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/gettext.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/glob.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/graphlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/grp.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/gzip.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/hashlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/heapq.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/hmac.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/html/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/html/entities.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/html/parser.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/http/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/http/client.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/http/cookiejar.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/http/cookies.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/http/server.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/imaplib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/imghdr.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/imp.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/_abc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/_bootstrap.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/_bootstrap_external.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/abc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/machinery.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/metadata/_meta.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/metadata/diagnose.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/readers.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/resources/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/resources/_common.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/resources/_functional.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/resources/abc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/resources/readers.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/resources/simple.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/simple.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/importlib/util.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/inspect.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/io.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ipaddress.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/itertools.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/json/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/json/decoder.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/json/encoder.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/json/scanner.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/json/tool.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/keyword.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/btm_matcher.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixer_base.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_apply.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_basestring.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_dict.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_except.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_exec.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_execfile.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_filter.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_future.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_has_key.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_idioms.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports2.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_input.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_intern.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools_imports.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_map.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_ne.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_numliterals.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_operator.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_print.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_raise.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_reload.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_repr.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_set_literal.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_standarderror.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_sys_exc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_tuple_params.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_types.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_ws_comma.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/fixes/fix_zip.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/main.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/pgen2/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/pgen2/driver.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/pgen2/grammar.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/pgen2/literals.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/pgen2/parse.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/pgen2/token.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/pygram.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/pytree.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lib2to3/refactor.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/linecache.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/locale.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/logging/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/logging/config.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/logging/handlers.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/lzma.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/mailbox.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/mailcap.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/marshal.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/math.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/mimetypes.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/mmap.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/modulefinder.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/msilib/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/msilib/schema.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/msilib/sequence.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/msilib/text.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/msvcrt.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/connection.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/context.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/dummy/connection.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/heap.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/managers.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/pool.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/popen_forkserver.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/popen_spawn_posix.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/popen_spawn_win32.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/process.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/queues.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/reduction.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/resource_sharer.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/resource_tracker.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/shared_memory.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/sharedctypes.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/spawn.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/synchronize.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/multiprocessing/util.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/netrc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/nis.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/nntplib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/nt.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ntpath.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/nturl2path.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/numbers.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/opcode.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/operator.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/optparse.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/os/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/os/path.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ossaudiodev.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/parser.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pathlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pdb.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pickle.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pickletools.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pipes.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pkgutil.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/platform.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/plistlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/poplib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/posix.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/posixpath.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pprint.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/profile.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pstats.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pty.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pwd.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/py_compile.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pyclbr.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pydoc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pydoc_data/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pydoc_data/topics.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pyexpat/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pyexpat/errors.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/pyexpat/model.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/queue.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/quopri.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/random.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/re.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/readline.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/reprlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/resource.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/rlcompleter.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/runpy.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sched.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/secrets.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/select.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/selectors.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/shelve.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/shlex.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/shutil.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/signal.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/site.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/smtpd.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/smtplib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sndhdr.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/socket.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/socketserver.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/spwd.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sqlite3/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sqlite3/dbapi2.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sqlite3/dump.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sre_compile.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sre_constants.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sre_parse.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/ssl.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/stat.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/statistics.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/string.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/stringprep.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/struct.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/subprocess.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sunau.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/symbol.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/symtable.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sys/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sys/_monitoring.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/sysconfig.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/syslog.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tabnanny.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tarfile.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/telnetlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tempfile.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/termios.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/textwrap.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/this.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/threading.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/time.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/timeit.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/colorchooser.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/commondialog.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/constants.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/dialog.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/dnd.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/filedialog.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/font.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/messagebox.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/scrolledtext.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/simpledialog.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/tix.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tkinter/ttk.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/token.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tokenize.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tomllib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/trace.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/traceback.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tracemalloc.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/tty.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/turtle.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/types.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/typing.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/typing_extensions.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unicodedata.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/_log.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/async_case.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/case.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/loader.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/main.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/mock.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/result.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/runner.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/signals.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/suite.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/unittest/util.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/urllib/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/urllib/error.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/urllib/parse.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/urllib/request.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/urllib/response.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/urllib/robotparser.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/uu.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/uuid.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/venv/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/warnings.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/wave.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/weakref.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/webbrowser.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/winreg.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/winsound.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/wsgiref/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/wsgiref/handlers.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/wsgiref/headers.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/wsgiref/simple_server.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/wsgiref/types.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/wsgiref/util.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/wsgiref/validate.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xdrlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/dom/NodeFilter.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/dom/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/dom/domreg.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/dom/minicompat.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/dom/minidom.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/dom/pulldom.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/etree/ElementInclude.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/etree/ElementPath.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/etree/ElementTree.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/etree/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/etree/cElementTree.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/parsers/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/parsers/expat/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/parsers/expat/errors.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/parsers/expat/model.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/sax/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/sax/expatreader.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/sax/handler.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/sax/saxutils.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xml/sax/xmlreader.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xmlrpc/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xmlrpc/client.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xmlrpc/server.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/xxlimited.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/zipapp.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/zipfile/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/zipfile/_path/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/zipfile/_path/glob.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/zipimport.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/zlib.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/zoneinfo/__init__.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/zoneinfo/_common.pyi (100%) rename crates/{red_knot_vendored => ty_vendored}/vendor/typeshed/stdlib/zoneinfo/_tzpath.pyi (100%) rename crates/{red_knot_wasm => ty_wasm}/Cargo.toml (83%) rename crates/{red_knot_wasm => ty_wasm}/src/lib.rs (98%) rename crates/{red_knot_wasm => ty_wasm}/tests/api.rs (92%) rename fuzz/fuzz_targets/{red_knot_check_invalid_syntax.rs => ty_check_invalid_syntax.rs} (95%) rename playground/{knot => ty}/index.html (80%) rename playground/{knot => ty}/package.json (69%) rename playground/{knot => ty}/public/Astral.png (100%) rename playground/{knot => ty}/public/apple-touch-icon.png (100%) rename playground/{knot => ty}/public/favicon-16x16.png (100%) rename playground/{knot => ty}/public/favicon-32x32.png (100%) rename playground/{knot => ty}/public/favicon.ico (100%) rename playground/{knot => ty}/src/Editor/Chrome.tsx (99%) rename playground/{knot => ty}/src/Editor/Diagnostics.tsx (97%) rename playground/{knot => ty}/src/Editor/Editor.tsx (93%) rename playground/{knot => ty}/src/Editor/Files.tsx (99%) rename playground/{knot => ty}/src/Editor/SecondaryPanel.tsx (100%) rename playground/{knot => ty}/src/Editor/SecondarySideBar.tsx (100%) rename playground/{knot => ty}/src/Editor/api.ts (100%) rename playground/{knot => ty}/src/Editor/persist.ts (100%) rename playground/{knot => ty}/src/Playground.tsx (93%) rename playground/{knot => ty}/src/index.css (100%) rename playground/{knot => ty}/src/main.tsx (100%) rename playground/{knot => ty}/src/third-party.d.ts (100%) rename playground/{knot => ty}/src/vite-env.d.ts (100%) rename playground/{knot => ty}/vite.config.ts (100%) rename scripts/{knot_benchmark => ty_benchmark}/README.md (72%) rename scripts/{knot_benchmark => ty_benchmark}/pyproject.toml (79%) rename scripts/{knot_benchmark => ty_benchmark}/src/benchmark/__init__.py (100%) rename scripts/{knot_benchmark => ty_benchmark}/src/benchmark/cases.py (95%) rename scripts/{knot_benchmark => ty_benchmark}/src/benchmark/projects.py (100%) rename scripts/{knot_benchmark => ty_benchmark}/src/benchmark/run.py (88%) rename scripts/{knot_benchmark => ty_benchmark}/uv.lock (76%) rename knot.schema.json => ty.schema.json (92%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4c58d30794721f..43e454d164ad24 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,11 +14,11 @@ # flake8-pyi /crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood -# Script for fuzzing the parser/red-knot etc. +# Script for fuzzing the parser/ty etc. /python/py-fuzzer/ @AlexWaygood -# red-knot -/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager +# ty +/crates/ty* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager /crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager -/scripts/knot_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager -/crates/red_knot_python_semantic @carljm @AlexWaygood @sharkdp @dcreager +/scripts/ty_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager +/crates/ty_python_semantic @carljm @AlexWaygood @sharkdp @dcreager diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ec094d1d43f166..b56b2431242c5d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,8 +36,8 @@ jobs: code: ${{ steps.check_code.outputs.changed }} # Flag that is raised when any code that affects the fuzzer is changed fuzz: ${{ steps.check_fuzzer.outputs.changed }} - # Flag that is set to "true" when code related to red-knot changes. - red_knot: ${{ steps.check_red_knot.outputs.changed }} + # Flag that is set to "true" when code related to ty changes. + ty: ${{ steps.check_ty.outputs.changed }} # Flag that is set to "true" when code related to the playground changes. playground: ${{ steps.check_playground.outputs.changed }} @@ -84,7 +84,7 @@ jobs: if git diff --quiet "${MERGE_BASE}...HEAD" -- ':Cargo.toml' \ ':Cargo.lock' \ ':crates/**' \ - ':!crates/red_knot*/**' \ + ':!crates/ty*/**' \ ':!crates/ruff_python_formatter/**' \ ':!crates/ruff_formatter/**' \ ':!crates/ruff_dev/**' \ @@ -145,7 +145,7 @@ jobs: run: | if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**' \ ':!**/*.md' \ - ':crates/red_knot_python_semantic/resources/mdtest/**/*.md' \ + ':crates/ty_python_semantic/resources/mdtest/**/*.md' \ ':!docs/**' \ ':!assets/**' \ ':.github/workflows/ci.yaml' \ @@ -168,15 +168,15 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi - - name: Check if the red-knot code changed - id: check_red_knot + - name: Check if the ty code changed + id: check_ty env: MERGE_BASE: ${{ steps.merge_base.outputs.sha }} run: | if git diff --quiet "${MERGE_BASE}...HEAD" -- \ ':Cargo.toml' \ ':Cargo.lock' \ - ':crates/red_knot*/**' \ + ':crates/ty*/**' \ ':crates/ruff_db/**' \ ':crates/ruff_annotate_snippets/**' \ ':crates/ruff_python_ast/**' \ @@ -221,7 +221,7 @@ jobs: - name: "Clippy" run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - name: "Clippy (wasm)" - run: cargo clippy -p ruff_wasm -p red_knot_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings + run: cargo clippy -p ruff_wasm -p ty_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings cargo-test-linux: name: "cargo test (linux)" @@ -246,14 +246,14 @@ jobs: uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 with: tool: cargo-insta - - name: Red-knot mdtests (GitHub annotations) - if: ${{ needs.determine_changes.outputs.red_knot == 'true' }} + - name: ty mdtests (GitHub annotations) + if: ${{ needs.determine_changes.outputs.ty == 'true' }} env: NO_COLOR: 1 MDTEST_GITHUB_ANNOTATIONS_FORMAT: 1 # Ignore errors if this step fails; we want to continue to later steps in the workflow anyway. # This step is just to get nice GitHub annotations on the PR diff in the files-changed tab. - run: cargo test -p red_knot_python_semantic --test mdtest || true + run: cargo test -p ty_python_semantic --test mdtest || true - name: "Run tests" shell: bash env: @@ -268,7 +268,7 @@ jobs: # sync, not just public items. Eventually we should do this for all # crates; for now add crates here as they are warning-clean to prevent # regression. - - run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p red_knot_test -p ruff_db --document-private-items + - run: cargo doc --no-deps -p ty_python_semantic -p ty -p ty_test -p ruff_db --document-private-items env: # Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025). RUSTDOCFLAGS: "-D warnings" @@ -278,8 +278,8 @@ jobs: path: target/debug/ruff - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: red_knot - path: target/debug/red_knot + name: ty + path: target/debug/ty cargo-test-linux-release: name: "cargo test (linux, release)" @@ -362,9 +362,9 @@ jobs: run: | cd crates/ruff_wasm wasm-pack test --node - - name: "Test red_knot_wasm" + - name: "Test ty_wasm" run: | - cd crates/red_knot_wasm + cd crates/ty_wasm wasm-pack test --node cargo-build-release: @@ -636,29 +636,29 @@ jobs: name: ecosystem-result path: ecosystem-result - fuzz-redknot: - name: "Fuzz for new red-knot panics" + fuzz-ty: + name: "Fuzz for new ty panics" runs-on: depot-ubuntu-22.04-16 needs: - cargo-test-linux - determine_changes # Only runs on pull requests, since that is the only we way we can find the base version for comparison. - if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.red_knot == 'true' }} + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.ty == 'true' }} timeout-minutes: 20 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 - name: Download new red-knot binary - id: redknot-new + name: Download new ty binary + id: ty-new with: - name: red_knot + name: ty path: target/debug - uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8 - name: Download baseline red-knot binary + name: Download baseline ty binary with: - name: red_knot + name: ty branch: ${{ github.event.pull_request.base.ref }} workflow: "ci.yaml" check_artifacts: true @@ -666,20 +666,20 @@ jobs: - name: Fuzz env: FORCE_COLOR: 1 - NEW_REDKNOT: ${{ steps.redknot-new.outputs.download-path }} + NEW_TY: ${{ steps.ty-new.outputs.download-path }} run: | # Make executable, since artifact download doesn't preserve this - chmod +x "${PWD}/red_knot" "${NEW_REDKNOT}/red_knot" + chmod +x "${PWD}/ty" "${NEW_TY}/ty" ( uvx \ --python="${PYTHON_VERSION}" \ --from=./python/py-fuzzer \ fuzz \ - --test-executable="${NEW_REDKNOT}/red_knot" \ - --baseline-executable="${PWD}/red_knot" \ + --test-executable="${NEW_TY}/ty" \ + --baseline-executable="${PWD}/ty" \ --only-new-bugs \ - --bin=red_knot \ + --bin=ty \ 0-500 ) diff --git a/.github/workflows/daily_property_tests.yaml b/.github/workflows/daily_property_tests.yaml index 6ca84050715e60..867298d101bf25 100644 --- a/.github/workflows/daily_property_tests.yaml +++ b/.github/workflows/daily_property_tests.yaml @@ -38,17 +38,17 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 - - name: Build Red Knot + - name: Build ty # A release build takes longer (2 min vs 1 min), but the property tests run much faster in release # mode (1.5 min vs 14 min), so the overall time is shorter with a release build. - run: cargo build --locked --release --package red_knot_python_semantic --tests + run: cargo build --locked --release --package ty_python_semantic --tests - name: Run property tests shell: bash run: | export QUICKCHECK_TESTS=100000 for _ in {1..5}; do - cargo test --locked --release --package red_knot_python_semantic -- --ignored list::property_tests - cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable + cargo test --locked --release --package ty_python_semantic -- --ignored list::property_tests + cargo test --locked --release --package ty_python_semantic -- --ignored types::property_tests::stable done create-issue-on-failure: @@ -68,5 +68,5 @@ jobs: repo: "ruff", title: `Daily property test run failed on ${new Date().toDateString()}`, body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - labels: ["bug", "red-knot", "testing"], + labels: ["bug", "ty", "testing"], }) diff --git a/.github/workflows/mypy_primer.yaml b/.github/workflows/mypy_primer.yaml index 381de8e386c6e8..a88e925734fcd8 100644 --- a/.github/workflows/mypy_primer.yaml +++ b/.github/workflows/mypy_primer.yaml @@ -5,7 +5,7 @@ permissions: {} on: pull_request: paths: - - "crates/red_knot*/**" + - "crates/ty*/**" - "crates/ruff_db" - "crates/ruff_python_ast" - "crates/ruff_python_parser" @@ -50,7 +50,7 @@ jobs: run: | cd ruff - PRIMER_SELECTOR="$(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt)" + PRIMER_SELECTOR="$(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt)" echo "new commit" git rev-list --format=%s --max-count=1 "$GITHUB_SHA" @@ -65,10 +65,10 @@ jobs: echo "Project selector: $PRIMER_SELECTOR" # Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs uvx \ - --from="git+https://github.com/hauntsaninja/mypy_primer@b83b9eade0b7ed2f4b9b129b163acac1ecb48f71" \ + --from="git+https://github.com/hauntsaninja/mypy_primer@4b15cf3b07db69db67bbfaebfffb2a8a28040933" \ mypy_primer \ --repo ruff \ - --type-checker knot \ + --type-checker ty \ --old base_commit \ --new "$GITHUB_SHA" \ --project-selector "/($PRIMER_SELECTOR)\$" \ diff --git a/.github/workflows/publish-knot-playground.yml b/.github/workflows/publish-ty-playground.yml similarity index 80% rename from .github/workflows/publish-knot-playground.yml rename to .github/workflows/publish-ty-playground.yml index 80b96525c4f480..b5dc37dc475f12 100644 --- a/.github/workflows/publish-knot-playground.yml +++ b/.github/workflows/publish-ty-playground.yml @@ -1,5 +1,5 @@ -# Publish the Red Knot playground. -name: "[Knot Playground] Release" +# Publish the ty playground. +name: "[ty Playground] Release" permissions: {} @@ -7,12 +7,12 @@ on: push: branches: [main] paths: - - "crates/red_knot*/**" + - "crates/ty*/**" - "crates/ruff_db/**" - "crates/ruff_python_ast/**" - "crates/ruff_python_parser/**" - "playground/**" - - ".github/workflows/publish-knot-playground.yml" + - ".github/workflows/publish-ty-playground.yml" concurrency: group: ${{ github.workflow }}-${{ github.ref_name }} @@ -45,8 +45,8 @@ jobs: - name: "Run TypeScript checks" run: npm run check working-directory: playground - - name: "Build Knot playground" - run: npm run build --workspace knot-playground + - name: "Build ty playground" + run: npm run build --workspace ty-playground working-directory: playground - name: "Deploy to Cloudflare Pages" if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} @@ -55,4 +55,4 @@ jobs: apiToken: ${{ secrets.CF_API_TOKEN }} accountId: ${{ secrets.CF_ACCOUNT_ID }} # `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production - command: pages deploy playground/knot/dist --project-name=knot-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA} + command: pages deploy playground/ty/dist --project-name=ty-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA} diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index a3128023f8a07f..cbdb4a0df37cd7 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -39,13 +39,13 @@ jobs: - name: Sync typeshed id: sync run: | - rm -rf ruff/crates/red_knot_vendored/vendor/typeshed - mkdir ruff/crates/red_knot_vendored/vendor/typeshed - cp typeshed/README.md ruff/crates/red_knot_vendored/vendor/typeshed - cp typeshed/LICENSE ruff/crates/red_knot_vendored/vendor/typeshed - cp -r typeshed/stdlib ruff/crates/red_knot_vendored/vendor/typeshed/stdlib - rm -rf ruff/crates/red_knot_vendored/vendor/typeshed/stdlib/@tests - git -C typeshed rev-parse HEAD > ruff/crates/red_knot_vendored/vendor/typeshed/source_commit.txt + rm -rf ruff/crates/ty_vendored/vendor/typeshed + mkdir ruff/crates/ty_vendored/vendor/typeshed + cp typeshed/README.md ruff/crates/ty_vendored/vendor/typeshed + cp typeshed/LICENSE ruff/crates/ty_vendored/vendor/typeshed + cp -r typeshed/stdlib ruff/crates/ty_vendored/vendor/typeshed/stdlib + rm -rf ruff/crates/ty_vendored/vendor/typeshed/stdlib/@tests + git -C typeshed rev-parse HEAD > ruff/crates/ty_vendored/vendor/typeshed/source_commit.txt - name: Commit the changes id: commit if: ${{ steps.sync.outcome == 'success' }} @@ -79,5 +79,5 @@ jobs: repo: "ruff", title: `Automated typeshed sync failed on ${new Date().toDateString()}`, body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - labels: ["bug", "red-knot"], + labels: ["bug", "ty"], }) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68f126eeb9a881..b7e4a79932db85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,8 +3,8 @@ fail_fast: false exclude: | (?x)^( .github/workflows/release.yml| - crates/red_knot_vendored/vendor/.*| - crates/red_knot_project/resources/.*| + crates/ty_vendored/vendor/.*| + crates/ty_project/resources/.*| crates/ruff_benchmark/resources/.*| crates/ruff_linter/resources/.*| crates/ruff_linter/src/rules/.*/snapshots/.*| diff --git a/Cargo.lock b/Cargo.lock index c775732fd41489..c99ff7b35bdf49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2472,216 +2472,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "red_knot" -version = "0.0.0" -dependencies = [ - "anyhow", - "argfile", - "clap", - "colored 3.0.0", - "countme", - "crossbeam", - "ctrlc", - "filetime", - "insta", - "insta-cmd", - "jiff", - "rayon", - "red_knot_project", - "red_knot_python_semantic", - "red_knot_server", - "regex", - "ruff_db", - "ruff_python_ast", - "ruff_python_trivia", - "salsa", - "tempfile", - "toml", - "tracing", - "tracing-flame", - "tracing-subscriber", - "tracing-tree", - "wild", -] - -[[package]] -name = "red_knot_ide" -version = "0.0.0" -dependencies = [ - "insta", - "red_knot_python_semantic", - "red_knot_vendored", - "ruff_db", - "ruff_python_ast", - "ruff_python_parser", - "ruff_text_size", - "rustc-hash 2.1.1", - "salsa", - "smallvec", - "tracing", -] - -[[package]] -name = "red_knot_project" -version = "0.0.0" -dependencies = [ - "anyhow", - "crossbeam", - "glob", - "insta", - "notify", - "pep440_rs", - "rayon", - "red_knot_ide", - "red_knot_python_semantic", - "red_knot_vendored", - "ruff_cache", - "ruff_db", - "ruff_macros", - "ruff_python_ast", - "ruff_python_formatter", - "ruff_text_size", - "rustc-hash 2.1.1", - "salsa", - "schemars", - "serde", - "thiserror 2.0.12", - "toml", - "tracing", -] - -[[package]] -name = "red_knot_python_semantic" -version = "0.0.0" -dependencies = [ - "anyhow", - "bitflags 2.9.0", - "camino", - "compact_str", - "countme", - "dir-test", - "drop_bomb", - "hashbrown 0.15.2", - "indexmap", - "insta", - "itertools 0.14.0", - "memchr", - "ordermap", - "quickcheck", - "quickcheck_macros", - "red_knot_test", - "red_knot_vendored", - "ruff_db", - "ruff_index", - "ruff_macros", - "ruff_python_ast", - "ruff_python_literal", - "ruff_python_parser", - "ruff_python_stdlib", - "ruff_python_trivia", - "ruff_source_file", - "ruff_text_size", - "rustc-hash 2.1.1", - "salsa", - "schemars", - "serde", - "smallvec", - "static_assertions", - "strum", - "strum_macros", - "tempfile", - "test-case", - "thiserror 2.0.12", - "tracing", -] - -[[package]] -name = "red_knot_server" -version = "0.0.0" -dependencies = [ - "anyhow", - "crossbeam", - "jod-thread", - "libc", - "lsp-server", - "lsp-types", - "red_knot_ide", - "red_knot_project", - "red_knot_python_semantic", - "ruff_db", - "ruff_notebook", - "ruff_source_file", - "ruff_text_size", - "rustc-hash 2.1.1", - "serde", - "serde_json", - "shellexpand", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "red_knot_test" -version = "0.0.0" -dependencies = [ - "anyhow", - "camino", - "colored 3.0.0", - "insta", - "memchr", - "red_knot_python_semantic", - "red_knot_vendored", - "regex", - "ruff_db", - "ruff_index", - "ruff_notebook", - "ruff_python_ast", - "ruff_python_trivia", - "ruff_source_file", - "ruff_text_size", - "rustc-hash 2.1.1", - "salsa", - "serde", - "smallvec", - "tempfile", - "thiserror 2.0.12", - "toml", - "tracing", -] - -[[package]] -name = "red_knot_vendored" -version = "0.0.0" -dependencies = [ - "path-slash", - "ruff_db", - "walkdir", - "zip", -] - -[[package]] -name = "red_knot_wasm" -version = "0.0.0" -dependencies = [ - "console_error_panic_hook", - "console_log", - "getrandom 0.3.2", - "js-sys", - "log", - "red_knot_ide", - "red_knot_project", - "red_knot_python_semantic", - "ruff_db", - "ruff_notebook", - "ruff_python_formatter", - "ruff_source_file", - "ruff_text_size", - "serde-wasm-bindgen", - "wasm-bindgen", - "wasm-bindgen-test", -] - [[package]] name = "redox_syscall" version = "0.5.10" @@ -2838,7 +2628,6 @@ dependencies = [ "criterion", "mimalloc", "rayon", - "red_knot_project", "ruff_db", "ruff_linter", "ruff_python_ast", @@ -2847,6 +2636,7 @@ dependencies = [ "ruff_python_trivia", "rustc-hash 2.1.1", "tikv-jemallocator", + "ty_project", ] [[package]] @@ -2913,7 +2703,6 @@ dependencies = [ "libcst", "pretty_assertions", "rayon", - "red_knot_project", "regex", "ruff", "ruff_diagnostics", @@ -2936,6 +2725,7 @@ dependencies = [ "tracing", "tracing-indicatif", "tracing-subscriber", + "ty_project", ] [[package]] @@ -2971,7 +2761,6 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "red_knot_python_semantic", "ruff_cache", "ruff_db", "ruff_linter", @@ -2981,6 +2770,7 @@ dependencies = [ "salsa", "schemars", "serde", + "ty_python_semantic", "zip", ] @@ -4150,6 +3940,216 @@ dependencies = [ "snapbox", ] +[[package]] +name = "ty" +version = "0.0.0" +dependencies = [ + "anyhow", + "argfile", + "clap", + "colored 3.0.0", + "countme", + "crossbeam", + "ctrlc", + "filetime", + "insta", + "insta-cmd", + "jiff", + "rayon", + "regex", + "ruff_db", + "ruff_python_ast", + "ruff_python_trivia", + "salsa", + "tempfile", + "toml", + "tracing", + "tracing-flame", + "tracing-subscriber", + "tracing-tree", + "ty_project", + "ty_python_semantic", + "ty_server", + "wild", +] + +[[package]] +name = "ty_ide" +version = "0.0.0" +dependencies = [ + "insta", + "ruff_db", + "ruff_python_ast", + "ruff_python_parser", + "ruff_text_size", + "rustc-hash 2.1.1", + "salsa", + "smallvec", + "tracing", + "ty_python_semantic", + "ty_vendored", +] + +[[package]] +name = "ty_project" +version = "0.0.0" +dependencies = [ + "anyhow", + "crossbeam", + "glob", + "insta", + "notify", + "pep440_rs", + "rayon", + "ruff_cache", + "ruff_db", + "ruff_macros", + "ruff_python_ast", + "ruff_python_formatter", + "ruff_text_size", + "rustc-hash 2.1.1", + "salsa", + "schemars", + "serde", + "thiserror 2.0.12", + "toml", + "tracing", + "ty_ide", + "ty_python_semantic", + "ty_vendored", +] + +[[package]] +name = "ty_python_semantic" +version = "0.0.0" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "camino", + "compact_str", + "countme", + "dir-test", + "drop_bomb", + "hashbrown 0.15.2", + "indexmap", + "insta", + "itertools 0.14.0", + "memchr", + "ordermap", + "quickcheck", + "quickcheck_macros", + "ruff_db", + "ruff_index", + "ruff_macros", + "ruff_python_ast", + "ruff_python_literal", + "ruff_python_parser", + "ruff_python_stdlib", + "ruff_python_trivia", + "ruff_source_file", + "ruff_text_size", + "rustc-hash 2.1.1", + "salsa", + "schemars", + "serde", + "smallvec", + "static_assertions", + "strum", + "strum_macros", + "tempfile", + "test-case", + "thiserror 2.0.12", + "tracing", + "ty_test", + "ty_vendored", +] + +[[package]] +name = "ty_server" +version = "0.0.0" +dependencies = [ + "anyhow", + "crossbeam", + "jod-thread", + "libc", + "lsp-server", + "lsp-types", + "ruff_db", + "ruff_notebook", + "ruff_source_file", + "ruff_text_size", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "shellexpand", + "tracing", + "tracing-subscriber", + "ty_ide", + "ty_project", + "ty_python_semantic", +] + +[[package]] +name = "ty_test" +version = "0.0.0" +dependencies = [ + "anyhow", + "camino", + "colored 3.0.0", + "insta", + "memchr", + "regex", + "ruff_db", + "ruff_index", + "ruff_notebook", + "ruff_python_ast", + "ruff_python_trivia", + "ruff_source_file", + "ruff_text_size", + "rustc-hash 2.1.1", + "salsa", + "serde", + "smallvec", + "tempfile", + "thiserror 2.0.12", + "toml", + "tracing", + "ty_python_semantic", + "ty_vendored", +] + +[[package]] +name = "ty_vendored" +version = "0.0.0" +dependencies = [ + "path-slash", + "ruff_db", + "walkdir", + "zip", +] + +[[package]] +name = "ty_wasm" +version = "0.0.0" +dependencies = [ + "console_error_panic_hook", + "console_log", + "getrandom 0.3.2", + "js-sys", + "log", + "ruff_db", + "ruff_notebook", + "ruff_python_formatter", + "ruff_source_file", + "ruff_text_size", + "serde-wasm-bindgen", + "ty_ide", + "ty_project", + "ty_python_semantic", + "wasm-bindgen", + "wasm-bindgen-test", +] + [[package]] name = "typed-arena" version = "2.0.2" diff --git a/Cargo.toml b/Cargo.toml index 039c225ff82bd1..3dd064483e5cbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,14 +35,14 @@ ruff_python_trivia = { path = "crates/ruff_python_trivia" } ruff_server = { path = "crates/ruff_server" } ruff_source_file = { path = "crates/ruff_source_file" } ruff_text_size = { path = "crates/ruff_text_size" } -red_knot_vendored = { path = "crates/red_knot_vendored" } ruff_workspace = { path = "crates/ruff_workspace" } -red_knot_ide = { path = "crates/red_knot_ide" } -red_knot_project = { path = "crates/red_knot_project", default-features = false } -red_knot_python_semantic = { path = "crates/red_knot_python_semantic" } -red_knot_server = { path = "crates/red_knot_server" } -red_knot_test = { path = "crates/red_knot_test" } +ty_ide = { path = "crates/ty_ide" } +ty_project = { path = "crates/ty_project", default-features = false } +ty_python_semantic = { path = "crates/ty_python_semantic" } +ty_server = { path = "crates/ty_server" } +ty_test = { path = "crates/ty_test" } +ty_vendored = { path = "crates/ty_vendored" } aho-corasick = { version = "1.1.3" } anstream = { version = "0.6.18" } diff --git a/_typos.toml b/_typos.toml index 2f38f52eea3184..402e1755337bb0 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,7 +1,7 @@ [files] # https://github.com/crate-ci/typos/issues/868 extend-exclude = [ - "crates/red_knot_vendored/vendor/**/*", + "crates/ty_vendored/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*", ] diff --git a/crates/red_knot/README.md b/crates/red_knot/README.md deleted file mode 100644 index de53ab22c9670f..00000000000000 --- a/crates/red_knot/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Red Knot - -Red Knot is an extremely fast type checker. -Currently, it is a work-in-progress and not ready for user testing. - -Red Knot is designed to prioritize good type inference, even in unannotated code, -and aims to avoid false positives. - -While Red Knot will produce similar results to mypy and pyright on many codebases, -100% compatibility with these tools is a non-goal. -On some codebases, Red Knot's design decisions lead to different outcomes -than you would get from running one of these more established tools. - -## Contributing - -Core type checking tests are written as Markdown code blocks. -They can be found in [`red_knot_python_semantic/resources/mdtest`][resources-mdtest]. -See [`red_knot_test/README.md`][mdtest-readme] for more information -on the test framework itself. - -The list of open issues can be found [here][open-issues]. - -[mdtest-readme]: ../red_knot_test/README.md -[open-issues]: https://github.com/astral-sh/ruff/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3Ared-knot -[resources-mdtest]: ../red_knot_python_semantic/resources/mdtest diff --git a/crates/red_knot_python_semantic/resources/mdtest/suppressions/knot_ignore.md b/crates/red_knot_python_semantic/resources/mdtest/suppressions/knot_ignore.md deleted file mode 100644 index fb8f78bd432bb8..00000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/suppressions/knot_ignore.md +++ /dev/null @@ -1,191 +0,0 @@ -# Suppressing errors with `knot: ignore` - -Type check errors can be suppressed by a `knot: ignore` comment on the same line as the violation. - -## Simple `knot: ignore` - -```py -a = 4 + test # knot: ignore -``` - -## Suppressing a specific code - -```py -a = 4 + test # knot: ignore[unresolved-reference] -``` - -## Unused suppression - -```py -test = 10 -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'" -a = test + 3 # knot: ignore[possibly-unresolved-reference] -``` - -## Unused suppression if the error codes don't match - -```py -# error: [unresolved-reference] -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'" -a = test + 3 # knot: ignore[possibly-unresolved-reference] -``` - -## Suppressed unused comment - -```py -# error: [unused-ignore-comment] -a = 10 / 2 # knot: ignore[division-by-zero] -a = 10 / 2 # knot: ignore[division-by-zero, unused-ignore-comment] -a = 10 / 2 # knot: ignore[unused-ignore-comment, division-by-zero] -a = 10 / 2 # knot: ignore[unused-ignore-comment] # type: ignore -a = 10 / 2 # type: ignore # knot: ignore[unused-ignore-comment] -``` - -## Unused ignore comment - -```py -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unused-ignore-comment'" -a = 10 / 0 # knot: ignore[division-by-zero, unused-ignore-comment] -``` - -## Multiple unused comments - -Today, Red Knot emits a diagnostic for every unused code. We might want to group the codes by -comment at some point in the future. - -```py -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'division-by-zero'" -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'" -a = 10 / 2 # knot: ignore[division-by-zero, unresolved-reference] - -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'invalid-assignment'" -# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'" -a = 10 / 0 # knot: ignore[invalid-assignment, division-by-zero, unresolved-reference] -``` - -## Multiple suppressions - -```py -# fmt: off -def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... # knot: ignore[fstring-type-annotation, byte-string-type-annotation] -``` - -## Can't suppress syntax errors - - - -```py -# error: [invalid-syntax] -# error: [unused-ignore-comment] -def test($): # knot: ignore - pass -``` - - - -## Can't suppress `revealed-type` diagnostics - -```py -a = 10 -# revealed: Literal[10] -# error: [unknown-rule] "Unknown rule `revealed-type`" -reveal_type(a) # knot: ignore[revealed-type] -``` - -## Extra whitespace in type ignore comments is allowed - -```py -a = 10 / 0 # knot : ignore -a = 10 / 0 # knot: ignore [ division-by-zero ] -``` - -## Whitespace is optional - -```py -# fmt: off -a = 10 / 0 #knot:ignore[division-by-zero] -``` - -## Trailing codes comma - -Trailing commas in the codes section are allowed: - -```py -a = 10 / 0 # knot: ignore[division-by-zero,] -``` - -## Invalid characters in codes - -```py -# error: [division-by-zero] -# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a alphanumeric character or `-` or `_` as code" -a = 10 / 0 # knot: ignore[*-*] -``` - -## Trailing whitespace - - - -```py -a = 10 / 0 # knot: ignore[division-by-zero] - # ^^^^^^ trailing whitespace -``` - - - -## Missing comma - -A missing comma results in an invalid suppression comment. We may want to recover from this in the -future. - -```py -# error: [unresolved-reference] -# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a comma separating the rule codes" -a = x / 0 # knot: ignore[division-by-zero unresolved-reference] -``` - -## Missing closing bracket - -```py -# error: [unresolved-reference] "Name `x` used when not defined" -# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a comma separating the rule codes" -a = x / 2 # knot: ignore[unresolved-reference -``` - -## Empty codes - -An empty codes array suppresses no-diagnostics and is always useless - -```py -# error: [division-by-zero] -# error: [unused-ignore-comment] "Unused `knot: ignore` without a code" -a = 4 / 0 # knot: ignore[] -``` - -## File-level suppression comments - -File level suppression comments are currently intentionally unsupported because we've yet to decide -if they should use a different syntax that also supports enabling rules or changing the rule's -severity: `knot: possibly-undefined-reference=error` - -```py -# error: [unused-ignore-comment] -# knot: ignore[division-by-zero] - -a = 4 / 0 # error: [division-by-zero] -``` - -## Unknown rule - -```py -# error: [unknown-rule] "Unknown rule `is-equal-14`" -a = 10 + 4 # knot: ignore[is-equal-14] -``` - -## Code with `lint:` prefix - -```py -# error:[unknown-rule] "Unknown rule `lint:division-by-zero`. Did you mean `division-by-zero`?" -# error: [division-by-zero] -a = 10 / 0 # knot: ignore[lint:division-by-zero] -``` diff --git a/crates/red_knot_vendored/knot_extensions/README.md b/crates/red_knot_vendored/knot_extensions/README.md deleted file mode 100644 index d26f2802b53d35..00000000000000 --- a/crates/red_knot_vendored/knot_extensions/README.md +++ /dev/null @@ -1,3 +0,0 @@ -The `knot_extensions.pyi` file in this directory will be symlinked into -the `vendor/typeshed/stdlib` directory every time we sync our `typeshed` -stubs (see `.github/workflows/sync_typeshed.yaml`). diff --git a/crates/ruff/tests/analyze_graph.rs b/crates/ruff/tests/analyze_graph.rs index 54da7dd7054695..e59a01c2949cc3 100644 --- a/crates/ruff/tests/analyze_graph.rs +++ b/crates/ruff/tests/analyze_graph.rs @@ -552,7 +552,7 @@ fn venv() -> Result<()> { }); // test the error message for a non-existent venv. it's important that the `ruff analyze graph` - // flag matches the red-knot flag used to generate the error message (`--python`) + // flag matches the ty flag used to generate the error message (`--python`) insta::with_settings!({ filters => INSTA_FILTERS.to_vec(), }, { diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index cea63ae29f4c12..b5c2a50a133a07 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -33,7 +33,7 @@ name = "formatter" harness = false [[bench]] -name = "red_knot" +name = "ty" harness = false [dependencies] @@ -49,7 +49,7 @@ ruff_python_ast = { workspace = true } ruff_python_formatter = { workspace = true } ruff_python_parser = { workspace = true } ruff_python_trivia = { workspace = true } -red_knot_project = { workspace = true } +ty_project = { workspace = true } [lints] workspace = true diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/ty.rs similarity index 94% rename from crates/ruff_benchmark/benches/red_knot.rs rename to crates/ruff_benchmark/benches/ty.rs index e0b0da246ba744..65731e9a911c77 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -7,16 +7,16 @@ use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; use rayon::ThreadPoolBuilder; use rustc_hash::FxHashSet; -use red_knot_project::metadata::options::{EnvironmentOptions, Options}; -use red_knot_project::metadata::value::RangedValue; -use red_knot_project::watch::{ChangeEvent, ChangedKind}; -use red_knot_project::{Db, ProjectDatabase, ProjectMetadata}; use ruff_benchmark::TestFile; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; use ruff_db::files::{system_path_to_file, File}; use ruff_db::source::source_text; use ruff_db::system::{MemoryFileSystem, SystemPath, SystemPathBuf, TestSystem}; use ruff_python_ast::PythonVersion; +use ty_project::metadata::options::{EnvironmentOptions, Options}; +use ty_project::metadata::value::RangedValue; +use ty_project::watch::{ChangeEvent, ChangedKind}; +use ty_project::{Db, ProjectDatabase, ProjectMetadata}; struct Case { db: ProjectDatabase, @@ -122,7 +122,7 @@ static RAYON_INITIALIZED: std::sync::Once = std::sync::Once::new(); fn setup_rayon() { // Initialize the rayon thread pool outside the benchmark because it has a significant cost. // We limit the thread pool to only one (the current thread) because we're focused on - // where red knot spends time and less about how well the code runs concurrently. + // where ty spends time and less about how well the code runs concurrently. // We might want to add a benchmark focusing on concurrency to detect congestion in the future. RAYON_INITIALIZED.call_once(|| { ThreadPoolBuilder::new() @@ -172,7 +172,7 @@ fn benchmark_incremental(criterion: &mut Criterion) { setup_rayon(); - criterion.bench_function("red_knot_check_file[incremental]", |b| { + criterion.bench_function("ty_check_file[incremental]", |b| { b.iter_batched_ref(setup, incremental, BatchSize::SmallInput); }); } @@ -180,7 +180,7 @@ fn benchmark_incremental(criterion: &mut Criterion) { fn benchmark_cold(criterion: &mut Criterion) { setup_rayon(); - criterion.bench_function("red_knot_check_file[cold]", |b| { + criterion.bench_function("ty_check_file[cold]", |b| { b.iter_batched_ref( setup_tomllib_case, |case| { @@ -257,7 +257,7 @@ fn setup_micro_case(code: &str) -> Case { fn benchmark_many_string_assignments(criterion: &mut Criterion) { setup_rayon(); - criterion.bench_function("red_knot_micro[many_string_assignments]", |b| { + criterion.bench_function("ty_micro[many_string_assignments]", |b| { b.iter_batched_ref( || { // This is a micro benchmark, but it is effectively identical to a code sample diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 412eec753059ee..6807f67888e872 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -134,20 +134,20 @@ impl Diagnostic { /// NOTE: At present, this routine will return the first primary /// annotation's message as the primary message when the main diagnostic /// message is empty. This is meant to facilitate an incremental migration - /// in Red Knot over to the new diagnostic data model. (The old data model + /// in ty over to the new diagnostic data model. (The old data model /// didn't distinguish between messages on the entire diagnostic and /// messages attached to a particular span.) pub fn primary_message(&self) -> &str { if !self.inner.message.as_str().is_empty() { return self.inner.message.as_str(); } - // FIXME: As a special case, while we're migrating Red Knot + // FIXME: As a special case, while we're migrating ty // to the new diagnostic data model, we'll look for a primary // message from the primary annotation. This is because most - // Red Knot diagnostics are created with an empty diagnostic + // ty diagnostics are created with an empty diagnostic // message and instead attach the message to the annotation. // Fixing this will require touching basically every diagnostic - // in Red Knot, so we do it this way for now to match the old + // in ty, so we do it this way for now to match the old // semantics. ---AG self.primary_annotation() .and_then(|ann| ann.get_message()) @@ -165,7 +165,7 @@ impl Diagnostic { /// /// The reason why we don't just always return both the main diagnostic /// message and the primary annotation message is because this was written - /// in the midst of an incremental migration of Red Knot over to the new + /// in the midst of an incremental migration of ty over to the new /// diagnostic data model. At time of writing, diagnostics were still /// constructed in the old model where the main diagnostic message and the /// primary annotation message were not distinguished from each other. So diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index e20e8f48aa837c..084b66d7692c50 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -624,7 +624,7 @@ impl<'r> RenderableAnnotation<'r> { /// For example, at time of writing (2025-03-07), the plan is (roughly) for /// Ruff to grow its own interner of file paths so that a `Span` can store an /// interned ID instead of a (roughly) `Arc`. This interner is planned -/// to be entirely separate from the Salsa interner used by Red Knot, and so, +/// to be entirely separate from the Salsa interner used by ty, and so, /// callers will need to pass in a different "resolver" for turning `Span`s /// into actual file paths/contents. The infrastructure for this isn't fully in /// place, but this type serves to demarcate the intended abstraction boundary. diff --git a/crates/ruff_db/src/testing.rs b/crates/ruff_db/src/testing.rs index 9bd683ccad8d83..57f264fa69ad08 100644 --- a/crates/ruff_db/src/testing.rs +++ b/crates/ruff_db/src/testing.rs @@ -107,7 +107,7 @@ fn query_name(_query: &Q) -> &'static str { .unwrap_or(full_qualified_query_name) } -/// Sets up logging for the current thread. It captures all `red_knot` and `ruff` events. +/// Sets up logging for the current thread. It captures all `ty` and `ruff` events. /// /// Useful for capturing the tracing output in a failing test. /// @@ -128,7 +128,7 @@ pub fn setup_logging() -> LoggingGuard { /// # Examples /// ``` /// use ruff_db::testing::setup_logging_with_filter; -/// let _logging = setup_logging_with_filter("red_knot_module_resolver::resolver"); +/// let _logging = setup_logging_with_filter("ty_module_resolver::resolver"); /// ``` /// /// # Filter @@ -148,11 +148,7 @@ impl LoggingBuilder { pub fn new() -> Self { Self { filter: EnvFilter::default() - .add_directive( - "red_knot=trace" - .parse() - .expect("Hardcoded directive to be valid"), - ) + .add_directive("ty=trace".parse().expect("Hardcoded directive to be valid")) .add_directive( "ruff=trace" .parse() diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml index 9432476a5aefd9..c0070058607194 100644 --- a/crates/ruff_dev/Cargo.toml +++ b/crates/ruff_dev/Cargo.toml @@ -11,7 +11,7 @@ repository = { workspace = true } license = { workspace = true } [dependencies] -red_knot_project = { workspace = true, features = ["schemars"] } +ty_project = { workspace = true, features = ["schemars"] } ruff = { workspace = true } ruff_diagnostics = { workspace = true } ruff_formatter = { workspace = true } diff --git a/crates/ruff_dev/src/generate_all.rs b/crates/ruff_dev/src/generate_all.rs index f22c562ae6ef4f..d4fd30a869ffae 100644 --- a/crates/ruff_dev/src/generate_all.rs +++ b/crates/ruff_dev/src/generate_all.rs @@ -2,7 +2,7 @@ use anyhow::Result; -use crate::{generate_cli_help, generate_docs, generate_json_schema, generate_knot_schema}; +use crate::{generate_cli_help, generate_docs, generate_json_schema, generate_ty_schema}; pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all"; @@ -33,7 +33,7 @@ impl Mode { pub(crate) fn main(args: &Args) -> Result<()> { generate_json_schema::main(&generate_json_schema::Args { mode: args.mode })?; - generate_knot_schema::main(&generate_knot_schema::Args { mode: args.mode })?; + generate_ty_schema::main(&generate_ty_schema::Args { mode: args.mode })?; generate_cli_help::main(&generate_cli_help::Args { mode: args.mode })?; generate_docs::main(&generate_docs::Args { dry_run: args.mode.is_dry_run(), diff --git a/crates/ruff_dev/src/generate_knot_schema.rs b/crates/ruff_dev/src/generate_ty_schema.rs similarity index 87% rename from crates/ruff_dev/src/generate_knot_schema.rs rename to crates/ruff_dev/src/generate_ty_schema.rs index 0c6f8320981cc2..77d8e076ec5475 100644 --- a/crates/ruff_dev/src/generate_knot_schema.rs +++ b/crates/ruff_dev/src/generate_ty_schema.rs @@ -9,11 +9,11 @@ use schemars::schema_for; use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND}; use crate::ROOT_DIR; -use red_knot_project::metadata::options::Options; +use ty_project::metadata::options::Options; #[derive(clap::Args)] pub(crate) struct Args { - /// Write the generated table to stdout (rather than to `knot.schema.json`). + /// Write the generated table to stdout (rather than to `ty.schema.json`). #[arg(long, default_value_t, value_enum)] pub(crate) mode: Mode, } @@ -21,7 +21,7 @@ pub(crate) struct Args { pub(crate) fn main(args: &Args) -> Result<()> { let schema = schema_for!(Options); let schema_string = serde_json::to_string_pretty(&schema).unwrap(); - let filename = "knot.schema.json"; + let filename = "ty.schema.json"; let schema_path = PathBuf::from(ROOT_DIR).join(filename); match args.mode { @@ -62,7 +62,7 @@ mod tests { #[test] fn test_generate_json_schema() -> Result<()> { - let mode = if env::var("KNOT_UPDATE_SCHEMA").as_deref() == Ok("1") { + let mode = if env::var("TY_UPDATE_SCHEMA").as_deref() == Ok("1") { Mode::Write } else { Mode::Check diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index 5fb10476da47bc..c2dd13d0f88402 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -13,9 +13,9 @@ mod generate_all; mod generate_cli_help; mod generate_docs; mod generate_json_schema; -mod generate_knot_schema; mod generate_options; mod generate_rules_table; +mod generate_ty_schema; mod print_ast; mod print_cst; mod print_tokens; @@ -40,8 +40,8 @@ enum Command { GenerateAll(generate_all::Args), /// Generate JSON schema for the TOML configuration file. GenerateJSONSchema(generate_json_schema::Args), - /// Generate JSON schema for the Red Knot TOML configuration file. - GenerateKnotSchema(generate_knot_schema::Args), + /// Generate JSON schema for the ty TOML configuration file. + GenerateTySchema(generate_ty_schema::Args), /// Generate a Markdown-compatible table of supported lint rules. GenerateRulesTable, /// Generate a Markdown-compatible listing of configuration options. @@ -86,7 +86,7 @@ fn main() -> Result { match command { Command::GenerateAll(args) => generate_all::main(&args)?, Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?, - Command::GenerateKnotSchema(args) => generate_knot_schema::main(&args)?, + Command::GenerateTySchema(args) => generate_ty_schema::main(&args)?, Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()), Command::GenerateOptions => println!("{}", generate_options::generate()), Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?, diff --git a/crates/ruff_graph/Cargo.toml b/crates/ruff_graph/Cargo.toml index 645c9cd2e42555..917a2f4e05c351 100644 --- a/crates/ruff_graph/Cargo.toml +++ b/crates/ruff_graph/Cargo.toml @@ -10,13 +10,13 @@ authors.workspace = true license.workspace = true [dependencies] -red_knot_python_semantic = { workspace = true } ruff_cache = { workspace = true } ruff_db = { workspace = true, features = ["os", "serde"] } ruff_linter = { workspace = true } ruff_macros = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_parser = { workspace = true } +ty_python_semantic = { workspace = true } anyhow = { workspace = true } clap = { workspace = true, optional = true } diff --git a/crates/ruff_graph/src/collector.rs b/crates/ruff_graph/src/collector.rs index 7946b2c5a6ef13..770241e8de98fa 100644 --- a/crates/ruff_graph/src/collector.rs +++ b/crates/ruff_graph/src/collector.rs @@ -1,8 +1,8 @@ -use red_knot_python_semantic::ModuleName; use ruff_python_ast::visitor::source_order::{ walk_expr, walk_module, walk_stmt, SourceOrderVisitor, }; use ruff_python_ast::{self as ast, Expr, Mod, Stmt}; +use ty_python_semantic::ModuleName; /// Collect all imports for a given Python file. #[derive(Default, Debug)] diff --git a/crates/ruff_graph/src/db.rs b/crates/ruff_graph/src/db.rs index e339262bff4fc4..d3866dec0ddb67 100644 --- a/crates/ruff_graph/src/db.rs +++ b/crates/ruff_graph/src/db.rs @@ -2,16 +2,16 @@ use anyhow::Result; use std::sync::Arc; use zip::CompressionMethod; -use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; -use red_knot_python_semantic::{ - default_lint_registry, Db, Program, ProgramSettings, PythonPath, PythonPlatform, - SearchPathSettings, -}; use ruff_db::files::{File, Files}; use ruff_db::system::{OsSystem, System, SystemPathBuf}; use ruff_db::vendored::{VendoredFileSystem, VendoredFileSystemBuilder}; use ruff_db::{Db as SourceDb, Upcast}; use ruff_python_ast::PythonVersion; +use ty_python_semantic::lint::{LintRegistry, RuleSelection}; +use ty_python_semantic::{ + default_lint_registry, Db, Program, ProgramSettings, PythonPath, PythonPlatform, + SearchPathSettings, +}; static EMPTY_VENDORED: std::sync::LazyLock = std::sync::LazyLock::new(|| { let mut builder = VendoredFileSystemBuilder::new(CompressionMethod::Stored); diff --git a/crates/ruff_graph/src/resolver.rs b/crates/ruff_graph/src/resolver.rs index f5eb04402092e4..b1b488fb67f02b 100644 --- a/crates/ruff_graph/src/resolver.rs +++ b/crates/ruff_graph/src/resolver.rs @@ -1,5 +1,5 @@ -use red_knot_python_semantic::resolve_module; use ruff_db::files::FilePath; +use ty_python_semantic::resolve_module; use crate::collector::CollectedImport; use crate::ModuleDb; diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S704.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S704.py index 7748a0ac40c236..41cc8bc44b840a 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S704.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S704.py @@ -12,7 +12,7 @@ Markup(object="safe") Markup(object="unsafe {}".format(content)) # Not currently detected -# NOTE: We may be able to get rid of these false positives with red-knot +# NOTE: We may be able to get rid of these false positives with ty # if it includes comprehensive constant expression detection/evaluation. Markup("*" * 8) # S704 (false positive) flask.Markup("hello {}".format("world")) # S704 (false positive) diff --git a/crates/ruff_linter/src/logging.rs b/crates/ruff_linter/src/logging.rs index 554c4e2c842442..5cf75d509b971b 100644 --- a/crates/ruff_linter/src/logging.rs +++ b/crates/ruff_linter/src/logging.rs @@ -151,7 +151,7 @@ pub fn set_up_logging(level: LogLevel) -> Result<()> { }) .level(level.level_filter()) .level_for("globset", log::LevelFilter::Warn) - .level_for("red_knot_python_semantic", log::LevelFilter::Warn) + .level_for("ty_python_semantic", log::LevelFilter::Warn) .level_for("salsa", log::LevelFilter::Warn) .chain(std::io::stderr()) .apply()?; diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S704_S704.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S704_S704.py.snap index 8ee7eb09b6c35a..02a25f90f6ecd3 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S704_S704.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S704_S704.py.snap @@ -42,7 +42,7 @@ S704.py:11:1: S704 Unsafe use of `flask.Markup` detected S704.py:17:1: S704 Unsafe use of `markupsafe.Markup` detected | -15 | # NOTE: We may be able to get rid of these false positives with red-knot +15 | # NOTE: We may be able to get rid of these false positives with ty 16 | # if it includes comprehensive constant expression detection/evaluation. 17 | Markup("*" * 8) # S704 (false positive) | ^^^^^^^^^^^^^^^ S704 diff --git a/crates/ruff_macros/src/lib.rs b/crates/ruff_macros/src/lib.rs index 05913ddee843b4..99bac807fe9207 100644 --- a/crates/ruff_macros/src/lib.rs +++ b/crates/ruff_macros/src/lib.rs @@ -36,8 +36,8 @@ pub fn derive_combine_options(input: TokenStream) -> TokenStream { .into() } -/// Automatically derives a `red_knot_project::project::Combine` implementation for the attributed type -/// that calls `red_knot_project::project::Combine::combine` for each field. +/// Automatically derives a `ty_project::project::Combine` implementation for the attributed type +/// that calls `ty_project::project::Combine::combine` for each field. /// /// The derive macro can only be used on structs. Enums aren't yet supported. #[proc_macro_derive(Combine)] diff --git a/crates/red_knot/Cargo.toml b/crates/ty/Cargo.toml similarity index 89% rename from crates/red_knot/Cargo.toml rename to crates/ty/Cargo.toml index 474a08f5b30c32..9ce23eb648e912 100644 --- a/crates/red_knot/Cargo.toml +++ b/crates/ty/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "red_knot" +name = "ty" version = "0.0.0" edition.workspace = true rust-version.workspace = true @@ -12,11 +12,11 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -red_knot_python_semantic = { workspace = true } -red_knot_project = { workspace = true, features = ["zstd"] } -red_knot_server = { workspace = true } ruff_db = { workspace = true, features = ["os", "cache"] } ruff_python_ast = { workspace = true } +ty_python_semantic = { workspace = true } +ty_project = { workspace = true, features = ["zstd"] } +ty_server = { workspace = true } anyhow = { workspace = true } argfile = { workspace = true } diff --git a/crates/ty/README.md b/crates/ty/README.md new file mode 100644 index 00000000000000..e0b644c3ea8aa7 --- /dev/null +++ b/crates/ty/README.md @@ -0,0 +1,25 @@ +# ty + +ty is an extremely fast type checker. +Currently, it is a work-in-progress and not ready for user testing. + +ty is designed to prioritize good type inference, even in unannotated code, +and aims to avoid false positives. + +While ty will produce similar results to mypy and pyright on many codebases, +100% compatibility with these tools is a non-goal. +On some codebases, ty's design decisions lead to different outcomes +than you would get from running one of these more established tools. + +## Contributing + +Core type checking tests are written as Markdown code blocks. +They can be found in [`ty_python_semantic/resources/mdtest`][resources-mdtest]. +See [`ty_test/README.md`][mdtest-readme] for more information +on the test framework itself. + +The list of open issues can be found [here][open-issues]. + +[mdtest-readme]: ../ty_test/README.md +[open-issues]: https://github.com/astral-sh/ty/issues +[resources-mdtest]: ../ty_python_semantic/resources/mdtest diff --git a/crates/red_knot/build.rs b/crates/ty/build.rs similarity index 94% rename from crates/red_knot/build.rs rename to crates/ty/build.rs index 3cd5edd64633c0..bcfcf6331d2589 100644 --- a/crates/red_knot/build.rs +++ b/crates/ty/build.rs @@ -59,8 +59,8 @@ fn commit_info(workspace_root: &Path) { let mut parts = stdout.split_whitespace(); let mut next = || parts.next().unwrap(); let _commit_hash = next(); - println!("cargo::rustc-env=RED_KNOT_COMMIT_SHORT_HASH={}", next()); - println!("cargo::rustc-env=RED_KNOT_COMMIT_DATE={}", next()); + println!("cargo::rustc-env=TY_COMMIT_SHORT_HASH={}", next()); + println!("cargo::rustc-env=TY_COMMIT_DATE={}", next()); // Describe can fail for some commits // https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emdescribeoptionsem @@ -70,7 +70,7 @@ fn commit_info(workspace_root: &Path) { // If this is the tagged commit, this component will be missing println!( - "cargo::rustc-env=RED_KNOT_LAST_TAG_DISTANCE={}", + "cargo::rustc-env=TY_LAST_TAG_DISTANCE={}", describe_parts.next().unwrap_or("0") ); } diff --git a/crates/red_knot/docs/mypy_primer.md b/crates/ty/docs/mypy_primer.md similarity index 89% rename from crates/red_knot/docs/mypy_primer.md rename to crates/ty/docs/mypy_primer.md index bcf288265effd1..95345c6282beea 100644 --- a/crates/red_knot/docs/mypy_primer.md +++ b/crates/ty/docs/mypy_primer.md @@ -22,7 +22,7 @@ To show the diagnostics diff between two Git revisions (e.g. your feature branch ```sh mypy_primer \ - --type-checker knot \ + --type-checker ty \ --old origin/main \ --new my/feature \ --debug \ @@ -31,15 +31,15 @@ mypy_primer \ ``` This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the -diff for all projects we currently enable in CI, use `--project-selector "/($(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt))\$"`. +diff for all projects we currently enable in CI, use `--project-selector "/($(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt))\$"`. -You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `knot_paths` configuration +You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `ty_paths` configuration option to work correctly. ## Avoiding recompilation If you want to run `mypy_primer` repeatedly, e.g. for different projects, but for the same combination of `--old` and `--new`, you -can use set the `MYPY_PRIMER_NO_REBUILD` environment variable to avoid recompilation of Red Knot: +can use set the `MYPY_PRIMER_NO_REBUILD` environment variable to avoid recompilation of ty: ```sh MYPY_PRIMER_NO_REBUILD=1 mypy_primer … diff --git a/crates/red_knot/docs/tracing-flamegraph.png b/crates/ty/docs/tracing-flamegraph.png similarity index 100% rename from crates/red_knot/docs/tracing-flamegraph.png rename to crates/ty/docs/tracing-flamegraph.png diff --git a/crates/red_knot/docs/tracing.md b/crates/ty/docs/tracing.md similarity index 78% rename from crates/red_knot/docs/tracing.md rename to crates/ty/docs/tracing.md index 360d3bc87fb33c..7bbf7f5806af0b 100644 --- a/crates/red_knot/docs/tracing.md +++ b/crates/ty/docs/tracing.md @@ -11,16 +11,16 @@ The CLI supports different verbosity levels. - default: Only show errors and warnings. - `-v` activates `info!`: Show generally useful information such as paths of configuration files, detected platform, etc., but it's not a lot of messages, it's something you'll activate in CI by default. cargo build e.g. shows you which packages are fresh. - `-vv` activates `debug!` and timestamps: This should be enough information to get to the bottom of bug reports. When you're processing many packages or files, you'll get pages and pages of output, but each line is link to a specific action or state change. -- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you almost certainly want to use `RED_KNOT_LOG` to filter it down to the area your investigating. +- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you almost certainly want to use `TY_LOG` to filter it down to the area your investigating. -## Better logging with `RED_KNOT_LOG` and `RAYON_NUM_THREADS` +## Better logging with `TY_LOG` and `RAYON_NUM_THREADS` -By default, the CLI shows messages from the `ruff` and `red_knot` crates. Tracing messages from other crates are not shown. -The `RED_KNOT_LOG` environment variable allows you to customize which messages are shown by specifying one +By default, the CLI shows messages from the `ruff` and `ty` crates. Tracing messages from other crates are not shown. +The `TY_LOG` environment variable allows you to customize which messages are shown by specifying one or more [filter directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives). -The `RAYON_NUM_THREADS` environment variable, meanwhile, can be used to control the level of concurrency red-knot uses. -By default, red-knot will attempt to parallelize its work so that multiple files are checked simultaneously, +The `RAYON_NUM_THREADS` environment variable, meanwhile, can be used to control the level of concurrency ty uses. +By default, ty will attempt to parallelize its work so that multiple files are checked simultaneously, but this can result in a confused logging output where messages from different threads are intertwined. To switch off concurrency entirely and have more readable logs, use `RAYON_NUM_THREADS=1`. @@ -31,23 +31,23 @@ To switch off concurrency entirely and have more readable logs, use `RAYON_NUM_T Shows debug messages from all crates. ```bash -RED_KNOT_LOG=debug +TY_LOG=debug ``` #### Show salsa query execution messages -Show the salsa `execute: my_query` messages in addition to all red knot messages. +Show the salsa `execute: my_query` messages in addition to all ty messages. ```bash -RED_KNOT_LOG=ruff=trace,red_knot=trace,salsa=info +TY_LOG=ruff=trace,ty=trace,salsa=info ``` #### Show typing traces -Only show traces for the `red_knot_python_semantic::types` module. +Only show traces for the `ty_python_semantic::types` module. ```bash -RED_KNOT_LOG="red_knot_python_semantic::types" +TY_LOG="ty_python_semantic::types" ``` Note: Ensure that you use `-vvv` to see tracing spans. @@ -57,7 +57,7 @@ Note: Ensure that you use `-vvv` to see tracing spans. Shows all messages that are inside of a span for a specific file. ```bash -RED_KNOT_LOG=red_knot[{file=/home/micha/astral/test/x.py}]=trace +TY_LOG=ty[{file=/home/micha/astral/test/x.py}]=trace ``` **Note**: Tracing still shows all spans because tracing can't know at the time of entering the span @@ -103,10 +103,10 @@ called **once**. ## Profiling -Red Knot generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable `RED_KNOT_LOG_PROFILE` to `1` or `true`. +ty generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable `TY_LOG_PROFILE` to `1` or `true`. ```bash -RED_KNOT_LOG_PROFILE=1 red_knot -- --current-directory=../test -vvv +TY_LOG_PROFILE=1 ty -- --current-directory=../test -vvv ``` You can convert the textual representation into a visual one using `inferno`. diff --git a/crates/red_knot/src/args.rs b/crates/ty/src/args.rs similarity index 94% rename from crates/red_knot/src/args.rs rename to crates/ty/src/args.rs index 00b40dd5ff9991..5049bd15510a92 100644 --- a/crates/red_knot/src/args.rs +++ b/crates/ty/src/args.rs @@ -1,17 +1,13 @@ use crate::logging::Verbosity; use crate::python_version::PythonVersion; use clap::{ArgAction, ArgMatches, Error, Parser}; -use red_knot_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions}; -use red_knot_project::metadata::value::{RangedValue, RelativePathBuf}; -use red_knot_python_semantic::lint; use ruff_db::system::SystemPathBuf; +use ty_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions}; +use ty_project::metadata::value::{RangedValue, RelativePathBuf}; +use ty_python_semantic::lint; #[derive(Debug, Parser)] -#[command( - author, - name = "red-knot", - about = "An extremely fast Python type checker." -)] +#[command(author, name = "ty", about = "An extremely fast Python type checker.")] #[command(version)] pub(crate) struct Args { #[command(subcommand)] @@ -26,7 +22,7 @@ pub(crate) enum Command { /// Start the language server Server, - /// Display Red Knot's version + /// Display ty's version Version, } @@ -48,11 +44,11 @@ pub(crate) struct CheckCommand { #[arg(long, value_name = "PROJECT")] pub(crate) project: Option, - /// Path to the Python installation from which Red Knot resolves type information and third-party dependencies. + /// Path to the Python installation from which ty resolves type information and third-party dependencies. /// - /// If not specified, Red Knot will look at the `VIRTUAL_ENV` environment variable. + /// If not specified, ty will look at the `VIRTUAL_ENV` environment variable. /// - /// Red Knot will search in the path's `site-packages` directories for type information and + /// ty will search in the path's `site-packages` directories for type information and /// third-party imports. /// /// This option is commonly used to specify the path to a virtual environment. diff --git a/crates/red_knot/src/logging.rs b/crates/ty/src/logging.rs similarity index 89% rename from crates/red_knot/src/logging.rs rename to crates/ty/src/logging.rs index c55878c60c9526..010dfda494ab4c 100644 --- a/crates/red_knot/src/logging.rs +++ b/crates/ty/src/logging.rs @@ -1,4 +1,4 @@ -//! Sets up logging for Red Knot +//! Sets up logging for ty use anyhow::Context; use colored::Colorize; @@ -42,14 +42,14 @@ impl Verbosity { #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub(crate) enum VerbosityLevel { - /// Default output level. Only shows Ruff and Red Knot events up to the [`WARN`](tracing::Level::WARN). + /// Default output level. Only shows Ruff and ty events up to the [`WARN`](tracing::Level::WARN). Default, - /// Enables verbose output. Emits Ruff and Red Knot events up to the [`INFO`](tracing::Level::INFO). + /// Enables verbose output. Emits Ruff and ty events up to the [`INFO`](tracing::Level::INFO). /// Corresponds to `-v`. Verbose, - /// Enables a more verbose tracing format and emits Ruff and Red Knot events up to [`DEBUG`](tracing::Level::DEBUG). + /// Enables a more verbose tracing format and emits Ruff and ty events up to [`DEBUG`](tracing::Level::DEBUG). /// Corresponds to `-vv` ExtraVerbose, @@ -79,11 +79,11 @@ impl VerbosityLevel { pub(crate) fn setup_tracing(level: VerbosityLevel) -> anyhow::Result { use tracing_subscriber::prelude::*; - // The `RED_KNOT_LOG` environment variable overrides the default log level. - let filter = if let Ok(log_env_variable) = std::env::var("RED_KNOT_LOG") { + // The `TY_LOG` environment variable overrides the default log level. + let filter = if let Ok(log_env_variable) = std::env::var("TY_LOG") { EnvFilter::builder() .parse(log_env_variable) - .context("Failed to parse directives specified in RED_KNOT_LOG environment variable.")? + .context("Failed to parse directives specified in TY_LOG environment variable.")? } else { match level { VerbosityLevel::Default => { @@ -93,9 +93,9 @@ pub(crate) fn setup_tracing(level: VerbosityLevel) -> anyhow::Result { let level_filter = level.level_filter(); - // Show info|debug|trace events, but allow `RED_KNOT_LOG` to override + // Show info|debug|trace events, but allow `TY_LOG` to override let filter = EnvFilter::default().add_directive( - format!("red_knot={level_filter}") + format!("ty={level_filter}") .parse() .expect("Hardcoded directive to be valid"), ); @@ -131,7 +131,7 @@ pub(crate) fn setup_tracing(level: VerbosityLevel) -> anyhow::Result() -> ( where S: Subscriber + for<'span> LookupSpan<'span>, { - if let Ok("1" | "true") = std::env::var("RED_KNOT_LOG_PROFILE").as_deref() { + if let Ok("1" | "true") = std::env::var("TY_LOG_PROFILE").as_deref() { let (layer, guard) = tracing_flame::FlameLayer::with_file("tracing.folded") .expect("Flame layer to be created"); (Some(layer), Some(guard)) @@ -168,14 +168,14 @@ pub(crate) struct TracingGuard { _flame_guard: Option>>, } -struct RedKnotFormat { +struct TyFormat { display_timestamp: bool, display_level: bool, show_spans: bool, } /// See -impl FormatEvent for RedKnotFormat +impl FormatEvent for TyFormat where S: Subscriber + for<'a> LookupSpan<'a>, N: for<'a> FormatFields<'a> + 'static, diff --git a/crates/red_knot/src/main.rs b/crates/ty/src/main.rs similarity index 96% rename from crates/red_knot/src/main.rs rename to crates/ty/src/main.rs index 4ed4065c0c8731..18a4deebae003f 100644 --- a/crates/red_knot/src/main.rs +++ b/crates/ty/src/main.rs @@ -10,14 +10,14 @@ use anyhow::{anyhow, Context}; use clap::Parser; use colored::Colorize; use crossbeam::channel as crossbeam_channel; -use red_knot_project::metadata::options::Options; -use red_knot_project::watch::ProjectWatcher; -use red_knot_project::{watch, Db}; -use red_knot_project::{ProjectDatabase, ProjectMetadata}; -use red_knot_server::run_server; use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity}; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use salsa::plumbing::ZalsaDatabase; +use ty_project::metadata::options::Options; +use ty_project::watch::ProjectWatcher; +use ty_project::{watch, Db}; +use ty_project::{ProjectDatabase, ProjectMetadata}; +use ty_server::run_server; mod args; mod logging; @@ -32,9 +32,9 @@ pub fn main() -> ExitStatus { // Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken. let mut stderr = std::io::stderr().lock(); - // This communicates that this isn't a linter error but Red Knot itself hard-errored for + // This communicates that this isn't a linter error but ty itself hard-errored for // some reason (e.g. failed to resolve the configuration) - writeln!(stderr, "{}", "Red Knot failed".red().bold()).ok(); + writeln!(stderr, "{}", "ty failed".red().bold()).ok(); // Currently we generally only see one error, but e.g. with io errors when resolving // the configuration it is help to chain errors ("resolving configuration failed" -> // "failed to read file: subdir/pyproject.toml") @@ -71,7 +71,7 @@ fn run() -> anyhow::Result { pub(crate) fn version() -> Result<()> { let mut stdout = BufWriter::new(io::stdout().lock()); let version_info = crate::version::version(); - writeln!(stdout, "red knot {}", &version_info)?; + writeln!(stdout, "ty {}", &version_info)?; Ok(()) } @@ -90,7 +90,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result { SystemPathBuf::from_path_buf(cwd) .map_err(|path| { anyhow!( - "The current working directory `{}` contains non-Unicode characters. Red Knot only supports Unicode paths.", + "The current working directory `{}` contains non-Unicode characters. ty only supports Unicode paths.", path.display() ) })? @@ -172,7 +172,7 @@ pub enum ExitStatus { /// Checking failed due to an invocation error (e.g. the current directory no longer exists, incorrect CLI arguments, ...) Error = 2, - /// Internal Red Knot error (panic, or any other error that isn't due to the user using the + /// Internal ty error (panic, or any other error that isn't due to the user using the /// program incorrectly or transient environment errors). InternalError = 101, } diff --git a/crates/red_knot/src/python_version.rs b/crates/ty/src/python_version.rs similarity index 100% rename from crates/red_knot/src/python_version.rs rename to crates/ty/src/python_version.rs diff --git a/crates/red_knot/src/version.rs b/crates/ty/src/version.rs similarity index 79% rename from crates/red_knot/src/version.rs rename to crates/ty/src/version.rs index 35ccf05f650ba3..871fb78af2cb15 100644 --- a/crates/red_knot/src/version.rs +++ b/crates/ty/src/version.rs @@ -1,16 +1,16 @@ -//! Code for representing Red Knot's release version number. +//! Code for representing ty's release version number. use std::fmt; -/// Information about the git repository where Red Knot was built from. +/// Information about the git repository where ty was built from. pub(crate) struct CommitInfo { short_commit_hash: String, commit_date: String, commits_since_last_tag: u32, } -/// Red Knot's version. +/// ty's version. pub(crate) struct VersionInfo { - /// Red Knot's version, such as "0.5.1" + /// ty's version, such as "0.5.1" version: String, /// Information about the git commit we may have been built from. /// @@ -34,7 +34,7 @@ impl fmt::Display for VersionInfo { } } -/// Returns information about Red Knot's version. +/// Returns information about ty's version. pub(crate) fn version() -> VersionInfo { // Environment variables are only read at compile-time macro_rules! option_env_str { @@ -47,14 +47,13 @@ pub(crate) fn version() -> VersionInfo { let version = option_env_str!("CARGO_PKG_VERSION").unwrap(); // Commit info is pulled from git and set by `build.rs` - let commit_info = - option_env_str!("RED_KNOT_COMMIT_SHORT_HASH").map(|short_commit_hash| CommitInfo { - short_commit_hash, - commit_date: option_env_str!("RED_KNOT_COMMIT_DATE").unwrap(), - commits_since_last_tag: option_env_str!("RED_KNOT_LAST_TAG_DISTANCE") - .as_deref() - .map_or(0, |value| value.parse::().unwrap_or(0)), - }); + let commit_info = option_env_str!("TY_COMMIT_SHORT_HASH").map(|short_commit_hash| CommitInfo { + short_commit_hash, + commit_date: option_env_str!("TY_COMMIT_DATE").unwrap(), + commits_since_last_tag: option_env_str!("TY_LAST_TAG_DISTANCE") + .as_deref() + .map_or(0, |value| value.parse::().unwrap_or(0)), + }); VersionInfo { version, diff --git a/crates/red_knot/tests/cli.rs b/crates/ty/tests/cli.rs similarity index 97% rename from crates/red_knot/tests/cli.rs rename to crates/ty/tests/cli.rs index 4364b8f4973cdd..fd46ca323c8934 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/ty/tests/cli.rs @@ -79,7 +79,7 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { "); // Test that we can set to false via config file - case.write_file("knot.toml", "respect-ignore-files = false")?; + case.write_file("ty.toml", "respect-ignore-files = false")?; assert_cmd_snapshot!(case.command(), @r" success: false exit_code: 1 @@ -97,7 +97,7 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { "); // Ensure CLI takes precedence - case.write_file("knot.toml", "respect-ignore-files = true")?; + case.write_file("ty.toml", "respect-ignore-files = true")?; assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" success: false exit_code: 1 @@ -123,7 +123,7 @@ fn config_override_python_version() -> anyhow::Result<()> { ( "pyproject.toml", r#" - [tool.knot.environment] + [tool.ty.environment] python-version = "3.11" "#, ), @@ -174,7 +174,7 @@ fn config_override_python_platform() -> anyhow::Result<()> { ( "pyproject.toml", r#" - [tool.knot.environment] + [tool.ty.environment] python-platform = "linux" "#, ), @@ -249,7 +249,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> { ( "pyproject.toml", r#" - [tool.knot.environment] + [tool.ty.environment] python-version = "3.11" "#, ), @@ -320,7 +320,7 @@ fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Re ( "pyproject.toml", r#" - [tool.knot.environment] + [tool.ty.environment] python-version = "3.11" extra-paths = ["libs"] "#, @@ -401,7 +401,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> { case.write_file( "pyproject.toml", r#" - [tool.knot.rules] + [tool.ty.rules] division-by-zero = "warn" # demote to warn possibly-unresolved-reference = "ignore" "#, @@ -604,14 +604,14 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { Ok(()) } -/// Red Knot warns about unknown rules specified in a configuration file +/// ty warns about unknown rules specified in a configuration file #[test] fn configuration_unknown_rules() -> anyhow::Result<()> { let case = TestCase::with_files([ ( "pyproject.toml", r#" - [tool.knot.rules] + [tool.ty.rules] division-by-zer = "warn" # incorrect rule name "#, ), @@ -625,7 +625,7 @@ fn configuration_unknown_rules() -> anyhow::Result<()> { warning: unknown-rule --> pyproject.toml:3:1 | - 2 | [tool.knot.rules] + 2 | [tool.ty.rules] 3 | division-by-zer = "warn" # incorrect rule name | ^^^^^^^^^^^^^^^ Unknown lint rule `division-by-zer` | @@ -638,7 +638,7 @@ fn configuration_unknown_rules() -> anyhow::Result<()> { Ok(()) } -/// Red Knot warns about unknown rules specified in a CLI argument +/// ty warns about unknown rules specified in a CLI argument #[test] fn cli_unknown_rules() -> anyhow::Result<()> { let case = TestCase::with_file("test.py", "print(10)")?; @@ -768,7 +768,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any let case = TestCase::with_files([ ("test.py", r"print(x) # [unresolved-reference]"), ( - "knot.toml", + "ty.toml", r#" [terminal] error-on-warning = true @@ -913,7 +913,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> { fn user_configuration() -> anyhow::Result<()> { let case = TestCase::with_files([ ( - "project/knot.toml", + "project/ty.toml", r#" [rules] division-by-zero = "warn" @@ -973,7 +973,7 @@ fn user_configuration() -> anyhow::Result<()> { // Changing the level for `division-by-zero` has no effect, because the project-level configuration // has higher precedence. case.write_file( - config_directory.join("knot/knot.toml"), + config_directory.join("ty/ty.toml"), r#" [rules] division-by-zero = "error" @@ -1267,7 +1267,7 @@ impl TestCase { } fn command(&self) -> Command { - let mut command = Command::new(get_cargo_bin("red_knot")); + let mut command = Command::new(get_cargo_bin("ty")); command.current_dir(&self.project_dir).arg("check"); command } diff --git a/crates/red_knot/tests/file_watching.rs b/crates/ty/tests/file_watching.rs similarity index 98% rename from crates/red_knot/tests/file_watching.rs rename to crates/ty/tests/file_watching.rs index acdb7071c7fde8..c8205c925195b3 100644 --- a/crates/red_knot/tests/file_watching.rs +++ b/crates/ty/tests/file_watching.rs @@ -3,12 +3,6 @@ use std::io::Write; use std::time::{Duration, Instant}; use anyhow::{anyhow, Context}; -use red_knot_project::metadata::options::{EnvironmentOptions, Options}; -use red_knot_project::metadata::pyproject::{PyProject, Tool}; -use red_knot_project::metadata::value::{RangedValue, RelativePathBuf}; -use red_knot_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher}; -use red_knot_project::{Db, ProjectDatabase, ProjectMetadata}; -use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform}; use ruff_db::files::{system_path_to_file, File, FileError}; use ruff_db::source::source_text; use ruff_db::system::{ @@ -16,6 +10,12 @@ use ruff_db::system::{ }; use ruff_db::{Db as _, Upcast}; use ruff_python_ast::PythonVersion; +use ty_project::metadata::options::{EnvironmentOptions, Options}; +use ty_project::metadata::pyproject::{PyProject, Tool}; +use ty_project::metadata::value::{RangedValue, RelativePathBuf}; +use ty_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher}; +use ty_project::{Db, ProjectDatabase, ProjectMetadata}; +use ty_python_semantic::{resolve_module, ModuleName, PythonPlatform}; struct TestCase { db: ProjectDatabase, @@ -173,9 +173,7 @@ impl TestCase { self.project_path("pyproject.toml").as_std_path(), toml::to_string(&PyProject { project: None, - tool: Some(Tool { - knot: Some(options), - }), + tool: Some(Tool { ty: Some(options) }), }) .context("Failed to serialize options")?, ) @@ -382,9 +380,7 @@ where project_path.join("pyproject.toml").as_std_path(), toml::to_string(&PyProject { project: None, - tool: Some(Tool { - knot: Some(options), - }), + tool: Some(Tool { ty: Some(options) }), }) .context("Failed to serialize options")?, ) @@ -1546,7 +1542,7 @@ mod unix { // // I further tested how good editor support is for symlinked files and it is not good ;) // * VS Code doesn't update the file content if a file gets changed through a symlink - // * PyCharm doesn't update diagnostics if a symlinked module is changed (same as red knot). + // * PyCharm doesn't update diagnostics if a symlinked module is changed (same as ty). // // That's why I think it's fine to not support this case for now. @@ -1658,7 +1654,7 @@ mod unix { // It would be nice if this is supported but the underlying file system watchers // only emit a single event. For reference // * VS Code doesn't update the file content if a file gets changed through a symlink - // * PyCharm doesn't update diagnostics if a symlinked module is changed (same as red knot). + // * PyCharm doesn't update diagnostics if a symlinked module is changed (same as ty). // We could add support for it by keeping a reverse map from `real_path` to symlinked path but // it doesn't seem worth doing considering that as prominent tools like PyCharm don't support it. // Pyright does support it, thanks to chokidar. @@ -1682,7 +1678,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> { [project] name = "inner" - [tool.knot] + [tool.ty] "#, )?; @@ -1692,7 +1688,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> { [project] name = "outer" - [tool.knot] + [tool.ty] "#, )?; @@ -1732,9 +1728,9 @@ fn changes_to_user_configuration() -> anyhow::Result<()> { )?; let config_directory = context.join_root_path("home/.config"); - std::fs::create_dir_all(config_directory.join("knot").as_std_path())?; + std::fs::create_dir_all(config_directory.join("ty").as_std_path())?; std::fs::write( - config_directory.join("knot/knot.toml").as_std_path(), + config_directory.join("ty/ty.toml").as_std_path(), r#" [rules] division-by-zero = "ignore" @@ -1765,14 +1761,14 @@ fn changes_to_user_configuration() -> anyhow::Result<()> { // Enable division-by-zero in the user configuration with warning severity update_file( - case.root_path().join("home/.config/knot/knot.toml"), + case.root_path().join("home/.config/ty/ty.toml"), r#" [rules] division-by-zero = "warn" "#, )?; - let changes = case.stop_watch(event_for_file("knot.toml")); + let changes = case.stop_watch(event_for_file("ty.toml")); case.apply_changes(changes); @@ -1793,7 +1789,7 @@ fn changes_to_user_configuration() -> anyhow::Result<()> { /// /// This test currently fails on case-insensitive systems because `Files` is case-sensitive /// but the `System::metadata` call isn't. This means that -/// Red Knot considers both `Lib.py` and `lib.py` to exist when only `lib.py` does +/// ty considers both `Lib.py` and `lib.py` to exist when only `lib.py` does /// /// The incoming change events then are no-ops because they don't change either file's /// status nor does it update their last modified time (renaming a file doesn't bump it's @@ -1805,7 +1801,7 @@ fn changes_to_user_configuration() -> anyhow::Result<()> { /// `System` calls should be case sensitive. This would be the most consistent /// but might be hard to pull off. /// -/// What the right solution is also depends on if Red Knot itself should be case +/// What the right solution is also depends on if ty itself should be case /// sensitive or not. E.g. should `include="src"` be case sensitive on all systems /// or only on case-sensitive systems? /// diff --git a/crates/red_knot_ide/Cargo.toml b/crates/ty_ide/Cargo.toml similarity index 85% rename from crates/red_knot_ide/Cargo.toml rename to crates/ty_ide/Cargo.toml index 5ac625385299dc..52a585bd80c42f 100644 --- a/crates/red_knot_ide/Cargo.toml +++ b/crates/ty_ide/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "red_knot_ide" +name = "ty_ide" version = "0.0.0" publish = false authors = { workspace = true } @@ -15,7 +15,7 @@ ruff_db = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_parser = { workspace = true } ruff_text_size = { workspace = true } -red_knot_python_semantic = { workspace = true } +ty_python_semantic = { workspace = true } rustc-hash = { workspace = true } salsa = { workspace = true } @@ -23,7 +23,7 @@ smallvec = { workspace = true } tracing = { workspace = true } [dev-dependencies] -red_knot_vendored = { workspace = true } +ty_vendored = { workspace = true } insta = { workspace = true, features = ["filters"] } diff --git a/crates/red_knot_ide/src/completion.rs b/crates/ty_ide/src/completion.rs similarity index 100% rename from crates/red_knot_ide/src/completion.rs rename to crates/ty_ide/src/completion.rs diff --git a/crates/red_knot_ide/src/db.rs b/crates/ty_ide/src/db.rs similarity index 92% rename from crates/red_knot_ide/src/db.rs rename to crates/ty_ide/src/db.rs index 9d98db1dcb663b..39ff4b0c043be6 100644 --- a/crates/red_knot_ide/src/db.rs +++ b/crates/ty_ide/src/db.rs @@ -1,5 +1,5 @@ -use red_knot_python_semantic::Db as SemanticDb; use ruff_db::{Db as SourceDb, Upcast}; +use ty_python_semantic::Db as SemanticDb; #[salsa::db] pub trait Db: SemanticDb + Upcast + Upcast {} @@ -9,12 +9,12 @@ pub(crate) mod tests { use std::sync::Arc; use super::Db; - use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; - 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; use ruff_db::{Db as SourceDb, Upcast}; + use ty_python_semantic::lint::{LintRegistry, RuleSelection}; + use ty_python_semantic::{default_lint_registry, Db as SemanticDb, Program}; #[salsa::db] #[derive(Clone)] @@ -33,7 +33,7 @@ pub(crate) mod tests { Self { storage: salsa::Storage::default(), system: TestSystem::default(), - vendored: red_knot_vendored::file_system().clone(), + vendored: ty_vendored::file_system().clone(), events: Arc::default(), files: Files::default(), rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())), diff --git a/crates/red_knot_ide/src/find_node.rs b/crates/ty_ide/src/find_node.rs similarity index 100% rename from crates/red_knot_ide/src/find_node.rs rename to crates/ty_ide/src/find_node.rs diff --git a/crates/red_knot_ide/src/goto.rs b/crates/ty_ide/src/goto.rs similarity index 99% rename from crates/red_knot_ide/src/goto.rs rename to crates/ty_ide/src/goto.rs index a2804bac0b51bb..33fb78a3df1087 100644 --- a/crates/red_knot_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -1,12 +1,12 @@ use crate::find_node::covering_node; use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue}; -use red_knot_python_semantic::types::Type; -use red_knot_python_semantic::{HasType, SemanticModel}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{parsed_module, ParsedModule}; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_parser::TokenKind; use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_python_semantic::types::Type; +use ty_python_semantic::{HasType, SemanticModel}; pub fn goto_type_definition( db: &dyn Db, diff --git a/crates/red_knot_ide/src/hover.rs b/crates/ty_ide/src/hover.rs similarity index 99% rename from crates/red_knot_ide/src/hover.rs rename to crates/ty_ide/src/hover.rs index 700f8276e119c4..df00a0ccb8e419 100644 --- a/crates/red_knot_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -1,12 +1,12 @@ use crate::goto::{find_goto_target, GotoTarget}; use crate::{Db, MarkupKind, RangedValue}; -use red_knot_python_semantic::types::Type; -use red_knot_python_semantic::SemanticModel; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; use std::fmt; use std::fmt::Formatter; +use ty_python_semantic::types::Type; +use ty_python_semantic::SemanticModel; pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option> { let parsed = parsed_module(db.upcast(), file); diff --git a/crates/red_knot_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs similarity index 98% rename from crates/red_knot_ide/src/inlay_hints.rs rename to crates/ty_ide/src/inlay_hints.rs index b282b0edfc1dc0..f3fc12accaa663 100644 --- a/crates/red_knot_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -1,6 +1,4 @@ use crate::Db; -use red_knot_python_semantic::types::Type; -use red_knot_python_semantic::{HasType, SemanticModel}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; @@ -8,6 +6,8 @@ use ruff_python_ast::{AnyNodeRef, Expr, Stmt}; use ruff_text_size::{Ranged, TextRange, TextSize}; use std::fmt; use std::fmt::Formatter; +use ty_python_semantic::types::Type; +use ty_python_semantic::{HasType, SemanticModel}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct InlayHint<'db> { @@ -157,11 +157,11 @@ mod tests { use crate::db::tests::TestDb; - use red_knot_python_semantic::{ - Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, - }; use ruff_db::system::{DbWithWritableSystem, SystemPathBuf}; use ruff_python_ast::PythonVersion; + use ty_python_semantic::{ + Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, + }; pub(super) fn inlay_hint_test(source: &str) -> InlayHintTest { const START: &str = ""; diff --git a/crates/red_knot_ide/src/lib.rs b/crates/ty_ide/src/lib.rs similarity index 98% rename from crates/red_knot_ide/src/lib.rs rename to crates/ty_ide/src/lib.rs index a85c0cd1fee4bf..4532b700c3435c 100644 --- a/crates/red_knot_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -16,9 +16,9 @@ pub use markup::MarkupKind; use rustc_hash::FxHashSet; use std::ops::{Deref, DerefMut}; -use red_knot_python_semantic::types::{Type, TypeDefinition}; use ruff_db::files::{File, FileRange}; use ruff_text_size::{Ranged, TextRange}; +use ty_python_semantic::types::{Type, TypeDefinition}; /// Information associated with a text range. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -201,14 +201,14 @@ impl HasNavigationTargets for TypeDefinition<'_> { mod tests { use crate::db::tests::TestDb; use insta::internals::SettingsBindDropGuard; - use red_knot_python_semantic::{ - Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, - }; use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig}; use ruff_db::files::{system_path_to_file, File}; use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf}; use ruff_python_ast::PythonVersion; use ruff_text_size::TextSize; + use ty_python_semantic::{ + Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, + }; pub(super) fn cursor_test(source: &str) -> CursorTest { let mut db = TestDb::new(); diff --git a/crates/red_knot_ide/src/markup.rs b/crates/ty_ide/src/markup.rs similarity index 100% rename from crates/red_knot_ide/src/markup.rs rename to crates/ty_ide/src/markup.rs diff --git a/crates/red_knot_project/Cargo.toml b/crates/ty_project/Cargo.toml similarity index 82% rename from crates/red_knot_project/Cargo.toml rename to crates/ty_project/Cargo.toml index f1659c9f33dbd0..c05c3a4a4f5d7d 100644 --- a/crates/red_knot_project/Cargo.toml +++ b/crates/ty_project/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "red_knot_project" +name = "ty_project" version = "0.0.0" edition.workspace = true rust-version.workspace = true @@ -18,9 +18,9 @@ ruff_macros = { workspace = true } ruff_python_ast = { workspace = true, features = ["serde"] } ruff_python_formatter = { workspace = true, optional = true } ruff_text_size = { workspace = true } -red_knot_ide = { workspace = true } -red_knot_python_semantic = { workspace = true, features = ["serde"] } -red_knot_vendored = { workspace = true } +ty_ide = { workspace = true } +ty_python_semantic = { workspace = true, features = ["serde"] } +ty_vendored = { workspace = true } anyhow = { workspace = true } crossbeam = { workspace = true } @@ -43,13 +43,13 @@ insta = { workspace = true, features = ["redactions", "ron"] } [features] default = ["zstd"] -deflate = ["red_knot_vendored/deflate"] +deflate = ["ty_vendored/deflate"] schemars = [ "dep:schemars", "ruff_db/schemars", - "red_knot_python_semantic/schemars", + "ty_python_semantic/schemars", ] -zstd = ["red_knot_vendored/zstd"] +zstd = ["ty_vendored/zstd"] format = ["ruff_python_formatter"] [lints] diff --git a/crates/red_knot_project/resources/test/corpus/00_const.py b/crates/ty_project/resources/test/corpus/00_const.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/00_const.py rename to crates/ty_project/resources/test/corpus/00_const.py diff --git a/crates/red_knot_project/resources/test/corpus/00_empty.py b/crates/ty_project/resources/test/corpus/00_empty.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/00_empty.py rename to crates/ty_project/resources/test/corpus/00_empty.py diff --git a/crates/red_knot_project/resources/test/corpus/00_expr_discard.py b/crates/ty_project/resources/test/corpus/00_expr_discard.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/00_expr_discard.py rename to crates/ty_project/resources/test/corpus/00_expr_discard.py diff --git a/crates/red_knot_project/resources/test/corpus/00_expr_var1.py b/crates/ty_project/resources/test/corpus/00_expr_var1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/00_expr_var1.py rename to crates/ty_project/resources/test/corpus/00_expr_var1.py diff --git a/crates/red_knot_project/resources/test/corpus/01_expr_unary.py b/crates/ty_project/resources/test/corpus/01_expr_unary.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/01_expr_unary.py rename to crates/ty_project/resources/test/corpus/01_expr_unary.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_attr.py b/crates/ty_project/resources/test/corpus/02_expr_attr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_attr.py rename to crates/ty_project/resources/test/corpus/02_expr_attr.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_attr_multiline.py b/crates/ty_project/resources/test/corpus/02_expr_attr_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_attr_multiline.py rename to crates/ty_project/resources/test/corpus/02_expr_attr_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_attr_multiline_assign.py b/crates/ty_project/resources/test/corpus/02_expr_attr_multiline_assign.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_attr_multiline_assign.py rename to crates/ty_project/resources/test/corpus/02_expr_attr_multiline_assign.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_bin_bool.py b/crates/ty_project/resources/test/corpus/02_expr_bin_bool.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_bin_bool.py rename to crates/ty_project/resources/test/corpus/02_expr_bin_bool.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_binary.py b/crates/ty_project/resources/test/corpus/02_expr_binary.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_binary.py rename to crates/ty_project/resources/test/corpus/02_expr_binary.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_bool_op_multiline.py b/crates/ty_project/resources/test/corpus/02_expr_bool_op_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_bool_op_multiline.py rename to crates/ty_project/resources/test/corpus/02_expr_bool_op_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_bool_op_multiline2.py b/crates/ty_project/resources/test/corpus/02_expr_bool_op_multiline2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_bool_op_multiline2.py rename to crates/ty_project/resources/test/corpus/02_expr_bool_op_multiline2.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_rel.py b/crates/ty_project/resources/test/corpus/02_expr_rel.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_rel.py rename to crates/ty_project/resources/test/corpus/02_expr_rel.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_rel_multiple.py b/crates/ty_project/resources/test/corpus/02_expr_rel_multiple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_rel_multiple.py rename to crates/ty_project/resources/test/corpus/02_expr_rel_multiple.py diff --git a/crates/red_knot_project/resources/test/corpus/02_expr_subscr.py b/crates/ty_project/resources/test/corpus/02_expr_subscr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/02_expr_subscr.py rename to crates/ty_project/resources/test/corpus/02_expr_subscr.py diff --git a/crates/red_knot_project/resources/test/corpus/03_dict.py b/crates/ty_project/resources/test/corpus/03_dict.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_dict.py rename to crates/ty_project/resources/test/corpus/03_dict.py diff --git a/crates/red_knot_project/resources/test/corpus/03_dict_ex.py b/crates/ty_project/resources/test/corpus/03_dict_ex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_dict_ex.py rename to crates/ty_project/resources/test/corpus/03_dict_ex.py diff --git a/crates/red_knot_project/resources/test/corpus/03_dict_literal_large.py b/crates/ty_project/resources/test/corpus/03_dict_literal_large.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_dict_literal_large.py rename to crates/ty_project/resources/test/corpus/03_dict_literal_large.py diff --git a/crates/red_knot_project/resources/test/corpus/03_dict_unpack_huge.py b/crates/ty_project/resources/test/corpus/03_dict_unpack_huge.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_dict_unpack_huge.py rename to crates/ty_project/resources/test/corpus/03_dict_unpack_huge.py diff --git a/crates/red_knot_project/resources/test/corpus/03_list.py b/crates/ty_project/resources/test/corpus/03_list.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_list.py rename to crates/ty_project/resources/test/corpus/03_list.py diff --git a/crates/red_knot_project/resources/test/corpus/03_list_ex.py b/crates/ty_project/resources/test/corpus/03_list_ex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_list_ex.py rename to crates/ty_project/resources/test/corpus/03_list_ex.py diff --git a/crates/red_knot_project/resources/test/corpus/03_list_large.py b/crates/ty_project/resources/test/corpus/03_list_large.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_list_large.py rename to crates/ty_project/resources/test/corpus/03_list_large.py diff --git a/crates/red_knot_project/resources/test/corpus/03_set.py b/crates/ty_project/resources/test/corpus/03_set.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_set.py rename to crates/ty_project/resources/test/corpus/03_set.py diff --git a/crates/red_knot_project/resources/test/corpus/03_set_multi.py b/crates/ty_project/resources/test/corpus/03_set_multi.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_set_multi.py rename to crates/ty_project/resources/test/corpus/03_set_multi.py diff --git a/crates/red_knot_project/resources/test/corpus/03_slice.py b/crates/ty_project/resources/test/corpus/03_slice.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_slice.py rename to crates/ty_project/resources/test/corpus/03_slice.py diff --git a/crates/red_knot_project/resources/test/corpus/03_slice_ext.py b/crates/ty_project/resources/test/corpus/03_slice_ext.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_slice_ext.py rename to crates/ty_project/resources/test/corpus/03_slice_ext.py diff --git a/crates/red_knot_project/resources/test/corpus/03_tuple.py b/crates/ty_project/resources/test/corpus/03_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_tuple.py rename to crates/ty_project/resources/test/corpus/03_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/03_tuple_ex.py b/crates/ty_project/resources/test/corpus/03_tuple_ex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/03_tuple_ex.py rename to crates/ty_project/resources/test/corpus/03_tuple_ex.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign.py b/crates/ty_project/resources/test/corpus/04_assign.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign.py rename to crates/ty_project/resources/test/corpus/04_assign.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_attr.py b/crates/ty_project/resources/test/corpus/04_assign_attr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_attr.py rename to crates/ty_project/resources/test/corpus/04_assign_attr.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_attr_func.py b/crates/ty_project/resources/test/corpus/04_assign_attr_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_attr_func.py rename to crates/ty_project/resources/test/corpus/04_assign_attr_func.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_invalid_target.py b/crates/ty_project/resources/test/corpus/04_assign_invalid_target.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_invalid_target.py rename to crates/ty_project/resources/test/corpus/04_assign_invalid_target.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_named_expr.py b/crates/ty_project/resources/test/corpus/04_assign_named_expr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_named_expr.py rename to crates/ty_project/resources/test/corpus/04_assign_named_expr.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_named_expr_invalid_target.py b/crates/ty_project/resources/test/corpus/04_assign_named_expr_invalid_target.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_named_expr_invalid_target.py rename to crates/ty_project/resources/test/corpus/04_assign_named_expr_invalid_target.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_subscr.py b/crates/ty_project/resources/test/corpus/04_assign_subscr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_subscr.py rename to crates/ty_project/resources/test/corpus/04_assign_subscr.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_unpack.py b/crates/ty_project/resources/test/corpus/04_assign_unpack.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_unpack.py rename to crates/ty_project/resources/test/corpus/04_assign_unpack.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_unpack_ex.py b/crates/ty_project/resources/test/corpus/04_assign_unpack_ex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_unpack_ex.py rename to crates/ty_project/resources/test/corpus/04_assign_unpack_ex.py diff --git a/crates/red_knot_project/resources/test/corpus/04_assign_unpack_tuple.py b/crates/ty_project/resources/test/corpus/04_assign_unpack_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_assign_unpack_tuple.py rename to crates/ty_project/resources/test/corpus/04_assign_unpack_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/04_aug_assign.py b/crates/ty_project/resources/test/corpus/04_aug_assign.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_aug_assign.py rename to crates/ty_project/resources/test/corpus/04_aug_assign.py diff --git a/crates/red_knot_project/resources/test/corpus/04_aug_assign_attr_multiline.py b/crates/ty_project/resources/test/corpus/04_aug_assign_attr_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_aug_assign_attr_multiline.py rename to crates/ty_project/resources/test/corpus/04_aug_assign_attr_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/04_aug_assign_attr_sub.py b/crates/ty_project/resources/test/corpus/04_aug_assign_attr_sub.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_aug_assign_attr_sub.py rename to crates/ty_project/resources/test/corpus/04_aug_assign_attr_sub.py diff --git a/crates/red_knot_project/resources/test/corpus/04_aug_assign_invalid_target.py b/crates/ty_project/resources/test/corpus/04_aug_assign_invalid_target.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/04_aug_assign_invalid_target.py rename to crates/ty_project/resources/test/corpus/04_aug_assign_invalid_target.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall.py b/crates/ty_project/resources/test/corpus/05_funcall.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall.py rename to crates/ty_project/resources/test/corpus/05_funcall.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_1.py b/crates/ty_project/resources/test/corpus/05_funcall_1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_1.py rename to crates/ty_project/resources/test/corpus/05_funcall_1.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_2.py b/crates/ty_project/resources/test/corpus/05_funcall_2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_2.py rename to crates/ty_project/resources/test/corpus/05_funcall_2.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_in_multiline_tuple.py b/crates/ty_project/resources/test/corpus/05_funcall_in_multiline_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_in_multiline_tuple.py rename to crates/ty_project/resources/test/corpus/05_funcall_in_multiline_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_kw.py b/crates/ty_project/resources/test/corpus/05_funcall_kw.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_kw.py rename to crates/ty_project/resources/test/corpus/05_funcall_kw.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_kw_many.py b/crates/ty_project/resources/test/corpus/05_funcall_kw_many.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_kw_many.py rename to crates/ty_project/resources/test/corpus/05_funcall_kw_many.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_kw_pos.py b/crates/ty_project/resources/test/corpus/05_funcall_kw_pos.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_kw_pos.py rename to crates/ty_project/resources/test/corpus/05_funcall_kw_pos.py diff --git a/crates/red_knot_project/resources/test/corpus/05_funcall_method_multiline.py b/crates/ty_project/resources/test/corpus/05_funcall_method_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/05_funcall_method_multiline.py rename to crates/ty_project/resources/test/corpus/05_funcall_method_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_kwargs.py b/crates/ty_project/resources/test/corpus/06_funcall_kwargs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_kwargs.py rename to crates/ty_project/resources/test/corpus/06_funcall_kwargs.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_many_args.py b/crates/ty_project/resources/test/corpus/06_funcall_many_args.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_many_args.py rename to crates/ty_project/resources/test/corpus/06_funcall_many_args.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_starargs_ex.py b/crates/ty_project/resources/test/corpus/06_funcall_starargs_ex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_starargs_ex.py rename to crates/ty_project/resources/test/corpus/06_funcall_starargs_ex.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_varargs.py b/crates/ty_project/resources/test/corpus/06_funcall_varargs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_varargs.py rename to crates/ty_project/resources/test/corpus/06_funcall_varargs.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_varargs_kwargs.py b/crates/ty_project/resources/test/corpus/06_funcall_varargs_kwargs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_varargs_kwargs.py rename to crates/ty_project/resources/test/corpus/06_funcall_varargs_kwargs.py diff --git a/crates/red_knot_project/resources/test/corpus/06_funcall_varargs_kwargs_mixed.py b/crates/ty_project/resources/test/corpus/06_funcall_varargs_kwargs_mixed.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/06_funcall_varargs_kwargs_mixed.py rename to crates/ty_project/resources/test/corpus/06_funcall_varargs_kwargs_mixed.py diff --git a/crates/red_knot_project/resources/test/corpus/07_ifexpr.py b/crates/ty_project/resources/test/corpus/07_ifexpr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/07_ifexpr.py rename to crates/ty_project/resources/test/corpus/07_ifexpr.py diff --git a/crates/red_knot_project/resources/test/corpus/07_ifexpr_multiline.py b/crates/ty_project/resources/test/corpus/07_ifexpr_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/07_ifexpr_multiline.py rename to crates/ty_project/resources/test/corpus/07_ifexpr_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/07_ifexpr_multiline2.py b/crates/ty_project/resources/test/corpus/07_ifexpr_multiline2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/07_ifexpr_multiline2.py rename to crates/ty_project/resources/test/corpus/07_ifexpr_multiline2.py diff --git a/crates/red_knot_project/resources/test/corpus/08_del.py b/crates/ty_project/resources/test/corpus/08_del.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/08_del.py rename to crates/ty_project/resources/test/corpus/08_del.py diff --git a/crates/red_knot_project/resources/test/corpus/08_del_multi.py b/crates/ty_project/resources/test/corpus/08_del_multi.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/08_del_multi.py rename to crates/ty_project/resources/test/corpus/08_del_multi.py diff --git a/crates/red_knot_project/resources/test/corpus/09_pass.py b/crates/ty_project/resources/test/corpus/09_pass.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/09_pass.py rename to crates/ty_project/resources/test/corpus/09_pass.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if.py b/crates/ty_project/resources/test/corpus/10_if.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if.py rename to crates/ty_project/resources/test/corpus/10_if.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if_chained_compare.py b/crates/ty_project/resources/test/corpus/10_if_chained_compare.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if_chained_compare.py rename to crates/ty_project/resources/test/corpus/10_if_chained_compare.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if_false.py b/crates/ty_project/resources/test/corpus/10_if_false.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if_false.py rename to crates/ty_project/resources/test/corpus/10_if_false.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if_invalid.py b/crates/ty_project/resources/test/corpus/10_if_invalid.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if_invalid.py rename to crates/ty_project/resources/test/corpus/10_if_invalid.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if_true.py b/crates/ty_project/resources/test/corpus/10_if_true.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if_true.py rename to crates/ty_project/resources/test/corpus/10_if_true.py diff --git a/crates/red_knot_project/resources/test/corpus/10_if_with_named_expr.py b/crates/ty_project/resources/test/corpus/10_if_with_named_expr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/10_if_with_named_expr.py rename to crates/ty_project/resources/test/corpus/10_if_with_named_expr.py diff --git a/crates/red_knot_project/resources/test/corpus/11_if_else.py b/crates/ty_project/resources/test/corpus/11_if_else.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/11_if_else.py rename to crates/ty_project/resources/test/corpus/11_if_else.py diff --git a/crates/red_knot_project/resources/test/corpus/11_if_else_deeply_nested_for.py b/crates/ty_project/resources/test/corpus/11_if_else_deeply_nested_for.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/11_if_else_deeply_nested_for.py rename to crates/ty_project/resources/test/corpus/11_if_else_deeply_nested_for.py diff --git a/crates/red_knot_project/resources/test/corpus/11_if_else_false.py b/crates/ty_project/resources/test/corpus/11_if_else_false.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/11_if_else_false.py rename to crates/ty_project/resources/test/corpus/11_if_else_false.py diff --git a/crates/red_knot_project/resources/test/corpus/11_if_else_true.py b/crates/ty_project/resources/test/corpus/11_if_else_true.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/11_if_else_true.py rename to crates/ty_project/resources/test/corpus/11_if_else_true.py diff --git a/crates/red_knot_project/resources/test/corpus/12_if_elif.py b/crates/ty_project/resources/test/corpus/12_if_elif.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/12_if_elif.py rename to crates/ty_project/resources/test/corpus/12_if_elif.py diff --git a/crates/red_knot_project/resources/test/corpus/12_if_elif_else.py b/crates/ty_project/resources/test/corpus/12_if_elif_else.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/12_if_elif_else.py rename to crates/ty_project/resources/test/corpus/12_if_elif_else.py diff --git a/crates/red_knot_project/resources/test/corpus/13_ifelse_complex1.py b/crates/ty_project/resources/test/corpus/13_ifelse_complex1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/13_ifelse_complex1.py rename to crates/ty_project/resources/test/corpus/13_ifelse_complex1.py diff --git a/crates/red_knot_project/resources/test/corpus/13_ifelse_many.py b/crates/ty_project/resources/test/corpus/13_ifelse_many.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/13_ifelse_many.py rename to crates/ty_project/resources/test/corpus/13_ifelse_many.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while.py b/crates/ty_project/resources/test/corpus/15_while.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while.py rename to crates/ty_project/resources/test/corpus/15_while.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break.py b/crates/ty_project/resources/test/corpus/15_while_break.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break.py rename to crates/ty_project/resources/test/corpus/15_while_break.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break_in_finally.py b/crates/ty_project/resources/test/corpus/15_while_break_in_finally.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break_in_finally.py rename to crates/ty_project/resources/test/corpus/15_while_break_in_finally.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break_invalid_in_class.py b/crates/ty_project/resources/test/corpus/15_while_break_invalid_in_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break_invalid_in_class.py rename to crates/ty_project/resources/test/corpus/15_while_break_invalid_in_class.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break_invalid_in_func.py b/crates/ty_project/resources/test/corpus/15_while_break_invalid_in_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break_invalid_in_func.py rename to crates/ty_project/resources/test/corpus/15_while_break_invalid_in_func.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break_non_empty.py b/crates/ty_project/resources/test/corpus/15_while_break_non_empty.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break_non_empty.py rename to crates/ty_project/resources/test/corpus/15_while_break_non_empty.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_break_non_exit.py b/crates/ty_project/resources/test/corpus/15_while_break_non_exit.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_break_non_exit.py rename to crates/ty_project/resources/test/corpus/15_while_break_non_exit.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_continue.py b/crates/ty_project/resources/test/corpus/15_while_continue.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_continue.py rename to crates/ty_project/resources/test/corpus/15_while_continue.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_false.py b/crates/ty_project/resources/test/corpus/15_while_false.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_false.py rename to crates/ty_project/resources/test/corpus/15_while_false.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_infinite.py b/crates/ty_project/resources/test/corpus/15_while_infinite.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_infinite.py rename to crates/ty_project/resources/test/corpus/15_while_infinite.py diff --git a/crates/red_knot_project/resources/test/corpus/15_while_true.py b/crates/ty_project/resources/test/corpus/15_while_true.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/15_while_true.py rename to crates/ty_project/resources/test/corpus/15_while_true.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for.py b/crates/ty_project/resources/test/corpus/16_for.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for.py rename to crates/ty_project/resources/test/corpus/16_for.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_break.py b/crates/ty_project/resources/test/corpus/16_for_break.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_break.py rename to crates/ty_project/resources/test/corpus/16_for_break.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_break_invalid_in_class.py b/crates/ty_project/resources/test/corpus/16_for_break_invalid_in_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_break_invalid_in_class.py rename to crates/ty_project/resources/test/corpus/16_for_break_invalid_in_class.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_break_invalid_in_func.py b/crates/ty_project/resources/test/corpus/16_for_break_invalid_in_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_break_invalid_in_func.py rename to crates/ty_project/resources/test/corpus/16_for_break_invalid_in_func.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_continue.py b/crates/ty_project/resources/test/corpus/16_for_continue.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_continue.py rename to crates/ty_project/resources/test/corpus/16_for_continue.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_else.py b/crates/ty_project/resources/test/corpus/16_for_else.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_else.py rename to crates/ty_project/resources/test/corpus/16_for_else.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_invalid.py b/crates/ty_project/resources/test/corpus/16_for_invalid.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_invalid.py rename to crates/ty_project/resources/test/corpus/16_for_invalid.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_list_literal.py b/crates/ty_project/resources/test/corpus/16_for_list_literal.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_list_literal.py rename to crates/ty_project/resources/test/corpus/16_for_list_literal.py diff --git a/crates/red_knot_project/resources/test/corpus/16_for_nested_ifs.py b/crates/ty_project/resources/test/corpus/16_for_nested_ifs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/16_for_nested_ifs.py rename to crates/ty_project/resources/test/corpus/16_for_nested_ifs.py diff --git a/crates/red_knot_project/resources/test/corpus/20_lambda.py b/crates/ty_project/resources/test/corpus/20_lambda.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/20_lambda.py rename to crates/ty_project/resources/test/corpus/20_lambda.py diff --git a/crates/red_knot_project/resources/test/corpus/20_lambda_const.py b/crates/ty_project/resources/test/corpus/20_lambda_const.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/20_lambda_const.py rename to crates/ty_project/resources/test/corpus/20_lambda_const.py diff --git a/crates/red_knot_project/resources/test/corpus/20_lambda_default_arg.py b/crates/ty_project/resources/test/corpus/20_lambda_default_arg.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/20_lambda_default_arg.py rename to crates/ty_project/resources/test/corpus/20_lambda_default_arg.py diff --git a/crates/red_knot_project/resources/test/corpus/20_lambda_ifelse.py b/crates/ty_project/resources/test/corpus/20_lambda_ifelse.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/20_lambda_ifelse.py rename to crates/ty_project/resources/test/corpus/20_lambda_ifelse.py diff --git a/crates/red_knot_project/resources/test/corpus/21_func1.py b/crates/ty_project/resources/test/corpus/21_func1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/21_func1.py rename to crates/ty_project/resources/test/corpus/21_func1.py diff --git a/crates/red_knot_project/resources/test/corpus/21_func1_ret.py b/crates/ty_project/resources/test/corpus/21_func1_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/21_func1_ret.py rename to crates/ty_project/resources/test/corpus/21_func1_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/21_func_assign.py b/crates/ty_project/resources/test/corpus/21_func_assign.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/21_func_assign.py rename to crates/ty_project/resources/test/corpus/21_func_assign.py diff --git a/crates/red_knot_project/resources/test/corpus/21_func_assign2.py b/crates/ty_project/resources/test/corpus/21_func_assign2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/21_func_assign2.py rename to crates/ty_project/resources/test/corpus/21_func_assign2.py diff --git a/crates/red_knot_project/resources/test/corpus/22_func_arg.py b/crates/ty_project/resources/test/corpus/22_func_arg.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/22_func_arg.py rename to crates/ty_project/resources/test/corpus/22_func_arg.py diff --git a/crates/red_knot_project/resources/test/corpus/22_func_vararg.py b/crates/ty_project/resources/test/corpus/22_func_vararg.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/22_func_vararg.py rename to crates/ty_project/resources/test/corpus/22_func_vararg.py diff --git a/crates/red_knot_project/resources/test/corpus/23_func_ret.py b/crates/ty_project/resources/test/corpus/23_func_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/23_func_ret.py rename to crates/ty_project/resources/test/corpus/23_func_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/23_func_ret_val.py b/crates/ty_project/resources/test/corpus/23_func_ret_val.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/23_func_ret_val.py rename to crates/ty_project/resources/test/corpus/23_func_ret_val.py diff --git a/crates/red_knot_project/resources/test/corpus/24_func_if_ret.py b/crates/ty_project/resources/test/corpus/24_func_if_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/24_func_if_ret.py rename to crates/ty_project/resources/test/corpus/24_func_if_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/24_func_ifelse_ret.py b/crates/ty_project/resources/test/corpus/24_func_ifelse_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/24_func_ifelse_ret.py rename to crates/ty_project/resources/test/corpus/24_func_ifelse_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/24_func_ifnot_ret.py b/crates/ty_project/resources/test/corpus/24_func_ifnot_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/24_func_ifnot_ret.py rename to crates/ty_project/resources/test/corpus/24_func_ifnot_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/25_func_annotations.py b/crates/ty_project/resources/test/corpus/25_func_annotations.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/25_func_annotations.py rename to crates/ty_project/resources/test/corpus/25_func_annotations.py diff --git a/crates/red_knot_project/resources/test/corpus/25_func_annotations_nested.py b/crates/ty_project/resources/test/corpus/25_func_annotations_nested.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/25_func_annotations_nested.py rename to crates/ty_project/resources/test/corpus/25_func_annotations_nested.py diff --git a/crates/red_knot_project/resources/test/corpus/25_func_annotations_same_name.py b/crates/ty_project/resources/test/corpus/25_func_annotations_same_name.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/25_func_annotations_same_name.py rename to crates/ty_project/resources/test/corpus/25_func_annotations_same_name.py diff --git a/crates/red_knot_project/resources/test/corpus/25_func_annotations_scope.py b/crates/ty_project/resources/test/corpus/25_func_annotations_scope.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/25_func_annotations_scope.py rename to crates/ty_project/resources/test/corpus/25_func_annotations_scope.py diff --git a/crates/red_knot_project/resources/test/corpus/25_func_annotations_starred.py b/crates/ty_project/resources/test/corpus/25_func_annotations_starred.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/25_func_annotations_starred.py rename to crates/ty_project/resources/test/corpus/25_func_annotations_starred.py diff --git a/crates/red_knot_project/resources/test/corpus/26_func_const_defaults.py b/crates/ty_project/resources/test/corpus/26_func_const_defaults.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/26_func_const_defaults.py rename to crates/ty_project/resources/test/corpus/26_func_const_defaults.py diff --git a/crates/red_knot_project/resources/test/corpus/26_func_defaults_same_name.py b/crates/ty_project/resources/test/corpus/26_func_defaults_same_name.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/26_func_defaults_same_name.py rename to crates/ty_project/resources/test/corpus/26_func_defaults_same_name.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic.py b/crates/ty_project/resources/test/corpus/27_func_generic.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic.py rename to crates/ty_project/resources/test/corpus/27_func_generic.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_bound.py b/crates/ty_project/resources/test/corpus/27_func_generic_bound.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_bound.py rename to crates/ty_project/resources/test/corpus/27_func_generic_bound.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_constraint.py b/crates/ty_project/resources/test/corpus/27_func_generic_constraint.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_constraint.py rename to crates/ty_project/resources/test/corpus/27_func_generic_constraint.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_default.py b/crates/ty_project/resources/test/corpus/27_func_generic_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_default.py rename to crates/ty_project/resources/test/corpus/27_func_generic_default.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_paramspec.py b/crates/ty_project/resources/test/corpus/27_func_generic_paramspec.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_paramspec.py rename to crates/ty_project/resources/test/corpus/27_func_generic_paramspec.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_paramspec_default.py b/crates/ty_project/resources/test/corpus/27_func_generic_paramspec_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_paramspec_default.py rename to crates/ty_project/resources/test/corpus/27_func_generic_paramspec_default.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_tuple.py b/crates/ty_project/resources/test/corpus/27_func_generic_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_tuple.py rename to crates/ty_project/resources/test/corpus/27_func_generic_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/27_func_generic_tuple_default.py b/crates/ty_project/resources/test/corpus/27_func_generic_tuple_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/27_func_generic_tuple_default.py rename to crates/ty_project/resources/test/corpus/27_func_generic_tuple_default.py diff --git a/crates/red_knot_project/resources/test/corpus/30_func_enclosed.py b/crates/ty_project/resources/test/corpus/30_func_enclosed.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/30_func_enclosed.py rename to crates/ty_project/resources/test/corpus/30_func_enclosed.py diff --git a/crates/red_knot_project/resources/test/corpus/30_func_enclosed_many.py b/crates/ty_project/resources/test/corpus/30_func_enclosed_many.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/30_func_enclosed_many.py rename to crates/ty_project/resources/test/corpus/30_func_enclosed_many.py diff --git a/crates/red_knot_project/resources/test/corpus/31_func_global.py b/crates/ty_project/resources/test/corpus/31_func_global.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/31_func_global.py rename to crates/ty_project/resources/test/corpus/31_func_global.py diff --git a/crates/red_knot_project/resources/test/corpus/31_func_global_annotated_later.py b/crates/ty_project/resources/test/corpus/31_func_global_annotated_later.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/31_func_global_annotated_later.py rename to crates/ty_project/resources/test/corpus/31_func_global_annotated_later.py diff --git a/crates/red_knot_project/resources/test/corpus/31_func_nonlocal.py b/crates/ty_project/resources/test/corpus/31_func_nonlocal.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/31_func_nonlocal.py rename to crates/ty_project/resources/test/corpus/31_func_nonlocal.py diff --git a/crates/red_knot_project/resources/test/corpus/32_func_global_nested.py b/crates/ty_project/resources/test/corpus/32_func_global_nested.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/32_func_global_nested.py rename to crates/ty_project/resources/test/corpus/32_func_global_nested.py diff --git a/crates/red_knot_project/resources/test/corpus/33_func_with_docstring_optimizable_tuple_and_return.py b/crates/ty_project/resources/test/corpus/33_func_with_docstring_optimizable_tuple_and_return.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/33_func_with_docstring_optimizable_tuple_and_return.py rename to crates/ty_project/resources/test/corpus/33_func_with_docstring_optimizable_tuple_and_return.py diff --git a/crates/red_knot_project/resources/test/corpus/40_import.py b/crates/ty_project/resources/test/corpus/40_import.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/40_import.py rename to crates/ty_project/resources/test/corpus/40_import.py diff --git a/crates/red_knot_project/resources/test/corpus/41_from_import.py b/crates/ty_project/resources/test/corpus/41_from_import.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/41_from_import.py rename to crates/ty_project/resources/test/corpus/41_from_import.py diff --git a/crates/red_knot_project/resources/test/corpus/42_import_from_dot.py b/crates/ty_project/resources/test/corpus/42_import_from_dot.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/42_import_from_dot.py rename to crates/ty_project/resources/test/corpus/42_import_from_dot.py diff --git a/crates/red_knot_project/resources/test/corpus/50_yield.py b/crates/ty_project/resources/test/corpus/50_yield.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/50_yield.py rename to crates/ty_project/resources/test/corpus/50_yield.py diff --git a/crates/red_knot_project/resources/test/corpus/51_gen_comp.py b/crates/ty_project/resources/test/corpus/51_gen_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/51_gen_comp.py rename to crates/ty_project/resources/test/corpus/51_gen_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/51_gen_comp2.py b/crates/ty_project/resources/test/corpus/51_gen_comp2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/51_gen_comp2.py rename to crates/ty_project/resources/test/corpus/51_gen_comp2.py diff --git a/crates/red_knot_project/resources/test/corpus/52_gen_comp_if.py b/crates/ty_project/resources/test/corpus/52_gen_comp_if.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/52_gen_comp_if.py rename to crates/ty_project/resources/test/corpus/52_gen_comp_if.py diff --git a/crates/red_knot_project/resources/test/corpus/53_dict_comp.py b/crates/ty_project/resources/test/corpus/53_dict_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/53_dict_comp.py rename to crates/ty_project/resources/test/corpus/53_dict_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/53_list_comp.py b/crates/ty_project/resources/test/corpus/53_list_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/53_list_comp.py rename to crates/ty_project/resources/test/corpus/53_list_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/53_list_comp_method.py b/crates/ty_project/resources/test/corpus/53_list_comp_method.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/53_list_comp_method.py rename to crates/ty_project/resources/test/corpus/53_list_comp_method.py diff --git a/crates/red_knot_project/resources/test/corpus/53_set_comp.py b/crates/ty_project/resources/test/corpus/53_set_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/53_set_comp.py rename to crates/ty_project/resources/test/corpus/53_set_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/54_list_comp_func.py b/crates/ty_project/resources/test/corpus/54_list_comp_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/54_list_comp_func.py rename to crates/ty_project/resources/test/corpus/54_list_comp_func.py diff --git a/crates/red_knot_project/resources/test/corpus/54_list_comp_lambda.py b/crates/ty_project/resources/test/corpus/54_list_comp_lambda.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/54_list_comp_lambda.py rename to crates/ty_project/resources/test/corpus/54_list_comp_lambda.py diff --git a/crates/red_knot_project/resources/test/corpus/54_list_comp_lambda_listcomp.py b/crates/ty_project/resources/test/corpus/54_list_comp_lambda_listcomp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/54_list_comp_lambda_listcomp.py rename to crates/ty_project/resources/test/corpus/54_list_comp_lambda_listcomp.py diff --git a/crates/red_knot_project/resources/test/corpus/54_list_comp_recur_func.py b/crates/ty_project/resources/test/corpus/54_list_comp_recur_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/54_list_comp_recur_func.py rename to crates/ty_project/resources/test/corpus/54_list_comp_recur_func.py diff --git a/crates/red_knot_project/resources/test/corpus/55_list_comp_nested.py b/crates/ty_project/resources/test/corpus/55_list_comp_nested.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/55_list_comp_nested.py rename to crates/ty_project/resources/test/corpus/55_list_comp_nested.py diff --git a/crates/red_knot_project/resources/test/corpus/56_yield_from.py b/crates/ty_project/resources/test/corpus/56_yield_from.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/56_yield_from.py rename to crates/ty_project/resources/test/corpus/56_yield_from.py diff --git a/crates/red_knot_project/resources/test/corpus/57_await.py b/crates/ty_project/resources/test/corpus/57_await.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/57_await.py rename to crates/ty_project/resources/test/corpus/57_await.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for.py b/crates/ty_project/resources/test/corpus/58_async_for.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for.py rename to crates/ty_project/resources/test/corpus/58_async_for.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_break.py b/crates/ty_project/resources/test/corpus/58_async_for_break.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_break.py rename to crates/ty_project/resources/test/corpus/58_async_for_break.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_continue.py b/crates/ty_project/resources/test/corpus/58_async_for_continue.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_continue.py rename to crates/ty_project/resources/test/corpus/58_async_for_continue.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_dict_comp.py b/crates/ty_project/resources/test/corpus/58_async_for_dict_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_dict_comp.py rename to crates/ty_project/resources/test/corpus/58_async_for_dict_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_else.py b/crates/ty_project/resources/test/corpus/58_async_for_else.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_else.py rename to crates/ty_project/resources/test/corpus/58_async_for_else.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_gen_comp.py b/crates/ty_project/resources/test/corpus/58_async_for_gen_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_gen_comp.py rename to crates/ty_project/resources/test/corpus/58_async_for_gen_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_list_comp.py b/crates/ty_project/resources/test/corpus/58_async_for_list_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_list_comp.py rename to crates/ty_project/resources/test/corpus/58_async_for_list_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/58_async_for_set_comp.py b/crates/ty_project/resources/test/corpus/58_async_for_set_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/58_async_for_set_comp.py rename to crates/ty_project/resources/test/corpus/58_async_for_set_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/59_async_with.py b/crates/ty_project/resources/test/corpus/59_async_with.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/59_async_with.py rename to crates/ty_project/resources/test/corpus/59_async_with.py diff --git a/crates/red_knot_project/resources/test/corpus/59_async_with_nested_with.py b/crates/ty_project/resources/test/corpus/59_async_with_nested_with.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/59_async_with_nested_with.py rename to crates/ty_project/resources/test/corpus/59_async_with_nested_with.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_except.py b/crates/ty_project/resources/test/corpus/60_try_except.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_except.py rename to crates/ty_project/resources/test/corpus/60_try_except.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_except2.py b/crates/ty_project/resources/test/corpus/60_try_except2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_except2.py rename to crates/ty_project/resources/test/corpus/60_try_except2.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_except_bare.py b/crates/ty_project/resources/test/corpus/60_try_except_bare.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_except_bare.py rename to crates/ty_project/resources/test/corpus/60_try_except_bare.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_finally.py b/crates/ty_project/resources/test/corpus/60_try_finally.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_finally.py rename to crates/ty_project/resources/test/corpus/60_try_finally.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_finally_codeobj.py b/crates/ty_project/resources/test/corpus/60_try_finally_codeobj.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_finally_codeobj.py rename to crates/ty_project/resources/test/corpus/60_try_finally_codeobj.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_finally_cond.py b/crates/ty_project/resources/test/corpus/60_try_finally_cond.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_finally_cond.py rename to crates/ty_project/resources/test/corpus/60_try_finally_cond.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_finally_for.py b/crates/ty_project/resources/test/corpus/60_try_finally_for.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_finally_for.py rename to crates/ty_project/resources/test/corpus/60_try_finally_for.py diff --git a/crates/red_knot_project/resources/test/corpus/60_try_finally_ret.py b/crates/ty_project/resources/test/corpus/60_try_finally_ret.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/60_try_finally_ret.py rename to crates/ty_project/resources/test/corpus/60_try_finally_ret.py diff --git a/crates/red_knot_project/resources/test/corpus/61_try_except_finally.py b/crates/ty_project/resources/test/corpus/61_try_except_finally.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/61_try_except_finally.py rename to crates/ty_project/resources/test/corpus/61_try_except_finally.py diff --git a/crates/red_knot_project/resources/test/corpus/62_try_except_as.py b/crates/ty_project/resources/test/corpus/62_try_except_as.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/62_try_except_as.py rename to crates/ty_project/resources/test/corpus/62_try_except_as.py diff --git a/crates/red_knot_project/resources/test/corpus/62_try_except_break.py b/crates/ty_project/resources/test/corpus/62_try_except_break.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/62_try_except_break.py rename to crates/ty_project/resources/test/corpus/62_try_except_break.py diff --git a/crates/red_knot_project/resources/test/corpus/62_try_except_cond.py b/crates/ty_project/resources/test/corpus/62_try_except_cond.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/62_try_except_cond.py rename to crates/ty_project/resources/test/corpus/62_try_except_cond.py diff --git a/crates/red_knot_project/resources/test/corpus/62_try_except_double_nested_inside_if_else.py b/crates/ty_project/resources/test/corpus/62_try_except_double_nested_inside_if_else.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/62_try_except_double_nested_inside_if_else.py rename to crates/ty_project/resources/test/corpus/62_try_except_double_nested_inside_if_else.py diff --git a/crates/red_knot_project/resources/test/corpus/62_try_except_return.py b/crates/ty_project/resources/test/corpus/62_try_except_return.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/62_try_except_return.py rename to crates/ty_project/resources/test/corpus/62_try_except_return.py diff --git a/crates/red_knot_project/resources/test/corpus/63_raise.py b/crates/ty_project/resources/test/corpus/63_raise.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/63_raise.py rename to crates/ty_project/resources/test/corpus/63_raise.py diff --git a/crates/red_knot_project/resources/test/corpus/63_raise_func.py b/crates/ty_project/resources/test/corpus/63_raise_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/63_raise_func.py rename to crates/ty_project/resources/test/corpus/63_raise_func.py diff --git a/crates/red_knot_project/resources/test/corpus/63_raise_x.py b/crates/ty_project/resources/test/corpus/63_raise_x.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/63_raise_x.py rename to crates/ty_project/resources/test/corpus/63_raise_x.py diff --git a/crates/red_knot_project/resources/test/corpus/63_raise_x_from_y.py b/crates/ty_project/resources/test/corpus/63_raise_x_from_y.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/63_raise_x_from_y.py rename to crates/ty_project/resources/test/corpus/63_raise_x_from_y.py diff --git a/crates/red_knot_project/resources/test/corpus/64_assert.py b/crates/ty_project/resources/test/corpus/64_assert.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/64_assert.py rename to crates/ty_project/resources/test/corpus/64_assert.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with.py b/crates/ty_project/resources/test/corpus/67_with.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with.py rename to crates/ty_project/resources/test/corpus/67_with.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_as.py b/crates/ty_project/resources/test/corpus/67_with_as.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_as.py rename to crates/ty_project/resources/test/corpus/67_with_as.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_as_func.py b/crates/ty_project/resources/test/corpus/67_with_as_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_as_func.py rename to crates/ty_project/resources/test/corpus/67_with_as_func.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_cond_return.py b/crates/ty_project/resources/test/corpus/67_with_cond_return.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_cond_return.py rename to crates/ty_project/resources/test/corpus/67_with_cond_return.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_inside_try_finally_multiple_terminal_elif.py b/crates/ty_project/resources/test/corpus/67_with_inside_try_finally_multiple_terminal_elif.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_inside_try_finally_multiple_terminal_elif.py rename to crates/ty_project/resources/test/corpus/67_with_inside_try_finally_multiple_terminal_elif.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_inside_try_finally_preceding_terminal_except.py b/crates/ty_project/resources/test/corpus/67_with_inside_try_finally_preceding_terminal_except.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_inside_try_finally_preceding_terminal_except.py rename to crates/ty_project/resources/test/corpus/67_with_inside_try_finally_preceding_terminal_except.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_multi_exit.py b/crates/ty_project/resources/test/corpus/67_with_multi_exit.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_multi_exit.py rename to crates/ty_project/resources/test/corpus/67_with_multi_exit.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_non_name_target.py b/crates/ty_project/resources/test/corpus/67_with_non_name_target.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_non_name_target.py rename to crates/ty_project/resources/test/corpus/67_with_non_name_target.py diff --git a/crates/red_knot_project/resources/test/corpus/67_with_return.py b/crates/ty_project/resources/test/corpus/67_with_return.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/67_with_return.py rename to crates/ty_project/resources/test/corpus/67_with_return.py diff --git a/crates/red_knot_project/resources/test/corpus/68_with2.py b/crates/ty_project/resources/test/corpus/68_with2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/68_with2.py rename to crates/ty_project/resources/test/corpus/68_with2.py diff --git a/crates/red_knot_project/resources/test/corpus/69_for_try_except_continue1.py b/crates/ty_project/resources/test/corpus/69_for_try_except_continue1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/69_for_try_except_continue1.py rename to crates/ty_project/resources/test/corpus/69_for_try_except_continue1.py diff --git a/crates/red_knot_project/resources/test/corpus/69_for_try_except_continue2.py b/crates/ty_project/resources/test/corpus/69_for_try_except_continue2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/69_for_try_except_continue2.py rename to crates/ty_project/resources/test/corpus/69_for_try_except_continue2.py diff --git a/crates/red_knot_project/resources/test/corpus/69_for_try_except_continue3.py b/crates/ty_project/resources/test/corpus/69_for_try_except_continue3.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/69_for_try_except_continue3.py rename to crates/ty_project/resources/test/corpus/69_for_try_except_continue3.py diff --git a/crates/red_knot_project/resources/test/corpus/70_class.py b/crates/ty_project/resources/test/corpus/70_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/70_class.py rename to crates/ty_project/resources/test/corpus/70_class.py diff --git a/crates/red_knot_project/resources/test/corpus/70_class_base.py b/crates/ty_project/resources/test/corpus/70_class_base.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/70_class_base.py rename to crates/ty_project/resources/test/corpus/70_class_base.py diff --git a/crates/red_knot_project/resources/test/corpus/70_class_doc_str.py b/crates/ty_project/resources/test/corpus/70_class_doc_str.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/70_class_doc_str.py rename to crates/ty_project/resources/test/corpus/70_class_doc_str.py diff --git a/crates/red_knot_project/resources/test/corpus/71_class_meth.py b/crates/ty_project/resources/test/corpus/71_class_meth.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/71_class_meth.py rename to crates/ty_project/resources/test/corpus/71_class_meth.py diff --git a/crates/red_knot_project/resources/test/corpus/71_class_var.py b/crates/ty_project/resources/test/corpus/71_class_var.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/71_class_var.py rename to crates/ty_project/resources/test/corpus/71_class_var.py diff --git a/crates/red_knot_project/resources/test/corpus/72_class_mix.py b/crates/ty_project/resources/test/corpus/72_class_mix.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/72_class_mix.py rename to crates/ty_project/resources/test/corpus/72_class_mix.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic.py b/crates/ty_project/resources/test/corpus/73_class_generic.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic.py rename to crates/ty_project/resources/test/corpus/73_class_generic.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_bounds.py b/crates/ty_project/resources/test/corpus/73_class_generic_bounds.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_bounds.py rename to crates/ty_project/resources/test/corpus/73_class_generic_bounds.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_constraints.py b/crates/ty_project/resources/test/corpus/73_class_generic_constraints.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_constraints.py rename to crates/ty_project/resources/test/corpus/73_class_generic_constraints.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_defaults.py b/crates/ty_project/resources/test/corpus/73_class_generic_defaults.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_defaults.py rename to crates/ty_project/resources/test/corpus/73_class_generic_defaults.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_paramspec.py b/crates/ty_project/resources/test/corpus/73_class_generic_paramspec.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_paramspec.py rename to crates/ty_project/resources/test/corpus/73_class_generic_paramspec.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_paramspec_default.py b/crates/ty_project/resources/test/corpus/73_class_generic_paramspec_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_paramspec_default.py rename to crates/ty_project/resources/test/corpus/73_class_generic_paramspec_default.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_tuple.py b/crates/ty_project/resources/test/corpus/73_class_generic_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_tuple.py rename to crates/ty_project/resources/test/corpus/73_class_generic_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/73_class_generic_tuple_default.py b/crates/ty_project/resources/test/corpus/73_class_generic_tuple_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/73_class_generic_tuple_default.py rename to crates/ty_project/resources/test/corpus/73_class_generic_tuple_default.py diff --git a/crates/red_knot_project/resources/test/corpus/74_class_kwargs.py b/crates/ty_project/resources/test/corpus/74_class_kwargs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/74_class_kwargs.py rename to crates/ty_project/resources/test/corpus/74_class_kwargs.py diff --git a/crates/red_knot_project/resources/test/corpus/74_class_kwargs_2.py b/crates/ty_project/resources/test/corpus/74_class_kwargs_2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/74_class_kwargs_2.py rename to crates/ty_project/resources/test/corpus/74_class_kwargs_2.py diff --git a/crates/red_knot_project/resources/test/corpus/74_class_super.py b/crates/ty_project/resources/test/corpus/74_class_super.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/74_class_super.py rename to crates/ty_project/resources/test/corpus/74_class_super.py diff --git a/crates/red_knot_project/resources/test/corpus/74_class_super_nested.py b/crates/ty_project/resources/test/corpus/74_class_super_nested.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/74_class_super_nested.py rename to crates/ty_project/resources/test/corpus/74_class_super_nested.py diff --git a/crates/red_knot_project/resources/test/corpus/74_just_super.py b/crates/ty_project/resources/test/corpus/74_just_super.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/74_just_super.py rename to crates/ty_project/resources/test/corpus/74_just_super.py diff --git a/crates/red_knot_project/resources/test/corpus/75_classderef.py b/crates/ty_project/resources/test/corpus/75_classderef.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/75_classderef.py rename to crates/ty_project/resources/test/corpus/75_classderef.py diff --git a/crates/red_knot_project/resources/test/corpus/75_classderef_no.py b/crates/ty_project/resources/test/corpus/75_classderef_no.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/75_classderef_no.py rename to crates/ty_project/resources/test/corpus/75_classderef_no.py diff --git a/crates/red_knot_project/resources/test/corpus/76_class_nonlocal1.py b/crates/ty_project/resources/test/corpus/76_class_nonlocal1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/76_class_nonlocal1.py rename to crates/ty_project/resources/test/corpus/76_class_nonlocal1.py diff --git a/crates/red_knot_project/resources/test/corpus/76_class_nonlocal2.py b/crates/ty_project/resources/test/corpus/76_class_nonlocal2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/76_class_nonlocal2.py rename to crates/ty_project/resources/test/corpus/76_class_nonlocal2.py diff --git a/crates/red_knot_project/resources/test/corpus/76_class_nonlocal3.py b/crates/ty_project/resources/test/corpus/76_class_nonlocal3.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/76_class_nonlocal3.py rename to crates/ty_project/resources/test/corpus/76_class_nonlocal3.py diff --git a/crates/red_knot_project/resources/test/corpus/76_class_nonlocal4.py b/crates/ty_project/resources/test/corpus/76_class_nonlocal4.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/76_class_nonlocal4.py rename to crates/ty_project/resources/test/corpus/76_class_nonlocal4.py diff --git a/crates/red_knot_project/resources/test/corpus/76_class_nonlocal5.py b/crates/ty_project/resources/test/corpus/76_class_nonlocal5.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/76_class_nonlocal5.py rename to crates/ty_project/resources/test/corpus/76_class_nonlocal5.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__.py b/crates/ty_project/resources/test/corpus/77_class__class__.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__.py rename to crates/ty_project/resources/test/corpus/77_class__class__.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__nested.py b/crates/ty_project/resources/test/corpus/77_class__class__nested.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__nested.py rename to crates/ty_project/resources/test/corpus/77_class__class__nested.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__no_class.py b/crates/ty_project/resources/test/corpus/77_class__class__no_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__no_class.py rename to crates/ty_project/resources/test/corpus/77_class__class__no_class.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__nonlocals.py b/crates/ty_project/resources/test/corpus/77_class__class__nonlocals.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__nonlocals.py rename to crates/ty_project/resources/test/corpus/77_class__class__nonlocals.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__nonlocals_2.py b/crates/ty_project/resources/test/corpus/77_class__class__nonlocals_2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__nonlocals_2.py rename to crates/ty_project/resources/test/corpus/77_class__class__nonlocals_2.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__param.py b/crates/ty_project/resources/test/corpus/77_class__class__param.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__param.py rename to crates/ty_project/resources/test/corpus/77_class__class__param.py diff --git a/crates/red_knot_project/resources/test/corpus/77_class__class__param_lambda.py b/crates/ty_project/resources/test/corpus/77_class__class__param_lambda.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/77_class__class__param_lambda.py rename to crates/ty_project/resources/test/corpus/77_class__class__param_lambda.py diff --git a/crates/red_knot_project/resources/test/corpus/78_class_body_cond.py b/crates/ty_project/resources/test/corpus/78_class_body_cond.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/78_class_body_cond.py rename to crates/ty_project/resources/test/corpus/78_class_body_cond.py diff --git a/crates/red_knot_project/resources/test/corpus/78_class_dec.py b/crates/ty_project/resources/test/corpus/78_class_dec.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/78_class_dec.py rename to crates/ty_project/resources/test/corpus/78_class_dec.py diff --git a/crates/red_knot_project/resources/test/corpus/78_class_dec_member.py b/crates/ty_project/resources/test/corpus/78_class_dec_member.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/78_class_dec_member.py rename to crates/ty_project/resources/test/corpus/78_class_dec_member.py diff --git a/crates/red_knot_project/resources/test/corpus/78_class_dec_member_func.py b/crates/ty_project/resources/test/corpus/78_class_dec_member_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/78_class_dec_member_func.py rename to crates/ty_project/resources/test/corpus/78_class_dec_member_func.py diff --git a/crates/red_knot_project/resources/test/corpus/79_metaclass.py b/crates/ty_project/resources/test/corpus/79_metaclass.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/79_metaclass.py rename to crates/ty_project/resources/test/corpus/79_metaclass.py diff --git a/crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs1.py b/crates/ty_project/resources/test/corpus/80_func_kwonlyargs1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs1.py rename to crates/ty_project/resources/test/corpus/80_func_kwonlyargs1.py diff --git a/crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs2.py b/crates/ty_project/resources/test/corpus/80_func_kwonlyargs2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs2.py rename to crates/ty_project/resources/test/corpus/80_func_kwonlyargs2.py diff --git a/crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs3.py b/crates/ty_project/resources/test/corpus/80_func_kwonlyargs3.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/80_func_kwonlyargs3.py rename to crates/ty_project/resources/test/corpus/80_func_kwonlyargs3.py diff --git a/crates/red_knot_project/resources/test/corpus/81_func_kwonlyargs_defaults.py b/crates/ty_project/resources/test/corpus/81_func_kwonlyargs_defaults.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/81_func_kwonlyargs_defaults.py rename to crates/ty_project/resources/test/corpus/81_func_kwonlyargs_defaults.py diff --git a/crates/red_knot_project/resources/test/corpus/83_jupyter_notebook_ipython_magic.ipynb b/crates/ty_project/resources/test/corpus/83_jupyter_notebook_ipython_magic.ipynb similarity index 100% rename from crates/red_knot_project/resources/test/corpus/83_jupyter_notebook_ipython_magic.ipynb rename to crates/ty_project/resources/test/corpus/83_jupyter_notebook_ipython_magic.ipynb diff --git a/crates/red_knot_project/resources/test/corpus/85_match.py b/crates/ty_project/resources/test/corpus/85_match.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match.py rename to crates/ty_project/resources/test/corpus/85_match.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_as.py b/crates/ty_project/resources/test/corpus/85_match_as.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_as.py rename to crates/ty_project/resources/test/corpus/85_match_as.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_attr.py b/crates/ty_project/resources/test/corpus/85_match_attr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_attr.py rename to crates/ty_project/resources/test/corpus/85_match_attr.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_class.py b/crates/ty_project/resources/test/corpus/85_match_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_class.py rename to crates/ty_project/resources/test/corpus/85_match_class.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_default.py b/crates/ty_project/resources/test/corpus/85_match_default.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_default.py rename to crates/ty_project/resources/test/corpus/85_match_default.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_guard.py b/crates/ty_project/resources/test/corpus/85_match_guard.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_guard.py rename to crates/ty_project/resources/test/corpus/85_match_guard.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_guard_with_named_expr.py b/crates/ty_project/resources/test/corpus/85_match_guard_with_named_expr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_guard_with_named_expr.py rename to crates/ty_project/resources/test/corpus/85_match_guard_with_named_expr.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_in_func.py b/crates/ty_project/resources/test/corpus/85_match_in_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_in_func.py rename to crates/ty_project/resources/test/corpus/85_match_in_func.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_in_func_with_rest.py b/crates/ty_project/resources/test/corpus/85_match_in_func_with_rest.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_in_func_with_rest.py rename to crates/ty_project/resources/test/corpus/85_match_in_func_with_rest.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_in_func_with_star.py b/crates/ty_project/resources/test/corpus/85_match_in_func_with_star.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_in_func_with_star.py rename to crates/ty_project/resources/test/corpus/85_match_in_func_with_star.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_invalid.py b/crates/ty_project/resources/test/corpus/85_match_invalid.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_invalid.py rename to crates/ty_project/resources/test/corpus/85_match_invalid.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_mapping.py b/crates/ty_project/resources/test/corpus/85_match_mapping.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_mapping.py rename to crates/ty_project/resources/test/corpus/85_match_mapping.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_mapping_subpattern.py b/crates/ty_project/resources/test/corpus/85_match_mapping_subpattern.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_mapping_subpattern.py rename to crates/ty_project/resources/test/corpus/85_match_mapping_subpattern.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_or.py b/crates/ty_project/resources/test/corpus/85_match_or.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_or.py rename to crates/ty_project/resources/test/corpus/85_match_or.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_sequence.py b/crates/ty_project/resources/test/corpus/85_match_sequence.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_sequence.py rename to crates/ty_project/resources/test/corpus/85_match_sequence.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_sequence_wildcard.py b/crates/ty_project/resources/test/corpus/85_match_sequence_wildcard.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_sequence_wildcard.py rename to crates/ty_project/resources/test/corpus/85_match_sequence_wildcard.py diff --git a/crates/red_knot_project/resources/test/corpus/85_match_singleton.py b/crates/ty_project/resources/test/corpus/85_match_singleton.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/85_match_singleton.py rename to crates/ty_project/resources/test/corpus/85_match_singleton.py diff --git a/crates/red_knot_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py b/crates/ty_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py rename to crates/ty_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py diff --git a/crates/red_knot_project/resources/test/corpus/88_regression_tuple_type_short_circuit.py b/crates/ty_project/resources/test/corpus/88_regression_tuple_type_short_circuit.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/88_regression_tuple_type_short_circuit.py rename to crates/ty_project/resources/test/corpus/88_regression_tuple_type_short_circuit.py diff --git a/crates/red_knot_project/resources/test/corpus/89_type_alias.py b/crates/ty_project/resources/test/corpus/89_type_alias.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/89_type_alias.py rename to crates/ty_project/resources/test/corpus/89_type_alias.py diff --git a/crates/red_knot_project/resources/test/corpus/89_type_alias_invalid_bound.py b/crates/ty_project/resources/test/corpus/89_type_alias_invalid_bound.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/89_type_alias_invalid_bound.py rename to crates/ty_project/resources/test/corpus/89_type_alias_invalid_bound.py diff --git a/crates/red_knot_project/resources/test/corpus/90_docstring_class.py b/crates/ty_project/resources/test/corpus/90_docstring_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/90_docstring_class.py rename to crates/ty_project/resources/test/corpus/90_docstring_class.py diff --git a/crates/red_knot_project/resources/test/corpus/90_docstring_func.py b/crates/ty_project/resources/test/corpus/90_docstring_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/90_docstring_func.py rename to crates/ty_project/resources/test/corpus/90_docstring_func.py diff --git a/crates/red_knot_project/resources/test/corpus/90_docstring_mod.py b/crates/ty_project/resources/test/corpus/90_docstring_mod.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/90_docstring_mod.py rename to crates/ty_project/resources/test/corpus/90_docstring_mod.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers1.py b/crates/ty_project/resources/test/corpus/91_line_numbers1.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers1.py rename to crates/ty_project/resources/test/corpus/91_line_numbers1.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers2.py b/crates/ty_project/resources/test/corpus/91_line_numbers2.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers2.py rename to crates/ty_project/resources/test/corpus/91_line_numbers2.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers2_comp.py b/crates/ty_project/resources/test/corpus/91_line_numbers2_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers2_comp.py rename to crates/ty_project/resources/test/corpus/91_line_numbers2_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers3.py b/crates/ty_project/resources/test/corpus/91_line_numbers3.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers3.py rename to crates/ty_project/resources/test/corpus/91_line_numbers3.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers4.py b/crates/ty_project/resources/test/corpus/91_line_numbers4.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers4.py rename to crates/ty_project/resources/test/corpus/91_line_numbers4.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers_dict.py b/crates/ty_project/resources/test/corpus/91_line_numbers_dict.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers_dict.py rename to crates/ty_project/resources/test/corpus/91_line_numbers_dict.py diff --git a/crates/red_knot_project/resources/test/corpus/91_line_numbers_dict_comp.py b/crates/ty_project/resources/test/corpus/91_line_numbers_dict_comp.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/91_line_numbers_dict_comp.py rename to crates/ty_project/resources/test/corpus/91_line_numbers_dict_comp.py diff --git a/crates/red_knot_project/resources/test/corpus/92_qual_class_in_class.py b/crates/ty_project/resources/test/corpus/92_qual_class_in_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/92_qual_class_in_class.py rename to crates/ty_project/resources/test/corpus/92_qual_class_in_class.py diff --git a/crates/red_knot_project/resources/test/corpus/92_qual_class_in_func.py b/crates/ty_project/resources/test/corpus/92_qual_class_in_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/92_qual_class_in_func.py rename to crates/ty_project/resources/test/corpus/92_qual_class_in_func.py diff --git a/crates/red_knot_project/resources/test/corpus/93_deadcode.py b/crates/ty_project/resources/test/corpus/93_deadcode.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/93_deadcode.py rename to crates/ty_project/resources/test/corpus/93_deadcode.py diff --git a/crates/red_knot_project/resources/test/corpus/94_strformat.py b/crates/ty_project/resources/test/corpus/94_strformat.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/94_strformat.py rename to crates/ty_project/resources/test/corpus/94_strformat.py diff --git a/crates/red_knot_project/resources/test/corpus/94_strformat_complex.py b/crates/ty_project/resources/test/corpus/94_strformat_complex.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/94_strformat_complex.py rename to crates/ty_project/resources/test/corpus/94_strformat_complex.py diff --git a/crates/red_knot_project/resources/test/corpus/94_strformat_conv.py b/crates/ty_project/resources/test/corpus/94_strformat_conv.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/94_strformat_conv.py rename to crates/ty_project/resources/test/corpus/94_strformat_conv.py diff --git a/crates/red_knot_project/resources/test/corpus/94_strformat_conversion.py b/crates/ty_project/resources/test/corpus/94_strformat_conversion.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/94_strformat_conversion.py rename to crates/ty_project/resources/test/corpus/94_strformat_conversion.py diff --git a/crates/red_knot_project/resources/test/corpus/94_strformat_spec.py b/crates/ty_project/resources/test/corpus/94_strformat_spec.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/94_strformat_spec.py rename to crates/ty_project/resources/test/corpus/94_strformat_spec.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_assign_subscript_no_rhs.py b/crates/ty_project/resources/test/corpus/95_annotation_assign_subscript_no_rhs.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_assign_subscript_no_rhs.py rename to crates/ty_project/resources/test/corpus/95_annotation_assign_subscript_no_rhs.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_assign_tuple.py b/crates/ty_project/resources/test/corpus/95_annotation_assign_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_assign_tuple.py rename to crates/ty_project/resources/test/corpus/95_annotation_assign_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_class.py b/crates/ty_project/resources/test/corpus/95_annotation_class.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_class.py rename to crates/ty_project/resources/test/corpus/95_annotation_class.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_class_multiline.py b/crates/ty_project/resources/test/corpus/95_annotation_class_multiline.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_class_multiline.py rename to crates/ty_project/resources/test/corpus/95_annotation_class_multiline.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_class_no_value.py b/crates/ty_project/resources/test/corpus/95_annotation_class_no_value.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_class_no_value.py rename to crates/ty_project/resources/test/corpus/95_annotation_class_no_value.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_fstring_invalid.py b/crates/ty_project/resources/test/corpus/95_annotation_fstring_invalid.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_fstring_invalid.py rename to crates/ty_project/resources/test/corpus/95_annotation_fstring_invalid.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_func.py b/crates/ty_project/resources/test/corpus/95_annotation_func.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_func.py rename to crates/ty_project/resources/test/corpus/95_annotation_func.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_func_future.py b/crates/ty_project/resources/test/corpus/95_annotation_func_future.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_func_future.py rename to crates/ty_project/resources/test/corpus/95_annotation_func_future.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_global.py b/crates/ty_project/resources/test/corpus/95_annotation_global.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_global.py rename to crates/ty_project/resources/test/corpus/95_annotation_global.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_global_simple.py b/crates/ty_project/resources/test/corpus/95_annotation_global_simple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_global_simple.py rename to crates/ty_project/resources/test/corpus/95_annotation_global_simple.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_local_attr.py b/crates/ty_project/resources/test/corpus/95_annotation_local_attr.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_local_attr.py rename to crates/ty_project/resources/test/corpus/95_annotation_local_attr.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_module.py b/crates/ty_project/resources/test/corpus/95_annotation_module.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_module.py rename to crates/ty_project/resources/test/corpus/95_annotation_module.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_string_tuple.py b/crates/ty_project/resources/test/corpus/95_annotation_string_tuple.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_string_tuple.py rename to crates/ty_project/resources/test/corpus/95_annotation_string_tuple.py diff --git a/crates/red_knot_project/resources/test/corpus/95_annotation_union.py b/crates/ty_project/resources/test/corpus/95_annotation_union.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/95_annotation_union.py rename to crates/ty_project/resources/test/corpus/95_annotation_union.py diff --git a/crates/red_knot_project/resources/test/corpus/96_debug.py b/crates/ty_project/resources/test/corpus/96_debug.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/96_debug.py rename to crates/ty_project/resources/test/corpus/96_debug.py diff --git a/crates/red_knot_project/resources/test/corpus/97_global_nonlocal_store.py b/crates/ty_project/resources/test/corpus/97_global_nonlocal_store.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/97_global_nonlocal_store.py rename to crates/ty_project/resources/test/corpus/97_global_nonlocal_store.py diff --git a/crates/red_knot_project/resources/test/corpus/98_ann_assign_annotation_future_annotations.py b/crates/ty_project/resources/test/corpus/98_ann_assign_annotation_future_annotations.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/98_ann_assign_annotation_future_annotations.py rename to crates/ty_project/resources/test/corpus/98_ann_assign_annotation_future_annotations.py diff --git a/crates/red_knot_project/resources/test/corpus/98_ann_assign_annotation_wrong_future.py b/crates/ty_project/resources/test/corpus/98_ann_assign_annotation_wrong_future.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/98_ann_assign_annotation_wrong_future.py rename to crates/ty_project/resources/test/corpus/98_ann_assign_annotation_wrong_future.py diff --git a/crates/red_knot_project/resources/test/corpus/98_ann_assign_invalid_target.py b/crates/ty_project/resources/test/corpus/98_ann_assign_invalid_target.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/98_ann_assign_invalid_target.py rename to crates/ty_project/resources/test/corpus/98_ann_assign_invalid_target.py diff --git a/crates/red_knot_project/resources/test/corpus/98_ann_assign_simple_annotation.py b/crates/ty_project/resources/test/corpus/98_ann_assign_simple_annotation.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/98_ann_assign_simple_annotation.py rename to crates/ty_project/resources/test/corpus/98_ann_assign_simple_annotation.py diff --git a/crates/red_knot_project/resources/test/corpus/99_empty_jump_target_insts.py b/crates/ty_project/resources/test/corpus/99_empty_jump_target_insts.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/99_empty_jump_target_insts.py rename to crates/ty_project/resources/test/corpus/99_empty_jump_target_insts.py diff --git a/crates/red_knot_project/resources/test/corpus/cycle_narrowing_constraints.py b/crates/ty_project/resources/test/corpus/cycle_narrowing_constraints.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/cycle_narrowing_constraints.py rename to crates/ty_project/resources/test/corpus/cycle_narrowing_constraints.py diff --git a/crates/red_knot_project/resources/test/corpus/cycle_negative_narrowing_constraints.py b/crates/ty_project/resources/test/corpus/cycle_negative_narrowing_constraints.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/cycle_negative_narrowing_constraints.py rename to crates/ty_project/resources/test/corpus/cycle_negative_narrowing_constraints.py diff --git a/crates/red_knot_project/resources/test/corpus/self_referential_function_annotation.py b/crates/ty_project/resources/test/corpus/self_referential_function_annotation.py similarity index 100% rename from crates/red_knot_project/resources/test/corpus/self_referential_function_annotation.py rename to crates/ty_project/resources/test/corpus/self_referential_function_annotation.py diff --git a/crates/red_knot_project/src/combine.rs b/crates/ty_project/src/combine.rs similarity index 97% rename from crates/red_knot_project/src/combine.rs rename to crates/ty_project/src/combine.rs index 659c23df5c26e7..69d6eae9db73e7 100644 --- a/crates/red_knot_project/src/combine.rs +++ b/crates/ty_project/src/combine.rs @@ -1,8 +1,8 @@ use std::{collections::HashMap, hash::BuildHasher}; -use red_knot_python_semantic::{PythonPath, PythonPlatform}; use ruff_db::system::SystemPathBuf; use ruff_python_ast::PythonVersion; +use ty_python_semantic::{PythonPath, PythonPlatform}; /// Combine two values, preferring the values in `self`. /// @@ -22,7 +22,7 @@ use ruff_python_ast::PythonVersion; /// For example: patterns coming last in file inclusion and exclusion patterns /// allow overriding earlier patterns, matching the `gitignore` behavior. /// Generally speaking, it feels more intuitive if later values override earlier values -/// than the other way around: `knot --exclude png --exclude "!important.png"`. +/// than the other way around: `ty --exclude png --exclude "!important.png"`. /// /// The main downside of this approach is that the ordering can be surprising in cases /// where the option has a "first match" semantic and not a "last match" wins. @@ -35,7 +35,7 @@ use ruff_python_ast::PythonVersion; /// ``` /// /// ```bash -/// knot --extra-paths a +/// ty --extra-paths a /// ``` /// /// That's why a user might expect that this configuration results in `["a", "b", "c"]`, diff --git a/crates/red_knot_project/src/db.rs b/crates/ty_project/src/db.rs similarity index 95% rename from crates/red_knot_project/src/db.rs rename to crates/ty_project/src/db.rs index d724ebc3f3f64d..71b473bd330e17 100644 --- a/crates/red_knot_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -3,9 +3,6 @@ use std::sync::Arc; use crate::DEFAULT_LINT_REGISTRY; use crate::{Project, ProjectMetadata}; -use red_knot_ide::Db as IdeDb; -use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; -use red_knot_python_semantic::{Db as SemanticDb, Program}; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::{File, Files}; use ruff_db::system::System; @@ -13,6 +10,9 @@ use ruff_db::vendored::VendoredFileSystem; use ruff_db::{Db as SourceDb, Upcast}; use salsa::plumbing::ZalsaDatabase; use salsa::{Cancelled, Event}; +use ty_ide::Db as IdeDb; +use ty_python_semantic::lint::{LintRegistry, RuleSelection}; +use ty_python_semantic::{Db as SemanticDb, Program}; mod changes; @@ -139,7 +139,7 @@ impl SemanticDb for ProjectDatabase { #[salsa::db] impl SourceDb for ProjectDatabase { fn vendored(&self) -> &VendoredFileSystem { - red_knot_vendored::file_system() + ty_vendored::file_system() } fn system(&self) -> &dyn System { @@ -210,12 +210,12 @@ pub(crate) mod tests { use salsa::Event; - use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; - 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; use ruff_db::{Db as SourceDb, Upcast}; + use ty_python_semantic::lint::{LintRegistry, RuleSelection}; + use ty_python_semantic::{Db as SemanticDb, Program}; use crate::db::Db; use crate::DEFAULT_LINT_REGISTRY; @@ -237,7 +237,7 @@ pub(crate) mod tests { let mut db = Self { storage: salsa::Storage::default(), system: TestSystem::default(), - vendored: red_knot_vendored::file_system().clone(), + vendored: ty_vendored::file_system().clone(), files: Files::default(), events: Arc::default(), project: None, @@ -310,7 +310,7 @@ pub(crate) mod tests { } #[salsa::db] - impl red_knot_python_semantic::Db for TestDb { + impl ty_python_semantic::Db for TestDb { fn is_file_open(&self, file: ruff_db::files::File) -> bool { !file.path(self).is_vendored_path() } diff --git a/crates/red_knot_project/src/db/changes.rs b/crates/ty_project/src/db/changes.rs similarity index 98% rename from crates/red_knot_project/src/db/changes.rs rename to crates/ty_project/src/db/changes.rs index 4ba90af10fce6f..a5bc9e38e3686a 100644 --- a/crates/red_knot_project/src/db/changes.rs +++ b/crates/ty_project/src/db/changes.rs @@ -5,11 +5,11 @@ use crate::{Project, ProjectMetadata}; use std::collections::BTreeSet; use crate::walk::ProjectFilesWalker; -use red_knot_python_semantic::Program; use ruff_db::files::{File, Files}; use ruff_db::system::SystemPath; use ruff_db::Db as _; use rustc_hash::FxHashSet; +use ty_python_semantic::Program; impl ProjectDatabase { #[tracing::instrument(level = "debug", skip(self, changes, cli_options))] @@ -44,7 +44,7 @@ impl ProjectDatabase { if let Some(path) = change.system_path() { if matches!( path.file_name(), - Some(".gitignore" | ".ignore" | "knot.toml" | "pyproject.toml") + Some(".gitignore" | ".ignore" | "ty.toml" | "pyproject.toml") ) { // Changes to ignore files or settings can change the project structure or add/remove files. project_changed = true; diff --git a/crates/red_knot_project/src/files.rs b/crates/ty_project/src/files.rs similarity index 100% rename from crates/red_knot_project/src/files.rs rename to crates/ty_project/src/files.rs diff --git a/crates/red_knot_project/src/lib.rs b/crates/ty_project/src/lib.rs similarity index 97% rename from crates/red_knot_project/src/lib.rs rename to crates/ty_project/src/lib.rs index cf953f2b26ec3d..3a91a003628496 100644 --- a/crates/red_knot_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -6,9 +6,6 @@ pub use db::{Db, ProjectDatabase}; use files::{Index, Indexed, IndexedFiles}; use metadata::settings::Settings; pub use metadata::{ProjectDiscoveryError, ProjectMetadata}; -use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection}; -use red_knot_python_semantic::register_lints; -use red_knot_python_semantic::types::check_types; use ruff_db::diagnostic::{ create_parse_diagnostic, create_unsupported_syntax_diagnostic, Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic, @@ -25,6 +22,9 @@ use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::sync::Arc; use thiserror::Error; use tracing::error; +use ty_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection}; +use ty_python_semantic::register_lints; +use ty_python_semantic::types::check_types; pub mod combine; @@ -84,7 +84,7 @@ pub struct Project { /// However, it's sometimes desired to only check a subset of the project, e.g. to see /// the diagnostics for a single file or a folder. /// - /// This list gets initialized by the paths passed to `knot check ` + /// This list gets initialized by the paths passed to `ty check ` /// /// ## How is this different from `open_files`? /// @@ -139,7 +139,7 @@ impl Project { /// Returns `true` if `path` is both part of the project and included (see `included_paths_list`). /// /// Unlike [Self::files], this method does not respect `.gitignore` files. It only checks - /// the project's include and exclude settings as well as the paths that were passed to `knot check `. + /// the project's include and exclude settings as well as the paths that were passed to `ty check `. /// This means, that this method is an over-approximation of `Self::files` and may return `true` for paths /// that won't be included when checking the project because they're ignored in a `.gitignore` file. pub fn is_path_included(self, db: &dyn Db, path: &SystemPath) -> bool { @@ -301,7 +301,7 @@ impl Project { /// /// The default is to check the entire project in which case this method returns /// the project root. However, users can specify to only check specific sub-folders or - /// even files of a project by using `knot check `. In that case, this method + /// even files of a project by using `ty check `. In that case, this method /// returns the provided absolute paths. /// /// Note: The CLI doesn't prohibit users from specifying paths outside the project root. @@ -605,10 +605,10 @@ where let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message); diagnostic.sub(SubDiagnostic::new( Severity::Info, - "This indicates a bug in Red Knot.", + "This indicates a bug in ty.", )); - let report_message = "If you could open an issue at https://github.com/astral-sh/ruff/issues/new?title=%5Bred-knot%5D:%20panic we'd be very appreciative!"; + let report_message = "If you could open an issue at https://github.com/astral-sh/ty/issues/new?title=%5Bpanic%5D we'd be very appreciative!"; diagnostic.sub(SubDiagnostic::new(Severity::Info, report_message)); diagnostic.sub(SubDiagnostic::new( Severity::Info, @@ -659,14 +659,14 @@ where 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; + use ty_python_semantic::types::check_types; + use ty_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings}; #[test] fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> { diff --git a/crates/red_knot_project/src/metadata.rs b/crates/ty_project/src/metadata.rs similarity index 93% rename from crates/red_knot_project/src/metadata.rs rename to crates/ty_project/src/metadata.rs index dc80da724e5fea..c0061019ebfcf5 100644 --- a/crates/red_knot_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -1,15 +1,15 @@ use configuration_file::{ConfigurationFile, ConfigurationFileError}; -use red_knot_python_semantic::ProgramSettings; use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_python_ast::name::Name; use std::sync::Arc; use thiserror::Error; +use ty_python_semantic::ProgramSettings; use crate::combine::Combine; use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError}; use crate::metadata::value::ValueSource; -use options::KnotTomlError; use options::Options; +use options::TyTomlError; mod configuration_file; pub mod options; @@ -54,10 +54,7 @@ impl ProjectMetadata { root: SystemPathBuf, ) -> Result { Self::from_options( - pyproject - .tool - .and_then(|tool| tool.knot) - .unwrap_or_default(), + pyproject.tool.and_then(|tool| tool.ty).unwrap_or_default(), root, pyproject.project.as_ref(), ) @@ -103,7 +100,7 @@ impl ProjectMetadata { /// The algorithm traverses upwards in the `path`'s ancestor chain and uses the following precedence /// the resolve the project's root. /// - /// 1. The closest `pyproject.toml` with a `tool.knot` section or `knot.toml`. + /// 1. The closest `pyproject.toml` with a `tool.ty` section or `ty.toml`. /// 1. The closest `pyproject.toml`. /// 1. Fallback to use `path` as the root and use the default settings. pub fn discover( @@ -138,17 +135,17 @@ impl ProjectMetadata { None }; - // A `knot.toml` takes precedence over a `pyproject.toml`. - let knot_toml_path = project_root.join("knot.toml"); - if let Ok(knot_str) = system.read_to_string(&knot_toml_path) { + // A `ty.toml` takes precedence over a `pyproject.toml`. + let ty_toml_path = project_root.join("ty.toml"); + if let Ok(ty_str) = system.read_to_string(&ty_toml_path) { let options = match Options::from_toml_str( - &knot_str, - ValueSource::File(Arc::new(knot_toml_path.clone())), + &ty_str, + ValueSource::File(Arc::new(ty_toml_path.clone())), ) { Ok(options) => options, Err(error) => { - return Err(ProjectDiscoveryError::InvalidKnotToml { - path: knot_toml_path, + return Err(ProjectDiscoveryError::InvalidTyToml { + path: ty_toml_path, source: Box::new(error), }) } @@ -156,10 +153,10 @@ impl ProjectMetadata { if pyproject .as_ref() - .is_some_and(|project| project.knot().is_some()) + .is_some_and(|project| project.ty().is_some()) { // TODO: Consider using a diagnostic here - tracing::warn!("Ignoring the `tool.knot` section in `{pyproject_path}` because `{knot_toml_path}` takes precedence."); + tracing::warn!("Ignoring the `tool.ty` section in `{pyproject_path}` because `{ty_toml_path}` takes precedence."); } tracing::debug!("Found project at '{}'", project_root); @@ -182,7 +179,7 @@ impl ProjectMetadata { } if let Some(pyproject) = pyproject { - let has_knot_section = pyproject.knot().is_some(); + let has_ty_section = pyproject.ty().is_some(); let metadata = ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf()) .map_err( @@ -192,7 +189,7 @@ impl ProjectMetadata { }, )?; - if has_knot_section { + if has_ty_section { tracing::debug!("Found project at '{}'", project_root); return Ok(metadata); @@ -208,7 +205,7 @@ impl ProjectMetadata { // No project found, but maybe a pyproject.toml was found. let metadata = if let Some(closest_project) = closest_project { tracing::debug!( - "Project without `tool.knot` section: '{}'", + "Project without `tool.ty` section: '{}'", closest_project.root() ); @@ -290,9 +287,9 @@ pub enum ProjectDiscoveryError { path: SystemPathBuf, }, - #[error("{path} is not a valid `knot.toml`: {source}")] - InvalidKnotToml { - source: Box, + #[error("{path} is not a valid `ty.toml`: {source}")] + InvalidTyToml { + source: Box, path: SystemPathBuf, }, @@ -400,7 +397,7 @@ mod tests { [project] name = "backend" - [tool.knot + [tool.ty "#, ), (root.join("db/__init__.py"), ""), @@ -413,10 +410,10 @@ mod tests { assert_error_eq( &error, - r#"/app/pyproject.toml is not a valid `pyproject.toml`: TOML parse error at line 5, column 31 + r#"/app/pyproject.toml is not a valid `pyproject.toml`: TOML parse error at line 5, column 29 | -5 | [tool.knot - | ^ +5 | [tool.ty + | ^ invalid table header expected `.`, `]` "#, @@ -439,7 +436,7 @@ expected `.`, `]` [project] name = "project-root" - [tool.knot.src] + [tool.ty.src] root = "src" "#, ), @@ -449,7 +446,7 @@ expected `.`, `]` [project] name = "nested-project" - [tool.knot.src] + [tool.ty.src] root = "src" "#, ), @@ -489,7 +486,7 @@ expected `.`, `]` [project] name = "project-root" - [tool.knot.src] + [tool.ty.src] root = "src" "#, ), @@ -499,7 +496,7 @@ expected `.`, `]` [project] name = "nested-project" - [tool.knot.src] + [tool.ty.src] root = "src" "#, ), @@ -526,7 +523,7 @@ expected `.`, `]` } #[test] - fn nested_projects_without_knot_sections() -> anyhow::Result<()> { + fn nested_projects_without_ty_sections() -> anyhow::Result<()> { let system = TestSystem::default(); let root = SystemPathBuf::from("/app"); @@ -566,7 +563,7 @@ expected `.`, `]` } #[test] - fn nested_projects_with_outer_knot_section() -> anyhow::Result<()> { + fn nested_projects_with_outer_ty_section() -> anyhow::Result<()> { let system = TestSystem::default(); let root = SystemPathBuf::from("/app"); @@ -579,7 +576,7 @@ expected `.`, `]` [project] name = "project-root" - [tool.knot.environment] + [tool.ty.environment] python-version = "3.10" "#, ), @@ -612,12 +609,12 @@ expected `.`, `]` Ok(()) } - /// A `knot.toml` takes precedence over any `pyproject.toml`. + /// A `ty.toml` takes precedence over any `pyproject.toml`. /// /// However, the `pyproject.toml` is still loaded to get the project name and, in the future, /// the requires-python constraint. #[test] - fn project_with_knot_and_pyproject_toml() -> anyhow::Result<()> { + fn project_with_ty_and_pyproject_toml() -> anyhow::Result<()> { let system = TestSystem::default(); let root = SystemPathBuf::from("/app"); @@ -631,12 +628,12 @@ expected `.`, `]` name = "super-app" requires-python = ">=3.12" - [tool.knot.src] + [tool.ty.src] root = "this_option_is_ignored" "#, ), ( - root.join("knot.toml"), + root.join("ty.toml"), r#" [src] root = "src" @@ -834,7 +831,7 @@ expected `.`, `]` [project] requires-python = ">=3.12" - [tool.knot.environment] + [tool.ty.environment] python-version = "3.10" "#, ) diff --git a/crates/red_knot_project/src/metadata/configuration_file.rs b/crates/ty_project/src/metadata/configuration_file.rs similarity index 66% rename from crates/red_knot_project/src/metadata/configuration_file.rs rename to crates/ty_project/src/metadata/configuration_file.rs index 03db373e36568c..4190d2cd8b8f65 100644 --- a/crates/red_knot_project/src/metadata/configuration_file.rs +++ b/crates/ty_project/src/metadata/configuration_file.rs @@ -5,9 +5,9 @@ use thiserror::Error; use crate::metadata::value::ValueSource; -use super::options::{KnotTomlError, Options}; +use super::options::{Options, TyTomlError}; -/// A `knot.toml` configuration file with the options it contains. +/// A `ty.toml` configuration file with the options it contains. pub(crate) struct ConfigurationFile { path: SystemPathBuf, options: Options, @@ -23,28 +23,28 @@ impl ConfigurationFile { return Ok(None); }; - let knot_toml_path = configuration_directory.join("knot").join("knot.toml"); + let ty_toml_path = configuration_directory.join("ty").join("ty.toml"); tracing::debug!( "Searching for a user-level configuration at `{path}`", - path = &knot_toml_path + path = &ty_toml_path ); - let Ok(knot_toml_str) = system.read_to_string(&knot_toml_path) else { + let Ok(ty_toml_str) = system.read_to_string(&ty_toml_path) else { return Ok(None); }; match Options::from_toml_str( - &knot_toml_str, - ValueSource::File(Arc::new(knot_toml_path.clone())), + &ty_toml_str, + ValueSource::File(Arc::new(ty_toml_path.clone())), ) { Ok(options) => Ok(Some(Self { - path: knot_toml_path, + path: ty_toml_path, options, })), - Err(error) => Err(ConfigurationFileError::InvalidKnotToml { + Err(error) => Err(ConfigurationFileError::InvalidTyToml { source: Box::new(error), - path: knot_toml_path, + path: ty_toml_path, }), } } @@ -61,9 +61,9 @@ impl ConfigurationFile { #[derive(Debug, Error)] pub enum ConfigurationFileError { - #[error("{path} is not a valid `knot.toml`: {source}")] - InvalidKnotToml { - source: Box, + #[error("{path} is not a valid `ty.toml`: {source}")] + InvalidTyToml { + source: Box, path: SystemPathBuf, }, } diff --git a/crates/red_knot_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs similarity index 94% rename from crates/red_knot_project/src/metadata/options.rs rename to crates/ty_project/src/metadata/options.rs index 47a24dd81d8ce6..796b8d509212df 100644 --- a/crates/red_knot_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -1,7 +1,5 @@ use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard}; use crate::Db; -use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection}; -use red_knot_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings}; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span}; use ruff_db::files::system_path_to_file; use ruff_db::system::{System, SystemPath}; @@ -11,6 +9,8 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use std::fmt::Debug; use thiserror::Error; +use ty_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection}; +use ty_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings}; use super::settings::{Settings, TerminalSettings}; @@ -38,7 +38,7 @@ pub struct Options { } impl Options { - pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result { + pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result { let _guard = ValueSourceGuard::new(source, true); let options = toml::from_str(content)?; Ok(options) @@ -233,17 +233,17 @@ pub struct EnvironmentOptions { /// Specifies the version of Python that will be used to analyze the source code. /// The version should be specified as a string in the format `M.m` where `M` is the major version /// and `m` is the minor (e.g. "3.0" or "3.6"). - /// If a version is provided, knot will generate errors if the source code makes use of language features + /// If a version is provided, ty will generate errors if the source code makes use of language features /// that are not supported in that version. /// It will also tailor its use of type stub files, which conditionalizes type definitions based on the version. #[serde(skip_serializing_if = "Option::is_none")] pub python_version: Option>, /// Specifies the target platform that will be used to analyze the source code. - /// If specified, Red Knot will tailor its use of type stub files, + /// If specified, ty will tailor its use of type stub files, /// which conditionalize type definitions based on the platform. /// - /// If no platform is specified, knot will use the current platform: + /// If no platform is specified, ty will use the current platform: /// - `win32` for Windows /// - `darwin` for macOS /// - `android` for Android @@ -264,9 +264,9 @@ pub struct EnvironmentOptions { #[serde(skip_serializing_if = "Option::is_none")] pub typeshed: Option, - /// Path to the Python installation from which Red Knot resolves type information and third-party dependencies. + /// Path to the Python installation from which ty resolves type information and third-party dependencies. /// - /// Red Knot will search in the path's `site-packages` directories for type information and + /// ty will search in the path's `site-packages` directories for type information and /// third-party imports. /// /// This option is commonly used to specify the path to a virtual environment. @@ -319,12 +319,12 @@ pub struct TerminalOptions { #[cfg(feature = "schemars")] mod schema { use crate::DEFAULT_LINT_REGISTRY; - use red_knot_python_semantic::lint::Level; use schemars::gen::SchemaGenerator; use schemars::schema::{ InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SubschemaValidation, }; use schemars::JsonSchema; + use ty_python_semantic::lint::Level; pub(super) struct Rules; @@ -366,8 +366,8 @@ mod schema { instance_type: Some(InstanceType::Object.into()), object: Some(Box::new(ObjectValidation { properties, - // Allow unknown rules: Red Knot will warn about them. - // It gives a better experience when using an older Red Knot version because + // Allow unknown rules: ty will warn about them. + // It gives a better experience when using an older ty version because // the schema will not deny rules that have been removed in newer versions. additional_properties: Some(Box::new(level_schema)), ..ObjectValidation::default() @@ -380,7 +380,7 @@ mod schema { } #[derive(Error, Debug)] -pub enum KnotTomlError { +pub enum TyTomlError { #[error(transparent)] TomlSyntax(#[from] toml::de::Error), } diff --git a/crates/red_knot_project/src/metadata/pyproject.rs b/crates/ty_project/src/metadata/pyproject.rs similarity index 98% rename from crates/red_knot_project/src/metadata/pyproject.rs rename to crates/ty_project/src/metadata/pyproject.rs index 2be857f50e419d..a32c3b60d4bc71 100644 --- a/crates/red_knot_project/src/metadata/pyproject.rs +++ b/crates/ty_project/src/metadata/pyproject.rs @@ -18,8 +18,8 @@ pub struct PyProject { } impl PyProject { - pub(crate) fn knot(&self) -> Option<&Options> { - self.tool.as_ref().and_then(|tool| tool.knot.as_ref()) + pub(crate) fn ty(&self) -> Option<&Options> { + self.tool.as_ref().and_then(|tool| tool.ty.as_ref()) } } @@ -126,7 +126,7 @@ pub enum ResolveRequiresPythonError { #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct Tool { - pub knot: Option, + pub ty: Option, } /// The normalized name of a package. diff --git a/crates/red_knot_project/src/metadata/settings.rs b/crates/ty_project/src/metadata/settings.rs similarity index 92% rename from crates/red_knot_project/src/metadata/settings.rs rename to crates/ty_project/src/metadata/settings.rs index f5572f6657428a..b635c5470de8de 100644 --- a/crates/red_knot_project/src/metadata/settings.rs +++ b/crates/ty_project/src/metadata/settings.rs @@ -1,7 +1,7 @@ use std::sync::Arc; -use red_knot_python_semantic::lint::RuleSelection; use ruff_db::diagnostic::DiagnosticFormat; +use ty_python_semantic::lint::RuleSelection; /// The resolved [`super::Options`] for the project. /// @@ -15,7 +15,7 @@ use ruff_db::diagnostic::DiagnosticFormat; /// changing the terminal settings shouldn't invalidate any core type-checking queries. /// This can be achieved by adding a salsa query for the type checking specific settings. /// -/// Settings that are part of [`red_knot_python_semantic::ProgramSettings`] are not included here. +/// Settings that are part of [`ty_python_semantic::ProgramSettings`] are not included here. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Settings { rules: Arc, diff --git a/crates/red_knot_project/src/metadata/value.rs b/crates/ty_project/src/metadata/value.rs similarity index 100% rename from crates/red_knot_project/src/metadata/value.rs rename to crates/ty_project/src/metadata/value.rs diff --git a/crates/red_knot_project/src/walk.rs b/crates/ty_project/src/walk.rs similarity index 99% rename from crates/red_knot_project/src/walk.rs rename to crates/ty_project/src/walk.rs index 582b1ac0324e98..c7cc7fd212d2c9 100644 --- a/crates/red_knot_project/src/walk.rs +++ b/crates/ty_project/src/walk.rs @@ -35,7 +35,7 @@ impl<'a> ProjectFilesFilter<'a> { /// Returns `true` if a file is part of the project and included in the paths to check. /// /// A file is included in the checked files if it is a sub path of the project's root - /// (when no CLI path arguments are specified) or if it is a sub path of any path provided on the CLI (`knot check `) AND: + /// (when no CLI path arguments are specified) or if it is a sub path of any path provided on the CLI (`ty check `) AND: /// /// * It matches a positive `include` pattern and isn't excluded by a later negative `include` pattern. /// * It doesn't match a positive `exclude` pattern or is re-included by a later negative `exclude` pattern. diff --git a/crates/red_knot_project/src/watch.rs b/crates/ty_project/src/watch.rs similarity index 100% rename from crates/red_knot_project/src/watch.rs rename to crates/ty_project/src/watch.rs diff --git a/crates/red_knot_project/src/watch/project_watcher.rs b/crates/ty_project/src/watch/project_watcher.rs similarity index 98% rename from crates/red_knot_project/src/watch/project_watcher.rs rename to crates/ty_project/src/watch/project_watcher.rs index 817957591950d0..33419c2a9d0037 100644 --- a/crates/red_knot_project/src/watch/project_watcher.rs +++ b/crates/ty_project/src/watch/project_watcher.rs @@ -3,10 +3,10 @@ use std::hash::Hasher; use tracing::info; -use red_knot_python_semantic::system_module_search_paths; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_db::system::{SystemPath, SystemPathBuf}; use ruff_db::Upcast; +use ty_python_semantic::system_module_search_paths; use crate::db::{Db, ProjectDatabase}; use crate::watch::Watcher; diff --git a/crates/red_knot_project/src/watch/watcher.rs b/crates/ty_project/src/watch/watcher.rs similarity index 100% rename from crates/red_knot_project/src/watch/watcher.rs rename to crates/ty_project/src/watch/watcher.rs diff --git a/crates/red_knot_project/tests/check.rs b/crates/ty_project/tests/check.rs similarity index 98% rename from crates/red_knot_project/tests/check.rs rename to crates/ty_project/tests/check.rs index 276612c3d81f93..8a56d3099a6c31 100644 --- a/crates/red_knot_project/tests/check.rs +++ b/crates/ty_project/tests/check.rs @@ -1,6 +1,4 @@ use anyhow::{anyhow, Context}; -use red_knot_project::{ProjectDatabase, ProjectMetadata}; -use red_knot_python_semantic::{HasType, SemanticModel}; use ruff_db::files::{system_path_to_file, File}; use ruff_db::parsed::parsed_module; use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem}; @@ -9,6 +7,8 @@ use ruff_python_ast::visitor::source_order::SourceOrderVisitor; use ruff_python_ast::{ self as ast, Alias, Comprehension, Expr, Parameter, ParameterWithDefault, Stmt, }; +use ty_project::{ProjectDatabase, ProjectMetadata}; +use ty_python_semantic::{HasType, SemanticModel}; fn setup_db(project_root: &SystemPath, system: TestSystem) -> anyhow::Result { let project = ProjectMetadata::discover(project_root, &system)?; @@ -72,7 +72,7 @@ fn linter_stubs_no_panic() -> anyhow::Result<()> { fn typeshed_no_panic() -> anyhow::Result<()> { let workspace_root = get_cargo_workspace_root()?; run_corpus_tests(&format!( - "{workspace_root}/crates/red_knot_vendored/vendor/typeshed/**/*.pyi" + "{workspace_root}/crates/ty_vendored/vendor/typeshed/**/*.pyi" )) } diff --git a/crates/red_knot_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml similarity index 91% rename from crates/red_knot_python_semantic/Cargo.toml rename to crates/ty_python_semantic/Cargo.toml index c1ef36cb146291..91e26b8fb5b285 100644 --- a/crates/red_knot_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "red_knot_python_semantic" +name = "ty_python_semantic" version = "0.0.0" publish = false authors = { workspace = true } @@ -42,14 +42,14 @@ smallvec = { workspace = true } static_assertions = { workspace = true } test-case = { workspace = true } memchr = { workspace = true } -strum = { workspace = true} -strum_macros = { workspace = true} +strum = { workspace = true } +strum_macros = { workspace = true } [dev-dependencies] ruff_db = { workspace = true, features = ["testing", "os"] } ruff_python_parser = { workspace = true } -red_knot_test = { workspace = true } -red_knot_vendored = { workspace = true } +ty_test = { workspace = true } +ty_vendored = { workspace = true } anyhow = { workspace = true } dir-test = { workspace = true } diff --git a/crates/red_knot_python_semantic/build.rs b/crates/ty_python_semantic/build.rs similarity index 100% rename from crates/red_knot_python_semantic/build.rs rename to crates/ty_python_semantic/build.rs diff --git a/crates/red_knot_python_semantic/mdtest.py b/crates/ty_python_semantic/mdtest.py similarity index 96% rename from crates/red_knot_python_semantic/mdtest.py rename to crates/ty_python_semantic/mdtest.py index 9869295479ec4f..e238c9ce89f65e 100644 --- a/crates/red_knot_python_semantic/mdtest.py +++ b/crates/ty_python_semantic/mdtest.py @@ -1,4 +1,4 @@ -"""A runner for Markdown-based tests for Red Knot""" +"""A runner for Markdown-based tests for ty""" # /// script # requires-python = ">=3.11" # dependencies = [ @@ -18,13 +18,13 @@ from rich.console import Console from watchfiles import Change, watch -CRATE_NAME: Final = "red_knot_python_semantic" +CRATE_NAME: Final = "ty_python_semantic" CRATE_ROOT: Final = Path(__file__).resolve().parent -RED_KNOT_VENDORED: Final = CRATE_ROOT.parent / "red_knot_vendored" +TY_VENDORED: Final = CRATE_ROOT.parent / "ty_vendored" DIRS_TO_WATCH: Final = ( CRATE_ROOT, - RED_KNOT_VENDORED, - CRATE_ROOT.parent / "red_knot_test/src", + TY_VENDORED, + CRATE_ROOT.parent / "ty_test/src", ) MDTEST_DIR: Final = CRATE_ROOT / "resources" / "mdtest" @@ -176,7 +176,7 @@ def watch(self) -> Never: match path.suffix: case ".rs": rust_code_has_changed = True - case ".pyi" if path.is_relative_to(RED_KNOT_VENDORED): + case ".pyi" if path.is_relative_to(TY_VENDORED): vendored_typeshed_has_changed = True case ".md": pass diff --git a/crates/red_knot_python_semantic/mdtest.py.lock b/crates/ty_python_semantic/mdtest.py.lock similarity index 100% rename from crates/red_knot_python_semantic/mdtest.py.lock rename to crates/ty_python_semantic/mdtest.py.lock diff --git a/crates/red_knot_python_semantic/resources/README.md b/crates/ty_python_semantic/resources/README.md similarity index 66% rename from crates/red_knot_python_semantic/resources/README.md rename to crates/ty_python_semantic/resources/README.md index 0a04772b9af87a..ea0a69f18cd1d4 100644 --- a/crates/red_knot_python_semantic/resources/README.md +++ b/crates/ty_python_semantic/resources/README.md @@ -1,4 +1,4 @@ Markdown files within the `mdtest/` subdirectory are tests of type inference and type checking; executed by the `tests/mdtest.rs` integration test. -See `crates/red_knot_test/README.md` for documentation of this test format. +See `crates/ty_test/README.md` for documentation of this test format. diff --git a/crates/red_knot_python_semantic/resources/mdtest/.mdformat.toml b/crates/ty_python_semantic/resources/mdtest/.mdformat.toml similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/.mdformat.toml rename to crates/ty_python_semantic/resources/mdtest/.mdformat.toml diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md rename to crates/ty_python_semantic/resources/mdtest/annotations/annotated.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md b/crates/ty_python_semantic/resources/mdtest/annotations/any.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/any.md rename to crates/ty_python_semantic/resources/mdtest/annotations/any.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md rename to crates/ty_python_semantic/resources/mdtest/annotations/callable.md index 9bd7ae87424326..1fe2aa65d5b91f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -173,7 +173,7 @@ def _( ```py from typing import Callable, Union -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not def _( c: Intersection[Callable[[Union[int, str]], int], int], diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md b/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md rename to crates/ty_python_semantic/resources/mdtest/annotations/deferred.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/int_float_complex.md b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/int_float_complex.md rename to crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md rename to crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index e2b522190f60bf..96a9dfe32d41eb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -6,7 +6,7 @@ Many types are illegal in the context of a type expression: ```py import typing -from knot_extensions import AlwaysTruthy, AlwaysFalsy +from ty_extensions import AlwaysTruthy, AlwaysFalsy from typing_extensions import Literal, Never class A: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md rename to crates/ty_python_semantic/resources/mdtest/annotations/literal.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md rename to crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/never.md b/crates/ty_python_semantic/resources/mdtest/annotations/never.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/never.md rename to crates/ty_python_semantic/resources/mdtest/annotations/never.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md similarity index 89% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/new_types.md rename to crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index 0c5142ef70d8b2..2fb342aeb4ba7d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -1,6 +1,6 @@ # NewType -Currently, red-knot doesn't support `typing.NewType` in type annotations. +Currently, ty doesn't support `typing.NewType` in type annotations. ## Valid forms diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/optional.md b/crates/ty_python_semantic/resources/mdtest/annotations/optional.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/optional.md rename to crates/ty_python_semantic/resources/mdtest/annotations/optional.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md rename to crates/ty_python_semantic/resources/mdtest/annotations/starred.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md rename to crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/string.md rename to crates/ty_python_semantic/resources/mdtest/annotations/string.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/union.md b/crates/ty_python_semantic/resources/mdtest/annotations/union.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/union.md rename to crates/ty_python_semantic/resources/mdtest/annotations/union.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md rename to crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 3f254ebe4737b6..14c595992cb55b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -2,8 +2,8 @@ ## Not yet supported -Several special forms are unsupported by red-knot currently. However, we also don't emit -false-positive errors if you use one in an annotation: +Several special forms are unsupported by ty currently. However, we also don't emit false-positive +errors if you use one in an annotation: ```py from typing_extensions import Self, TypeVarTuple, Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, TypeAlias, Callable, TypeVar diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md rename to crates/ty_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md index fcc61160f284d8..271b6fd014c3c0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md @@ -2,8 +2,8 @@ ## Not yet fully supported -Several type qualifiers are unsupported by red-knot currently. However, we also don't emit -false-positive errors if you use one in an annotation: +Several type qualifiers are unsupported by ty currently. However, we also don't emit false-positive +errors if you use one in an annotation: ```py from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md rename to crates/ty_python_semantic/resources/mdtest/assignment/annotations.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md rename to crates/ty_python_semantic/resources/mdtest/assignment/augmented.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/multi_target.md b/crates/ty_python_semantic/resources/mdtest/assignment/multi_target.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/assignment/multi_target.md rename to crates/ty_python_semantic/resources/mdtest/assignment/multi_target.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md b/crates/ty_python_semantic/resources/mdtest/assignment/unbound.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md rename to crates/ty_python_semantic/resources/mdtest/assignment/unbound.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/walrus.md b/crates/ty_python_semantic/resources/mdtest/assignment/walrus.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/assignment/walrus.md rename to crates/ty_python_semantic/resources/mdtest/assignment/walrus.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/attributes.md rename to crates/ty_python_semantic/resources/mdtest/attributes.md index 72dca325f931f2..682cc3a9818646 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -1220,7 +1220,7 @@ A.X = 100 ### Attribute only available on one element ```py -from knot_extensions import Intersection +from ty_extensions import Intersection class A: x: int = 1 @@ -1242,7 +1242,7 @@ def _(a_and_b: Intersection[type[A], type[B]]): ### Attribute available on both elements ```py -from knot_extensions import Intersection +from ty_extensions import Intersection class P: ... class Q: ... @@ -1267,7 +1267,7 @@ def _(a_and_b: Intersection[type[A], type[B]]): ### Possible unboundness ```py -from knot_extensions import Intersection +from ty_extensions import Intersection class P: ... class Q: ... @@ -1355,7 +1355,7 @@ def _(flag: bool): ### Intersection of implicit instance attributes ```py -from knot_extensions import Intersection +from ty_extensions import Intersection class P: ... class Q: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md b/crates/ty_python_semantic/resources/mdtest/binary/booleans.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md rename to crates/ty_python_semantic/resources/mdtest/binary/booleans.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/classes.md b/crates/ty_python_semantic/resources/mdtest/binary/classes.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/binary/classes.md rename to crates/ty_python_semantic/resources/mdtest/binary/classes.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/custom.md b/crates/ty_python_semantic/resources/mdtest/binary/custom.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/binary/custom.md rename to crates/ty_python_semantic/resources/mdtest/binary/custom.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/ty_python_semantic/resources/mdtest/binary/instances.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/binary/instances.md rename to crates/ty_python_semantic/resources/mdtest/binary/instances.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md b/crates/ty_python_semantic/resources/mdtest/binary/integers.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/binary/integers.md rename to crates/ty_python_semantic/resources/mdtest/binary/integers.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/tuples.md b/crates/ty_python_semantic/resources/mdtest/binary/tuples.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/binary/tuples.md rename to crates/ty_python_semantic/resources/mdtest/binary/tuples.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/unions.md b/crates/ty_python_semantic/resources/mdtest/binary/unions.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/binary/unions.md rename to crates/ty_python_semantic/resources/mdtest/binary/unions.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md b/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md rename to crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md b/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md rename to crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/annotation.md b/crates/ty_python_semantic/resources/mdtest/call/annotation.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/annotation.md rename to crates/ty_python_semantic/resources/mdtest/call/annotation.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/call/builtins.md rename to crates/ty_python_semantic/resources/mdtest/call/builtins.md index a7b9935ee67ed6..a58162a15a712b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -16,8 +16,8 @@ bool(NotBool()) ## Calls to `type()` A single-argument call to `type()` returns an object that has the argument's meta-type. (This is -tested more extensively in `crates/red_knot_python_semantic/resources/mdtest/attributes.md`, -alongside the tests for the `__class__` attribute.) +tested more extensively in `crates/ty_python_semantic/resources/mdtest/attributes.md`, alongside the +tests for the `__class__` attribute.) ```py reveal_type(type(1)) # revealed: Literal[int] diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md b/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md rename to crates/ty_python_semantic/resources/mdtest/call/callable_instance.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md b/crates/ty_python_semantic/resources/mdtest/call/constructor.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/constructor.md rename to crates/ty_python_semantic/resources/mdtest/call/constructor.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/dunder.md b/crates/ty_python_semantic/resources/mdtest/call/dunder.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/dunder.md rename to crates/ty_python_semantic/resources/mdtest/call/dunder.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md similarity index 98% rename from crates/red_knot_python_semantic/resources/mdtest/call/function.md rename to crates/ty_python_semantic/resources/mdtest/call/function.md index 54c35384c80b12..5a56f6c58d401a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -291,7 +291,7 @@ reveal_type(1, 2) ### `static_assert` ```py -from knot_extensions import static_assert +from ty_extensions import static_assert # error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`" static_assert() @@ -313,7 +313,7 @@ len([], 1) ### Type API predicates ```py -from knot_extensions import is_subtype_of, is_fully_static +from ty_extensions import is_subtype_of, is_fully_static # error: [missing-argument] is_subtype_of() diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md b/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md rename to crates/ty_python_semantic/resources/mdtest/call/getattr_static.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/call/invalid_syntax.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/invalid_syntax.md rename to crates/ty_python_semantic/resources/mdtest/call/invalid_syntax.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/methods.md rename to crates/ty_python_semantic/resources/mdtest/call/methods.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/never.md b/crates/ty_python_semantic/resources/mdtest/call/never.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/never.md rename to crates/ty_python_semantic/resources/mdtest/call/never.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/str_startswith.md b/crates/ty_python_semantic/resources/mdtest/call/str_startswith.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/call/str_startswith.md rename to crates/ty_python_semantic/resources/mdtest/call/str_startswith.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/subclass_of.md b/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md similarity index 97% rename from crates/red_knot_python_semantic/resources/mdtest/call/subclass_of.md rename to crates/ty_python_semantic/resources/mdtest/call/subclass_of.md index 9fe47b9b6c9e37..696d578d845678 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/subclass_of.md +++ b/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md @@ -32,7 +32,7 @@ def _(subclass_of_c: type[C]): ```py from typing import Any -from knot_extensions import Unknown +from ty_extensions import Unknown def _(subclass_of_any: type[Any], subclass_of_unknown: type[Unknown]): reveal_type(subclass_of_any()) # revealed: Any diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md similarity index 97% rename from crates/red_knot_python_semantic/resources/mdtest/call/union.md rename to crates/ty_python_semantic/resources/mdtest/call/union.md index eef65c4557a422..8c34c83bb8f97b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -166,7 +166,7 @@ def _(flag: bool): ```py from typing import Literal -from knot_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to +from ty_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[AlwaysFalsy])) static_assert(is_subtype_of(Not[AlwaysFalsy], Literal["", "a"] | Not[AlwaysFalsy])) @@ -203,7 +203,7 @@ def _( ## Cannot use an argument as both a value and a type form ```py -from knot_extensions import is_fully_static +from ty_extensions import is_fully_static def _(flag: bool): if flag: @@ -246,7 +246,7 @@ If two types are gradually equivalent, we can keep just one of them in a union: ```py from typing import Any, Union -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]): reveal_type(x) # revealed: Any & ~int diff --git a/crates/red_knot_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/class/super.md rename to crates/ty_python_semantic/resources/mdtest/class/super.md index 5c5ca3a0639972..5ec8d09af1731b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -271,7 +271,7 @@ python-version = "3.12" ``` ```py -from knot_extensions import TypeOf, static_assert, is_subtype_of +from ty_extensions import TypeOf, static_assert, is_subtype_of class A[T]: def f(self, a: T) -> T: diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/byte_literals.md b/crates/ty_python_semantic/resources/mdtest/comparison/byte_literals.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/byte_literals.md rename to crates/ty_python_semantic/resources/mdtest/comparison/byte_literals.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/identity.md b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/identity.md rename to crates/ty_python_semantic/resources/mdtest/comparison/identity.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md rename to crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md rename to crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md b/crates/ty_python_semantic/resources/mdtest/comparison/integers.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md rename to crates/ty_python_semantic/resources/mdtest/comparison/integers.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md rename to crates/ty_python_semantic/resources/mdtest/comparison/intersections.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md b/crates/ty_python_semantic/resources/mdtest/comparison/non_bool_returns.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md rename to crates/ty_python_semantic/resources/mdtest/comparison/non_bool_returns.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md b/crates/ty_python_semantic/resources/mdtest/comparison/strings.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md rename to crates/ty_python_semantic/resources/mdtest/comparison/strings.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md rename to crates/ty_python_semantic/resources/mdtest/comparison/tuples.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md b/crates/ty_python_semantic/resources/mdtest/comparison/unions.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md rename to crates/ty_python_semantic/resources/mdtest/comparison/unions.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md b/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md rename to crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md rename to crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md rename to crates/ty_python_semantic/resources/mdtest/comprehensions/invalid_syntax.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md b/crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md rename to crates/ty_python_semantic/resources/mdtest/conditional/if_expression.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md b/crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md rename to crates/ty_python_semantic/resources/mdtest/conditional/if_statement.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md b/crates/ty_python_semantic/resources/mdtest/conditional/match.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/conditional/match.md rename to crates/ty_python_semantic/resources/mdtest/conditional/match.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclass_transform.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/dataclass_transform.md rename to crates/ty_python_semantic/resources/mdtest/dataclass_transform.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/dataclasses.md rename to crates/ty_python_semantic/resources/mdtest/dataclasses.md index 0f23feea9f1c76..7d8c1edbdbab03 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses.md @@ -170,7 +170,7 @@ But if there is a variable annotation with a function or class literal type, the `__init__` will include this field: ```py -from knot_extensions import TypeOf +from ty_extensions import TypeOf class SomeClass: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md b/crates/ty_python_semantic/resources/mdtest/declaration/error.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/declaration/error.md rename to crates/ty_python_semantic/resources/mdtest/declaration/error.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/decorators.md rename to crates/ty_python_semantic/resources/mdtest/decorators.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md rename to crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md index 059cb4e5e9a8d8..c07a3a2067141c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md @@ -40,8 +40,8 @@ stat = add(10, 15) ## Using `from` with an unknown current module -This is another case handled separately in Red Knot, where a `.` provokes relative module name -resolution, but where the module name is not resolvable. +This is another case handled separately in ty, where a `.` provokes relative module name resolution, +but where the module name is not resolvable. ```py from .does_not_exist import add # error: [unresolved-import] diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md rename to crates/ty_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md similarity index 98% rename from crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md rename to crates/ty_python_semantic/resources/mdtest/directives/assert_never.md index 712fbad35cda85..1866515a7b753d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/assert_never.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md @@ -7,7 +7,7 @@ ```py from typing_extensions import assert_never, Never, Any -from knot_extensions import Unknown +from ty_extensions import Unknown def _(never: Never, any_: Any, unknown: Unknown, flag: bool): assert_never(never) # fine diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/assert_type.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/directives/assert_type.md rename to crates/ty_python_semantic/resources/mdtest/directives/assert_type.md index af0291493d63d3..336e82e0eecbd9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/assert_type.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md @@ -55,7 +55,7 @@ def _(a: type[int]): from typing import Any from typing_extensions import Literal, assert_type -from knot_extensions import Unknown +from ty_extensions import Unknown # Any and Unknown are considered equivalent def _(a: Unknown, b: Any): @@ -80,7 +80,7 @@ Tuple types with the same elements are the same. ```py from typing_extensions import Any, assert_type -from knot_extensions import Unknown +from ty_extensions import Unknown def _(a: tuple[int, str, bytes]): assert_type(a, tuple[int, str, bytes]) # fine @@ -122,7 +122,7 @@ regardless of order. ```py from typing_extensions import assert_type -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class A: ... class B: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md similarity index 81% rename from crates/red_knot_python_semantic/resources/mdtest/directives/cast.md rename to crates/ty_python_semantic/resources/mdtest/directives/cast.md index 1cfcf5c1a5ffa0..9731d4cb37fe03 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -51,13 +51,13 @@ def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]): ``` A cast from `Todo` or `Unknown` to `Any` is not considered a "redundant cast": even if these are -understood as gradually equivalent types by red-knot, they are understood as different types by -human readers of red-knot's output. For `Unknown` in particular, we may consider it differently in -the context of some opt-in diagnostics, as it indicates that the gradual type has come about due to -an invalid annotation, missing annotation or missing type argument somewhere. +understood as gradually equivalent types by ty, they are understood as different types by human +readers of ty's output. For `Unknown` in particular, we may consider it differently in the context +of some opt-in diagnostics, as it indicates that the gradual type has come about due to an invalid +annotation, missing annotation or missing type argument somewhere. ```py -from knot_extensions import Unknown +from ty_extensions import Unknown def f(x: Any, y: Unknown, z: Any | str | int): a = cast(dict[str, Any], x) diff --git a/crates/red_knot_python_semantic/resources/mdtest/doc/README.md b/crates/ty_python_semantic/resources/mdtest/doc/README.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/doc/README.md rename to crates/ty_python_semantic/resources/mdtest/doc/README.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md b/crates/ty_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md rename to crates/ty_python_semantic/resources/mdtest/doc/public_type_undeclared_symbols.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md b/crates/ty_python_semantic/resources/mdtest/exception/basic.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/exception/basic.md rename to crates/ty_python_semantic/resources/mdtest/exception/basic.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md b/crates/ty_python_semantic/resources/mdtest/exception/control_flow.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md rename to crates/ty_python_semantic/resources/mdtest/exception/control_flow.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md b/crates/ty_python_semantic/resources/mdtest/exception/except_star.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md rename to crates/ty_python_semantic/resources/mdtest/exception/except_star.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/exception/invalid_syntax.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/exception/invalid_syntax.md rename to crates/ty_python_semantic/resources/mdtest/exception/invalid_syntax.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md b/crates/ty_python_semantic/resources/mdtest/expression/assert.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/expression/assert.md rename to crates/ty_python_semantic/resources/mdtest/expression/assert.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/attribute.md b/crates/ty_python_semantic/resources/mdtest/expression/attribute.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/expression/attribute.md rename to crates/ty_python_semantic/resources/mdtest/expression/attribute.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md rename to crates/ty_python_semantic/resources/mdtest/expression/boolean.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/if.md b/crates/ty_python_semantic/resources/mdtest/expression/if.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/expression/if.md rename to crates/ty_python_semantic/resources/mdtest/expression/if.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md b/crates/ty_python_semantic/resources/mdtest/expression/lambda.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md rename to crates/ty_python_semantic/resources/mdtest/expression/lambda.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/len.md b/crates/ty_python_semantic/resources/mdtest/expression/len.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/expression/len.md rename to crates/ty_python_semantic/resources/mdtest/expression/len.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/final.md b/crates/ty_python_semantic/resources/mdtest/final.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/final.md rename to crates/ty_python_semantic/resources/mdtest/final.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md b/crates/ty_python_semantic/resources/mdtest/function/parameters.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/function/parameters.md rename to crates/ty_python_semantic/resources/mdtest/function/parameters.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md b/crates/ty_python_semantic/resources/mdtest/function/return_type.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/function/return_type.md rename to crates/ty_python_semantic/resources/mdtest/function/return_type.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/builtins.md b/crates/ty_python_semantic/resources/mdtest/generics/builtins.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/generics/builtins.md rename to crates/ty_python_semantic/resources/mdtest/generics/builtins.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/generics/legacy/classes.md rename to crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 935aa2e7d9549d..cecf23de0a24f7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -6,7 +6,7 @@ At its simplest, to define a generic class using the legacy syntax, you inherit `typing.Generic` special form, which is "specialized" with the generic class's type variables. ```py -from knot_extensions import generic_context +from ty_extensions import generic_context from typing import Generic, TypeVar T = TypeVar("T") diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md rename to crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/generics/legacy/variables.md rename to crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/generics/pep695/classes.md rename to crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 400f4dc8d4ce50..948ec47f4496e9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -11,7 +11,7 @@ At its simplest, to define a generic class using PEP 695 syntax, you add a list the class name. ```py -from knot_extensions import generic_context +from ty_extensions import generic_context class SingleTypevar[T]: ... class MultipleTypevars[T, S]: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md rename to crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index d09049544f947b..ffd208920c8fc2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -239,7 +239,7 @@ Protocol types can be used as TypeVar bounds, just like nominal types. ```py from typing import Any, Protocol -from knot_extensions import static_assert, is_assignable_to +from ty_extensions import static_assert, is_assignable_to class SupportsClose(Protocol): def close(self) -> None: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md similarity index 98% rename from crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variables.md rename to crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md index 67a4fdc4f08844..2c84f02c951493 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -102,7 +102,7 @@ is similar to how you can assign an expression whose type is not fully static to is.) ```py -from knot_extensions import is_fully_static, static_assert +from ty_extensions import is_fully_static, static_assert from typing import Any def unbounded_unconstrained[T](t: T) -> None: @@ -133,7 +133,7 @@ specialization. Thus, the typevar is a subtype of itself and of `object`, but no (including other typevars). ```py -from knot_extensions import is_assignable_to, is_subtype_of, static_assert +from ty_extensions import is_assignable_to, is_subtype_of, static_assert class Super: ... class Base(Super): ... @@ -233,7 +233,7 @@ the constraints individually. None of the constraints are subtypes of the typeva intersection of all of its constraints is a subtype of the typevar. ```py -from knot_extensions import Intersection +from ty_extensions import Intersection def constrained[T: (Base, Unrelated)](t: T) -> None: static_assert(not is_assignable_to(T, Super)) @@ -329,7 +329,7 @@ An unbounded, unconstrained typevar is not a singleton, because it can be specia non-singleton type. ```py -from knot_extensions import is_singleton, is_single_valued, static_assert +from ty_extensions import is_singleton, is_single_valued, static_assert def unbounded_unconstrained[T](t: T) -> None: static_assert(not is_singleton(T)) @@ -443,7 +443,7 @@ The intersection of an unbounded unconstrained typevar with any other type canno since there is no guarantee what type the typevar will be specialized to. ```py -from knot_extensions import Intersection +from ty_extensions import Intersection from typing import Any class Super: ... @@ -530,7 +530,7 @@ We can simplify the intersection similarly when removing a type from a constrain this is modeled internally as an intersection with a negation. ```py -from knot_extensions import Not +from ty_extensions import Not def remove_constraint[T: (int, str, bool)](t: T) -> None: def _(x: Intersection[T, Not[int]]) -> None: @@ -557,7 +557,7 @@ The intersection of a typevar with any other type is assignable to (and if fully of) itself. ```py -from knot_extensions import is_assignable_to, is_subtype_of, static_assert, Not +from ty_extensions import is_assignable_to, is_subtype_of, static_assert, Not def intersection_is_assignable[T](t: T) -> None: static_assert(is_assignable_to(Intersection[T, None], T)) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variance.md rename to crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md index 489e74a77e8168..65576d6c18fe50 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -22,7 +22,7 @@ Types that "produce" data on demand are covariant in their typevar. If you expec get from the sequence is a valid `int`. ```py -from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any class A: ... @@ -80,7 +80,7 @@ Types that "consume" data are contravariant in their typevar. If you expect a co that you pass into the consumer is a valid `int`. ```py -from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any class A: ... @@ -150,7 +150,7 @@ In the end, if you expect a mutable list, you must always be given a list of exa since we can't know in advance which of the allowed methods you'll want to use. ```py -from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any class A: ... @@ -206,7 +206,7 @@ at all. (If it did, it would have to be covariant, contravariant, or invariant, the typevar was used.) ```py -from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any class A: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md rename to crates/ty_python_semantic/resources/mdtest/generics/scoping.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/basic.md b/crates/ty_python_semantic/resources/mdtest/import/basic.md similarity index 97% rename from crates/red_knot_python_semantic/resources/mdtest/import/basic.md rename to crates/ty_python_semantic/resources/mdtest/import/basic.md index df890bffdab7fb..48fa5fe4bde752 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/import/basic.md @@ -144,8 +144,8 @@ import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`" ## Long paths It's unlikely that a single module component is as long as in this example, but Windows treats paths -that are longer than 200 and something specially. This test ensures that Red Knot can handle those -paths gracefully. +that are longer than 200 and something specially. This test ensures that ty can handle those paths +gracefully. ```toml system = "os" diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md b/crates/ty_python_semantic/resources/mdtest/import/builtins.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/builtins.md rename to crates/ty_python_semantic/resources/mdtest/import/builtins.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/case_sensitive.md b/crates/ty_python_semantic/resources/mdtest/import/case_sensitive.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/case_sensitive.md rename to crates/ty_python_semantic/resources/mdtest/import/case_sensitive.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md b/crates/ty_python_semantic/resources/mdtest/import/conditional.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/conditional.md rename to crates/ty_python_semantic/resources/mdtest/import/conditional.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/conflicts.md b/crates/ty_python_semantic/resources/mdtest/import/conflicts.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/conflicts.md rename to crates/ty_python_semantic/resources/mdtest/import/conflicts.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/conventions.md b/crates/ty_python_semantic/resources/mdtest/import/conventions.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/import/conventions.md rename to crates/ty_python_semantic/resources/mdtest/import/conventions.md index bb7cf890b0a6cc..8781c38de1cf2c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/conventions.md +++ b/crates/ty_python_semantic/resources/mdtest/import/conventions.md @@ -8,10 +8,9 @@ Reference: ## Builtins scope -When looking up for a name, red knot will fallback to using the builtins scope if the name is not -found in the global scope. The `builtins.pyi` file, that will be used to resolve any symbol in the -builtins scope, contains multiple symbols from other modules (e.g., `typing`) but those are not -re-exported. +When looking up for a name, ty will fallback to using the builtins scope if the name is not found in +the global scope. The `builtins.pyi` file, that will be used to resolve any symbol in the builtins +scope, contains multiple symbols from other modules (e.g., `typing`) but those are not re-exported. ```py # These symbols are being imported in `builtins.pyi` but shouldn't be considered as being diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/errors.md b/crates/ty_python_semantic/resources/mdtest/import/errors.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/errors.md rename to crates/ty_python_semantic/resources/mdtest/import/errors.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/import/invalid_syntax.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/invalid_syntax.md rename to crates/ty_python_semantic/resources/mdtest/import/invalid_syntax.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/relative.md b/crates/ty_python_semantic/resources/mdtest/import/relative.md similarity index 98% rename from crates/red_knot_python_semantic/resources/mdtest/import/relative.md rename to crates/ty_python_semantic/resources/mdtest/import/relative.md index a07ac61b9bf131..85e03404ccbf89 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/relative.md +++ b/crates/ty_python_semantic/resources/mdtest/import/relative.md @@ -222,8 +222,8 @@ reveal_type(package.foo.X) # revealed: Unknown ## Relative imports at the top of a search path Relative imports at the top of a search path result in a runtime error: -`ImportError: attempted relative import with no known parent package`. That's why Red Knot should -disallow them. +`ImportError: attempted relative import with no known parent package`. That's why ty should disallow +them. `parser.py`: diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/star.md b/crates/ty_python_semantic/resources/mdtest/import/star.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/import/star.md rename to crates/ty_python_semantic/resources/mdtest/import/star.md index 49cf3bac004e90..0b91b3d35fbce9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/star.md +++ b/crates/ty_python_semantic/resources/mdtest/import/star.md @@ -485,7 +485,7 @@ reveal_type(s) # revealed: Unknown reveal_type(t) # revealed: Unknown # TODO: these should all reveal `Unknown | int` and should not emit errors. -# (We don't generally model elsewhere in red-knot that bindings from walruses +# (We don't generally model elsewhere in ty that bindings from walruses # "leak" from comprehension scopes into outer scopes, but we should.) # See https://github.com/astral-sh/ruff/issues/16954 # error: [unresolved-reference] @@ -1374,8 +1374,7 @@ from foo import * # error: [unresolved-import] "Cannot resolve import `foo`" ### Nested scope -A `*` import in a nested scope are always a syntax error. Red-knot does not infer any bindings from -them: +A `*` import in a nested scope are always a syntax error. Ty does not infer any bindings from them: `exporter.py`: diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/stub_packages.md b/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/stub_packages.md rename to crates/ty_python_semantic/resources/mdtest/import/stub_packages.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/stubs.md b/crates/ty_python_semantic/resources/mdtest/import/stubs.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/stubs.md rename to crates/ty_python_semantic/resources/mdtest/import/stubs.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/tracking.md b/crates/ty_python_semantic/resources/mdtest/import/tracking.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/import/tracking.md rename to crates/ty_python_semantic/resources/mdtest/import/tracking.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/intersection_types.md rename to crates/ty_python_semantic/resources/mdtest/intersection_types.md index d2f409036c58b3..4accdfe3685cf1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md @@ -8,7 +8,7 @@ intersection types (note that we display negative contributions at the end; the matter): ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... class Q: ... @@ -35,7 +35,7 @@ classes. We use `P`, `Q`, `R`, … to denote types that are non-disjoint: ```py -from knot_extensions import static_assert, is_disjoint_from +from ty_extensions import static_assert, is_disjoint_from class P: ... class Q: ... @@ -56,7 +56,7 @@ We use `Literal[1]`, `Literal[2]`, … as examples of pairwise-disjoint types, a supertype of these: ```py -from knot_extensions import static_assert, is_disjoint_from, is_subtype_of +from ty_extensions import static_assert, is_disjoint_from, is_subtype_of from typing import Literal static_assert(is_disjoint_from(Literal[1], Literal[2])) @@ -73,7 +73,7 @@ static_assert(is_subtype_of(Literal[3], int)) Finally, we use `A <: B <: C` and `A <: B1`, `A <: B2` to denote hierarchies of (proper) subtypes: ```py -from knot_extensions import static_assert, is_subtype_of, is_disjoint_from +from ty_extensions import static_assert, is_subtype_of, is_disjoint_from class A: ... class B(A): ... @@ -111,7 +111,7 @@ If we have an intersection with a single element, we can simplify to that elemen show an intersection with a single negative contribution as just the negation of that element. ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... @@ -128,7 +128,7 @@ def _( We eagerly flatten nested intersections types. ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... class Q: ... @@ -179,7 +179,7 @@ We always normalize our representation to a _union of intersections_, so when we intersection_, we distribute the union over the respective elements: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... class Q: ... @@ -231,7 +231,7 @@ Distribution also applies to a negation operation. This is a manifestation of on [De Morgan's laws], namely `~(P | Q) = ~P & ~Q`: ```py -from knot_extensions import Not +from ty_extensions import Not from typing import Literal class P: ... @@ -251,7 +251,7 @@ def example_literals(i: Not[Literal[1, 2]]) -> None: The other of [De Morgan's laws], `~(P & Q) = ~P | ~Q`, also holds: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... class Q: ... @@ -272,7 +272,7 @@ def _( of the [complement laws] of set theory. ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing_extensions import Never def _( @@ -290,7 +290,7 @@ in intersections, and can be eagerly simplified out. `object & P` is equivalent `object & ~P` is equivalent to `~P` for any type `P`. ```py -from knot_extensions import Intersection, Not, is_equivalent_to, static_assert +from ty_extensions import Intersection, Not, is_equivalent_to, static_assert class P: ... @@ -304,7 +304,7 @@ Continuing with more [complement laws], if we see both `P` and `~P` in an inters simplify to `Never`, even in the presence of other types: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Any class P: ... @@ -331,7 +331,7 @@ def _( Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `object`. ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... class Q: ... @@ -353,7 +353,7 @@ def _( The final of the [complement laws] states that negating twice is equivalent to not negating at all: ```py -from knot_extensions import Not +from ty_extensions import Not class P: ... @@ -380,7 +380,7 @@ If we intersect with `Never`, we can simplify the whole intersection to `Never`, dynamic types involved: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing_extensions import Never, Any class P: ... @@ -405,7 +405,7 @@ def _( If we intersect disjoint types, we can simplify to `Never`, even in the presence of other types: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Literal, Any class P: ... @@ -443,7 +443,7 @@ If we intersect a type `X` with the negation `~Y` of a disjoint type `Y`, we can contribution `~Y`, as `~Y` must fully contain the positive contribution `X` as a subtype: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Literal def _( @@ -476,7 +476,7 @@ Subtypes are contained within their supertypes, so we can simplify intersections superfluous supertypes: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Any class A: ... @@ -535,7 +535,7 @@ def _( For negative contributions, this property is reversed. Here we can remove superfluous _subtypes_: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Any class A: ... @@ -594,7 +594,7 @@ def _( If there are multiple negative subtypes, all of them can be removed: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class A: ... class B1(A): ... @@ -622,7 +622,7 @@ When `A` is a supertype of `B`, its negation `~A` is disjoint from `B`, so we ca intersection to `Never`: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not from typing import Any class A: ... @@ -662,7 +662,7 @@ Nonetheless, intersections of `AlwaysFalsy` or `AlwaysTruthy` with `bool` _can_ to the fact that `bool` is a `@final` class at runtime that cannot be subclassed. ```py -from knot_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy +from ty_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy from typing_extensions import Literal class P: ... @@ -709,7 +709,7 @@ simplified, due to the fact that a `LiteralString` inhabitant is known to have ` exactly `str` (and not a subclass of `str`): ```py -from knot_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy, Unknown +from ty_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy, Unknown from typing_extensions import LiteralString def f( @@ -742,7 +742,7 @@ This slightly strange-looking test is a regression test for a mistake that was n . ```py -from knot_extensions import AlwaysFalsy, Intersection, Unknown +from ty_extensions import AlwaysFalsy, Intersection, Unknown from typing_extensions import Literal def _(x: Intersection[str, Unknown, AlwaysFalsy, Literal[""]]): @@ -758,7 +758,7 @@ is still an unknown set of runtime values, so `~Any` is equivalent to `Any`. We simplify `~Any` to `Any` in intersections. The same applies to `Unknown`. ```py -from knot_extensions import Intersection, Not, Unknown +from ty_extensions import Intersection, Not, Unknown from typing_extensions import Any, Never class P: ... @@ -788,7 +788,7 @@ The intersection of an unknown set of runtime values with (another) unknown set still an unknown set of runtime values: ```py -from knot_extensions import Intersection, Not, Unknown +from ty_extensions import Intersection, Not, Unknown from typing_extensions import Any class P: ... @@ -823,7 +823,7 @@ of another unknown set of values is not necessarily empty, so we keep the positi ```py from typing import Any -from knot_extensions import Intersection, Not, Unknown +from ty_extensions import Intersection, Not, Unknown def any( i1: Intersection[Any, Not[Any]], @@ -846,7 +846,7 @@ Gradually-equivalent types can be simplified out of intersections: ```py from typing import Any -from knot_extensions import Intersection, Not, Unknown +from ty_extensions import Intersection, Not, Unknown def mixed( i1: Intersection[Any, Unknown], @@ -863,13 +863,13 @@ def mixed( ## Invalid ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not -# error: [invalid-type-form] "`knot_extensions.Intersection` requires at least one argument when used in a type expression" +# error: [invalid-type-form] "`ty_extensions.Intersection` requires at least one argument when used in a type expression" def f(x: Intersection) -> None: reveal_type(x) # revealed: Unknown -# error: [invalid-type-form] "`knot_extensions.Not` requires exactly one argument when used in a type expression" +# error: [invalid-type-form] "`ty_extensions.Not` requires exactly one argument when used in a type expression" def f(x: Not) -> None: reveal_type(x) # revealed: Unknown ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/invalid_syntax.md rename to crates/ty_python_semantic/resources/mdtest/invalid_syntax.md index 2de26eb1d30c20..cc90879401628d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/invalid_syntax.md +++ b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md @@ -1,6 +1,6 @@ # Syntax errors -Test cases to ensure that red knot does not panic if there are syntax errors in the source code. +Test cases to ensure that ty does not panic if there are syntax errors in the source code. The parser cannot recover from certain syntax errors completely which is why the number of syntax errors could be more than expected in the following examples. For instance, if there's a keyword diff --git a/crates/red_knot_python_semantic/resources/mdtest/known_constants.md b/crates/ty_python_semantic/resources/mdtest/known_constants.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/known_constants.md rename to crates/ty_python_semantic/resources/mdtest/known_constants.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/boolean.md b/crates/ty_python_semantic/resources/mdtest/literal/boolean.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/boolean.md rename to crates/ty_python_semantic/resources/mdtest/literal/boolean.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/bytes.md b/crates/ty_python_semantic/resources/mdtest/literal/bytes.md similarity index 77% rename from crates/red_knot_python_semantic/resources/mdtest/literal/bytes.md rename to crates/ty_python_semantic/resources/mdtest/literal/bytes.md index 0cf6222c534ee7..391eff8ad5546a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/literal/bytes.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/bytes.md @@ -3,7 +3,7 @@ ## Simple ```py -reveal_type(b"red" b"knot") # revealed: Literal[b"redknot"] +reveal_type(b"t" b"y") # revealed: Literal[b"ty"] reveal_type(b"hello") # revealed: Literal[b"hello"] reveal_type(b"world" + b"!") # revealed: Literal[b"world!"] reveal_type(b"\xff\x00") # revealed: Literal[b"\xff\x00"] diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md rename to crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/list.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/collections/list.md rename to crates/ty_python_semantic/resources/mdtest/literal/collections/list.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/set.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/collections/set.md rename to crates/ty_python_semantic/resources/mdtest/literal/collections/set.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/tuple.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/tuple.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/collections/tuple.md rename to crates/ty_python_semantic/resources/mdtest/literal/collections/tuple.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/complex.md b/crates/ty_python_semantic/resources/mdtest/literal/complex.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/complex.md rename to crates/ty_python_semantic/resources/mdtest/literal/complex.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/ellipsis.md b/crates/ty_python_semantic/resources/mdtest/literal/ellipsis.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/ellipsis.md rename to crates/ty_python_semantic/resources/mdtest/literal/ellipsis.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/f_string.md b/crates/ty_python_semantic/resources/mdtest/literal/f_string.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/f_string.md rename to crates/ty_python_semantic/resources/mdtest/literal/f_string.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/float.md b/crates/ty_python_semantic/resources/mdtest/literal/float.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/float.md rename to crates/ty_python_semantic/resources/mdtest/literal/float.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/integer.md b/crates/ty_python_semantic/resources/mdtest/literal/integer.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/integer.md rename to crates/ty_python_semantic/resources/mdtest/literal/integer.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/string.md b/crates/ty_python_semantic/resources/mdtest/literal/string.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/literal/string.md rename to crates/ty_python_semantic/resources/mdtest/literal/string.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/async_for.md b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/loops/async_for.md rename to crates/ty_python_semantic/resources/mdtest/loops/async_for.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/loops/for.md rename to crates/ty_python_semantic/resources/mdtest/loops/for.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md b/crates/ty_python_semantic/resources/mdtest/loops/iterators.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md rename to crates/ty_python_semantic/resources/mdtest/loops/iterators.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md b/crates/ty_python_semantic/resources/mdtest/loops/while_loop.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md rename to crates/ty_python_semantic/resources/mdtest/loops/while_loop.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/mdtest_config.md b/crates/ty_python_semantic/resources/mdtest/mdtest_config.md similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/mdtest_config.md rename to crates/ty_python_semantic/resources/mdtest/mdtest_config.md index d028c7a84a7a75..6db92ff3210ef0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/mdtest_config.md +++ b/crates/ty_python_semantic/resources/mdtest/mdtest_config.md @@ -1,5 +1,5 @@ -This test makes sure that `red_knot_test` correctly parses the TOML configuration blocks and applies -the correct settings hierarchically. +This test makes sure that `ty_test` correctly parses the TOML configuration blocks and applies the +correct settings hierarchically. The following configuration will be attached to the *root* section (without any heading): diff --git a/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md b/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md rename to crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md b/crates/ty_python_semantic/resources/mdtest/metaclass.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/metaclass.md rename to crates/ty_python_semantic/resources/mdtest/metaclass.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/mro.md rename to crates/ty_python_semantic/resources/mdtest/mro.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/named_tuple.md rename to crates/ty_python_semantic/resources/mdtest/named_tuple.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md b/crates/ty_python_semantic/resources/mdtest/narrow/assert.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/assert.md rename to crates/ty_python_semantic/resources/mdtest/narrow/assert.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md b/crates/ty_python_semantic/resources/mdtest/narrow/bool-call.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md rename to crates/ty_python_semantic/resources/mdtest/narrow/bool-call.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/boolean.md b/crates/ty_python_semantic/resources/mdtest/narrow/boolean.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/boolean.md rename to crates/ty_python_semantic/resources/mdtest/narrow/boolean.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/boolean.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/boolean.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/eq.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/in.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/not.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not.md rename to crates/ty_python_semantic/resources/mdtest/narrow/conditionals/not.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md similarity index 98% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md rename to crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 1c4e05e6788375..6eb475715a4851 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -181,7 +181,7 @@ def _(x: object, y: type[int]): We used to incorrectly infer `Literal` booleans for some of these. ```py -from knot_extensions import Not, Intersection, AlwaysTruthy, AlwaysFalsy +from ty_extensions import Not, Intersection, AlwaysTruthy, AlwaysFalsy class P: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md rename to crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/match.md rename to crates/ty_python_semantic/resources/mdtest/narrow/match.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/post_if_statement.md b/crates/ty_python_semantic/resources/mdtest/narrow/post_if_statement.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/post_if_statement.md rename to crates/ty_python_semantic/resources/mdtest/narrow/post_if_statement.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md rename to crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md b/crates/ty_python_semantic/resources/mdtest/narrow/type.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/type.md rename to crates/ty_python_semantic/resources/mdtest/narrow/type.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/while.md b/crates/ty_python_semantic/resources/mdtest/narrow/while.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/narrow/while.md rename to crates/ty_python_semantic/resources/mdtest/narrow/while.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/overloads.md rename to crates/ty_python_semantic/resources/mdtest/overloads.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/pep695_type_aliases.md rename to crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 43a0cf6c6a7e31..6f84dc5481f233 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -79,7 +79,7 @@ reveal_type(ListOrSet.__type_params__) # revealed: @Todo(full tuple[...] suppor Two `TypeAliasType`s are distinct and disjoint, even if they refer to the same type ```py -from knot_extensions import static_assert, is_equivalent_to, is_disjoint_from, TypeOf +from ty_extensions import static_assert, is_equivalent_to, is_disjoint_from, TypeOf type Alias1 = int type Alias2 = int diff --git a/crates/red_knot_python_semantic/resources/mdtest/properties.md b/crates/ty_python_semantic/resources/mdtest/properties.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/properties.md rename to crates/ty_python_semantic/resources/mdtest/properties.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md similarity index 97% rename from crates/red_knot_python_semantic/resources/mdtest/protocols.md rename to crates/ty_python_semantic/resources/mdtest/protocols.md index cc3f8143f8e964..a2f11fa5794375 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -193,7 +193,7 @@ of that protocol (more on that below). However, classes that explicitly inherit class are understood as subtypes of that protocol, the same as with nominal types: ```py -from knot_extensions import static_assert, is_subtype_of, is_assignable_to +from ty_extensions import static_assert, is_subtype_of, is_assignable_to static_assert(is_subtype_of(SubclassOfMyProtocol, MyProtocol)) static_assert(is_assignable_to(SubclassOfMyProtocol, MyProtocol)) @@ -253,7 +253,7 @@ reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool ```py import typing import typing_extensions -from knot_extensions import static_assert, is_equivalent_to, TypeOf +from ty_extensions import static_assert, is_equivalent_to, TypeOf static_assert(is_equivalent_to(TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol])) static_assert(is_equivalent_to(int | str | TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol] | str | int)) @@ -491,7 +491,7 @@ python-version = "3.12" ```py from typing import Protocol -from knot_extensions import static_assert, is_assignable_to, is_subtype_of +from ty_extensions import static_assert, is_assignable_to, is_subtype_of class HasX(Protocol): x: int @@ -631,7 +631,7 @@ def f(arg: HasXWithDefault): reveal_type(type(arg).x) # revealed: int ``` -Assignments in a class body of a protocol -- of any kind -- are not permitted by red-knot unless the +Assignments in a class body of a protocol -- of any kind -- are not permitted by ty unless the symbol being assigned to is also explicitly declared in the protocol's class body. Note that this is stricter validation of protocol members than many other type checkers currently apply (as of 2025/04/21). @@ -738,7 +738,7 @@ static_assert(is_subtype_of(object, UniversalSet)) Which means that `UniversalSet` here is in fact an equivalent type to `object`: ```py -from knot_extensions import is_equivalent_to +from ty_extensions import is_equivalent_to static_assert(is_equivalent_to(UniversalSet, object)) ``` @@ -789,7 +789,7 @@ different names: ```py from typing import Protocol -from knot_extensions import is_equivalent_to, static_assert +from ty_extensions import is_equivalent_to, static_assert class HasX(Protocol): x: int @@ -839,7 +839,7 @@ from both `X` and `Y`: ```py from typing import Protocol -from knot_extensions import Intersection, static_assert, is_equivalent_to +from ty_extensions import Intersection, static_assert, is_equivalent_to class HasX(Protocol): x: int @@ -869,7 +869,7 @@ that would lead to it satisfying `X`'s interface: ```py from typing import final -from knot_extensions import is_disjoint_from +from ty_extensions import is_disjoint_from class NotFinalNominal: ... @@ -902,7 +902,7 @@ x: int = 42 ```py import module from typing import Protocol -from knot_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf +from ty_extensions import is_subtype_of, is_assignable_to, static_assert, TypeOf class HasX(Protocol): x: int @@ -944,7 +944,7 @@ a readable `x` attribute must be accessible on any inhabitant of `ClassVarX`, an ```py from typing import ClassVar, Protocol -from knot_extensions import is_subtype_of, is_assignable_to, static_assert +from ty_extensions import is_subtype_of, is_assignable_to, static_assert class ClassVarXProto(Protocol): x: ClassVar[int] @@ -992,7 +992,7 @@ read/write property, a `Final` attribute, or a `ClassVar` attribute: ```py from typing import ClassVar, Final, Protocol -from knot_extensions import is_subtype_of, is_assignable_to, static_assert +from ty_extensions import is_subtype_of, is_assignable_to, static_assert class HasXProperty(Protocol): @property @@ -1098,7 +1098,7 @@ A protocol with a read/write property `x` is exactly equivalent to a protocol wi attribute `x`. Both are subtypes of a protocol with a read-only prooperty `x`: ```py -from knot_extensions import is_equivalent_to +from ty_extensions import is_equivalent_to class HasMutableXAttr(Protocol): x: int @@ -1358,7 +1358,7 @@ A protocol is only fully static if all of its members are fully static: ```py from typing import Protocol, Any -from knot_extensions import is_fully_static, static_assert +from ty_extensions import is_fully_static, static_assert class FullyStatic(Protocol): x: int @@ -1374,7 +1374,7 @@ Non-fully-static protocols do not participate in subtyping or equivalence, only gradual equivalence: ```py -from knot_extensions import is_subtype_of, is_assignable_to, is_equivalent_to, is_gradual_equivalent_to +from ty_extensions import is_subtype_of, is_assignable_to, is_equivalent_to, is_gradual_equivalent_to class NominalWithX: x: int = 42 @@ -1473,7 +1473,7 @@ right signature: ```py from typing import Callable -from knot_extensions import is_subtype_of, is_assignable_to, static_assert +from ty_extensions import is_subtype_of, is_assignable_to, static_assert static_assert(is_subtype_of(CallMeMaybe, Callable[[int], str])) static_assert(is_assignable_to(CallMeMaybe, Callable[[int], str])) @@ -1492,7 +1492,7 @@ signature implied by the `Callable` type is assignable to the signature of the ` specified by the protocol: ```py -from knot_extensions import TypeOf +from ty_extensions import TypeOf class Foo(Protocol): def __call__(self, x: int, /) -> str: ... @@ -1530,7 +1530,7 @@ worth it. Such cases should anyway be exceedingly rare and/or contrived. ```py from typing import Protocol, Callable -from knot_extensions import is_singleton, is_single_valued +from ty_extensions import is_singleton, is_single_valued class WeirdAndWacky(Protocol): @property diff --git a/crates/red_knot_python_semantic/resources/mdtest/regression/14334_diagnostics_in_wrong_file.md b/crates/ty_python_semantic/resources/mdtest/regression/14334_diagnostics_in_wrong_file.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/regression/14334_diagnostics_in_wrong_file.md rename to crates/ty_python_semantic/resources/mdtest/regression/14334_diagnostics_in_wrong_file.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/scopes/builtin.md rename to crates/ty_python_semantic/resources/mdtest/scopes/builtin.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md b/crates/ty_python_semantic/resources/mdtest/scopes/eager.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md rename to crates/ty_python_semantic/resources/mdtest/scopes/eager.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/global.md b/crates/ty_python_semantic/resources/mdtest/scopes/global.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/scopes/global.md rename to crates/ty_python_semantic/resources/mdtest/scopes/global.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md rename to crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md b/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md rename to crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md rename to crates/ty_python_semantic/resources/mdtest/scopes/unbound.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md b/crates/ty_python_semantic/resources/mdtest/shadowing/class.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md rename to crates/ty_python_semantic/resources/mdtest/shadowing/class.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md b/crates/ty_python_semantic/resources/mdtest/shadowing/function.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md rename to crates/ty_python_semantic/resources/mdtest/shadowing/function.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md b/crates/ty_python_semantic/resources/mdtest/shadowing/variable_declaration.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md rename to crates/ty_python_semantic/resources/mdtest/shadowing/variable_declaration.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/slots.md b/crates/ty_python_semantic/resources/mdtest/slots.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/slots.md rename to crates/ty_python_semantic/resources/mdtest/slots.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap similarity index 87% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap index 29d179dfcd5870..60d36fee9e8faf 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_`__set__`_method_signature.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid `__set__` method signature -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap similarity index 87% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap index b56d17edf721ca..c14f98d9e141c6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Data_descriptors_-_Invalid_argument_type.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid argument type -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap similarity index 88% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap index 83bd2f0f12eef5..2e9753c8effd4c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Instance_attributes_with_class-level_defaults.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: attribute_assignment.md - Attribute assignment - Instance attributes with class-level defaults -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap similarity index 87% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap index 19d1547e5a0d47..088c2c3a821dd6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Possibly-unbound_attributes.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: attribute_assignment.md - Attribute assignment - Possibly-unbound attributes -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap similarity index 88% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap index 560e04c18f4bd1..8be4e61a07a9e0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Pure_instance_attributes.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: attribute_assignment.md - Attribute assignment - Pure instance attributes -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap similarity index 88% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap index d1db352068e1cc..36d60e8ecbd81f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Setting_attributes_on_union_types.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: attribute_assignment.md - Attribute assignment - Setting attributes on union types -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap index 003bd123d3adfd..5d85558448b1d3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_Unknown_attributes.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: attribute_assignment.md - Attribute assignment - Unknown attributes -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap similarity index 87% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap index 61882d1faf8635..8912478ea622a5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment.md_-_Attribute_assignment_-_`ClassVar`s.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: attribute_assignment.md - Attribute assignment - `ClassVar`s -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap similarity index 81% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap index 1301dfb6d0ae5e..ac235bc2374e10 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_import.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: basic.md - Structures - Unresolvable module import -mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md +mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap similarity index 89% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap index 3b1b2c43107fb6..b6f94e9f1f0178 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodule_imports.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: basic.md - Structures - Unresolvable submodule imports -mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md +mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap index 37f5cd5df05cc9..780cb36c1f5191 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Bad `__getitem__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap similarity index 82% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap index 01d7edcf57909e..1412c3831bc6de 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Invalid iterable -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap similarity index 85% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap index adf5904e9bbe08..7f015993d5909e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - New over old style iteration protocol -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap similarity index 88% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap index 6ef81246c85283..1603d6f183bfd2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - No `__iter__` method and `__getitem__` is not callable -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap index 3d3362acf45837..1da698c4f80a38 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Possibly-not-callable `__getitem__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap index 8b5ff406e47d08..e6247cb48b41c7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Possibly invalid `__getitem__` methods -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap index 758a101b4a5fa1..37f27c77797166 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Possibly invalid `__iter__` methods -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap index 7b5200bc243473..fb60e1a7915bb6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Possibly invalid `__next__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap index ca7f8bd055b3b9..42934ea96c01f3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Possibly unbound `__iter__` and bad `__getitem__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap index 8157542d95a4b8..8000a1a9719789 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly invalid `__getitem__` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap similarity index 92% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap index 58fc572d7476b5..85478fc88cb9c8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly unbound `__getitem__` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap index 1d6cf8f415becc..b4be009d43f9b8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Union type as iterable where one union element has invalid `__iter__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap index bf2af5ac7554d9..61aece908cff84 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - Union type as iterable where one union element has no `__iter__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap similarity index 92% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap index 4e4d5807b5872f..378a82551d0bde 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - With non-callable iterator -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap similarity index 88% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap index 1360f85797f57f..a676d7ed225470 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - `__iter__` does not return an iterator -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap index 515921e9b8d05e..c310a44802e73f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - `__iter__` method with a bad signature -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap index f5d3a90199d2c6..862f86dfc91af7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: for.md - For loops - `__iter__` returns an iterator with an invalid `__next__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md +mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap index 902b25a6e7acb5..f98a9c13827abd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: functions.md - Generic functions: Legacy syntax - Inferring a bound typevar -mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap index f84cf8c7831869..6d99336f3f17b6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: functions.md - Generic functions: Legacy syntax - Inferring a constrained typevar -mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap index 5c03b2551b6a60..f3e2ed614c1b1b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: functions.md - Generic functions: PEP 695 syntax - Inferring a bound typevar -mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap index d1b259b7303546..7f4dd4177ee8cf 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: functions.md - Generic functions: PEP 695 syntax - Inferring a constrained typevar -mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md +mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap similarity index 84% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap index ab24c67fdbf3ad..76f8b961558919 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: instances.md - Binary operations on instances - Operations involving types with invalid `__bool__` methods -mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +mdtest path: crates/ty_python_semantic/resources/mdtest/binary/instances.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap similarity index 83% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap index d7c380f6f10ab4..4957245c99aea9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Basic.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Basic -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap similarity index 85% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap index e4e379e49963c1..ea486b27b70aaa 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Calls_to_methods.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Calls to methods -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap similarity index 84% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap index 3a0857fb507daa..28984229f3ccd1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_files.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different files -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap similarity index 85% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap index ca4201e6241678..53dac38fa69725 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Different_source_order.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different source order -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap similarity index 84% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap index c96af22564d9ff..3f4412c4ab6e69 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap similarity index 85% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap index 872613113e6775..6274470af60f8d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_across_multiple_lines.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters across multiple lines -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap similarity index 92% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap index 7fa5b1c59d21e4..77f42c8017f025 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Many_parameters_with_multiple_invalid_arguments.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters with multiple invalid arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap index 4dab8917ec9d60..6468bec1b6624e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Test_calling_a_function_whose_type_is_vendored_from_`typeshed`.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Test calling a function whose type is vendored from `typeshed` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap index e0c157292b3028..e0bdd6c0b80449 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Keyword_only_arguments.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Keyword only arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap index 591bea50baec04..4376b02042f41b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Mix_of_arguments.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Mix of arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap index 0ca5219152f5a1..5e99c713ebc8c8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_One_keyword_argument.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - One keyword argument -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap similarity index 85% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap index 912c60f2e14436..9134209cdc579f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Only_positional.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Only positional -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap similarity index 85% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap index 7cbf4908a80bb3..8eb32d34e014e0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Synthetic_arguments.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Synthetic arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap similarity index 85% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap index b729a73e2cc9b6..d93f3e31bbb18f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_arguments.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap index a3a8c87e8f19e1..92e554c7797078 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_type.md_-_Invalid_argument_type_diagnostics_-_Tests_for_a_variety_of_argument_types_-_Variadic_keyword_arguments.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic keyword arguments -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap similarity index 89% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap index 831d23cda8b1ab..e68be65dce15af 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: membership_test.md - Comparison: Membership Test - Return type that doesn't implement `__bool__` correctly -mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md +mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap similarity index 77% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap index 73b3e60e6bf119..004791aa56cdf7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: no_matching_overload.md - No matching overload diagnostics - Calls to overloaded functions -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap similarity index 83% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap index f3b7734eb9cdb4..f2c196b0eacb07 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: not.md - Unary not - Object that implements `__bool__` incorrectly -mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md +mdtest path: crates/ty_python_semantic/resources/mdtest/unary/not.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap index 246dff3c952ba3..7912b94fd1b503 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloads.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: overloads.md - Overloads - Invalid - At least two overloads -mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md +mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap similarity index 97% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap index aef6a58ff12275..f129dd2d871f3e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@classmethod`.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: overloads.md - Overloads - Invalid - Inconsistent decorators - `@classmethod` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md +mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap index 97692ad5b8bf93..b4b2f8118122df 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@final`.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: overloads.md - Overloads - Invalid - Inconsistent decorators - `@final` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md +mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap index bae8cd1bc7a4e1..cab6c0f2d8894b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorators_-_`@override`.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: overloads.md - Overloads - Invalid - Inconsistent decorators - `@override` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md +mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap similarity index 92% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap index f92141e10cdfb5..0ca51d8c653575 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_implementation_-_Regular_modules.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: overloads.md - Overloads - Invalid - Overload without an implementation - Regular modules -mdtest path: crates/red_knot_python_semantic/resources/mdtest/overloads.md +mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap similarity index 97% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap index a223adb68137dd..877b2ce74620d5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: protocols.md - Protocols - Calls to protocol classes -mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md +mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap index ca38907009374e..7300c2f77dbf1b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`get_protocol_members()`.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: protocols.md - Protocols - Invalid calls to `get_protocol_members()` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md +mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap similarity index 98% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap index 4d6894f835786b..05fdb625668570 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protocols.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: protocols.md - Protocols - Narrowing of protocols -mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md +mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap index aebad91a20cf65..d319cbd20630b8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: return_type.md - Function return type - Invalid conditional return type -mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap index f03d7011c0106a..2fd61b50b8a0c8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: return_type.md - Function return type - Invalid implicit return type -mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap index 7e439527f1e84d..f02798142de76d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: return_type.md - Function return type - Invalid return type -mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap index 5e2334447fff1d..bbb2df8a3282e2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: return_type.md - Function return type - Invalid return type in stub file -mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap index 7da9f44a682c2a..44d10c111c9375 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: rich_comparison.md - Comparison: Rich Comparison - Chained comparisons with objects that don't implement `__bool__` correctly -mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap index ff4fd7a4fadc42..44a166a3ed3019 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_errors.md_-_Semantic_syntax_error_diagnostics_-_`async`_comprehensions_in_synchronous_comprehensions_-_Python_3.10.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: semantic_syntax_errors.md - Semantic syntax error diagnostics - `async` comprehensions in synchronous comprehensions - Python 3.10 -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap similarity index 78% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap index 19d5bb12d766e6..aff6a28e981171 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shadowing.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: shadowing.md - Shadowing - Implicit class shadowing -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap similarity index 79% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap index ec187843c5395e..fab3dbd25320dc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_shadowing.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: shadowing.md - Shadowing - Implicit function shadowing -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap similarity index 88% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap index 09c17b82b71df6..050643d42df7d5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: tuples.md - Comparison: Tuples - Chained comparisons with elements that incorrectly implement `__bool__` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md +mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap similarity index 85% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap index 222b3e132459f2..8edb1dad3ed329 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: tuples.md - Comparison: Tuples - Equality with elements that incorrectly implement `__bool__` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md +mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap similarity index 76% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap index eed266117fd539..aba153647b9a70 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_few_values_to_unpack.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unpacking.md - Unpacking - Exactly too few values to unpack -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap similarity index 77% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap index 771602ec42d5ce..c89af563be9ec6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Exactly_too_many_values_to_unpack.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unpacking.md - Unpacking - Exactly too many values to unpack -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap similarity index 78% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap index ab2fe8d044615e..0a20d08fdb2a5d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unpacking.md - Unpacking - Right hand side not iterable -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap similarity index 78% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap index 834d4080e3ffab..2b7dbc050ee989 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Too_few_values_to_unpack.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unpacking.md - Unpacking - Too few values to unpack -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap similarity index 80% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap index 45d5aea73a5c0f..efcb90ec6ef0b0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_An_unresolvable_import_that_does_not_use_`from`.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unresolved_import.md - Unresolved import diagnostics - An unresolvable import that does not use `from` -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap similarity index 82% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap index 051ba77378612c..5de84ecc3ae706 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_a_resolvable_module_but_unresolvable_item.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with a resolvable module but unresolvable item -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap similarity index 80% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap index e311847c4e9ca6..090cba80560791 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_current_module.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown current module -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap similarity index 81% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap index 3cf055505f52d0..0123ff0edf037b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unknown_nested_module.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown nested module -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap similarity index 80% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap index 88a68630078fdd..999a1379a92a0c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_an_unresolvable_module.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unresolvable module -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap similarity index 83% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap index b088fc704fee74..4b7e84df00c96c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_diagnostics_-_Using_`from`_with_too_many_leading_dots.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with too many leading dots -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_import.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap similarity index 82% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap index c2745e3ce3d0b2..9f228605b35825 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Has a `__bool__` attribute, but it's not callable -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap index ac326c9bf7fed1..c84856aec24310 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Has a `__bool__` method, but has an incorrect return type -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap similarity index 86% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap index 2aec49f346aa53..bfd905314a3b47 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Has a `__bool__` method, but has incorrect parameters -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap similarity index 87% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap index 9624f725271e20..7da1d628d046ab 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`unsupported-bool-conversion`_can_occur_-_Part_of_a_union_where_at_least_one_member_has_incorrect_`__bool__`_method.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_test/src/lib.rs expression: snapshot --- --- mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Part of a union where at least one member has incorrect `__bool__` method -mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md --- # Python source files 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/ty_python_semantic/resources/mdtest/snapshots/version_related_syntax_errors.md_-_Version-related_syntax_error_diagnostics_-_`match`_statement_-_Before_3.10.snap similarity index 83% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/version_related_syntax_errors.md_-_Version-related_syntax_error_diagnostics_-_`match`_statement_-_Before_3.10.snap rename to crates/ty_python_semantic/resources/mdtest/snapshots/version_related_syntax_errors.md_-_Version-related_syntax_error_diagnostics_-_`match`_statement_-_Before_3.10.snap index ab4887c2cb6bf1..ad47fde3f12ce0 100644 --- 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/ty_python_semantic/resources/mdtest/snapshots/version_related_syntax_errors.md_-_Version-related_syntax_error_diagnostics_-_`match`_statement_-_Before_3.10.snap @@ -1,10 +1,10 @@ --- -source: crates/red_knot_test/src/lib.rs +source: crates/ty_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 +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md --- # Python source files diff --git a/crates/red_knot_python_semantic/resources/mdtest/statically_known_branches.md b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/statically_known_branches.md rename to crates/ty_python_semantic/resources/mdtest/statically_known_branches.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md b/crates/ty_python_semantic/resources/mdtest/stubs/class.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/stubs/class.md rename to crates/ty_python_semantic/resources/mdtest/stubs/class.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsis.md b/crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsis.md rename to crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/locals.md b/crates/ty_python_semantic/resources/mdtest/stubs/locals.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/stubs/locals.md rename to crates/ty_python_semantic/resources/mdtest/stubs/locals.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md b/crates/ty_python_semantic/resources/mdtest/subscript/bytes.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md rename to crates/ty_python_semantic/resources/mdtest/subscript/bytes.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md b/crates/ty_python_semantic/resources/mdtest/subscript/class.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/subscript/class.md rename to crates/ty_python_semantic/resources/mdtest/subscript/class.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md rename to crates/ty_python_semantic/resources/mdtest/subscript/instance.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/ty_python_semantic/resources/mdtest/subscript/lists.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md rename to crates/ty_python_semantic/resources/mdtest/subscript/lists.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/stepsize_zero.md b/crates/ty_python_semantic/resources/mdtest/subscript/stepsize_zero.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/subscript/stepsize_zero.md rename to crates/ty_python_semantic/resources/mdtest/subscript/stepsize_zero.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md b/crates/ty_python_semantic/resources/mdtest/subscript/string.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/subscript/string.md rename to crates/ty_python_semantic/resources/mdtest/subscript/string.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md rename to crates/ty_python_semantic/resources/mdtest/subscript/tuple.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/suppressions/no_type_check.md b/crates/ty_python_semantic/resources/mdtest/suppressions/no_type_check.md similarity index 90% rename from crates/red_knot_python_semantic/resources/mdtest/suppressions/no_type_check.md rename to crates/ty_python_semantic/resources/mdtest/suppressions/no_type_check.md index 21f2cb9dc4b71b..37e32020613a78 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/suppressions/no_type_check.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/no_type_check.md @@ -93,8 +93,8 @@ def test() -> Undefined: ## `no_type_check` on classes isn't supported -Red Knot does not support decorating classes with `no_type_check`. The behaviour of `no_type_check` -when applied to classes is +ty does not support decorating classes with `no_type_check`. The behaviour of `no_type_check` when +applied to classes is [not specified currently](https://typing.python.org/en/latest/spec/directives.html#no-type-check), and is not supported by Pyright or mypy. @@ -117,6 +117,6 @@ from typing import no_type_check @no_type_check def test(): - # error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'" - return x + 5 # knot: ignore[unresolved-reference] + # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'" + return x + 5 # ty: ignore[unresolved-reference] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md new file mode 100644 index 00000000000000..9a1930f0156846 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md @@ -0,0 +1,191 @@ +# Suppressing errors with `ty: ignore` + +Type check errors can be suppressed by a `ty: ignore` comment on the same line as the violation. + +## Simple `ty: ignore` + +```py +a = 4 + test # ty: ignore +``` + +## Suppressing a specific code + +```py +a = 4 + test # ty: ignore[unresolved-reference] +``` + +## Unused suppression + +```py +test = 10 +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'possibly-unresolved-reference'" +a = test + 3 # ty: ignore[possibly-unresolved-reference] +``` + +## Unused suppression if the error codes don't match + +```py +# error: [unresolved-reference] +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'possibly-unresolved-reference'" +a = test + 3 # ty: ignore[possibly-unresolved-reference] +``` + +## Suppressed unused comment + +```py +# error: [unused-ignore-comment] +a = 10 / 2 # ty: ignore[division-by-zero] +a = 10 / 2 # ty: ignore[division-by-zero, unused-ignore-comment] +a = 10 / 2 # ty: ignore[unused-ignore-comment, division-by-zero] +a = 10 / 2 # ty: ignore[unused-ignore-comment] # type: ignore +a = 10 / 2 # type: ignore # ty: ignore[unused-ignore-comment] +``` + +## Unused ignore comment + +```py +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unused-ignore-comment'" +a = 10 / 0 # ty: ignore[division-by-zero, unused-ignore-comment] +``` + +## Multiple unused comments + +Today, ty emits a diagnostic for every unused code. We might want to group the codes by comment at +some point in the future. + +```py +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'division-by-zero'" +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'" +a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference] + +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'" +# error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'unresolved-reference'" +a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference] +``` + +## Multiple suppressions + +```py +# fmt: off +def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... # ty: ignore[fstring-type-annotation, byte-string-type-annotation] +``` + +## Can't suppress syntax errors + + + +```py +# error: [invalid-syntax] +# error: [unused-ignore-comment] +def test($): # ty: ignore + pass +``` + + + +## Can't suppress `revealed-type` diagnostics + +```py +a = 10 +# revealed: Literal[10] +# error: [unknown-rule] "Unknown rule `revealed-type`" +reveal_type(a) # ty: ignore[revealed-type] +``` + +## Extra whitespace in type ignore comments is allowed + +```py +a = 10 / 0 # ty : ignore +a = 10 / 0 # ty: ignore [ division-by-zero ] +``` + +## Whitespace is optional + +```py +# fmt: off +a = 10 / 0 #ty:ignore[division-by-zero] +``` + +## Trailing codes comma + +Trailing commas in the codes section are allowed: + +```py +a = 10 / 0 # ty: ignore[division-by-zero,] +``` + +## Invalid characters in codes + +```py +# error: [division-by-zero] +# error: [invalid-ignore-comment] "Invalid `ty: ignore` comment: expected a alphanumeric character or `-` or `_` as code" +a = 10 / 0 # ty: ignore[*-*] +``` + +## Trailing whitespace + + + +```py +a = 10 / 0 # ty: ignore[division-by-zero] + # ^^^^^^ trailing whitespace +``` + + + +## Missing comma + +A missing comma results in an invalid suppression comment. We may want to recover from this in the +future. + +```py +# error: [unresolved-reference] +# error: [invalid-ignore-comment] "Invalid `ty: ignore` comment: expected a comma separating the rule codes" +a = x / 0 # ty: ignore[division-by-zero unresolved-reference] +``` + +## Missing closing bracket + +```py +# error: [unresolved-reference] "Name `x` used when not defined" +# error: [invalid-ignore-comment] "Invalid `ty: ignore` comment: expected a comma separating the rule codes" +a = x / 2 # ty: ignore[unresolved-reference +``` + +## Empty codes + +An empty codes array suppresses no-diagnostics and is always useless + +```py +# error: [division-by-zero] +# error: [unused-ignore-comment] "Unused `ty: ignore` without a code" +a = 4 / 0 # ty: ignore[] +``` + +## File-level suppression comments + +File level suppression comments are currently intentionally unsupported because we've yet to decide +if they should use a different syntax that also supports enabling rules or changing the rule's +severity: `ty: possibly-undefined-reference=error` + +```py +# error: [unused-ignore-comment] +# ty: ignore[division-by-zero] + +a = 4 / 0 # error: [division-by-zero] +``` + +## Unknown rule + +```py +# error: [unknown-rule] "Unknown rule `is-equal-14`" +a = 10 + 4 # ty: ignore[is-equal-14] +``` + +## Code with `lint:` prefix + +```py +# error:[unknown-rule] "Unknown rule `lint:division-by-zero`. Did you mean `division-by-zero`?" +# error: [division-by-zero] +a = 10 / 0 # ty: ignore[lint:division-by-zero] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/suppressions/type_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/suppressions/type_ignore.md rename to crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md index 87cf6ee69e23b9..43e9eedcc22fd3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/suppressions/type_ignore.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md @@ -87,8 +87,8 @@ a = test \ ## Codes -Mypy supports `type: ignore[code]`. Red Knot doesn't understand mypy's rule names. Therefore, ignore -the codes and suppress all errors. +Mypy supports `type: ignore[code]`. ty doesn't understand mypy's rule names. Therefore, ignore the +codes and suppress all errors. ```py a = test # type: ignore[name-defined] @@ -116,7 +116,7 @@ a = test + 2 # type: ignoree ## Invalid - ignore on opening parentheses `type: ignore` comments after an opening parentheses suppress any type errors inside the parentheses -in Pyright. Neither Ruff, nor mypy support this and neither does Red Knot. +in Pyright. Neither Ruff, nor mypy support this and neither does ty. ```py # fmt: off diff --git a/crates/red_knot_python_semantic/resources/mdtest/sys_platform.md b/crates/ty_python_semantic/resources/mdtest/sys_platform.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/sys_platform.md rename to crates/ty_python_semantic/resources/mdtest/sys_platform.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md b/crates/ty_python_semantic/resources/mdtest/sys_version_info.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md rename to crates/ty_python_semantic/resources/mdtest/sys_version_info.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/terminal_statements.md b/crates/ty_python_semantic/resources/mdtest/terminal_statements.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/terminal_statements.md rename to crates/ty_python_semantic/resources/mdtest/terminal_statements.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_api.md b/crates/ty_python_semantic/resources/mdtest/type_api.md similarity index 83% rename from crates/red_knot_python_semantic/resources/mdtest/type_api.md rename to crates/ty_python_semantic/resources/mdtest/type_api.md index c767fe98a0008f..5e1621ccb22a8b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_api.md +++ b/crates/ty_python_semantic/resources/mdtest/type_api.md @@ -1,14 +1,14 @@ -# Type API (`knot_extensions`) +# Type API (`ty_extensions`) -This document describes the internal `knot_extensions` API for creating and manipulating types as -well as testing various type system properties. +This document describes the internal `ty_extensions` API for creating and manipulating types as well +as testing various type system properties. ## Type extensions The Python language itself allows us to perform a variety of operations on types. For example, we can build a union of types like `int | None`, or we can use type constructors such as `list[int]` -and `type[int]` to create new types. But some type-level operations that we rely on in Red Knot, -like intersections, cannot yet be expressed in Python. The `knot_extensions` module provides the +and `type[int]` to create new types. But some type-level operations that we rely on in ty, like +intersections, cannot yet be expressed in Python. The `ty_extensions` module provides the `Intersection` and `Not` type constructors (special forms) which allow us to construct these types directly. @@ -16,14 +16,14 @@ directly. ```py from typing import Literal -from knot_extensions import Not, static_assert +from ty_extensions import Not, static_assert def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None: reveal_type(n1) # revealed: ~int reveal_type(n2) # revealed: int reveal_type(n3) # revealed: ~int -# error: "Special form `knot_extensions.Not` expected exactly one type parameter" +# error: "Special form `ty_extensions.Not` expected exactly one type parameter" n: Not[int, str] def static_truthiness(not_one: Not[Literal[1]]) -> None: @@ -48,7 +48,7 @@ python-version = "3.12" ``` ```py -from knot_extensions import Intersection, Not, is_subtype_of, static_assert +from ty_extensions import Intersection, Not, is_subtype_of, static_assert from typing_extensions import Literal, Never class S: ... @@ -87,7 +87,7 @@ The `Unknown` type is a special type that we use to represent actually unknown t annotation), as opposed to `Any` which represents an explicitly unknown type. ```py -from knot_extensions import Unknown, static_assert, is_assignable_to, is_fully_static +from ty_extensions import Unknown, static_assert, is_assignable_to, is_fully_static static_assert(is_assignable_to(Unknown, int)) static_assert(is_assignable_to(int, Unknown)) @@ -108,7 +108,7 @@ class C(Unknown): ... # revealed: tuple[Literal[C], Unknown, Literal[object]] reveal_type(C.__mro__) -# error: "Special form `knot_extensions.Unknown` expected no type parameter" +# error: "Special form `ty_extensions.Unknown` expected no type parameter" u: Unknown[str] ``` @@ -122,7 +122,7 @@ They do not accept any type arguments. ```py from typing_extensions import Literal -from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_subtype_of, static_assert +from ty_extensions import AlwaysFalsy, AlwaysTruthy, is_subtype_of, static_assert static_assert(is_subtype_of(Literal[True], AlwaysTruthy)) static_assert(is_subtype_of(Literal[False], AlwaysFalsy)) @@ -146,12 +146,12 @@ def f( ### Basics -The `knot_extensions` module provides a `static_assert` function that can be used to enforce +The `ty_extensions` module provides a `static_assert` function that can be used to enforce properties at type-check time. The function takes an arbitrary expression and raises a type error if the expression is not of statically known truthiness. ```py -from knot_extensions import static_assert +from ty_extensions import static_assert from typing import TYPE_CHECKING import sys @@ -182,7 +182,7 @@ static_assert(sys.version_info >= (3, 6)) Static assertions can be used to enforce narrowing constraints: ```py -from knot_extensions import static_assert +from ty_extensions import static_assert def f(x: int | None) -> None: if x is not None: @@ -196,7 +196,7 @@ def f(x: int | None) -> None: See also: ```py -from knot_extensions import static_assert +from ty_extensions import static_assert static_assert(True) static_assert(False) # error: "Static assertion error: argument evaluates to `False`" @@ -221,7 +221,7 @@ static_assert(b"") # error: "Static assertion error: argument of type `Literal[ We provide various tailored error messages for wrong argument types to `static_assert`: ```py -from knot_extensions import static_assert +from ty_extensions import static_assert static_assert(2 * 3 == 6) @@ -244,7 +244,7 @@ static_assert(InvalidBoolDunder()) Alternatively, users can provide custom error messages: ```py -from knot_extensions import static_assert +from ty_extensions import static_assert # error: "Static assertion error: I really want this to be true" static_assert(1 + 1 == 3, "I really want this to be true") @@ -266,14 +266,14 @@ static_assert(False, shouted_message) ## Type predicates -The `knot_extensions` module also provides predicates to test various properties of types. These are +The `ty_extensions` module also provides predicates to test various properties of types. These are implemented as functions that return `Literal[True]` or `Literal[False]` depending on the result of the test. ### Equivalence ```py -from knot_extensions import is_equivalent_to, static_assert +from ty_extensions import is_equivalent_to, static_assert from typing_extensions import Never, Union static_assert(is_equivalent_to(type, type[object])) @@ -287,7 +287,7 @@ static_assert(not is_equivalent_to(int | str, int | str | bytes)) ### Subtyping ```py -from knot_extensions import is_subtype_of, static_assert +from ty_extensions import is_subtype_of, static_assert static_assert(is_subtype_of(bool, int)) static_assert(not is_subtype_of(str, int)) @@ -311,7 +311,7 @@ static_assert(not is_subtype_of(Base, Unrelated)) ### Assignability ```py -from knot_extensions import is_assignable_to, static_assert +from ty_extensions import is_assignable_to, static_assert from typing import Any static_assert(is_assignable_to(int, Any)) @@ -322,7 +322,7 @@ static_assert(not is_assignable_to(int, str)) ### Disjointness ```py -from knot_extensions import is_disjoint_from, static_assert +from ty_extensions import is_disjoint_from, static_assert from typing import Literal static_assert(is_disjoint_from(None, int)) @@ -332,7 +332,7 @@ static_assert(not is_disjoint_from(Literal[2] | str, int)) ### Fully static types ```py -from knot_extensions import is_fully_static, static_assert +from ty_extensions import is_fully_static, static_assert from typing import Any static_assert(is_fully_static(int | str)) @@ -345,7 +345,7 @@ static_assert(not is_fully_static(type[Any])) ### Singleton types ```py -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert from typing import Literal static_assert(is_singleton(None)) @@ -358,7 +358,7 @@ static_assert(not is_singleton(Literal["a"])) ### Single-valued types ```py -from knot_extensions import is_single_valued, static_assert +from ty_extensions import is_single_valued, static_assert from typing import Literal static_assert(is_single_valued(None)) @@ -378,7 +378,7 @@ type `str` itself is a subtype of `type[str]`. Instead, we can use `TypeOf[str]` the expression `str`: ```py -from knot_extensions import TypeOf, is_subtype_of, static_assert +from ty_extensions import TypeOf, is_subtype_of, static_assert # This is incorrect and therefore fails with ... # error: "Static assertion error: argument evaluates to `False`" @@ -402,10 +402,10 @@ def type_of_annotation() -> None: s1: type[Base] = Base s2: type[Base] = Derived # no error here -# error: "Special form `knot_extensions.TypeOf` expected exactly one type parameter" +# error: "Special form `ty_extensions.TypeOf` expected exactly one type parameter" t: TypeOf[int, str, bytes] -# error: [invalid-type-form] "`knot_extensions.TypeOf` requires exactly one argument when used in a type expression" +# error: [invalid-type-form] "`ty_extensions.TypeOf` requires exactly one argument when used in a type expression" def f(x: TypeOf) -> None: reveal_type(x) # revealed: Unknown ``` @@ -419,7 +419,7 @@ which can then be used to test various type properties. It accepts a single type parameter which is expected to be a callable object. ```py -from knot_extensions import CallableTypeOf +from ty_extensions import CallableTypeOf def f1(): return @@ -430,13 +430,13 @@ def f2() -> int: def f3(x: int, y: str) -> None: return -# error: [invalid-type-form] "Special form `knot_extensions.CallableTypeOf` expected exactly one type parameter" +# error: [invalid-type-form] "Special form `ty_extensions.CallableTypeOf` expected exactly one type parameter" c1: CallableTypeOf[f1, f2] -# error: [invalid-type-form] "Expected the first argument to `knot_extensions.CallableTypeOf` to be a callable object, but got an object of type `Literal["foo"]`" +# error: [invalid-type-form] "Expected the first argument to `ty_extensions.CallableTypeOf` to be a callable object, but got an object of type `Literal["foo"]`" c2: CallableTypeOf["foo"] -# error: [invalid-type-form] "`knot_extensions.CallableTypeOf` requires exactly one argument when used in a type expression" +# error: [invalid-type-form] "`ty_extensions.CallableTypeOf` requires exactly one argument when used in a type expression" def f(x: CallableTypeOf) -> None: reveal_type(x) # revealed: Unknown ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md rename to crates/ty_python_semantic/resources/mdtest/type_of/basic.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md b/crates/ty_python_semantic/resources/mdtest/type_of/dynamic.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md rename to crates/ty_python_semantic/resources/mdtest/type_of/dynamic.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/typing_dot_Type.md b/crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/type_of/typing_dot_Type.md rename to crates/ty_python_semantic/resources/mdtest/type_of/typing_dot_Type.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 840c4f48e343c1..93238ed1377d6b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -13,7 +13,7 @@ Fully static types participate in subtyping. If a type `S` is a subtype of `T`, assignable to `T`. Two equivalent types are subtypes of each other: ```py -from knot_extensions import static_assert, is_assignable_to +from ty_extensions import static_assert, is_assignable_to class Parent: ... class Child1(Parent): ... @@ -40,7 +40,7 @@ Gradual types do not participate in subtyping, but can still be assignable to ot static types can be assignable to gradual types): ```py -from knot_extensions import static_assert, is_assignable_to, Unknown +from ty_extensions import static_assert, is_assignable_to, Unknown from typing import Any, Literal static_assert(is_assignable_to(Unknown, Literal[1])) @@ -64,7 +64,7 @@ static_assert(not is_assignable_to(int, SubtypeOfAny)) which is in turn a subtype of `int`: ```py -from knot_extensions import static_assert, is_assignable_to +from ty_extensions import static_assert, is_assignable_to from typing import Literal static_assert(is_assignable_to(Literal[True], Literal[True])) @@ -78,7 +78,7 @@ static_assert(not is_assignable_to(bool, Literal[True])) ### Integer literals ```py -from knot_extensions import static_assert, is_assignable_to +from ty_extensions import static_assert, is_assignable_to from typing import Literal static_assert(is_assignable_to(Literal[1], Literal[1])) @@ -95,7 +95,7 @@ All string-literal types are subtypes of (and therefore assignable to) `LiteralS turn a subtype of `str`: ```py -from knot_extensions import static_assert, is_assignable_to +from ty_extensions import static_assert, is_assignable_to from typing_extensions import Literal, LiteralString static_assert(is_assignable_to(Literal["foo"], Literal["foo"])) @@ -112,7 +112,7 @@ static_assert(not is_assignable_to(str, LiteralString)) ### Byte literals ```py -from knot_extensions import static_assert, is_assignable_to +from ty_extensions import static_assert, is_assignable_to from typing_extensions import Literal, LiteralString static_assert(is_assignable_to(Literal[b"foo"], bytes)) @@ -136,7 +136,7 @@ Both `TypeOf[str]` and `type[str]` are subtypes of `type` and `type[object]`, wh is known to be no larger than the set of possible objects represented by `type`. ```py -from knot_extensions import static_assert, is_assignable_to, Unknown, TypeOf +from ty_extensions import static_assert, is_assignable_to, Unknown, TypeOf from typing import Any static_assert(is_assignable_to(type, type)) @@ -190,7 +190,7 @@ static_assert(is_assignable_to(Meta, type[Unknown])) ## Tuple types ```py -from knot_extensions import static_assert, is_assignable_to, AlwaysTruthy, AlwaysFalsy +from ty_extensions import static_assert, is_assignable_to, AlwaysTruthy, AlwaysFalsy from typing import Literal, Any static_assert(is_assignable_to(tuple[()], tuple[()])) @@ -224,7 +224,7 @@ static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str])) ## Union types ```py -from knot_extensions import AlwaysTruthy, AlwaysFalsy, static_assert, is_assignable_to, Unknown +from ty_extensions import AlwaysTruthy, AlwaysFalsy, static_assert, is_assignable_to, Unknown from typing_extensions import Literal, Any, LiteralString static_assert(is_assignable_to(int, int | str)) @@ -259,7 +259,7 @@ static_assert(not is_assignable_to(Literal[True] | AlwaysFalsy, Literal[False] | ## Intersection types ```py -from knot_extensions import static_assert, is_assignable_to, Intersection, Not, AlwaysTruthy, AlwaysFalsy +from ty_extensions import static_assert, is_assignable_to, Intersection, Not, AlwaysTruthy, AlwaysFalsy from typing_extensions import Any, Literal, final, LiteralString class Parent: ... @@ -347,7 +347,7 @@ See also: our property tests in `property_tests.rs`. `object` is Python's top type; the set of all possible objects at runtime: ```py -from knot_extensions import static_assert, is_assignable_to, Unknown +from ty_extensions import static_assert, is_assignable_to, Unknown from typing import Literal, Any static_assert(is_assignable_to(str, object)) @@ -367,7 +367,7 @@ static_assert(is_assignable_to(type[Any], object)) any type is assignable to them: ```py -from knot_extensions import static_assert, is_assignable_to, Unknown +from ty_extensions import static_assert, is_assignable_to, Unknown from typing import Literal, Any static_assert(is_assignable_to(str, Any)) @@ -397,7 +397,7 @@ static_assert(is_assignable_to(type[Any], Unknown)) assignable to any arbitrary type. ```py -from knot_extensions import static_assert, is_assignable_to, Unknown +from ty_extensions import static_assert, is_assignable_to, Unknown from typing_extensions import Never, Any, Literal static_assert(is_assignable_to(Never, str)) @@ -420,7 +420,7 @@ are covered in the [subtyping tests](./is_subtype_of.md#callable). ### Return type ```py -from knot_extensions import CallableTypeOf, Unknown, static_assert, is_assignable_to +from ty_extensions import CallableTypeOf, Unknown, static_assert, is_assignable_to from typing import Any, Callable static_assert(is_assignable_to(Callable[[], Any], Callable[[], int])) @@ -450,7 +450,7 @@ A `Callable` which uses the gradual form (`...`) for the parameter types is cons input signature. ```py -from knot_extensions import CallableTypeOf, static_assert, is_assignable_to +from ty_extensions import CallableTypeOf, static_assert, is_assignable_to from typing import Any, Callable static_assert(is_assignable_to(Callable[[], None], Callable[..., None])) @@ -576,7 +576,7 @@ c: Callable[[int], str] = overloaded ```py from typing import Callable, Any -from knot_extensions import static_assert, is_assignable_to +from ty_extensions import static_assert, is_assignable_to class TakesAny: def __call__(self, a: Any) -> str: diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md similarity index 92% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index 981851a6c751bf..2671887d954104 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -7,7 +7,7 @@ This means that it is known that no possible runtime object inhabits both types ```py from typing_extensions import Literal, LiteralString, Any -from knot_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert +from ty_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert static_assert(is_disjoint_from(bool, str)) static_assert(not is_disjoint_from(bool, bool)) @@ -27,7 +27,7 @@ static_assert(not is_disjoint_from(str, type[Any])) ## Class hierarchies ```py -from knot_extensions import is_disjoint_from, static_assert, Intersection, is_subtype_of +from ty_extensions import is_disjoint_from, static_assert, Intersection, is_subtype_of from typing import final class A: ... @@ -75,7 +75,7 @@ static_assert(is_disjoint_from(UsesMeta1, UsesMeta2)) ```py from typing_extensions import Literal, Never -from knot_extensions import TypeOf, is_disjoint_from, static_assert +from ty_extensions import TypeOf, is_disjoint_from, static_assert static_assert(is_disjoint_from(tuple[()], TypeOf[object])) static_assert(is_disjoint_from(tuple[()], TypeOf[Literal])) @@ -97,7 +97,7 @@ static_assert(not is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[ ```py from typing_extensions import Literal -from knot_extensions import Intersection, is_disjoint_from, static_assert +from ty_extensions import Intersection, is_disjoint_from, static_assert static_assert(is_disjoint_from(Literal[1, 2], Literal[3])) static_assert(is_disjoint_from(Literal[1, 2], Literal[3, 4])) @@ -110,7 +110,7 @@ static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3])) ```py from typing_extensions import Literal, final, Any -from knot_extensions import Intersection, is_disjoint_from, static_assert, Not +from ty_extensions import Intersection, is_disjoint_from, static_assert, Not @final class P: ... @@ -175,7 +175,7 @@ static_assert(is_disjoint_from(Not[int], Intersection[int, Any])) ```py from typing_extensions import Never -from knot_extensions import is_disjoint_from, static_assert +from ty_extensions import is_disjoint_from, static_assert static_assert(is_disjoint_from(Never, Never)) static_assert(is_disjoint_from(Never, None)) @@ -187,7 +187,7 @@ static_assert(is_disjoint_from(Never, object)) ```py from typing_extensions import Literal, LiteralString -from knot_extensions import is_disjoint_from, static_assert, Intersection, Not +from ty_extensions import is_disjoint_from, static_assert, Intersection, Not static_assert(is_disjoint_from(None, Literal[True])) static_assert(is_disjoint_from(None, Literal[1])) @@ -209,7 +209,7 @@ static_assert(is_disjoint_from(None, Intersection[int, Not[str]])) ```py from typing_extensions import Literal, LiteralString -from knot_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert, AlwaysFalsy, AlwaysTruthy +from ty_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert, AlwaysFalsy, AlwaysTruthy static_assert(is_disjoint_from(Literal[True], Literal[False])) static_assert(is_disjoint_from(Literal[True], Literal[1])) @@ -266,7 +266,7 @@ python-version = "3.12" ```py from types import ModuleType, FunctionType -from knot_extensions import TypeOf, is_disjoint_from, static_assert +from ty_extensions import TypeOf, is_disjoint_from, static_assert class A: ... class B: ... @@ -306,7 +306,7 @@ static_assert(not is_disjoint_from(TypeOf[f], object)) ### `AlwaysTruthy` and `AlwaysFalsy` ```py -from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert +from ty_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert from typing import Literal static_assert(is_disjoint_from(None, AlwaysTruthy)) @@ -327,7 +327,7 @@ the instance type is not a subclass of `T`'s metaclass. ```py from typing import final -from knot_extensions import is_disjoint_from, static_assert +from ty_extensions import is_disjoint_from, static_assert @final class Foo: ... @@ -360,7 +360,7 @@ metaclass of `T` is disjoint from the metaclass of `S`. ```py from typing import final -from knot_extensions import static_assert, is_disjoint_from +from ty_extensions import static_assert, is_disjoint_from @final class Meta1(type): ... @@ -383,7 +383,7 @@ As such, for any two callable types, it is possible to conceive of a runtime cal would inhabit both types simultaneously. ```py -from knot_extensions import CallableTypeOf, is_disjoint_from, static_assert +from ty_extensions import CallableTypeOf, is_disjoint_from, static_assert from typing_extensions import Callable, Literal, Never def mixed(a: int, /, b: str, *args: int, c: int = 2, **kwargs: int) -> None: ... @@ -404,7 +404,7 @@ static_assert(not is_disjoint_from(Callable[[Never], str], Callable[[Never], int A callable type is disjoint from all literal types. ```py -from knot_extensions import CallableTypeOf, is_disjoint_from, static_assert +from ty_extensions import CallableTypeOf, is_disjoint_from, static_assert from typing_extensions import Callable, Literal, Never static_assert(is_disjoint_from(Callable[[], None], Literal[""])) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index 69efe5958abc30..885153d32b8b97 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -9,7 +9,7 @@ Two types `A` and `B` are equivalent iff `A` is a subtype of `B` and `B` is a su ```py from typing import Any from typing_extensions import Literal -from knot_extensions import Unknown, is_equivalent_to, static_assert +from ty_extensions import Unknown, is_equivalent_to, static_assert static_assert(is_equivalent_to(Literal[1, 2], Literal[1, 2])) static_assert(is_equivalent_to(type[object], type)) @@ -25,7 +25,7 @@ static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 2, 3])) ```py from typing_extensions import Literal -from knot_extensions import is_equivalent_to, static_assert +from ty_extensions import is_equivalent_to, static_assert static_assert(is_equivalent_to(type, type[object])) static_assert(not is_equivalent_to(Literal[1, 0], Literal[1, 2])) @@ -35,7 +35,7 @@ static_assert(not is_equivalent_to(Literal[1, 2, 3], Literal[1, 2])) ## Differently ordered intersections and unions are equivalent ```py -from knot_extensions import is_equivalent_to, static_assert, Intersection, Not +from ty_extensions import is_equivalent_to, static_assert, Intersection, Not class P: ... class Q: ... @@ -69,7 +69,7 @@ static_assert(is_equivalent_to(Intersection[Q | R, Not[P | S]], Intersection[Not ## Tuples containing equivalent but differently ordered unions/intersections are equivalent ```py -from knot_extensions import is_equivalent_to, TypeOf, static_assert, Intersection, Not +from ty_extensions import is_equivalent_to, TypeOf, static_assert, Intersection, Not from typing import Literal class P: ... @@ -87,7 +87,7 @@ static_assert( ## Unions containing tuples containing tuples containing unions (etc.) ```py -from knot_extensions import is_equivalent_to, static_assert, Intersection +from ty_extensions import is_equivalent_to, static_assert, Intersection class P: ... class Q: ... @@ -109,7 +109,7 @@ static_assert( ## Intersections containing tuples containing unions ```py -from knot_extensions import is_equivalent_to, static_assert, Intersection +from ty_extensions import is_equivalent_to, static_assert, Intersection class P: ... class Q: ... @@ -127,7 +127,7 @@ the parameter in one of the callable has a default value then the corresponding other callable should also have a default value. ```py -from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert +from ty_extensions import CallableTypeOf, is_equivalent_to, static_assert from typing import Callable def f1(a: int = 1) -> None: ... @@ -163,7 +163,7 @@ static_assert(is_equivalent_to(CallableTypeOf[f5] | bool | CallableTypeOf[f6], C There are multiple cases when two callable types are not equivalent which are enumerated below. ```py -from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert +from ty_extensions import CallableTypeOf, is_equivalent_to, static_assert from typing import Callable ``` @@ -249,7 +249,7 @@ Differently ordered unions inside `Callable`s inside unions can still be equival ```py from typing import Callable -from knot_extensions import is_equivalent_to, static_assert +from ty_extensions import is_equivalent_to, static_assert static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str | int], None] | int)) ``` @@ -276,7 +276,7 @@ def overloaded(a: Grandparent) -> None: ... ``` ```py -from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert +from ty_extensions import CallableTypeOf, is_equivalent_to, static_assert from overloaded import Grandparent, Parent, Child, overloaded def grandparent(a: Grandparent) -> None: ... @@ -310,7 +310,7 @@ def cpg(a: Grandparent) -> None: ... ``` ```py -from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert +from ty_extensions import CallableTypeOf, is_equivalent_to, static_assert from overloaded import pg, cpg static_assert(is_equivalent_to(CallableTypeOf[pg], CallableTypeOf[cpg])) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md similarity index 89% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md index 9a44ce090bbd5d..3551742960f78c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_fully_static.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md @@ -6,7 +6,7 @@ A type is fully static iff it does not contain any gradual forms. ```py from typing_extensions import Literal, LiteralString, Never, Callable -from knot_extensions import Intersection, Not, TypeOf, is_fully_static, static_assert +from ty_extensions import Intersection, Not, TypeOf, is_fully_static, static_assert static_assert(is_fully_static(Never)) static_assert(is_fully_static(None)) @@ -39,7 +39,7 @@ static_assert(is_fully_static(type[object])) ```py from typing_extensions import Any, Literal, LiteralString, Callable -from knot_extensions import Intersection, Not, TypeOf, Unknown, is_fully_static, static_assert +from ty_extensions import Intersection, Not, TypeOf, Unknown, is_fully_static, static_assert static_assert(not is_fully_static(Any)) static_assert(not is_fully_static(Unknown)) @@ -57,7 +57,7 @@ static_assert(not is_fully_static(type[Any])) ```py from typing_extensions import Callable, Any -from knot_extensions import Unknown, is_fully_static, static_assert +from ty_extensions import Unknown, is_fully_static, static_assert static_assert(is_fully_static(Callable[[], int])) static_assert(is_fully_static(Callable[[int, str], int])) @@ -80,7 +80,7 @@ Using function literals, we can check more variations of callable types as it al parameters without annotations and no return type. ```py -from knot_extensions import CallableTypeOf, is_fully_static, static_assert +from ty_extensions import CallableTypeOf, is_fully_static, static_assert def f00() -> None: ... def f01(a: int, b: str) -> None: ... @@ -121,7 +121,7 @@ def static(x: str) -> str: ... ``` ```py -from knot_extensions import CallableTypeOf, TypeOf, is_fully_static, static_assert +from ty_extensions import CallableTypeOf, TypeOf, is_fully_static, static_assert from overloaded import gradual, static static_assert(is_fully_static(TypeOf[gradual])) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md index 98061b31033767..e3de427e4ee92e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md @@ -8,7 +8,7 @@ materializations of `B`, and all materializations of `B` are also materializatio ```py from typing import Any from typing_extensions import Literal, LiteralString, Never -from knot_extensions import AlwaysFalsy, AlwaysTruthy, TypeOf, Unknown, is_gradual_equivalent_to, static_assert +from ty_extensions import AlwaysFalsy, AlwaysTruthy, TypeOf, Unknown, is_gradual_equivalent_to, static_assert static_assert(is_gradual_equivalent_to(Any, Any)) static_assert(is_gradual_equivalent_to(Unknown, Unknown)) @@ -34,7 +34,7 @@ static_assert(not is_gradual_equivalent_to(type[object], type[Any])) ```py from typing import Any -from knot_extensions import Intersection, Not, Unknown, is_gradual_equivalent_to, static_assert +from ty_extensions import Intersection, Not, Unknown, is_gradual_equivalent_to, static_assert static_assert(is_gradual_equivalent_to(str | int, str | int)) static_assert(is_gradual_equivalent_to(str | int | Any, str | int | Unknown)) @@ -54,7 +54,7 @@ static_assert(is_gradual_equivalent_to(Unknown, Intersection[Unknown, Any])) ## Tuples ```py -from knot_extensions import Unknown, is_gradual_equivalent_to, static_assert +from ty_extensions import Unknown, is_gradual_equivalent_to, static_assert from typing import Any static_assert(is_gradual_equivalent_to(tuple[str, Any], tuple[str, Unknown])) @@ -70,7 +70,7 @@ gradual types. The cases with fully static types and using different combination are covered in the [equivalence tests](./is_equivalent_to.md#callable). ```py -from knot_extensions import Unknown, CallableTypeOf, is_gradual_equivalent_to, static_assert +from ty_extensions import Unknown, CallableTypeOf, is_gradual_equivalent_to, static_assert from typing import Any, Callable static_assert(is_gradual_equivalent_to(Callable[..., int], Callable[..., int])) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_single_valued.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md similarity index 94% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/is_single_valued.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md index b6339cc6b0d7d2..e947c248831f7f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_single_valued.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md @@ -5,7 +5,7 @@ A type is single-valued iff it is not empty and all inhabitants of it compare eq ```py import types from typing_extensions import Any, Literal, LiteralString, Never, Callable -from knot_extensions import is_single_valued, static_assert, TypeOf +from ty_extensions import is_single_valued, static_assert, TypeOf static_assert(is_single_valued(None)) static_assert(is_single_valued(Literal[True])) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_singleton.md similarity index 88% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/is_singleton.md index 4673affed67bc0..a3c41449bc76cb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_singleton.md @@ -6,7 +6,7 @@ A type is a singleton type iff it has exactly one inhabitant. ```py from typing_extensions import Literal, Never, Callable -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert static_assert(is_singleton(None)) static_assert(is_singleton(Literal[True])) @@ -39,7 +39,7 @@ python-version = "3.12" ```py from typing_extensions import _NoDefaultType -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert static_assert(is_singleton(_NoDefaultType)) ``` @@ -53,7 +53,7 @@ python-version = "3.13" ```py from typing import _NoDefaultType -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert static_assert(is_singleton(_NoDefaultType)) ``` @@ -72,7 +72,7 @@ python-version = "3.9" ``` ```py -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert static_assert(is_singleton(Ellipsis.__class__)) static_assert(is_singleton((...).__class__)) @@ -90,7 +90,7 @@ python-version = "3.10" ```py import types -from knot_extensions import static_assert, is_singleton +from ty_extensions import static_assert, is_singleton static_assert(is_singleton(types.EllipsisType)) ``` @@ -108,7 +108,7 @@ python-version = "3.9" ``` ```py -from knot_extensions import is_singleton, static_assert +from ty_extensions import is_singleton, static_assert static_assert(is_singleton(NotImplemented.__class__)) ``` @@ -126,7 +126,7 @@ python-version = "3.10" ```py import types -from knot_extensions import static_assert, is_singleton +from ty_extensions import static_assert, is_singleton static_assert(is_singleton(types.NotImplementedType)) ``` @@ -145,7 +145,7 @@ have to hold true; it's more of a unit test for our current implementation. ```py import types from typing import Callable -from knot_extensions import static_assert, is_singleton, TypeOf +from ty_extensions import static_assert, is_singleton, TypeOf class A: def method(self): ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 413edf54b12d21..42b2f23572aa27 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -21,7 +21,7 @@ See the [typing documentation] for more information. as `int | float` and `int | float | complex`, respectively. ```py -from knot_extensions import is_subtype_of, static_assert, TypeOf +from ty_extensions import is_subtype_of, static_assert, TypeOf type JustFloat = TypeOf[1.0] type JustComplex = TypeOf[1j] @@ -49,7 +49,7 @@ static_assert(is_subtype_of(FloatingPointError, Exception)) ## Class hierarchies ```py -from knot_extensions import is_subtype_of, static_assert +from ty_extensions import is_subtype_of, static_assert from typing_extensions import Never class A: ... @@ -88,7 +88,7 @@ static_assert(is_subtype_of(C, object)) ```py from typing_extensions import Literal, LiteralString -from knot_extensions import is_subtype_of, static_assert, TypeOf +from ty_extensions import is_subtype_of, static_assert, TypeOf type JustFloat = TypeOf[1.0] @@ -121,7 +121,7 @@ static_assert(is_subtype_of(Literal[b"foo"], object)) ## Tuple types ```py -from knot_extensions import is_subtype_of, static_assert +from ty_extensions import is_subtype_of, static_assert class A1: ... class B1(A1): ... @@ -157,7 +157,7 @@ static_assert(is_subtype_of(tuple[int], tuple)) ## Union types ```py -from knot_extensions import is_subtype_of, static_assert +from ty_extensions import is_subtype_of, static_assert from typing import Literal class A: ... @@ -198,7 +198,7 @@ static_assert(not is_subtype_of(Literal[1, "two", 3], int)) ```py from typing_extensions import Literal, LiteralString -from knot_extensions import Intersection, Not, is_subtype_of, static_assert +from ty_extensions import Intersection, Not, is_subtype_of, static_assert class A: ... class B1(A): ... @@ -266,7 +266,7 @@ static_assert(not is_subtype_of(Literal[1], Intersection[int, Not[Literal[1]]])) ```py from typing_extensions import Literal, Never -from knot_extensions import AlwaysTruthy, AlwaysFalsy, is_subtype_of, static_assert +from ty_extensions import AlwaysTruthy, AlwaysFalsy, is_subtype_of, static_assert static_assert(is_subtype_of(Never, Never)) static_assert(is_subtype_of(Never, Literal[True])) @@ -281,7 +281,7 @@ static_assert(is_subtype_of(Never, AlwaysFalsy)) ### `AlwaysTruthy` and `AlwaysFalsy` ```py -from knot_extensions import AlwaysTruthy, AlwaysFalsy, Intersection, Not, is_subtype_of, static_assert +from ty_extensions import AlwaysTruthy, AlwaysFalsy, Intersection, Not, is_subtype_of, static_assert from typing_extensions import Literal, LiteralString static_assert(is_subtype_of(Literal[1], AlwaysTruthy)) @@ -323,7 +323,7 @@ static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], ```py from types import ModuleType -from knot_extensions import TypeOf, is_subtype_of, static_assert +from ty_extensions import TypeOf, is_subtype_of, static_assert from typing_extensions import assert_type import typing @@ -335,7 +335,7 @@ static_assert(is_subtype_of(TypeOf[typing], ModuleType)) ### Slice literals ```py -from knot_extensions import TypeOf, is_subtype_of, static_assert +from ty_extensions import TypeOf, is_subtype_of, static_assert static_assert(is_subtype_of(TypeOf[1:2:3], slice)) ``` @@ -344,7 +344,7 @@ static_assert(is_subtype_of(TypeOf[1:2:3], slice)) ```py from typing import _SpecialForm, Literal -from knot_extensions import TypeOf, is_subtype_of, static_assert +from ty_extensions import TypeOf, is_subtype_of, static_assert static_assert(is_subtype_of(TypeOf[Literal], _SpecialForm)) static_assert(is_subtype_of(TypeOf[Literal], object)) @@ -359,7 +359,7 @@ static_assert(not is_subtype_of(_SpecialForm, TypeOf[Literal])) ```py from typing import _SpecialForm from typing_extensions import Literal, assert_type -from knot_extensions import TypeOf, is_subtype_of, static_assert +from ty_extensions import TypeOf, is_subtype_of, static_assert class Meta(type): ... class HasCustomMetaclass(metaclass=Meta): ... @@ -425,7 +425,7 @@ static_assert(not is_subtype_of(Meta, type[type])) ```py from typing_extensions import assert_type -from knot_extensions import TypeOf, is_subtype_of, static_assert +from ty_extensions import TypeOf, is_subtype_of, static_assert class Base: ... class Derived(Base): ... @@ -458,7 +458,7 @@ static_assert(is_subtype_of(LiteralBase | LiteralUnrelated, object)) `Any`, `Unknown`, `Todo` and derivatives thereof do not participate in subtyping. ```py -from knot_extensions import Unknown, is_subtype_of, static_assert, Intersection +from ty_extensions import Unknown, is_subtype_of, static_assert, Intersection from typing_extensions import Any static_assert(not is_subtype_of(Any, Any)) @@ -499,7 +499,7 @@ Return types are covariant. ```py from typing import Callable -from knot_extensions import is_subtype_of, static_assert, TypeOf +from ty_extensions import is_subtype_of, static_assert, TypeOf static_assert(is_subtype_of(Callable[[], int], Callable[[], float])) static_assert(not is_subtype_of(Callable[[], float], Callable[[], int])) @@ -509,7 +509,7 @@ static_assert(not is_subtype_of(Callable[[], float], Callable[[], int])) ```py from typing import Callable -from knot_extensions import is_subtype_of, static_assert, TypeOf +from ty_extensions import is_subtype_of, static_assert, TypeOf flag: bool = True @@ -535,7 +535,7 @@ Parameter types are contravariant. ```py from typing import Callable -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf def float_param(a: float, /) -> None: ... def int_param(a: int, /) -> None: ... @@ -580,7 +580,7 @@ corresponding position in the supertype does not need to have a default value. ```py from typing import Callable -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf def float_with_default(a: float = 1, /) -> None: ... def int_with_default(a: int = 1, /) -> None: ... @@ -626,7 +626,7 @@ If a parameter is declared as positional-only, then the corresponding parameter cannot be any other parameter kind. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def positional_only(a: int, /) -> None: ... def standard(a: int) -> None: ... @@ -647,7 +647,7 @@ A standard parameter is either a positional or a keyword parameter. Unlike positional-only parameters, standard parameters should have the same name in the subtype. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def int_param_a(a: int) -> None: ... def int_param_b(b: int) -> None: ... @@ -709,7 +709,7 @@ parameter in the subtype with the same name. This is because a standard paramete than a keyword-only parameter. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def standard_a(a: int) -> None: ... def keyword_b(*, b: int) -> None: ... @@ -747,7 +747,7 @@ parameter in the subtype at the same position. This is because a standard parame than a positional-only parameter. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def standard_a(a: int) -> None: ... def positional_b(b: int, /) -> None: ... @@ -787,7 +787,7 @@ A variadic or keyword-variadic parameter in the supertype cannot be substituted parameter in the subtype. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def standard(a: int) -> None: ... def variadic(*a: int) -> None: ... @@ -802,7 +802,7 @@ static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keyword The name of the variadic parameter does not need to be the same in the subtype. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def variadic_float(*args2: float) -> None: ... def variadic_int(*args1: int) -> None: ... @@ -826,7 +826,7 @@ If the subtype has a variadic parameter then any unmatched positional-only param supertype should be checked against the variadic parameter. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def variadic(a: int, /, *args: float) -> None: ... @@ -846,7 +846,7 @@ Variadic parameter in a subtype can only be used to match against an unmatched p parameters from the supertype, not any other parameter kind. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def variadic(*args: int) -> None: ... @@ -914,7 +914,7 @@ static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[stand For keyword-only parameters, the name should be the same: ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def keyword_int(*, a: int) -> None: ... def keyword_float(*, a: float) -> None: ... @@ -938,7 +938,7 @@ static_assert(not is_subtype_of(CallableTypeOf[keyword_ba], CallableTypeOf[keywo #### Keyword-only with default ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def float_with_default(*, a: float = 1) -> None: ... def int_with_default(*, a: int = 1) -> None: ... @@ -969,7 +969,7 @@ static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[mixe #### Keyword-only with standard ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def keywords1(*, a: int, b: int) -> None: ... def standard(b: float, a: float) -> None: ... @@ -1019,7 +1019,7 @@ static_assert(is_subtype_of(CallableTypeOf[mixed_variadic], CallableTypeOf[keywo The name of the keyword-variadic parameter does not need to be the same in the subtype. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def kwargs_float(**kwargs2: float) -> None: ... def kwargs_int(**kwargs1: int) -> None: ... @@ -1043,7 +1043,7 @@ If the subtype has a keyword-variadic parameter then any unmatched keyword-only supertype should be checked against the keyword-variadic parameter. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def kwargs(**kwargs: float) -> None: ... def keyword_only(*, a: int, b: float, c: bool) -> None: ... @@ -1071,7 +1071,7 @@ When the supertype has an empty list of parameters, then the subtype can have an as long as they contain the default values for non-variadic parameters. ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert def empty() -> None: ... def mixed(a: int = 1, /, b: int = 2, *args: int, c: int = 3, **kwargs: int) -> None: ... @@ -1083,7 +1083,7 @@ static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[mixed])) #### Object ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf from typing import Callable def f1(a: int, b: str, /, *c: float, d: int = 1, **e: float) -> None: ... @@ -1108,7 +1108,7 @@ static_assert(not is_subtype_of(object, TypeOf[C.foo])) ```py from typing import Callable -from knot_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to +from ty_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to class A: def __call__(self, a: int) -> int: @@ -1132,7 +1132,7 @@ f(a) ```py from typing import Callable, overload from typing_extensions import Self -from knot_extensions import TypeOf, static_assert, is_subtype_of +from ty_extensions import TypeOf, static_assert, is_subtype_of class MetaWithReturn(type): def __call__(cls) -> "A": @@ -1170,7 +1170,7 @@ static_assert(is_subtype_of(TypeOf[C], Callable[[], str])) ```py from typing import Callable -from knot_extensions import TypeOf, static_assert, is_subtype_of +from ty_extensions import TypeOf, static_assert, is_subtype_of class A: def __new__(cls, a: int) -> int: @@ -1202,7 +1202,7 @@ If `__call__` and `__new__` are both present, `__call__` takes precedence. ```py from typing import Callable -from knot_extensions import TypeOf, static_assert, is_subtype_of +from ty_extensions import TypeOf, static_assert, is_subtype_of class MetaWithIntReturn(type): def __call__(cls) -> int: @@ -1220,7 +1220,7 @@ static_assert(not is_subtype_of(TypeOf[F], Callable[[], str])) ```py from typing import Callable -from knot_extensions import TypeOf, static_assert, is_subtype_of +from ty_extensions import TypeOf, static_assert, is_subtype_of class A: def f(self, a: int) -> int: @@ -1267,7 +1267,7 @@ def overloaded(x: B) -> None: ... ``` ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert from overloaded import A, B, C, overloaded def accepts_a(x: A) -> None: ... @@ -1302,7 +1302,7 @@ def overloaded(a: Grandparent) -> None: ... ``` ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert from overloaded import Grandparent, Parent, Child, overloaded # This is a subtype of only the first overload @@ -1373,7 +1373,7 @@ def empty_cp(a: Parent) -> None: ... ``` ```py -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert from overloaded import pg, po, go, cpg, empty_go, empty_cp static_assert(is_subtype_of(CallableTypeOf[pg], CallableTypeOf[cpg])) @@ -1420,7 +1420,7 @@ def overload_ba(x: A) -> None: ... ```py from overloaded import overload_ab, overload_ba -from knot_extensions import CallableTypeOf, is_subtype_of, static_assert +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert static_assert(is_subtype_of(CallableTypeOf[overload_ab], CallableTypeOf[overload_ba])) static_assert(is_subtype_of(CallableTypeOf[overload_ba], CallableTypeOf[overload_ab])) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/str_repr.md b/crates/ty_python_semantic/resources/mdtest/type_properties/str_repr.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/str_repr.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/str_repr.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md similarity index 93% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md index 00e7c095390885..4cb8bdbc458e6c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -4,7 +4,7 @@ ```py from typing_extensions import Literal, LiteralString -from knot_extensions import AlwaysFalsy, AlwaysTruthy +from ty_extensions import AlwaysFalsy, AlwaysTruthy def _( a: Literal[1], @@ -94,8 +94,8 @@ reveal_type(bool(PossiblyUnboundTrue())) # revealed: bool ### Special-cased classes -Some special-cased `@final` classes are known by red-knot to have instances that are either always -truthy or always falsy. +Some special-cased `@final` classes are known by ty to have instances that are either always truthy +or always falsy. ```toml [environment] @@ -106,7 +106,7 @@ python-version = "3.12" import types import typing import sys -from knot_extensions import AlwaysTruthy, static_assert, is_subtype_of +from ty_extensions import AlwaysTruthy, static_assert, is_subtype_of from typing_extensions import _NoDefaultType static_assert(is_subtype_of(sys.version_info.__class__, AlwaysTruthy)) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md b/crates/ty_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md rename to crates/ty_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md index b0a1ad3077167f..5536b84c98d10f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/tuples_containing_never.md @@ -10,7 +10,7 @@ zero element in multiplication, similar to how a Cartesian product with the empt set. ```py -from knot_extensions import static_assert, is_equivalent_to +from ty_extensions import static_assert, is_equivalent_to from typing_extensions import Never, NoReturn static_assert(is_equivalent_to(Never, tuple[Never])) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/classvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/classvar.md rename to crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/final.md rename to crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/typed_dict.md rename to crates/ty_python_semantic/resources/mdtest/typed_dict.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md b/crates/ty_python_semantic/resources/mdtest/unary/custom.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/unary/custom.md rename to crates/ty_python_semantic/resources/mdtest/unary/custom.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/integers.md b/crates/ty_python_semantic/resources/mdtest/unary/integers.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/unary/integers.md rename to crates/ty_python_semantic/resources/mdtest/unary/integers.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/invert_add_usub.md b/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/unary/invert_add_usub.md rename to crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md b/crates/ty_python_semantic/resources/mdtest/unary/not.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/unary/not.md rename to crates/ty_python_semantic/resources/mdtest/unary/not.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md similarity index 96% rename from crates/red_knot_python_semantic/resources/mdtest/union_types.md rename to crates/ty_python_semantic/resources/mdtest/union_types.md index 398847ecbe0e0e..50d20efb1248d1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/union_types.md +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -117,7 +117,7 @@ def _( ## Do not erase `Unknown` ```py -from knot_extensions import Unknown +from ty_extensions import Unknown def _(u1: Unknown | str, u2: str | Unknown) -> None: reveal_type(u1) # revealed: Unknown | str @@ -130,7 +130,7 @@ Since `Unknown` is a gradual type, it is not a subtype of anything, but multiple union are still redundant: ```py -from knot_extensions import Unknown +from ty_extensions import Unknown def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None: reveal_type(u1) # revealed: Unknown | str @@ -143,7 +143,7 @@ def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknow Simplifications still apply when `Unknown` is present. ```py -from knot_extensions import Unknown +from ty_extensions import Unknown def _(u1: int | Unknown | bool) -> None: reveal_type(u1) # revealed: int | Unknown @@ -154,7 +154,7 @@ def _(u1: int | Unknown | bool) -> None: We can simplify unions of intersections: ```py -from knot_extensions import Intersection, Not +from ty_extensions import Intersection, Not class P: ... class Q: ... @@ -176,7 +176,7 @@ python-version = "3.12" ```py from typing import Literal -from knot_extensions import AlwaysTruthy, AlwaysFalsy +from ty_extensions import AlwaysTruthy, AlwaysFalsy type strings = Literal["foo", ""] type ints = Literal[0, 1] diff --git a/crates/red_knot_python_semantic/resources/mdtest/unpacking.md b/crates/ty_python_semantic/resources/mdtest/unpacking.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/unpacking.md rename to crates/ty_python_semantic/resources/mdtest/unpacking.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/unreachable.md b/crates/ty_python_semantic/resources/mdtest/unreachable.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/unreachable.md rename to crates/ty_python_semantic/resources/mdtest/unreachable.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/with/async.md b/crates/ty_python_semantic/resources/mdtest/with/async.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/with/async.md rename to crates/ty_python_semantic/resources/mdtest/with/async.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/with/sync.md b/crates/ty_python_semantic/resources/mdtest/with/sync.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/with/sync.md rename to crates/ty_python_semantic/resources/mdtest/with/sync.md diff --git a/crates/red_knot_python_semantic/resources/primer/bad.txt b/crates/ty_python_semantic/resources/primer/bad.txt similarity index 100% rename from crates/red_knot_python_semantic/resources/primer/bad.txt rename to crates/ty_python_semantic/resources/primer/bad.txt diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/ty_python_semantic/resources/primer/good.txt similarity index 100% rename from crates/red_knot_python_semantic/resources/primer/good.txt rename to crates/ty_python_semantic/resources/primer/good.txt diff --git a/crates/red_knot_python_semantic/src/ast_node_ref.rs b/crates/ty_python_semantic/src/ast_node_ref.rs similarity index 100% rename from crates/red_knot_python_semantic/src/ast_node_ref.rs rename to crates/ty_python_semantic/src/ast_node_ref.rs diff --git a/crates/red_knot_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs similarity index 98% rename from crates/red_knot_python_semantic/src/db.rs rename to crates/ty_python_semantic/src/db.rs index 00956ccf07b295..5fff691aac99eb 100644 --- a/crates/red_knot_python_semantic/src/db.rs +++ b/crates/ty_python_semantic/src/db.rs @@ -48,7 +48,7 @@ pub(crate) mod tests { Self { storage: salsa::Storage::default(), system: TestSystem::default(), - vendored: red_knot_vendored::file_system().clone(), + vendored: ty_vendored::file_system().clone(), events: Arc::default(), files: Files::default(), rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())), diff --git a/crates/red_knot_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs similarity index 100% rename from crates/red_knot_python_semantic/src/lib.rs rename to crates/ty_python_semantic/src/lib.rs diff --git a/crates/red_knot_python_semantic/src/lint.rs b/crates/ty_python_semantic/src/lint.rs similarity index 99% rename from crates/red_knot_python_semantic/src/lint.rs rename to crates/ty_python_semantic/src/lint.rs index 0e229536664a43..17a778696a68fe 100644 --- a/crates/red_knot_python_semantic/src/lint.rs +++ b/crates/ty_python_semantic/src/lint.rs @@ -210,8 +210,8 @@ impl LintStatus { /// Declares a lint rule with the given metadata. /// /// ```rust -/// use red_knot_python_semantic::declare_lint; -/// use red_knot_python_semantic::lint::{LintStatus, Level}; +/// use ty_python_semantic::declare_lint; +/// use ty_python_semantic::lint::{LintStatus, Level}; /// /// declare_lint! { /// /// ## What it does diff --git a/crates/red_knot_python_semantic/src/list.rs b/crates/ty_python_semantic/src/list.rs similarity index 100% rename from crates/red_knot_python_semantic/src/list.rs rename to crates/ty_python_semantic/src/list.rs diff --git a/crates/red_knot_python_semantic/src/module_name.rs b/crates/ty_python_semantic/src/module_name.rs similarity index 96% rename from crates/red_knot_python_semantic/src/module_name.rs rename to crates/ty_python_semantic/src/module_name.rs index 9d08cec93ef9b1..48162fe3d934b9 100644 --- a/crates/red_knot_python_semantic/src/module_name.rs +++ b/crates/ty_python_semantic/src/module_name.rs @@ -47,7 +47,7 @@ impl ModuleName { /// ## Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert_eq!(ModuleName::new_static("foo.bar").as_deref(), Some("foo.bar")); /// assert_eq!(ModuleName::new_static(""), None); @@ -73,7 +73,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().components().collect::>(), vec!["foo", "bar", "baz"]); /// ``` @@ -87,7 +87,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert_eq!(ModuleName::new_static("foo.bar").unwrap().parent(), Some(ModuleName::new_static("foo").unwrap())); /// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().parent(), Some(ModuleName::new_static("foo.bar").unwrap())); @@ -106,7 +106,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert!(ModuleName::new_static("foo.bar").unwrap().starts_with(&ModuleName::new_static("foo").unwrap())); /// @@ -138,7 +138,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert_eq!(&*ModuleName::from_components(["a"]).unwrap(), "a"); /// assert_eq!(&*ModuleName::from_components(["a", "b"]).unwrap(), "a.b"); @@ -179,7 +179,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// let mut module_name = ModuleName::new_static("foo").unwrap(); /// module_name.extend(&ModuleName::new_static("bar").unwrap()); @@ -197,7 +197,7 @@ impl ModuleName { /// # Examples /// /// ``` - /// use red_knot_python_semantic::ModuleName; + /// use ty_python_semantic::ModuleName; /// /// assert_eq!( /// ModuleName::new_static("foo.bar.baz").unwrap().ancestors().collect::>(), diff --git a/crates/red_knot_python_semantic/src/module_resolver/mod.rs b/crates/ty_python_semantic/src/module_resolver/mod.rs similarity index 100% rename from crates/red_knot_python_semantic/src/module_resolver/mod.rs rename to crates/ty_python_semantic/src/module_resolver/mod.rs diff --git a/crates/red_knot_python_semantic/src/module_resolver/module.rs b/crates/ty_python_semantic/src/module_resolver/module.rs similarity index 96% rename from crates/red_knot_python_semantic/src/module_resolver/module.rs rename to crates/ty_python_semantic/src/module_resolver/module.rs index afcc6687bac21d..ecd8959f2dd261 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/module.rs +++ b/crates/ty_python_semantic/src/module_resolver/module.rs @@ -119,7 +119,7 @@ pub enum KnownModule { Dataclasses, Collections, Inspect, - KnotExtensions, + TyExtensions, } impl KnownModule { @@ -136,7 +136,7 @@ impl KnownModule { Self::Dataclasses => "dataclasses", Self::Collections => "collections", Self::Inspect => "inspect", - Self::KnotExtensions => "knot_extensions", + Self::TyExtensions => "ty_extensions", } } @@ -164,8 +164,8 @@ impl KnownModule { matches!(self, Self::Typing) } - pub const fn is_knot_extensions(self) -> bool { - matches!(self, Self::KnotExtensions) + pub const fn is_ty_extensions(self) -> bool { + matches!(self, Self::TyExtensions) } pub const fn is_inspect(self) -> bool { diff --git a/crates/red_knot_python_semantic/src/module_resolver/path.rs b/crates/ty_python_semantic/src/module_resolver/path.rs similarity index 100% rename from crates/red_knot_python_semantic/src/module_resolver/path.rs rename to crates/ty_python_semantic/src/module_resolver/path.rs diff --git a/crates/red_knot_python_semantic/src/module_resolver/resolver.rs b/crates/ty_python_semantic/src/module_resolver/resolver.rs similarity index 100% rename from crates/red_knot_python_semantic/src/module_resolver/resolver.rs rename to crates/ty_python_semantic/src/module_resolver/resolver.rs diff --git a/crates/red_knot_python_semantic/src/module_resolver/testing.rs b/crates/ty_python_semantic/src/module_resolver/testing.rs similarity index 100% rename from crates/red_knot_python_semantic/src/module_resolver/testing.rs rename to crates/ty_python_semantic/src/module_resolver/testing.rs diff --git a/crates/red_knot_python_semantic/src/module_resolver/typeshed.rs b/crates/ty_python_semantic/src/module_resolver/typeshed.rs similarity index 99% rename from crates/red_knot_python_semantic/src/module_resolver/typeshed.rs rename to crates/ty_python_semantic/src/module_resolver/typeshed.rs index 8a3fe2e645690b..fdfdac7527be83 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/typeshed.rs +++ b/crates/ty_python_semantic/src/module_resolver/typeshed.rs @@ -392,7 +392,7 @@ mod tests { let db = TestDb::new(); let vendored_typeshed_versions = vendored_typeshed_versions(&db); let vendored_typeshed_dir = - Path::new(env!("CARGO_MANIFEST_DIR")).join("../red_knot_vendored/vendor/typeshed"); + Path::new(env!("CARGO_MANIFEST_DIR")).join("../ty_vendored/vendor/typeshed"); let mut empty_iterator = true; diff --git a/crates/red_knot_python_semantic/src/node_key.rs b/crates/ty_python_semantic/src/node_key.rs similarity index 100% rename from crates/red_knot_python_semantic/src/node_key.rs rename to crates/ty_python_semantic/src/node_key.rs diff --git a/crates/red_knot_python_semantic/src/program.rs b/crates/ty_python_semantic/src/program.rs similarity index 98% rename from crates/red_knot_python_semantic/src/program.rs rename to crates/ty_python_semantic/src/program.rs index af3bbe0ce235e3..682a055114b504 100644 --- a/crates/red_knot_python_semantic/src/program.rs +++ b/crates/ty_python_semantic/src/program.rs @@ -111,7 +111,7 @@ pub struct SearchPathSettings { /// bundled as a zip file in the binary pub custom_typeshed: Option, - /// Path to the Python installation from which Red Knot resolves third party dependencies + /// Path to the Python installation from which ty resolves third party dependencies /// and their type information. pub python_path: PythonPath, } diff --git a/crates/red_knot_python_semantic/src/python_platform.rs b/crates/ty_python_semantic/src/python_platform.rs similarity index 100% rename from crates/red_knot_python_semantic/src/python_platform.rs rename to crates/ty_python_semantic/src/python_platform.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index.rs rename to crates/ty_python_semantic/src/semantic_index.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/ast_ids.rs b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/ast_ids.rs rename to crates/ty_python_semantic/src/semantic_index/ast_ids.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/builder.rs rename to crates/ty_python_semantic/src/semantic_index/builder.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder/except_handlers.rs b/crates/ty_python_semantic/src/semantic_index/builder/except_handlers.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/builder/except_handlers.rs rename to crates/ty_python_semantic/src/semantic_index/builder/except_handlers.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/definition.rs rename to crates/ty_python_semantic/src/semantic_index/definition.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/expression.rs b/crates/ty_python_semantic/src/semantic_index/expression.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/expression.rs rename to crates/ty_python_semantic/src/semantic_index/expression.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/narrowing_constraints.rs b/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/narrowing_constraints.rs rename to crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/predicate.rs b/crates/ty_python_semantic/src/semantic_index/predicate.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/predicate.rs rename to crates/ty_python_semantic/src/semantic_index/predicate.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/re_exports.rs b/crates/ty_python_semantic/src/semantic_index/re_exports.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/re_exports.rs rename to crates/ty_python_semantic/src/semantic_index/re_exports.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs b/crates/ty_python_semantic/src/semantic_index/symbol.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/symbol.rs rename to crates/ty_python_semantic/src/semantic_index/symbol.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/use_def.rs rename to crates/ty_python_semantic/src/semantic_index/use_def.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/use_def/symbol_state.rs b/crates/ty_python_semantic/src/semantic_index/use_def/symbol_state.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/use_def/symbol_state.rs rename to crates/ty_python_semantic/src/semantic_index/use_def/symbol_state.rs diff --git a/crates/red_knot_python_semantic/src/semantic_index/visibility_constraints.rs b/crates/ty_python_semantic/src/semantic_index/visibility_constraints.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_index/visibility_constraints.rs rename to crates/ty_python_semantic/src/semantic_index/visibility_constraints.rs diff --git a/crates/red_knot_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs similarity index 100% rename from crates/red_knot_python_semantic/src/semantic_model.rs rename to crates/ty_python_semantic/src/semantic_model.rs diff --git a/crates/red_knot_python_semantic/src/site_packages.rs b/crates/ty_python_semantic/src/site_packages.rs similarity index 99% rename from crates/red_knot_python_semantic/src/site_packages.rs rename to crates/ty_python_semantic/src/site_packages.rs index c4b76d877e4dd5..a11274378b031e 100644 --- a/crates/red_knot_python_semantic/src/site_packages.rs +++ b/crates/ty_python_semantic/src/site_packages.rs @@ -4,7 +4,7 @@ //! The routines exposed by this module have different behaviour depending //! on the platform of the *host machine*, which may be //! different from the *target platform for type checking*. (A user -//! might be running red-knot on a Windows machine, but might +//! might be running ty on a Windows machine, but might //! reasonably ask us to type-check code assuming that the code runs //! on Linux.) diff --git a/crates/red_knot_python_semantic/src/suppression.rs b/crates/ty_python_semantic/src/suppression.rs similarity index 96% rename from crates/red_knot_python_semantic/src/suppression.rs rename to crates/ty_python_semantic/src/suppression.rs index ebfc62afd803b2..dcccf571949adb 100644 --- a/crates/red_knot_python_semantic/src/suppression.rs +++ b/crates/ty_python_semantic/src/suppression.rs @@ -14,7 +14,7 @@ use thiserror::Error; declare_lint! { /// ## What it does - /// Checks for `type: ignore` or `knot: ignore` directives that are no longer applicable. + /// Checks for `type: ignore` or `ty: ignore` directives that are no longer applicable. /// /// ## Why is this bad? /// A `type: ignore` directive that no longer matches any diagnostic violations is likely @@ -22,7 +22,7 @@ declare_lint! { /// /// ## Examples /// ```py - /// a = 20 / 2 # knot: ignore[division-by-zero] + /// a = 20 / 2 # ty: ignore[division-by-zero] /// ``` /// /// Use instead: @@ -39,24 +39,24 @@ declare_lint! { declare_lint! { /// ## What it does - /// Checks for `knot: ignore[code]` where `code` isn't a known lint rule. + /// Checks for `ty: ignore[code]` where `code` isn't a known lint rule. /// /// ## Why is this bad? - /// A `knot: ignore[code]` directive with a `code` that doesn't match + /// A `ty: ignore[code]` directive with a `code` that doesn't match /// any known rule will not suppress any type errors, and is probably a mistake. /// /// ## Examples /// ```py - /// a = 20 / 0 # knot: ignore[division-by-zer] + /// a = 20 / 0 # ty: ignore[division-by-zer] /// ``` /// /// Use instead: /// /// ```py - /// a = 20 / 0 # knot: ignore[division-by-zero] + /// a = 20 / 0 # ty: ignore[division-by-zero] /// ``` pub(crate) static UNKNOWN_RULE = { - summary: "detects `knot: ignore` comments that reference unknown rules", + summary: "detects `ty: ignore` comments that reference unknown rules", status: LintStatus::preview("1.0.0"), default_level: Level::Warn, } @@ -64,7 +64,7 @@ declare_lint! { declare_lint! { /// ## What it does - /// Checks for `type: ignore` and `knot: ignore` comments that are syntactically incorrect. + /// Checks for `type: ignore` and `ty: ignore` comments that are syntactically incorrect. /// /// ## Why is this bad? /// A syntactically incorrect ignore comment is probably a mistake and is useless. @@ -141,7 +141,7 @@ pub(crate) fn check_suppressions(db: &dyn Db, file: File, diagnostics: &mut Type check_unused_suppressions(&mut context); } -/// Checks for `knot: ignore` comments that reference unknown rules. +/// Checks for `ty: ignore` comments that reference unknown rules. fn check_unknown_rule(context: &mut CheckSuppressionsContext) { if context.is_lint_disabled(&UNKNOWN_RULE) { return; @@ -241,7 +241,7 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) { // This looks silly but it's necessary to check again if a `unused-ignore-comment` is indeed unused // in case the "unused" directive comes after it: // ```py - // a = 10 / 2 # knot: ignore[unused-ignore-comment, division-by-zero] + // a = 10 / 2 # ty: ignore[unused-ignore-comment, division-by-zero] // ``` if context.diagnostics.is_used(suppression.id()) { continue; @@ -416,7 +416,7 @@ impl<'a> IntoIterator for &'a Suppressions { } } -/// A `type: ignore` or `knot: ignore` suppression. +/// A `type: ignore` or `ty: ignore` suppression. /// /// Suppression comments that suppress multiple codes /// create multiple suppressions: one for every code. @@ -474,7 +474,7 @@ enum SuppressionTarget { /// Suppress the lint with the given id Lint(LintId), - /// Suppresses no lint, e.g. `knot: ignore[]` + /// Suppresses no lint, e.g. `ty: ignore[]` Empty, } @@ -577,7 +577,7 @@ impl<'a> SuppressionsBuilder<'a> { }); } - // `knot: ignore[]` + // `ty: ignore[]` Some([]) => { self.line.push(Suppression { target: SuppressionTarget::Empty, @@ -588,7 +588,7 @@ impl<'a> SuppressionsBuilder<'a> { }); } - // `knot: ignore[a, b]` + // `ty: ignore[a, b]` Some(codes) => { for code_range in codes { let code = &self.source[*code_range]; @@ -695,8 +695,8 @@ impl<'src> SuppressionParser<'src> { fn eat_kind(&mut self) -> Option { let kind = if self.cursor.as_str().starts_with("type") { SuppressionKind::TypeIgnore - } else if self.cursor.as_str().starts_with("knot") { - SuppressionKind::Knot + } else if self.cursor.as_str().starts_with("ty") { + SuppressionKind::Ty } else { return None; }; @@ -737,7 +737,7 @@ impl<'src> SuppressionParser<'src> { self.eat_whitespace(); - // `knot: ignore[]` or `knot: ignore[a,]` + // `ty: ignore[]` or `ty: ignore[a,]` if self.cursor.eat_char(']') { break Ok(Some(codes)); } @@ -757,7 +757,7 @@ impl<'src> SuppressionParser<'src> { if self.cursor.eat_char(']') { break Ok(Some(codes)); } - // `knot: ignore[a b] + // `ty: ignore[a b] return self.syntax_error(ParseErrorKind::CodesMissingComma(kind)); } } @@ -843,7 +843,7 @@ struct SuppressionComment { #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum SuppressionKind { TypeIgnore, - Knot, + Ty, } impl SuppressionKind { @@ -854,7 +854,7 @@ impl SuppressionKind { fn len_utf8(self) -> usize { match self { SuppressionKind::TypeIgnore => "type".len(), - SuppressionKind::Knot => "knot".len(), + SuppressionKind::Ty => "ty".len(), } } } @@ -863,7 +863,7 @@ impl fmt::Display for SuppressionKind { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { SuppressionKind::TypeIgnore => f.write_str("type: ignore"), - SuppressionKind::Knot => f.write_str("knot: ignore"), + SuppressionKind::Ty => f.write_str("ty: ignore"), } } } @@ -911,11 +911,11 @@ enum ParseErrorKind { #[error("expected a comma separating the rule codes")] CodesMissingComma(SuppressionKind), - /// `knot: ignore[*.*]` + /// `ty: ignore[*.*]` #[error("expected a alphanumeric character or `-` or `_` as code")] InvalidCode(SuppressionKind), - /// `knot: ignore[a, b` + /// `ty: ignore[a, b` #[error("expected a closing bracket")] CodesMissingClosingBracket(SuppressionKind), } diff --git a/crates/red_knot_python_semantic/src/symbol.rs b/crates/ty_python_semantic/src/symbol.rs similarity index 100% rename from crates/red_knot_python_semantic/src/symbol.rs rename to crates/ty_python_semantic/src/symbol.rs diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs similarity index 99% rename from crates/red_knot_python_semantic/src/types.rs rename to crates/ty_python_semantic/src/types.rs index 049f2de3be06f0..cd4d4b0fae89af 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -257,7 +257,7 @@ fn member_lookup_cycle_initial<'db>( Symbol::bound(Type::Never).into() } -/// Meta data for `Type::Todo`, which represents a known limitation in red-knot. +/// Meta data for `Type::Todo`, which represents a known limitation in ty. #[cfg(debug_assertions)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct TodoType(pub &'static str); @@ -1246,7 +1246,7 @@ impl<'db> Type<'db> { .metaclass_instance_type(db) .is_subtype_of(db, target), - // `type[str]` (== `SubclassOf("str")` in red-knot) describes all possible runtime subclasses + // `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses // of the class object `str`. It is a subtype of `type` (== `Instance("type")`) because `str` // is an instance of `type`, and so all possible subclasses of `str` will also be instances of `type`. // @@ -5379,7 +5379,7 @@ pub enum DynamicType { Unknown, /// Temporary type for symbols that can't be inferred yet because of missing implementations. /// - /// This variant should eventually be removed once red-knot is spec-compliant. + /// This variant should eventually be removed once ty is spec-compliant. /// /// General rule: `Todo` should only propagate when the presence of the input `Todo` caused the /// output to be unknown. An output should only be `Todo` if fixing all `Todo` inputs to be not @@ -6906,25 +6906,25 @@ pub enum KnownFunction { /// `inspect.getattr_static` GetattrStatic, - /// `knot_extensions.static_assert` + /// `ty_extensions.static_assert` StaticAssert, - /// `knot_extensions.is_equivalent_to` + /// `ty_extensions.is_equivalent_to` IsEquivalentTo, - /// `knot_extensions.is_subtype_of` + /// `ty_extensions.is_subtype_of` IsSubtypeOf, - /// `knot_extensions.is_assignable_to` + /// `ty_extensions.is_assignable_to` IsAssignableTo, - /// `knot_extensions.is_disjoint_from` + /// `ty_extensions.is_disjoint_from` IsDisjointFrom, - /// `knot_extensions.is_gradual_equivalent_to` + /// `ty_extensions.is_gradual_equivalent_to` IsGradualEquivalentTo, - /// `knot_extensions.is_fully_static` + /// `ty_extensions.is_fully_static` IsFullyStatic, - /// `knot_extensions.is_singleton` + /// `ty_extensions.is_singleton` IsSingleton, - /// `knot_extensions.is_single_valued` + /// `ty_extensions.is_single_valued` IsSingleValued, - /// `knot_extensions.generic_context` + /// `ty_extensions.generic_context` GenericContext, } @@ -6982,7 +6982,7 @@ impl KnownFunction { | Self::IsSingleton | Self::IsSubtypeOf | Self::GenericContext - | Self::StaticAssert => module.is_knot_extensions(), + | Self::StaticAssert => module.is_ty_extensions(), } } } @@ -8385,7 +8385,7 @@ pub(crate) mod tests { | KnownFunction::IsSingleValued | KnownFunction::IsAssignableTo | KnownFunction::IsEquivalentTo - | KnownFunction::IsGradualEquivalentTo => KnownModule::KnotExtensions, + | KnownFunction::IsGradualEquivalentTo => KnownModule::TyExtensions, }; let function_definition = known_module_symbol(&db, module, function_name) diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/builder.rs rename to crates/ty_python_semantic/src/types/builder.rs diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/call.rs rename to crates/ty_python_semantic/src/types/call.rs diff --git a/crates/red_knot_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/call/arguments.rs rename to crates/ty_python_semantic/src/types/call/arguments.rs diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/call/bind.rs rename to crates/ty_python_semantic/src/types/call/bind.rs diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs similarity index 99% rename from crates/red_knot_python_semantic/src/types/class.rs rename to crates/ty_python_semantic/src/types/class.rs index 664fa0706eed5a..315809a08616de 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1802,7 +1802,7 @@ impl InheritanceCycle { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(test, derive(strum_macros::EnumIter))] pub enum KnownClass { - // To figure out where an stdlib symbol is defined, you can go into `crates/red_knot_vendored` + // To figure out where an stdlib symbol is defined, you can go into `crates/ty_vendored` // and grep for the symbol name in any `.pyi` file. // Builtins diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/class_base.rs rename to crates/ty_python_semantic/src/types/class_base.rs diff --git a/crates/red_knot_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/context.rs rename to crates/ty_python_semantic/src/types/context.rs diff --git a/crates/red_knot_python_semantic/src/types/definition.rs b/crates/ty_python_semantic/src/types/definition.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/definition.rs rename to crates/ty_python_semantic/src/types/definition.rs diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs similarity index 99% rename from crates/red_knot_python_semantic/src/types/diagnostic.rs rename to crates/ty_python_semantic/src/types/diagnostic.rs index 6b0b9fb4ad43d3..3ace0181d0e7f1 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -1052,7 +1052,7 @@ declare_lint! { /// /// ## Examples /// ```python - /// from knot_extensions import static_assert + /// from ty_extensions import static_assert /// /// static_assert(1 + 1 == 3) # error: evaluates to `False` /// diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/display.rs rename to crates/ty_python_semantic/src/types/display.rs diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/generics.rs rename to crates/ty_python_semantic/src/types/generics.rs diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/infer.rs rename to crates/ty_python_semantic/src/types/infer.rs diff --git a/crates/red_knot_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/instance.rs rename to crates/ty_python_semantic/src/types/instance.rs diff --git a/crates/red_knot_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs similarity index 94% rename from crates/red_knot_python_semantic/src/types/known_instance.rs rename to crates/ty_python_semantic/src/types/known_instance.rs index b1cce6d9798c73..5971a424436c7f 100644 --- a/crates/red_knot_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -69,19 +69,19 @@ pub enum KnownInstanceType<'db> { TypeVar(TypeVarInstance<'db>), /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) TypeAliasType(TypeAliasType<'db>), - /// The symbol `knot_extensions.Unknown` + /// The symbol `ty_extensions.Unknown` Unknown, - /// The symbol `knot_extensions.AlwaysTruthy` + /// The symbol `ty_extensions.AlwaysTruthy` AlwaysTruthy, - /// The symbol `knot_extensions.AlwaysFalsy` + /// The symbol `ty_extensions.AlwaysFalsy` AlwaysFalsy, - /// The symbol `knot_extensions.Not` + /// The symbol `ty_extensions.Not` Not, - /// The symbol `knot_extensions.Intersection` + /// The symbol `ty_extensions.Intersection` Intersection, - /// The symbol `knot_extensions.TypeOf` + /// The symbol `ty_extensions.TypeOf` TypeOf, - /// The symbol `knot_extensions.CallableTypeOf` + /// The symbol `ty_extensions.CallableTypeOf` CallableTypeOf, /// The symbol `typing.Callable` /// (which can also be found as `typing_extensions.Callable` or as `collections.abc.Callable`) @@ -334,7 +334,7 @@ impl<'db> KnownInstanceType<'db> { | Self::Not | Self::Intersection | Self::TypeOf - | Self::CallableTypeOf => module.is_knot_extensions(), + | Self::CallableTypeOf => module.is_ty_extensions(), } } @@ -396,13 +396,13 @@ impl Display for KnownInstanceRepr<'_> { // have a `Type::TypeVar(_)`, which is rendered as the typevar's name. KnownInstanceType::TypeVar(_) => f.write_str("typing.TypeVar"), KnownInstanceType::TypeAliasType(_) => f.write_str("typing.TypeAliasType"), - KnownInstanceType::Unknown => f.write_str("knot_extensions.Unknown"), - KnownInstanceType::AlwaysTruthy => f.write_str("knot_extensions.AlwaysTruthy"), - KnownInstanceType::AlwaysFalsy => f.write_str("knot_extensions.AlwaysFalsy"), - KnownInstanceType::Not => f.write_str("knot_extensions.Not"), - KnownInstanceType::Intersection => f.write_str("knot_extensions.Intersection"), - KnownInstanceType::TypeOf => f.write_str("knot_extensions.TypeOf"), - KnownInstanceType::CallableTypeOf => f.write_str("knot_extensions.CallableTypeOf"), + KnownInstanceType::Unknown => f.write_str("ty_extensions.Unknown"), + KnownInstanceType::AlwaysTruthy => f.write_str("ty_extensions.AlwaysTruthy"), + KnownInstanceType::AlwaysFalsy => f.write_str("ty_extensions.AlwaysFalsy"), + KnownInstanceType::Not => f.write_str("ty_extensions.Not"), + KnownInstanceType::Intersection => f.write_str("ty_extensions.Intersection"), + KnownInstanceType::TypeOf => f.write_str("ty_extensions.TypeOf"), + KnownInstanceType::CallableTypeOf => f.write_str("ty_extensions.CallableTypeOf"), } } } diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/mro.rs rename to crates/ty_python_semantic/src/types/mro.rs diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/narrow.rs rename to crates/ty_python_semantic/src/types/narrow.rs diff --git a/crates/red_knot_python_semantic/src/types/property_tests.rs b/crates/ty_python_semantic/src/types/property_tests.rs similarity index 97% rename from crates/red_knot_python_semantic/src/types/property_tests.rs rename to crates/ty_python_semantic/src/types/property_tests.rs index 77c83e04e0f1b3..e0bca1c509f828 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests.rs +++ b/crates/ty_python_semantic/src/types/property_tests.rs @@ -4,7 +4,7 @@ //! run them explicitly using: //! //! ```sh -//! cargo test -p red_knot_python_semantic -- --ignored types::property_tests::stable +//! cargo test -p ty_python_semantic -- --ignored types::property_tests::stable //! ``` //! //! The number of tests (default: 100) can be controlled by setting the `QUICKCHECK_TESTS` @@ -20,7 +20,7 @@ //! //! ```sh //! export QUICKCHECK_TESTS=100000 -//! while cargo test --release -p red_knot_python_semantic -- \ +//! while cargo test --release -p ty_python_semantic -- \ //! --ignored types::property_tests::stable; do :; done //! ``` mod setup; @@ -292,7 +292,7 @@ mod flaky { // If `S <: T`, then `~T <: ~S`. // // DO NOT STABILISE this test until the mdtests here pass: - // https://github.com/astral-sh/ruff/blob/2711e08eb8eb38d1ce323aae0517fede371cba15/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md?plain=1#L276-L315 + // https://github.com/astral-sh/ruff/blob/2711e08eb8eb38d1ce323aae0517fede371cba15/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md?plain=1#L276-L315 // // This test has flakes relating to those subtyping and simplification tests // (see https://github.com/astral-sh/ruff/issues/16913), but it is hard to diff --git a/crates/red_knot_python_semantic/src/types/property_tests/setup.rs b/crates/ty_python_semantic/src/types/property_tests/setup.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/property_tests/setup.rs rename to crates/ty_python_semantic/src/types/property_tests/setup.rs diff --git a/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs rename to crates/ty_python_semantic/src/types/property_tests/type_generation.rs diff --git a/crates/red_knot_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/protocol_class.rs rename to crates/ty_python_semantic/src/types/protocol_class.rs diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/signatures.rs rename to crates/ty_python_semantic/src/types/signatures.rs diff --git a/crates/red_knot_python_semantic/src/types/slots.rs b/crates/ty_python_semantic/src/types/slots.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/slots.rs rename to crates/ty_python_semantic/src/types/slots.rs diff --git a/crates/red_knot_python_semantic/src/types/string_annotation.rs b/crates/ty_python_semantic/src/types/string_annotation.rs similarity index 92% rename from crates/red_knot_python_semantic/src/types/string_annotation.rs rename to crates/ty_python_semantic/src/types/string_annotation.rs index 0c76ad69958965..ae811bbbb1712c 100644 --- a/crates/red_knot_python_semantic/src/types/string_annotation.rs +++ b/crates/ty_python_semantic/src/types/string_annotation.rs @@ -13,7 +13,7 @@ declare_lint! { /// Checks for f-strings in type annotation positions. /// /// ## Why is this bad? - /// Static analysis tools like Red Knot can't analyse type annotations that use f-string notation. + /// Static analysis tools like ty can't analyse type annotations that use f-string notation. /// /// ## Examples /// ```python @@ -38,7 +38,7 @@ declare_lint! { /// Checks for byte-strings in type annotation positions. /// /// ## Why is this bad? - /// Static analysis tools like Red Knot can't analyse type annotations that use byte-string notation. + /// Static analysis tools like ty can't analyse type annotations that use byte-string notation. /// /// ## Examples /// ```python @@ -63,7 +63,7 @@ declare_lint! { /// Checks for raw-strings in type annotation positions. /// /// ## Why is this bad? - /// Static analysis tools like Red Knot can't analyse type annotations that use raw-string notation. + /// Static analysis tools like ty can't analyse type annotations that use raw-string notation. /// /// ## Examples /// ```python @@ -88,7 +88,7 @@ declare_lint! { /// Checks for implicit concatenated strings in type annotation positions. /// /// ## Why is this bad? - /// Static analysis tools like Red Knot can't analyse type annotations that use implicit concatenated strings. + /// Static analysis tools like ty can't analyse type annotations that use implicit concatenated strings. /// /// ## Examples /// ```python diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/subclass_of.rs rename to crates/ty_python_semantic/src/types/subclass_of.rs diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/type_ordering.rs rename to crates/ty_python_semantic/src/types/type_ordering.rs diff --git a/crates/red_knot_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs similarity index 100% rename from crates/red_knot_python_semantic/src/types/unpacker.rs rename to crates/ty_python_semantic/src/types/unpacker.rs diff --git a/crates/red_knot_python_semantic/src/unpack.rs b/crates/ty_python_semantic/src/unpack.rs similarity index 100% rename from crates/red_knot_python_semantic/src/unpack.rs rename to crates/ty_python_semantic/src/unpack.rs diff --git a/crates/red_knot_python_semantic/src/util/mod.rs b/crates/ty_python_semantic/src/util/mod.rs similarity index 100% rename from crates/red_knot_python_semantic/src/util/mod.rs rename to crates/ty_python_semantic/src/util/mod.rs diff --git a/crates/red_knot_python_semantic/src/util/subscript.rs b/crates/ty_python_semantic/src/util/subscript.rs similarity index 100% rename from crates/red_knot_python_semantic/src/util/subscript.rs rename to crates/ty_python_semantic/src/util/subscript.rs diff --git a/crates/red_knot_python_semantic/tests/mdtest.rs b/crates/ty_python_semantic/tests/mdtest.rs similarity index 94% rename from crates/red_knot_python_semantic/tests/mdtest.rs rename to crates/ty_python_semantic/tests/mdtest.rs index b2ea0f141c268d..0c651167af3f2c 100644 --- a/crates/red_knot_python_semantic/tests/mdtest.rs +++ b/crates/ty_python_semantic/tests/mdtest.rs @@ -1,8 +1,8 @@ use camino::Utf8Path; use dir_test::{dir_test, Fixture}; -use red_knot_test::OutputFormat; +use ty_test::OutputFormat; -/// See `crates/red_knot_test/README.md` for documentation on these tests. +/// See `crates/ty_test/README.md` for documentation on these tests. #[dir_test( dir: "$CARGO_MANIFEST_DIR/resources/mdtest", glob: "**/*.md" @@ -25,7 +25,7 @@ fn mdtest(fixture: Fixture<&str>) { OutputFormat::Cli }; - red_knot_test::run( + ty_test::run( absolute_fixture_path, relative_fixture_path, &snapshot_path, diff --git a/crates/red_knot_server/Cargo.toml b/crates/ty_server/Cargo.toml similarity index 86% rename from crates/red_knot_server/Cargo.toml rename to crates/ty_server/Cargo.toml index c5c17c156190a1..0bae064fda3741 100644 --- a/crates/red_knot_server/Cargo.toml +++ b/crates/ty_server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "red_knot_server" +name = "ty_server" version = "0.0.0" publish = false authors = { workspace = true } @@ -11,15 +11,15 @@ repository = { workspace = true } license = { workspace = true } [dependencies] -red_knot_ide = { workspace = true } -red_knot_project = { workspace = true } -red_knot_python_semantic = { workspace = true } - ruff_db = { workspace = true, features = ["os"] } ruff_notebook = { workspace = true } ruff_source_file = { workspace = true } ruff_text_size = { workspace = true } +ty_ide = { workspace = true } +ty_project = { workspace = true } +ty_python_semantic = { workspace = true } + anyhow = { workspace = true } crossbeam = { workspace = true } jod-thread = { workspace = true } diff --git a/crates/red_knot_server/src/document.rs b/crates/ty_server/src/document.rs similarity index 100% rename from crates/red_knot_server/src/document.rs rename to crates/ty_server/src/document.rs diff --git a/crates/red_knot_server/src/document/location.rs b/crates/ty_server/src/document/location.rs similarity index 88% rename from crates/red_knot_server/src/document/location.rs rename to crates/ty_server/src/document/location.rs index c2e73fcb246c46..441a33f0bd1dd3 100644 --- a/crates/red_knot_server/src/document/location.rs +++ b/crates/ty_server/src/document/location.rs @@ -2,21 +2,17 @@ use crate::document::{FileRangeExt, ToRangeExt}; use crate::system::file_to_url; use crate::PositionEncoding; use lsp_types::Location; -use red_knot_ide::{Db, NavigationTarget}; use ruff_db::files::FileRange; use ruff_db::source::{line_index, source_text}; use ruff_text_size::Ranged; +use ty_ide::{Db, NavigationTarget}; pub(crate) trait ToLink { - fn to_location( - &self, - db: &dyn red_knot_ide::Db, - encoding: PositionEncoding, - ) -> Option; + fn to_location(&self, db: &dyn ty_ide::Db, encoding: PositionEncoding) -> Option; fn to_link( &self, - db: &dyn red_knot_ide::Db, + db: &dyn ty_ide::Db, src: Option, encoding: PositionEncoding, ) -> Option; diff --git a/crates/red_knot_server/src/document/notebook.rs b/crates/ty_server/src/document/notebook.rs similarity index 100% rename from crates/red_knot_server/src/document/notebook.rs rename to crates/ty_server/src/document/notebook.rs diff --git a/crates/red_knot_server/src/document/range.rs b/crates/ty_server/src/document/range.rs similarity index 99% rename from crates/red_knot_server/src/document/range.rs rename to crates/ty_server/src/document/range.rs index 492f2f86ee308e..4a9e49ef3112c4 100644 --- a/crates/red_knot_server/src/document/range.rs +++ b/crates/ty_server/src/document/range.rs @@ -5,13 +5,13 @@ use crate::system::file_to_url; use lsp_types as types; use lsp_types::Location; -use red_knot_python_semantic::Db; use ruff_db::files::FileRange; use ruff_db::source::{line_index, source_text}; use ruff_notebook::NotebookIndex; use ruff_source_file::LineIndex; use ruff_source_file::{OneIndexed, SourceLocation}; use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_python_semantic::Db; #[expect(dead_code)] pub(crate) struct NotebookRange { diff --git a/crates/red_knot_server/src/document/text_document.rs b/crates/ty_server/src/document/text_document.rs similarity index 100% rename from crates/red_knot_server/src/document/text_document.rs rename to crates/ty_server/src/document/text_document.rs diff --git a/crates/red_knot_server/src/lib.rs b/crates/ty_server/src/lib.rs similarity index 90% rename from crates/red_knot_server/src/lib.rs rename to crates/ty_server/src/lib.rs index ccd35d702571aa..efc27184c72192 100644 --- a/crates/red_knot_server/src/lib.rs +++ b/crates/ty_server/src/lib.rs @@ -13,8 +13,8 @@ mod server; mod session; mod system; -pub(crate) const SERVER_NAME: &str = "red-knot"; -pub(crate) const DIAGNOSTIC_NAME: &str = "Red Knot"; +pub(crate) const SERVER_NAME: &str = "ty"; +pub(crate) const DIAGNOSTIC_NAME: &str = "ty"; /// A common result type used in most cases where a /// result type is needed. diff --git a/crates/red_knot_server/src/logging.rs b/crates/ty_server/src/logging.rs similarity index 96% rename from crates/red_knot_server/src/logging.rs rename to crates/ty_server/src/logging.rs index bde8c57d099823..a322ef249975a1 100644 --- a/crates/red_knot_server/src/logging.rs +++ b/crates/ty_server/src/logging.rs @@ -1,4 +1,4 @@ -//! The logging system for `red_knot server`. +//! The logging system for `ty server`. //! //! Log messages are controlled by the `logLevel` setting which defaults to `"info"`. Log messages //! are written to `stderr` by default, which should appear in the logs for most LSP clients. A @@ -99,7 +99,7 @@ impl tracing_subscriber::layer::Filter for LogLevelFilter { meta: &tracing::Metadata<'_>, _: &tracing_subscriber::layer::Context<'_, S>, ) -> bool { - let filter = if meta.target().starts_with("red_knot") { + let filter = if meta.target().starts_with("ty") { self.filter.trace_level() } else { tracing::Level::WARN diff --git a/crates/red_knot_server/src/message.rs b/crates/ty_server/src/message.rs similarity index 100% rename from crates/red_knot_server/src/message.rs rename to crates/ty_server/src/message.rs diff --git a/crates/red_knot_server/src/server.rs b/crates/ty_server/src/server.rs similarity index 100% rename from crates/red_knot_server/src/server.rs rename to crates/ty_server/src/server.rs diff --git a/crates/red_knot_server/src/server/api.rs b/crates/ty_server/src/server/api.rs similarity index 100% rename from crates/red_knot_server/src/server/api.rs rename to crates/ty_server/src/server/api.rs diff --git a/crates/red_knot_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs similarity index 100% rename from crates/red_knot_server/src/server/api/diagnostics.rs rename to crates/ty_server/src/server/api/diagnostics.rs diff --git a/crates/red_knot_server/src/server/api/notifications.rs b/crates/ty_server/src/server/api/notifications.rs similarity index 100% rename from crates/red_knot_server/src/server/api/notifications.rs rename to crates/ty_server/src/server/api/notifications.rs diff --git a/crates/red_knot_server/src/server/api/notifications/did_change.rs b/crates/ty_server/src/server/api/notifications/did_change.rs similarity index 97% rename from crates/red_knot_server/src/server/api/notifications/did_change.rs rename to crates/ty_server/src/server/api/notifications/did_change.rs index 93e23f01e17aa4..c230f7be0e6219 100644 --- a/crates/red_knot_server/src/server/api/notifications/did_change.rs +++ b/crates/ty_server/src/server/api/notifications/did_change.rs @@ -2,7 +2,7 @@ use lsp_server::ErrorCode; use lsp_types::notification::DidChangeTextDocument; use lsp_types::DidChangeTextDocumentParams; -use red_knot_project::watch::ChangeEvent; +use ty_project::watch::ChangeEvent; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; use crate::server::api::LSPResult; diff --git a/crates/red_knot_server/src/server/api/notifications/did_close.rs b/crates/ty_server/src/server/api/notifications/did_close.rs similarity index 97% rename from crates/red_knot_server/src/server/api/notifications/did_close.rs rename to crates/ty_server/src/server/api/notifications/did_close.rs index 9aea65ccd475d4..a63348d1c5a0c1 100644 --- a/crates/red_knot_server/src/server/api/notifications/did_close.rs +++ b/crates/ty_server/src/server/api/notifications/did_close.rs @@ -1,7 +1,7 @@ use lsp_server::ErrorCode; use lsp_types::notification::DidCloseTextDocument; use lsp_types::DidCloseTextDocumentParams; -use red_knot_project::watch::ChangeEvent; +use ty_project::watch::ChangeEvent; use crate::server::api::diagnostics::clear_diagnostics; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; diff --git a/crates/red_knot_server/src/server/api/notifications/did_close_notebook.rs b/crates/ty_server/src/server/api/notifications/did_close_notebook.rs similarity index 96% rename from crates/red_knot_server/src/server/api/notifications/did_close_notebook.rs rename to crates/ty_server/src/server/api/notifications/did_close_notebook.rs index 240d7beebbce62..741f436f0af28e 100644 --- a/crates/red_knot_server/src/server/api/notifications/did_close_notebook.rs +++ b/crates/ty_server/src/server/api/notifications/did_close_notebook.rs @@ -1,7 +1,7 @@ use lsp_types::notification::DidCloseNotebookDocument; use lsp_types::DidCloseNotebookDocumentParams; -use red_knot_project::watch::ChangeEvent; +use ty_project::watch::ChangeEvent; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; use crate::server::api::LSPResult; diff --git a/crates/red_knot_server/src/server/api/notifications/did_open.rs b/crates/ty_server/src/server/api/notifications/did_open.rs similarity index 97% rename from crates/red_knot_server/src/server/api/notifications/did_open.rs rename to crates/ty_server/src/server/api/notifications/did_open.rs index 2530640bf6231d..3bd1698f10f8e6 100644 --- a/crates/red_knot_server/src/server/api/notifications/did_open.rs +++ b/crates/ty_server/src/server/api/notifications/did_open.rs @@ -1,8 +1,8 @@ use lsp_types::notification::DidOpenTextDocument; use lsp_types::{DidOpenTextDocumentParams, TextDocumentItem}; -use red_knot_project::watch::ChangeEvent; use ruff_db::Db; +use ty_project::watch::ChangeEvent; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; use crate::server::client::{Notifier, Requester}; diff --git a/crates/red_knot_server/src/server/api/notifications/did_open_notebook.rs b/crates/ty_server/src/server/api/notifications/did_open_notebook.rs similarity index 97% rename from crates/red_knot_server/src/server/api/notifications/did_open_notebook.rs rename to crates/ty_server/src/server/api/notifications/did_open_notebook.rs index ea355e7e0fc8b8..5bf3c4ca3e205e 100644 --- a/crates/red_knot_server/src/server/api/notifications/did_open_notebook.rs +++ b/crates/ty_server/src/server/api/notifications/did_open_notebook.rs @@ -2,8 +2,8 @@ use lsp_server::ErrorCode; use lsp_types::notification::DidOpenNotebookDocument; use lsp_types::DidOpenNotebookDocumentParams; -use red_knot_project::watch::ChangeEvent; use ruff_db::Db; +use ty_project::watch::ChangeEvent; use crate::document::NotebookDocument; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; diff --git a/crates/red_knot_server/src/server/api/requests.rs b/crates/ty_server/src/server/api/requests.rs similarity index 100% rename from crates/red_knot_server/src/server/api/requests.rs rename to crates/ty_server/src/server/api/requests.rs diff --git a/crates/red_knot_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs similarity index 96% rename from crates/red_knot_server/src/server/api/requests/completion.rs rename to crates/ty_server/src/server/api/requests/completion.rs index 93a1c9d8ccfe34..6ca705711324f4 100644 --- a/crates/red_knot_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -2,9 +2,9 @@ use std::borrow::Cow; use lsp_types::request::Completion; use lsp_types::{CompletionItem, CompletionParams, CompletionResponse, Url}; -use red_knot_ide::completion; -use red_knot_project::ProjectDatabase; use ruff_db::source::{line_index, source_text}; +use ty_ide::completion; +use ty_project::ProjectDatabase; use crate::document::PositionExt; use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; diff --git a/crates/red_knot_server/src/server/api/requests/diagnostic.rs b/crates/ty_server/src/server/api/requests/diagnostic.rs similarity index 97% rename from crates/red_knot_server/src/server/api/requests/diagnostic.rs rename to crates/ty_server/src/server/api/requests/diagnostic.rs index 8b3d03d34b2dc6..e128e22a4426e9 100644 --- a/crates/red_knot_server/src/server/api/requests/diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/diagnostic.rs @@ -11,9 +11,9 @@ use crate::document::ToRangeExt; use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; use crate::server::{client::Notifier, Result}; use crate::session::DocumentSnapshot; -use red_knot_project::{Db, ProjectDatabase}; use ruff_db::diagnostic::Severity; use ruff_db::source::{line_index, source_text}; +use ty_project::{Db, ProjectDatabase}; pub(crate) struct DocumentDiagnosticRequestHandler; @@ -98,7 +98,7 @@ fn to_lsp_diagnostic( tags: None, code: Some(NumberOrString::String(diagnostic.id().to_string())), code_description: None, - source: Some("red-knot".into()), + source: Some("ty".into()), message: diagnostic.concise_message().to_string(), related_information: None, data: None, diff --git a/crates/red_knot_server/src/server/api/requests/goto_type_definition.rs b/crates/ty_server/src/server/api/requests/goto_type_definition.rs similarity index 96% rename from crates/red_knot_server/src/server/api/requests/goto_type_definition.rs rename to crates/ty_server/src/server/api/requests/goto_type_definition.rs index bb3a4e6e5804db..8ced577d19b32b 100644 --- a/crates/red_knot_server/src/server/api/requests/goto_type_definition.rs +++ b/crates/ty_server/src/server/api/requests/goto_type_definition.rs @@ -2,9 +2,9 @@ use std::borrow::Cow; use lsp_types::request::{GotoTypeDefinition, GotoTypeDefinitionParams}; use lsp_types::{GotoDefinitionResponse, Url}; -use red_knot_ide::goto_type_definition; -use red_knot_project::ProjectDatabase; use ruff_db::source::{line_index, source_text}; +use ty_ide::goto_type_definition; +use ty_project::ProjectDatabase; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; diff --git a/crates/red_knot_server/src/server/api/requests/hover.rs b/crates/ty_server/src/server/api/requests/hover.rs similarity index 96% rename from crates/red_knot_server/src/server/api/requests/hover.rs rename to crates/ty_server/src/server/api/requests/hover.rs index 2677f67c3231fd..c9b45ee84f3480 100644 --- a/crates/red_knot_server/src/server/api/requests/hover.rs +++ b/crates/ty_server/src/server/api/requests/hover.rs @@ -6,10 +6,10 @@ use crate::server::client::Notifier; use crate::DocumentSnapshot; use lsp_types::request::HoverRequest; use lsp_types::{HoverContents, HoverParams, MarkupContent, Url}; -use red_knot_ide::{hover, MarkupKind}; -use red_knot_project::ProjectDatabase; use ruff_db::source::{line_index, source_text}; use ruff_text_size::Ranged; +use ty_ide::{hover, MarkupKind}; +use ty_project::ProjectDatabase; pub(crate) struct HoverRequestHandler; diff --git a/crates/red_knot_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs similarity index 96% rename from crates/red_knot_server/src/server/api/requests/inlay_hints.rs rename to crates/ty_server/src/server/api/requests/inlay_hints.rs index f299fdf9757a33..1bc1b330d52e68 100644 --- a/crates/red_knot_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -6,9 +6,9 @@ use crate::server::client::Notifier; use crate::DocumentSnapshot; use lsp_types::request::InlayHintRequest; use lsp_types::{InlayHintParams, Url}; -use red_knot_ide::inlay_hints; -use red_knot_project::ProjectDatabase; use ruff_db::source::{line_index, source_text}; +use ty_ide::inlay_hints; +use ty_project::ProjectDatabase; pub(crate) struct InlayHintRequestHandler; diff --git a/crates/red_knot_server/src/server/api/traits.rs b/crates/ty_server/src/server/api/traits.rs similarity index 98% rename from crates/red_knot_server/src/server/api/traits.rs rename to crates/ty_server/src/server/api/traits.rs index e5c9a609078c53..89db4c38332fbf 100644 --- a/crates/red_knot_server/src/server/api/traits.rs +++ b/crates/ty_server/src/server/api/traits.rs @@ -5,7 +5,7 @@ use crate::session::{DocumentSnapshot, Session}; use lsp_types::notification::Notification as LSPNotification; use lsp_types::request::Request; -use red_knot_project::ProjectDatabase; +use ty_project::ProjectDatabase; /// A supertrait for any server request handler. pub(super) trait RequestHandler { diff --git a/crates/red_knot_server/src/server/client.rs b/crates/ty_server/src/server/client.rs similarity index 100% rename from crates/red_knot_server/src/server/client.rs rename to crates/ty_server/src/server/client.rs diff --git a/crates/red_knot_server/src/server/connection.rs b/crates/ty_server/src/server/connection.rs similarity index 100% rename from crates/red_knot_server/src/server/connection.rs rename to crates/ty_server/src/server/connection.rs diff --git a/crates/red_knot_server/src/server/schedule.rs b/crates/ty_server/src/server/schedule.rs similarity index 100% rename from crates/red_knot_server/src/server/schedule.rs rename to crates/ty_server/src/server/schedule.rs diff --git a/crates/red_knot_server/src/server/schedule/task.rs b/crates/ty_server/src/server/schedule/task.rs similarity index 100% rename from crates/red_knot_server/src/server/schedule/task.rs rename to crates/ty_server/src/server/schedule/task.rs diff --git a/crates/red_knot_server/src/server/schedule/thread.rs b/crates/ty_server/src/server/schedule/thread.rs similarity index 100% rename from crates/red_knot_server/src/server/schedule/thread.rs rename to crates/ty_server/src/server/schedule/thread.rs diff --git a/crates/red_knot_server/src/server/schedule/thread/pool.rs b/crates/ty_server/src/server/schedule/thread/pool.rs similarity index 100% rename from crates/red_knot_server/src/server/schedule/thread/pool.rs rename to crates/ty_server/src/server/schedule/thread/pool.rs diff --git a/crates/red_knot_server/src/server/schedule/thread/priority.rs b/crates/ty_server/src/server/schedule/thread/priority.rs similarity index 100% rename from crates/red_knot_server/src/server/schedule/thread/priority.rs rename to crates/ty_server/src/server/schedule/thread/priority.rs diff --git a/crates/red_knot_server/src/session.rs b/crates/ty_server/src/session.rs similarity index 98% rename from crates/red_knot_server/src/session.rs rename to crates/ty_server/src/session.rs index 470592e8bda920..a429b492e5a213 100644 --- a/crates/red_knot_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -8,10 +8,10 @@ use std::sync::Arc; use anyhow::anyhow; use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url}; -use red_knot_project::{ProjectDatabase, ProjectMetadata}; use ruff_db::files::{system_path_to_file, File}; use ruff_db::system::SystemPath; use ruff_db::Db; +use ty_project::{ProjectDatabase, ProjectMetadata}; use crate::document::{DocumentKey, DocumentVersion, NotebookDocument}; use crate::system::{url_to_any_system_path, AnySystemPath, LSPSystem}; @@ -113,13 +113,13 @@ impl Session { /// Returns a reference to the default project [`ProjectDatabase`]. The default project is the /// minimum root path in the project map. pub(crate) fn default_project_db(&self) -> &ProjectDatabase { - // SAFETY: Currently, red knot only support a single project. + // SAFETY: Currently, ty only support a single project. self.projects_by_workspace_folder.values().next().unwrap() } /// Returns a mutable reference to the default project [`ProjectDatabase`]. pub(crate) fn default_project_db_mut(&mut self) -> &mut ProjectDatabase { - // SAFETY: Currently, red knot only support a single project. + // SAFETY: Currently, ty only support a single project. self.projects_by_workspace_folder .values_mut() .next() diff --git a/crates/red_knot_server/src/session/capabilities.rs b/crates/ty_server/src/session/capabilities.rs similarity index 100% rename from crates/red_knot_server/src/session/capabilities.rs rename to crates/ty_server/src/session/capabilities.rs diff --git a/crates/red_knot_server/src/session/index.rs b/crates/ty_server/src/session/index.rs similarity index 100% rename from crates/red_knot_server/src/session/index.rs rename to crates/ty_server/src/session/index.rs diff --git a/crates/red_knot_server/src/session/settings.rs b/crates/ty_server/src/session/settings.rs similarity index 100% rename from crates/red_knot_server/src/session/settings.rs rename to crates/ty_server/src/session/settings.rs diff --git a/crates/red_knot_server/src/system.rs b/crates/ty_server/src/system.rs similarity index 99% rename from crates/red_knot_server/src/system.rs rename to crates/ty_server/src/system.rs index 12d4057a727589..918e5c81bc03ab 100644 --- a/crates/red_knot_server/src/system.rs +++ b/crates/ty_server/src/system.rs @@ -3,7 +3,6 @@ use std::fmt::Display; use std::sync::Arc; use lsp_types::Url; -use red_knot_python_semantic::Db; use ruff_db::file_revision::FileRevision; use ruff_db::files::{File, FilePath}; use ruff_db::system::walk_directory::WalkDirectoryBuilder; @@ -12,6 +11,7 @@ use ruff_db::system::{ System, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, }; use ruff_notebook::{Notebook, NotebookError}; +use ty_python_semantic::Db; use crate::session::index::Index; use crate::DocumentQuery; diff --git a/crates/red_knot_test/Cargo.toml b/crates/ty_test/Cargo.toml similarity index 88% rename from crates/red_knot_test/Cargo.toml rename to crates/ty_test/Cargo.toml index 73aeb8daa48275..06b83c5ad43e0a 100644 --- a/crates/red_knot_test/Cargo.toml +++ b/crates/ty_test/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "red_knot_test" +name = "ty_test" version = "0.0.0" publish = false edition.workspace = true @@ -11,8 +11,6 @@ authors.workspace = true license.workspace = true [dependencies] -red_knot_python_semantic = { workspace = true, features = ["serde"] } -red_knot_vendored = { workspace = true } ruff_db = { workspace = true, features = ["os", "testing"] } ruff_index = { workspace = true } ruff_notebook = { workspace = true } @@ -20,6 +18,8 @@ ruff_python_trivia = { workspace = true } ruff_source_file = { workspace = true } ruff_text_size = { workspace = true } ruff_python_ast = { workspace = true } +ty_python_semantic = { workspace = true, features = ["serde"] } +ty_vendored = { workspace = true } anyhow = { workspace = true } camino = { workspace = true } diff --git a/crates/red_knot_test/README.md b/crates/ty_test/README.md similarity index 93% rename from crates/red_knot_test/README.md rename to crates/ty_test/README.md index 893c4d3b36961c..6930a0952813ae 100644 --- a/crates/red_knot_test/README.md +++ b/crates/ty_test/README.md @@ -2,8 +2,8 @@ Any Markdown file can be a test suite. -In order for it to be run as one, `red_knot_test::run` must be called with its path; see -`crates/red_knot_python_semantic/tests/mdtest.rs` for an example that treats all Markdown files +In order for it to be run as one, `ty_test::run` must be called with its path; see +`crates/ty_python_semantic/tests/mdtest.rs` for an example that treats all Markdown files under a certain directory as test suites. A Markdown test suite can contain any number of tests. A test consists of one or more embedded @@ -32,14 +32,14 @@ syntax, it's just how this README embeds an example mdtest Markdown document.) ---> See actual example mdtest suites in -[`crates/red_knot_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/resources/mdtest). +[`crates/ty_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/ty_python_semantic/resources/mdtest). > [!NOTE] > If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory, -> as with the example in `crates/red_knot_python_semantic/tests/mdtest.rs`, +> as with the example in `crates/ty_python_semantic/tests/mdtest.rs`, > you will likely want to also make sure that the crate the tests are in is rebuilt every time a > Markdown file is added or removed from the directory. See -> [`crates/red_knot_python_semantic/build.rs`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/build.rs) +> [`crates/ty_python_semantic/build.rs`](https://github.com/astral-sh/ruff/tree/main/crates/ty_python_semantic/build.rs) > for an example of how to do this. > > This is because these macros generate their tests at build time rather than at runtime. @@ -237,7 +237,7 @@ This test suite contains two tests, one named "Same-file invalid assignment" and "Cross-file invalid assignment". The first test involves only a single embedded file, and the second test involves two embedded files. -The tests are run independently, in independent in-memory file systems and with new red-knot +The tests are run independently, in independent in-memory file systems and with new ty [Salsa](https://github.com/salsa-rs/salsa) databases. This means that each is a from-scratch run of the type checker, with no data persisting from any previous test. @@ -284,7 +284,7 @@ deeply-nested headers (headers with more `#`), but it cannot contain both. ## Configuration -The test framework supports a TOML-based configuration format, which is a subset of the full red-knot +The test framework supports a TOML-based configuration format, which is a subset of the full ty configuration format. This configuration can be specified in fenced code blocks with `toml` as the language tag: @@ -298,7 +298,7 @@ python-version = "3.10" This configuration will apply to all tests in the same section, and all nested sections within that section. Nested sections can override configurations from their parent sections. -See [`MarkdownTestConfig`](https://github.com/astral-sh/ruff/blob/main/crates/red_knot_test/src/config.rs) for the full list of supported configuration options. +See [`MarkdownTestConfig`](https://github.com/astral-sh/ruff/blob/main/crates/ty_test/src/config.rs) for the full list of supported configuration options. ### Specifying a custom typeshed @@ -326,7 +326,7 @@ python = ".venv" ``` ```` -Red-knot will reject virtual environments that do not have valid `pyvenv.cfg` files at the +ty will reject virtual environments that do not have valid `pyvenv.cfg` files at the virtual-environment directory root (here, `.venv/pyvenv.cfg`). However, if a `pyvenv.cfg` file does not have its contents specified by the test, mdtest will automatically generate one for you, to make mocking a virtual environment more ergonomic. @@ -373,13 +373,13 @@ All Markdown-based tests are executed in a normal `cargo test` / `cargo run next *only*, you can filter the tests using `mdtest__`: ```bash -cargo test -p red_knot_python_semantic -- mdtest__ +cargo test -p ty_python_semantic -- mdtest__ ``` Alternatively, you can use the `mdtest.py` runner which has a watch mode that will re-run corresponding tests when Markdown files change, and recompile automatically when Rust code changes: ```bash -uv run crates/red_knot_python_semantic/mdtest.py +uv run crates/ty_python_semantic/mdtest.py ``` ## Planned features @@ -406,10 +406,10 @@ an assertion ended by `>>>>`, etc. ### Configuring search paths and kinds -The red-knot TOML configuration format hasn't been finalized, and we may want to implement +The ty TOML configuration format hasn't been finalized, and we may want to implement support in the test framework for configuring search paths before it is designed. If so, we can define some configuration options for now under the `[tests]` namespace. In the future, perhaps -some of these can be replaced by real red-knot configuration options; some or all may also be +some of these can be replaced by real ty configuration options; some or all may also be kept long-term as test-specific options. Some configuration options we will want to provide: @@ -454,7 +454,7 @@ x = 1 reveal_type(x) ``` -This is just an example, not a proposal that red-knot would ever actually output diagnostics in +This is just an example, not a proposal that ty would ever actually output diagnostics in precisely this format: ```output @@ -538,5 +538,5 @@ cold, to validate equivalence of cold and incremental check results. [^extensions]: `typing-extensions` is a third-party module, but typeshed, and thus type checkers also, treat it as part of the standard library. -[custom-typeshed markdown test]: ../red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md +[custom-typeshed markdown test]: ../ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md [typeshed `versions`]: https://github.com/python/typeshed/blob/c546278aae47de0b2b664973da4edb613400f6ce/stdlib/VERSIONS#L1-L18%3E diff --git a/crates/red_knot_test/src/assertion.rs b/crates/ty_test/src/assertion.rs similarity index 99% rename from crates/red_knot_test/src/assertion.rs rename to crates/ty_test/src/assertion.rs index 8f31b5de666a77..529f39eee1bec8 100644 --- a/crates/red_knot_test/src/assertion.rs +++ b/crates/ty_test/src/assertion.rs @@ -489,12 +489,12 @@ 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; + use ty_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings}; fn get_assertions(source: &str) -> InlineFileAssertions { let mut db = Db::setup(); diff --git a/crates/red_knot_test/src/config.rs b/crates/ty_test/src/config.rs similarity index 89% rename from crates/red_knot_test/src/config.rs rename to crates/ty_test/src/config.rs index 8f2c4c3caaa436..ba279007192c44 100644 --- a/crates/red_knot_test/src/config.rs +++ b/crates/ty_test/src/config.rs @@ -1,18 +1,18 @@ -//! TOML-deserializable Red Knot configuration, similar to `knot.toml`, to be able to +//! TOML-deserializable ty configuration, similar to `ty.toml`, to be able to //! control some configuration options from Markdown files. For now, this supports the //! following limited structure: //! //! ```toml -//! log = true # or log = "red_knot=WARN" +//! log = true # or log = "ty=WARN" //! [environment] //! python-version = "3.10" //! ``` use anyhow::Context; -use red_knot_python_semantic::PythonPlatform; use ruff_db::system::{SystemPath, SystemPathBuf}; use ruff_python_ast::PythonVersion; use serde::{Deserialize, Serialize}; +use ty_python_semantic::PythonPlatform; #[derive(Deserialize, Debug, Default, Clone)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] @@ -68,9 +68,9 @@ pub(crate) struct Environment { /// Additional search paths to consider when resolving modules. pub(crate) extra_paths: Option>, - /// Path to the Python installation from which Red Knot resolves type information and third-party dependencies. + /// Path to the Python installation from which ty resolves type information and third-party dependencies. /// - /// Red Knot will search in the path's `site-packages` directories for type information and + /// ty will search in the path's `site-packages` directories for type information and /// third-party imports. /// /// This option is commonly used to specify the path to a virtual environment. diff --git a/crates/red_knot_test/src/db.rs b/crates/ty_test/src/db.rs similarity index 97% rename from crates/red_knot_test/src/db.rs rename to crates/ty_test/src/db.rs index c26206e66a3b95..3c43bc96fa5aec 100644 --- a/crates/red_knot_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -1,6 +1,4 @@ use camino::{Utf8Component, Utf8PathBuf}; -use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; -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, @@ -12,6 +10,8 @@ use ruff_notebook::{Notebook, NotebookError}; use std::borrow::Cow; use std::sync::Arc; use tempfile::TempDir; +use ty_python_semantic::lint::{LintRegistry, RuleSelection}; +use ty_python_semantic::{default_lint_registry, Db as SemanticDb, Program}; #[salsa::db] #[derive(Clone)] @@ -30,7 +30,7 @@ impl Db { Self { system: MdtestSystem::in_memory(), storage: salsa::Storage::default(), - vendored: red_knot_vendored::file_system().clone(), + vendored: ty_vendored::file_system().clone(), files: Files::default(), rule_selection: Arc::new(rule_selection), } diff --git a/crates/red_knot_test/src/diagnostic.rs b/crates/ty_test/src/diagnostic.rs similarity index 100% rename from crates/red_knot_test/src/diagnostic.rs rename to crates/ty_test/src/diagnostic.rs diff --git a/crates/red_knot_test/src/lib.rs b/crates/ty_test/src/lib.rs similarity index 99% rename from crates/red_knot_test/src/lib.rs rename to crates/ty_test/src/lib.rs index 4ae5dca0332a62..b6dab4fb024e12 100644 --- a/crates/red_knot_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -4,10 +4,6 @@ use camino::Utf8Path; use colored::Colorize; use config::SystemKind; use parser as test_parser; -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, create_unsupported_syntax_diagnostic, Diagnostic, DisplayDiagnosticConfig, @@ -20,6 +16,10 @@ use ruff_db::testing::{setup_logging, setup_logging_with_filter}; use ruff_source_file::{LineIndex, OneIndexed}; use std::backtrace::BacktraceStatus; use std::fmt::Write; +use ty_python_semantic::types::check_types; +use ty_python_semantic::{ + Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, SysPrefixPathOrigin, +}; mod assertion; mod config; @@ -104,7 +104,7 @@ pub fn run( "\nTo rerun this specific test, set the environment variable: {MDTEST_TEST_FILTER}='{escaped_test_name}'", ); println!( - "{MDTEST_TEST_FILTER}='{escaped_test_name}' cargo test -p red_knot_python_semantic --test mdtest -- {test_name}", + "{MDTEST_TEST_FILTER}='{escaped_test_name}' cargo test -p ty_python_semantic --test mdtest -- {test_name}", ); } } diff --git a/crates/red_knot_test/src/matcher.rs b/crates/ty_test/src/matcher.rs similarity index 99% rename from crates/red_knot_test/src/matcher.rs rename to crates/ty_test/src/matcher.rs index 4be6658b00868d..4beeb478e7857c 100644 --- a/crates/red_knot_test/src/matcher.rs +++ b/crates/ty_test/src/matcher.rs @@ -343,7 +343,6 @@ 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 _; @@ -351,6 +350,7 @@ mod tests { use ruff_python_trivia::textwrap::dedent; use ruff_source_file::OneIndexed; use ruff_text_size::TextRange; + use ty_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings}; struct ExpectedDiagnostic { id: DiagnosticId, diff --git a/crates/red_knot_test/src/parser.rs b/crates/ty_test/src/parser.rs similarity index 100% rename from crates/red_knot_test/src/parser.rs rename to crates/ty_test/src/parser.rs diff --git a/crates/red_knot_vendored/.gitignore b/crates/ty_vendored/.gitignore similarity index 100% rename from crates/red_knot_vendored/.gitignore rename to crates/ty_vendored/.gitignore diff --git a/crates/red_knot_vendored/Cargo.toml b/crates/ty_vendored/Cargo.toml similarity index 95% rename from crates/red_knot_vendored/Cargo.toml rename to crates/ty_vendored/Cargo.toml index b740bef3f4db59..bd8f8e175bfd7c 100644 --- a/crates/red_knot_vendored/Cargo.toml +++ b/crates/ty_vendored/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "red_knot_vendored" +name = "ty_vendored" version = "0.0.0" publish = false authors = { workspace = true } diff --git a/crates/red_knot_vendored/README.md b/crates/ty_vendored/README.md similarity index 69% rename from crates/red_knot_vendored/README.md rename to crates/ty_vendored/README.md index dd9a5849b00bdb..40ab2fc04f18fe 100644 --- a/crates/red_knot_vendored/README.md +++ b/crates/ty_vendored/README.md @@ -1,5 +1,5 @@ # Vendored types for the stdlib -This crate vendors [typeshed](https://github.com/python/typeshed)'s stubs for the standard library. The vendored stubs can be found in `crates/red_knot_vendored/vendor/typeshed`. The file `crates/red_knot_vendored/vendor/typeshed/source_commit.txt` tells you the typeshed commit that our vendored stdlib stubs currently correspond to. +This crate vendors [typeshed](https://github.com/python/typeshed)'s stubs for the standard library. The vendored stubs can be found in `crates/ty_vendored/vendor/typeshed`. The file `crates/ty_vendored/vendor/typeshed/source_commit.txt` tells you the typeshed commit that our vendored stdlib stubs currently correspond to. The typeshed stubs are updated every two weeks via an automated PR using the `sync_typeshed.yaml` workflow in the `.github/workflows` directory. This workflow can also be triggered at any time via [workflow dispatch](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow#running-a-workflow). diff --git a/crates/red_knot_vendored/build.rs b/crates/ty_vendored/build.rs similarity index 86% rename from crates/red_knot_vendored/build.rs rename to crates/ty_vendored/build.rs index 2062db3d6e76ab..86b1fb843adcd8 100644 --- a/crates/red_knot_vendored/build.rs +++ b/crates/ty_vendored/build.rs @@ -3,7 +3,7 @@ //! //! This script should be automatically run at build time //! whenever the script itself changes, or whenever any files -//! in `crates/red_knot_vendored/vendor/typeshed` change. +//! in `crates/ty_vendored/vendor/typeshed` change. use std::fs::File; use std::io::Write; @@ -15,11 +15,11 @@ use zip::write::{FileOptions, ZipWriter}; use zip::CompressionMethod; const TYPESHED_SOURCE_DIR: &str = "vendor/typeshed"; -const KNOT_EXTENSIONS_STUBS: &str = "knot_extensions/knot_extensions.pyi"; +const TY_EXTENSIONS_STUBS: &str = "ty_extensions/ty_extensions.pyi"; const TYPESHED_ZIP_LOCATION: &str = "/zipped_typeshed.zip"; /// Recursively zip the contents of the entire typeshed directory and patch typeshed -/// on the fly to include the `knot_extensions` module. +/// on the fly to include the `ty_extensions` module. /// /// This routine is adapted from a recipe at /// @@ -62,9 +62,9 @@ fn write_zipped_typeshed_to(writer: File) -> ZipResult { let mut f = File::open(absolute_path)?; std::io::copy(&mut f, &mut zip).unwrap(); - // Patch the VERSIONS file to make `knot_extensions` available + // Patch the VERSIONS file to make `ty_extensions` available if normalized_relative_path == "stdlib/VERSIONS" { - writeln!(&mut zip, "knot_extensions: 3.0-")?; + writeln!(&mut zip, "ty_extensions: 3.0-")?; } } else if !normalized_relative_path.is_empty() { // Only if not root! Avoids path spec / warning @@ -74,10 +74,10 @@ fn write_zipped_typeshed_to(writer: File) -> ZipResult { } } - // Patch typeshed and add the stubs for the `knot_extensions` module - println!("adding file {KNOT_EXTENSIONS_STUBS} as stdlib/knot_extensions.pyi ..."); - zip.start_file("stdlib/knot_extensions.pyi", options)?; - let mut f = File::open(KNOT_EXTENSIONS_STUBS)?; + // Patch typeshed and add the stubs for the `ty_extensions` module + println!("adding file {TY_EXTENSIONS_STUBS} as stdlib/ty_extensions.pyi ..."); + zip.start_file("stdlib/ty_extensions.pyi", options)?; + let mut f = File::open(TY_EXTENSIONS_STUBS)?; std::io::copy(&mut f, &mut zip).unwrap(); zip.finish() diff --git a/crates/red_knot_vendored/src/lib.rs b/crates/ty_vendored/src/lib.rs similarity index 100% rename from crates/red_knot_vendored/src/lib.rs rename to crates/ty_vendored/src/lib.rs diff --git a/crates/ty_vendored/ty_extensions/README.md b/crates/ty_vendored/ty_extensions/README.md new file mode 100644 index 00000000000000..594f6c98faea01 --- /dev/null +++ b/crates/ty_vendored/ty_extensions/README.md @@ -0,0 +1,2 @@ +The `build.rs` copies the `ty_extensions.pyi` file in this directory into +the `vendor/typeshed/stdlib` directory. diff --git a/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi similarity index 100% rename from crates/red_knot_vendored/knot_extensions/knot_extensions.pyi rename to crates/ty_vendored/ty_extensions/ty_extensions.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/LICENSE b/crates/ty_vendored/vendor/typeshed/LICENSE similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/LICENSE rename to crates/ty_vendored/vendor/typeshed/LICENSE diff --git a/crates/red_knot_vendored/vendor/typeshed/README.md b/crates/ty_vendored/vendor/typeshed/README.md similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/README.md rename to crates/ty_vendored/vendor/typeshed/README.md diff --git a/crates/red_knot_vendored/vendor/typeshed/source_commit.txt b/crates/ty_vendored/vendor/typeshed/source_commit.txt similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/source_commit.txt rename to crates/ty_vendored/vendor/typeshed/source_commit.txt diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS b/crates/ty_vendored/vendor/typeshed/stdlib/VERSIONS similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS rename to crates/ty_vendored/vendor/typeshed/stdlib/VERSIONS diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/__future__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/__future__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/__future__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/__future__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/__main__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/__main__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/__main__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/__main__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_ast.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_ast.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_ast.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_ast.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_asyncio.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_asyncio.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_asyncio.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_asyncio.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_bisect.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_bisect.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_bisect.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_bisect.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_blake2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_blake2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_blake2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_blake2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_bootlocale.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_bootlocale.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_bootlocale.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_bootlocale.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_bz2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_bz2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_bz2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_bz2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_codecs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_codecs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_codecs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_codecs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_collections_abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_collections_abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_collections_abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_collections_abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_compat_pickle.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_compat_pickle.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_compat_pickle.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_compat_pickle.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_compression.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_compression.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_compression.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_compression.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_contextvars.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_contextvars.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_contextvars.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_contextvars.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_csv.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_csv.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_csv.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_csv.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_ctypes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_ctypes.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_curses.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_curses.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_curses.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_curses.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_curses_panel.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_curses_panel.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_dbm.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_dbm.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_dbm.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_dbm.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_decimal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_decimal.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_decimal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_decimal.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_gdbm.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_gdbm.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_gdbm.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_gdbm.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_hashlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_hashlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_hashlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_hashlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_heapq.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_heapq.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_heapq.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_heapq.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_imp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_imp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_imp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_imp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_interpchannels.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_interpchannels.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_interpchannels.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_interpchannels.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_interpqueues.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_interpqueues.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_interpqueues.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_interpqueues.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_interpreters.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_interpreters.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_interpreters.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_interpreters.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_io.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_io.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_io.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_io.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_json.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_json.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_json.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_json.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_locale.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_locale.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_locale.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_locale.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_lsprof.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_lsprof.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_lsprof.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_lsprof.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_lzma.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_lzma.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_lzma.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_lzma.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_markupbase.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_markupbase.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_markupbase.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_markupbase.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_msi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_msi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_msi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_msi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_multibytecodec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_multibytecodec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_multibytecodec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_multibytecodec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_operator.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_operator.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_operator.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_operator.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_osx_support.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_osx_support.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_osx_support.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_osx_support.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_pickle.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_pickle.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_pickle.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_pickle.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_posixsubprocess.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_posixsubprocess.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_posixsubprocess.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_posixsubprocess.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_py_abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_py_abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_py_abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_py_abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_pydecimal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_pydecimal.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_pydecimal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_pydecimal.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_queue.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_queue.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_queue.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_queue.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_random.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_random.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_random.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_random.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_socket.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_socket.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_socket.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_socket.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_sqlite3.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_sqlite3.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_sqlite3.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_sqlite3.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_ssl.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_ssl.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_ssl.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_ssl.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_stat.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_stat.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_stat.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_stat.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_struct.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_struct.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_struct.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_struct.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_thread.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_thread.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_threading_local.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_threading_local.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_threading_local.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_threading_local.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_tkinter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_tkinter.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_tkinter.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_tkinter.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_tracemalloc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_tracemalloc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_tracemalloc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_tracemalloc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/README.md b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/README.md similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/README.md rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/README.md diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/dbapi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/dbapi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/dbapi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/dbapi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/importlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/importlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/importlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/importlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/wsgi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/wsgi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/wsgi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/wsgi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/xml.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/xml.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_typeshed/xml.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/xml.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_warnings.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_warnings.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_warnings.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_warnings.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_weakref.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_weakref.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_weakref.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_weakref.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_weakrefset.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_weakrefset.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_weakrefset.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_weakrefset.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/_winapi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_winapi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/_winapi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/_winapi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/aifc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/aifc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/aifc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/aifc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/antigravity.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/antigravity.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/antigravity.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/antigravity.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/argparse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/argparse.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/argparse.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/argparse.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/array.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/array.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/array.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/array.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ast.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ast.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ast.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ast.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asynchat.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asynchat.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asynchat.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asynchat.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_events.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_events.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_subprocess.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_tasks.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_tasks.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/base_tasks.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_tasks.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/constants.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/constants.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/constants.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/constants.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/exceptions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/exceptions.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/exceptions.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/exceptions.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/format_helpers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/format_helpers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/format_helpers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/format_helpers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/futures.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/futures.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/futures.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/futures.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/locks.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/locks.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/locks.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/locks.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/log.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/log.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/log.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/log.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/mixins.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/mixins.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/mixins.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/mixins.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/proactor_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/proactor_events.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/proactor_events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/proactor_events.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/protocols.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/protocols.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/protocols.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/protocols.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/queues.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/queues.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/queues.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/queues.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/runners.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/runners.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/runners.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/runners.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/selector_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/selector_events.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/selector_events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/selector_events.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/sslproto.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/sslproto.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/sslproto.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/sslproto.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/staggered.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/staggered.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/staggered.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/staggered.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/streams.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/streams.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/streams.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/streams.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/subprocess.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/subprocess.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/subprocess.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/subprocess.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/taskgroups.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/taskgroups.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/taskgroups.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/taskgroups.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/tasks.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/threads.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/threads.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/threads.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/threads.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/timeouts.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/timeouts.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/timeouts.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/timeouts.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/transports.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/transports.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/transports.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/transports.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/unix_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/unix_events.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/unix_events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/unix_events.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/windows_utils.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_utils.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncio/windows_utils.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_utils.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/asyncore.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncore.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/asyncore.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/asyncore.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/atexit.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/atexit.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/atexit.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/atexit.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/audioop.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/audioop.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/audioop.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/audioop.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/base64.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/base64.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/base64.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/base64.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/bdb.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/bdb.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/bdb.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/bdb.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/binascii.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/binascii.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/binascii.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/binascii.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/binhex.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/binhex.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/binhex.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/binhex.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/bisect.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/bisect.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/bisect.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/bisect.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/builtins.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/builtins.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/bz2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/bz2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/bz2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/bz2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/cProfile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cProfile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/cProfile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/cProfile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/calendar.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/calendar.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/calendar.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/calendar.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/cgi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cgi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/cgi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/cgi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/cgitb.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cgitb.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/cgitb.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/cgitb.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/chunk.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/chunk.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/chunk.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/chunk.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/cmath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cmath.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/cmath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/cmath.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/cmd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/cmd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/cmd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/cmd.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/code.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/code.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/code.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/code.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/codecs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/codecs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/codecs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/codecs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/codeop.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/codeop.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/codeop.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/codeop.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/collections/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/collections/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/collections/abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/collections/abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/collections/abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/collections/abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/colorsys.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/colorsys.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/colorsys.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/colorsys.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/compileall.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/compileall.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/compileall.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/compileall.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/concurrent/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/_base.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/thread.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/thread.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/concurrent/futures/thread.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/thread.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/configparser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/configparser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/configparser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/configparser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/contextlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/contextlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/contextvars.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/contextvars.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/contextvars.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/contextvars.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/copy.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/copy.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/copy.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/copy.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/copyreg.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/copyreg.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/copyreg.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/copyreg.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/crypt.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/crypt.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/crypt.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/crypt.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/csv.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/csv.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/csv.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/csv.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/_endian.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/_endian.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/_endian.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/_endian.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dyld.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/dylib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dylib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/dylib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/dylib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/framework.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/framework.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/macholib/framework.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/macholib/framework.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/wintypes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/wintypes.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ctypes/wintypes.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ctypes/wintypes.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/curses/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/curses/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/ascii.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/ascii.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/curses/ascii.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/curses/ascii.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/has_key.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/has_key.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/curses/has_key.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/curses/has_key.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/panel.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/panel.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/curses/panel.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/curses/panel.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/curses/textpad.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/curses/textpad.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/curses/textpad.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/curses/textpad.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dataclasses.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dataclasses.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/datetime.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/datetime.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/datetime.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/datetime.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dbm/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/dumb.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/dumb.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/dumb.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dbm/dumb.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/gnu.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/gnu.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/gnu.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dbm/gnu.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/ndbm.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/ndbm.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/ndbm.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dbm/ndbm.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/sqlite3.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dbm/sqlite3.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dbm/sqlite3.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dbm/sqlite3.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/decimal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/decimal.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/decimal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/decimal.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/difflib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/difflib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/difflib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/difflib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/dis.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dis.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/dis.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/dis.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/_msvccompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/_msvccompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/_msvccompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/_msvccompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/archive_util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/archive_util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/archive_util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/archive_util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/bcppcompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/bcppcompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/bcppcompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/bcppcompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/ccompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/ccompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/ccompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/ccompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/cmd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/cmd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/cmd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/cmd.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_dumb.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_msi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_msi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_msi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_msi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_packager.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_packager.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_packager.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_packager.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_rpm.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_wininst.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_wininst.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/bdist_wininst.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/bdist_wininst.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_clib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_ext.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_py.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_py.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_py.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_py.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/build_scripts.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/check.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/check.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/check.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/check.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/clean.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/clean.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/clean.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/clean.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/config.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/config.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/config.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/config.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_data.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_data.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_data.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_data.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_egg_info.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_headers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_lib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/install_scripts.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/register.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/register.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/register.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/register.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/sdist.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/sdist.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/sdist.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/sdist.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/upload.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/upload.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/command/upload.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/command/upload.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/config.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/config.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/config.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/config.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/core.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/core.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/core.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/core.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/cygwinccompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/cygwinccompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/cygwinccompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/cygwinccompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/debug.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/debug.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/debug.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/debug.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dep_util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/dep_util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dep_util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/dep_util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dir_util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/dir_util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dir_util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/dir_util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dist.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/dist.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/dist.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/dist.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/errors.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/errors.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/errors.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/errors.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/extension.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/extension.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/extension.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/extension.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/fancy_getopt.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/fancy_getopt.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/fancy_getopt.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/fancy_getopt.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/file_util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/file_util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/file_util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/file_util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/filelist.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/filelist.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/filelist.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/filelist.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/log.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/log.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/log.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/log.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/msvccompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/msvccompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/msvccompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/msvccompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/spawn.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/spawn.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/spawn.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/spawn.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/sysconfig.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/sysconfig.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/sysconfig.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/sysconfig.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/text_file.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/text_file.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/text_file.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/text_file.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/unixccompiler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/unixccompiler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/unixccompiler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/unixccompiler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/version.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/distutils/version.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/distutils/version.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/distutils/version.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/doctest.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/doctest.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/doctest.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/doctest.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/_policybase.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/_policybase.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/_policybase.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/_policybase.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/base64mime.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/base64mime.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/base64mime.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/base64mime.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/charset.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/charset.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/charset.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/charset.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/contentmanager.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/contentmanager.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/contentmanager.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/contentmanager.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/encoders.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/encoders.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/encoders.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/encoders.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/errors.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/errors.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/errors.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/errors.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/feedparser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/feedparser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/feedparser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/feedparser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/generator.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/generator.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/generator.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/generator.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/header.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/header.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/header.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/header.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/headerregistry.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/headerregistry.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/headerregistry.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/headerregistry.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/iterators.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/iterators.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/iterators.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/iterators.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/message.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/message.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/message.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/message.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/application.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/application.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/application.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/application.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/audio.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/audio.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/audio.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/audio.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/base.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/base.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/base.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/base.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/image.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/image.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/image.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/image.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/message.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/message.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/message.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/message.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/multipart.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/multipart.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/multipart.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/multipart.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/nonmultipart.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/nonmultipart.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/nonmultipart.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/nonmultipart.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/text.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/mime/text.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/mime/text.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/mime/text.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/parser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/parser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/parser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/parser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/policy.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/policy.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/policy.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/policy.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/quoprimime.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/quoprimime.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/quoprimime.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/quoprimime.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/email/utils.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/email/utils.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/email/utils.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/email/utils.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/aliases.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/aliases.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/aliases.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/aliases.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/ascii.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/ascii.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/ascii.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/ascii.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/base64_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/base64_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/base64_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/base64_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/big5.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/big5.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/big5.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/big5.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/big5hkscs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/big5hkscs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/big5hkscs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/big5hkscs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/bz2_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/bz2_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/bz2_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/bz2_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/charmap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/charmap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/charmap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/charmap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp037.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp037.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp037.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp037.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1006.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1006.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1006.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1006.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1026.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1026.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1026.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1026.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1125.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1125.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1125.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1125.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1140.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1140.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1140.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1140.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1250.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1250.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1250.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1250.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1251.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1251.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1251.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1251.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1252.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1252.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1252.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1252.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1253.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1253.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1253.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1253.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1254.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1254.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1254.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1254.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1255.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1255.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1255.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1255.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1256.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1256.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1256.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1256.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1257.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1257.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1257.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1257.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1258.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1258.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp1258.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp1258.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp273.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp273.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp273.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp273.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp424.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp424.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp424.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp424.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp437.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp437.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp437.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp437.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp500.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp500.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp500.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp500.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp720.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp720.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp720.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp720.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp737.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp737.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp737.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp737.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp775.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp775.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp775.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp775.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp850.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp850.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp850.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp850.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp852.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp852.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp852.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp852.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp855.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp855.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp855.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp855.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp856.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp856.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp856.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp856.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp857.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp857.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp857.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp857.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp858.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp858.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp858.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp858.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp860.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp860.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp860.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp860.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp861.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp861.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp861.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp861.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp862.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp862.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp862.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp862.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp863.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp863.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp863.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp863.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp864.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp864.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp864.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp864.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp865.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp865.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp865.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp865.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp866.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp866.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp866.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp866.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp869.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp869.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp869.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp869.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp874.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp874.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp874.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp874.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp875.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp875.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp875.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp875.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp932.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp932.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp932.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp932.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp949.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp949.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp949.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp949.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp950.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp950.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/cp950.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/cp950.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jis_2004.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jis_2004.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jis_2004.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jis_2004.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jisx0213.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jisx0213.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jisx0213.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jisx0213.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_jp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_jp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_kr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_kr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/euc_kr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/euc_kr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gb18030.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/gb18030.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gb18030.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/gb18030.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gb2312.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/gb2312.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gb2312.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/gb2312.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gbk.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/gbk.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/gbk.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/gbk.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hex_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/hex_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hex_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/hex_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hp_roman8.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/hp_roman8.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hp_roman8.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/hp_roman8.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hz.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/hz.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/hz.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/hz.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/idna.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/idna.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/idna.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/idna.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_1.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_1.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_1.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_1.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2004.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2004.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2004.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_2004.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_3.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_3.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_3.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_3.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_ext.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_ext.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_ext.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_jp_ext.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_kr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_kr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso2022_kr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso2022_kr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_1.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_1.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_1.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_1.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_10.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_10.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_10.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_10.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_11.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_11.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_11.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_11.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_13.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_13.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_13.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_13.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_14.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_14.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_14.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_14.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_15.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_15.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_15.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_15.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_16.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_16.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_16.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_16.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_3.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_3.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_3.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_3.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_4.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_4.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_4.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_4.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_5.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_5.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_5.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_5.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_6.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_6.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_6.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_6.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_7.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_7.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_7.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_7.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_8.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_8.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_8.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_8.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_9.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_9.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/iso8859_9.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/iso8859_9.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/johab.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/johab.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/johab.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/johab.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_r.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_r.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_r.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_r.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_t.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_t.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_t.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_t.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_u.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_u.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/koi8_u.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/koi8_u.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/kz1048.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/kz1048.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/kz1048.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/kz1048.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/latin_1.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/latin_1.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/latin_1.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/latin_1.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_arabic.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_arabic.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_arabic.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_arabic.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_croatian.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_croatian.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_croatian.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_croatian.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_cyrillic.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_cyrillic.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_cyrillic.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_cyrillic.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_farsi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_farsi.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_farsi.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_farsi.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_greek.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_greek.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_greek.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_greek.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_iceland.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_iceland.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_iceland.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_iceland.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_latin2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_latin2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_latin2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_latin2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_roman.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_roman.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_roman.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_roman.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_romanian.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_romanian.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_romanian.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_romanian.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_turkish.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_turkish.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mac_turkish.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mac_turkish.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mbcs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/mbcs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/mbcs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/mbcs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/oem.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/oem.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/oem.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/oem.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/palmos.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/palmos.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/palmos.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/palmos.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/ptcp154.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/ptcp154.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/ptcp154.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/ptcp154.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/punycode.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/punycode.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/punycode.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/punycode.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/quopri_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/quopri_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/quopri_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/quopri_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/raw_unicode_escape.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/raw_unicode_escape.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/raw_unicode_escape.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/raw_unicode_escape.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/rot_13.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/rot_13.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/rot_13.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/rot_13.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jis.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jis.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jis.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jis.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jis_2004.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jis_2004.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jis_2004.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jis_2004.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jisx0213.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jisx0213.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/shift_jisx0213.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/shift_jisx0213.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/tis_620.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/tis_620.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/tis_620.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/tis_620.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/undefined.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/undefined.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/undefined.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/undefined.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/unicode_escape.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/unicode_escape.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/unicode_escape.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/unicode_escape.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16_be.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16_be.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16_be.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16_be.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16_le.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16_le.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_16_le.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_16_le.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32_be.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32_be.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32_be.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32_be.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32_le.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32_le.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_32_le.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_32_le.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_7.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_7.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_7.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_7.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_8.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_8.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/utf_8_sig.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/uu_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/uu_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/uu_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/uu_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/zlib_codec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/encodings/zlib_codec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/encodings/zlib_codec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/encodings/zlib_codec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ensurepip/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ensurepip/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ensurepip/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ensurepip/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/enum.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/enum.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/enum.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/enum.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/errno.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/errno.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/errno.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/errno.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/faulthandler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/faulthandler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/faulthandler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/faulthandler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/fcntl.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/fcntl.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/fcntl.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/fcntl.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/filecmp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/filecmp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/filecmp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/filecmp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/fileinput.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/fileinput.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/fileinput.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/fileinput.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/fnmatch.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/fnmatch.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/fnmatch.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/fnmatch.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/formatter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/formatter.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/formatter.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/formatter.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/fractions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/fractions.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/fractions.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/fractions.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ftplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ftplib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ftplib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ftplib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/functools.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/functools.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/functools.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/functools.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/gc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/gc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/gc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/gc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/genericpath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/genericpath.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/genericpath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/genericpath.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/getopt.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/getopt.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/getopt.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/getopt.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/getpass.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/getpass.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/getpass.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/getpass.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/gettext.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/gettext.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/gettext.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/gettext.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/glob.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/glob.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/glob.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/glob.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/graphlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/graphlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/graphlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/graphlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/grp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/grp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/grp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/grp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/gzip.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/gzip.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/gzip.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/gzip.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/hashlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/hashlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/hashlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/hashlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/heapq.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/heapq.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/heapq.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/heapq.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/hmac.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/hmac.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/hmac.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/hmac.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/html/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/html/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/html/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/html/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/html/entities.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/html/entities.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/html/entities.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/html/entities.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/html/parser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/html/parser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/html/parser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/html/parser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/http/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/http/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/client.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/client.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/http/client.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/http/client.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/cookies.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/cookies.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/http/cookies.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/http/cookies.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/http/server.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/server.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/http/server.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/http/server.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/imaplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/imaplib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/imaplib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/imghdr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/imghdr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/imghdr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/imghdr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/imp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/imp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/imp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/imp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/_abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/_bootstrap_external.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/machinery.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/machinery.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/machinery.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/machinery.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/_meta.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/_meta.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/_meta.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/_meta.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/diagnose.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/diagnose.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/metadata/diagnose.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/metadata/diagnose.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/readers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/readers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/_common.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/_common.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/_common.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/_common.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/_functional.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/_functional.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/_functional.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/_functional.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/readers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/readers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/readers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/readers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/simple.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/simple.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/simple.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/simple.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/importlib/util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/importlib/util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/inspect.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/inspect.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/inspect.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/inspect.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/io.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/io.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/io.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/io.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ipaddress.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ipaddress.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ipaddress.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ipaddress.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/itertools.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/itertools.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/itertools.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/itertools.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/json/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/json/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/json/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/json/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/json/decoder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/json/decoder.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/json/decoder.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/json/decoder.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/json/encoder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/json/encoder.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/json/encoder.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/json/encoder.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/json/scanner.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/json/scanner.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/json/scanner.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/json/scanner.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/json/tool.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/json/tool.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/json/tool.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/json/tool.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/keyword.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/keyword.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/keyword.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/keyword.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/btm_matcher.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/btm_matcher.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/btm_matcher.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/btm_matcher.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixer_base.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_apply.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_apply.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_apply.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_apply.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_asserts.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_basestring.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_basestring.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_basestring.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_basestring.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_buffer.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_dict.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_dict.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_dict.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_dict.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_except.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_except.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_except.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_except.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exec.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exec.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exec.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exec.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_execfile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_execfile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_execfile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_execfile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_exitfunc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_filter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_filter.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_filter.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_filter.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_funcattrs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_future.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_future.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_future.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_future.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_getcwdu.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_has_key.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_has_key.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_has_key.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_has_key.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_idioms.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_idioms.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_idioms.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_idioms.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_import.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_imports2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_input.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_input.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_input.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_input.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_intern.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_intern.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_intern.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_intern.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_isinstance.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools_imports.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools_imports.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools_imports.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_itertools_imports.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_long.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_map.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_map.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_map.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_map.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_metaclass.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_methodattrs.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ne.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ne.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ne.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ne.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_next.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_nonzero.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_numliterals.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_numliterals.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_numliterals.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_numliterals.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_operator.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_operator.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_operator.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_operator.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_paren.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_print.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_print.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_print.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_print.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raise.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raise.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raise.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raise.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_raw_input.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reduce.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reload.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reload.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reload.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_reload.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_renames.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_repr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_repr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_repr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_repr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_set_literal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_set_literal.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_set_literal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_set_literal.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_standarderror.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_standarderror.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_standarderror.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_standarderror.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_sys_exc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_sys_exc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_sys_exc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_sys_exc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_throw.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_tuple_params.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_tuple_params.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_tuple_params.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_tuple_params.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_types.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_types.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_types.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_types.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_unicode.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_urllib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ws_comma.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ws_comma.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ws_comma.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_ws_comma.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xrange.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_xreadlines.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_zip.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_zip.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_zip.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/fixes/fix_zip.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/main.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/main.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/main.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/main.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/driver.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/driver.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/driver.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/driver.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/grammar.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/grammar.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/grammar.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/grammar.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/literals.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/literals.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/literals.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/literals.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/parse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/parse.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/parse.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/parse.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/token.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/token.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/token.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/token.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/tokenize.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pygram.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pygram.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pygram.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pygram.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/linecache.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/linecache.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/linecache.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/linecache.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/locale.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/locale.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/locale.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/locale.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/logging/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/logging/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/logging/config.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/logging/config.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/logging/config.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/logging/config.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/logging/handlers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/logging/handlers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/logging/handlers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/logging/handlers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/lzma.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lzma.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/lzma.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/lzma.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/mailbox.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/mailbox.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/mailbox.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/mailbox.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/mailcap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/mailcap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/mailcap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/mailcap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/marshal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/marshal.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/marshal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/marshal.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/math.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/math.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/math.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/math.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/mimetypes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/mimetypes.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/mimetypes.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/mimetypes.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/mmap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/mmap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/mmap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/mmap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/modulefinder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/modulefinder.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/modulefinder.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/modulefinder.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/msilib/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/msilib/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/schema.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/msilib/schema.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/schema.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/msilib/schema.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/sequence.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/msilib/sequence.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/sequence.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/msilib/sequence.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/text.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/msilib/text.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/msilib/text.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/msilib/text.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/msvcrt.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/msvcrt.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/msvcrt.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/msvcrt.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/connection.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/connection.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/connection.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/connection.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/context.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/context.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/context.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/context.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/connection.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/connection.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/connection.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/dummy/connection.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/forkserver.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/heap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/heap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/heap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/heap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/managers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/pool.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/pool.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/pool.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/pool.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_fork.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_forkserver.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_forkserver.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_forkserver.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_forkserver.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_posix.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_posix.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_posix.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_posix.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_win32.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_win32.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_win32.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/popen_spawn_win32.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/process.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/process.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/process.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/process.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/queues.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/reduction.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/reduction.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/reduction.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/reduction.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/resource_sharer.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/resource_sharer.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/resource_sharer.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/resource_sharer.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/resource_tracker.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/resource_tracker.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/resource_tracker.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/resource_tracker.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/shared_memory.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/shared_memory.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/shared_memory.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/shared_memory.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/sharedctypes.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/spawn.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/spawn.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/spawn.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/spawn.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/synchronize.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/synchronize.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/synchronize.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/synchronize.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/multiprocessing/util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/multiprocessing/util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/netrc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/netrc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/netrc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/netrc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/nis.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/nis.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/nis.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/nis.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/nntplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/nntplib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/nntplib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/nntplib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/nt.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/nt.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/nt.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/nt.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ntpath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ntpath.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ntpath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ntpath.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/nturl2path.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/nturl2path.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/numbers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/numbers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/numbers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/numbers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/opcode.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/opcode.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/opcode.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/opcode.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/operator.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/operator.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/operator.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/operator.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/optparse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/optparse.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/optparse.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/optparse.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/os/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/os/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/os/path.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/os/path.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/os/path.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/os/path.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ossaudiodev.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ossaudiodev.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ossaudiodev.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ossaudiodev.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/parser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/parser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/parser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/parser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pathlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pathlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pathlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pathlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pdb.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pdb.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pdb.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pdb.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pickle.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pickle.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pickle.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pickle.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pickletools.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pickletools.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pickletools.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pickletools.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pipes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pipes.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pipes.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pipes.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pkgutil.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pkgutil.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/platform.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/platform.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/platform.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/platform.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/plistlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/plistlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/plistlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/plistlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/poplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/poplib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/poplib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/poplib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/posix.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/posix.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/posix.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/posix.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/posixpath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/posixpath.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/posixpath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/posixpath.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pprint.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pprint.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pprint.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pprint.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/profile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/profile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/profile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/profile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pstats.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pstats.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pstats.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pstats.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pty.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pty.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pty.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pty.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pwd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pwd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pwd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pwd.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/py_compile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/py_compile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/py_compile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/py_compile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pyclbr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pyclbr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pyclbr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pyclbr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pydoc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc_data/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc_data/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc_data/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pydoc_data/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc_data/topics.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc_data/topics.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pydoc_data/topics.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pydoc_data/topics.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/errors.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/errors.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/errors.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/errors.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/model.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/model.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/pyexpat/model.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/pyexpat/model.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/queue.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/queue.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/queue.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/queue.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/quopri.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/quopri.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/quopri.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/quopri.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/random.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/random.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/random.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/random.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/re.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/re.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/re.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/re.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/readline.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/readline.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/readline.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/readline.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/reprlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/reprlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/reprlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/reprlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/resource.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/resource.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/resource.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/resource.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/rlcompleter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/rlcompleter.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/rlcompleter.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/rlcompleter.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/runpy.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/runpy.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/runpy.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/runpy.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sched.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sched.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sched.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sched.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/secrets.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/secrets.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/secrets.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/secrets.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/select.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/select.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/select.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/select.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/selectors.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/selectors.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/selectors.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/selectors.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/shelve.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/shelve.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/shelve.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/shelve.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/shlex.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/shlex.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/shlex.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/shlex.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/shutil.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/shutil.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/shutil.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/shutil.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/signal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/signal.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/signal.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/signal.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/site.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/site.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/site.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/site.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/smtpd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/smtpd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/smtpd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/smtpd.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/smtplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/smtplib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/smtplib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/smtplib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sndhdr.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sndhdr.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sndhdr.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sndhdr.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/socket.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/socket.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/socket.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/socket.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/socketserver.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/socketserver.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/socketserver.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/socketserver.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/spwd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/spwd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/spwd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/spwd.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dbapi2.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/dump.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dump.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sqlite3/dump.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sqlite3/dump.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sre_compile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sre_compile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sre_compile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sre_compile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sre_constants.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sre_constants.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sre_constants.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sre_constants.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sre_parse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sre_parse.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sre_parse.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sre_parse.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/ssl.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ssl.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/ssl.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/ssl.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/stat.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/stat.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/stat.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/stat.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/statistics.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/statistics.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/statistics.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/statistics.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/string.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/string.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/string.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/string.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/stringprep.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/stringprep.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/stringprep.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/stringprep.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/struct.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/struct.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/struct.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/struct.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/subprocess.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/subprocess.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/subprocess.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/subprocess.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sunau.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sunau.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sunau.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sunau.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/symbol.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/symbol.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/symbol.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/symbol.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/symtable.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/symtable.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/symtable.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/symtable.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sys/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sys/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sys/_monitoring.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sys/_monitoring.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sys/_monitoring.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sys/_monitoring.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/sysconfig.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/sysconfig.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/syslog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/syslog.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/syslog.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/syslog.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tabnanny.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tabnanny.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tabnanny.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tabnanny.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tarfile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tarfile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tarfile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/telnetlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/telnetlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/telnetlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/telnetlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tempfile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tempfile.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tempfile.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tempfile.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/termios.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/termios.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/termios.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/termios.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/textwrap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/textwrap.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/textwrap.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/textwrap.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/this.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/this.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/this.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/this.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/threading.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/threading.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/threading.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/threading.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/time.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/time.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/time.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/time.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/timeit.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/timeit.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/timeit.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/timeit.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/constants.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/constants.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/constants.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/constants.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/dnd.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dnd.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/dnd.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dnd.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/filedialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/filedialog.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/filedialog.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/filedialog.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/font.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/font.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/scrolledtext.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/scrolledtext.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/scrolledtext.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/scrolledtext.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/tix.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/tix.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/tix.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/tix.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/token.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/token.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/token.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/token.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tokenize.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tokenize.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tokenize.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tokenize.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tomllib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tomllib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tomllib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tomllib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/trace.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/trace.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/trace.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/trace.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/traceback.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/traceback.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/traceback.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/traceback.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tracemalloc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tracemalloc.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tracemalloc.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tracemalloc.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/tty.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tty.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/tty.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/tty.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/turtle.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/turtle.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/turtle.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/turtle.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/types.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/types.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/types.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/typing.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/typing.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/typing_extensions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/typing_extensions.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unicodedata.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unicodedata.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unicodedata.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unicodedata.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/_log.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/_log.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/_log.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/_log.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/async_case.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/async_case.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/async_case.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/async_case.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/case.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/case.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/loader.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/loader.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/main.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/main.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/mock.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/mock.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/result.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/result.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/result.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/result.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/runner.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/runner.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/runner.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/runner.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/signals.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/signals.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/signals.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/signals.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/suite.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/suite.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/suite.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/suite.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/unittest/util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/unittest/util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/error.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/error.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/error.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/error.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/parse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/parse.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/parse.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/parse.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/request.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/request.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/response.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/response.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/response.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/response.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/robotparser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/robotparser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/urllib/robotparser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/urllib/robotparser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/uu.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/uu.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/uu.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/uu.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/uuid.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/uuid.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/uuid.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/uuid.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/venv/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/venv/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/venv/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/venv/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/warnings.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/warnings.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/warnings.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/warnings.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wave.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wave.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wave.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wave.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/weakref.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/weakref.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/weakref.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/weakref.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/webbrowser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/webbrowser.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/winreg.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/winreg.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/winreg.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/winreg.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/winsound.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/winsound.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/winsound.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/winsound.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/handlers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/handlers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/handlers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/handlers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/headers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/headers.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/headers.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/headers.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/simple_server.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/simple_server.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/simple_server.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/simple_server.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/types.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/types.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/types.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/types.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/util.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/util.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/util.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/util.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xdrlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xdrlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xdrlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xdrlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/NodeFilter.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/NodeFilter.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/NodeFilter.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/NodeFilter.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/domreg.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/domreg.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/domreg.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/domreg.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/minicompat.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minicompat.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/minicompat.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minicompat.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementInclude.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementInclude.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementInclude.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementInclude.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementPath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementPath.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementPath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementPath.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementTree.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementTree.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/ElementTree.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/ElementTree.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/cElementTree.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/cElementTree.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/etree/cElementTree.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/etree/cElementTree.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/errors.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/errors.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/errors.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/errors.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/model.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/model.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/parsers/expat/model.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/parsers/expat/model.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/expatreader.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/xmlreader.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/client.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/client.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/client.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/client.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/server.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/server.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xmlrpc/server.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xmlrpc/server.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/xxlimited.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xxlimited.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/xxlimited.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/xxlimited.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipapp.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zipapp.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zipapp.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zipapp.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zipfile/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/_path/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/_path/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/_path/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zipfile/_path/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/_path/glob.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zipfile/_path/glob.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zipfile/_path/glob.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zipfile/_path/glob.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zipimport.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zipimport.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zipimport.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zipimport.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zlib.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zlib.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zlib.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/__init__.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/_common.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/_common.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/_common.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/_common.pyi diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/_tzpath.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/_tzpath.pyi similarity index 100% rename from crates/red_knot_vendored/vendor/typeshed/stdlib/zoneinfo/_tzpath.pyi rename to crates/ty_vendored/vendor/typeshed/stdlib/zoneinfo/_tzpath.pyi diff --git a/crates/red_knot_wasm/Cargo.toml b/crates/ty_wasm/Cargo.toml similarity index 83% rename from crates/red_knot_wasm/Cargo.toml rename to crates/ty_wasm/Cargo.toml index 14bc206629959c..3c96fa77e812b2 100644 --- a/crates/red_knot_wasm/Cargo.toml +++ b/crates/ty_wasm/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "red_knot_wasm" +name = "ty_wasm" version = "0.0.0" publish = false authors = { workspace = true } @@ -9,7 +9,7 @@ homepage = { workspace = true } documentation = { workspace = true } repository = { workspace = true } license = { workspace = true } -description = "WebAssembly bindings for Red Knot" +description = "WebAssembly bindings for ty" [lib] crate-type = ["cdylib", "rlib"] @@ -19,12 +19,12 @@ doctest = false default = ["console_error_panic_hook"] [dependencies] -red_knot_ide = { workspace = true } -red_knot_project = { workspace = true, default-features = false, features = [ +ty_ide = { workspace = true } +ty_project = { workspace = true, default-features = false, features = [ "deflate", "format" ] } -red_knot_python_semantic = { workspace = true } +ty_python_semantic = { workspace = true } ruff_db = { workspace = true, default-features = false, features = [] } ruff_notebook = { workspace = true } diff --git a/crates/red_knot_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs similarity index 98% rename from crates/red_knot_wasm/src/lib.rs rename to crates/ty_wasm/src/lib.rs index 918aaaf67acf25..53969603aca582 100644 --- a/crates/red_knot_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -1,13 +1,6 @@ use std::any::Any; use js_sys::{Error, JsString}; -use red_knot_ide::{goto_type_definition, hover, inlay_hints, MarkupKind}; -use red_knot_project::metadata::options::Options; -use red_knot_project::metadata::value::ValueSource; -use red_knot_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; -use red_knot_project::ProjectMetadata; -use red_knot_project::{Db, ProjectDatabase}; -use red_knot_python_semantic::Program; use ruff_db::diagnostic::{self, DisplayDiagnosticConfig}; use ruff_db::files::{system_path_to_file, File, FileRange}; use ruff_db::source::{line_index, source_text}; @@ -21,6 +14,13 @@ use ruff_notebook::Notebook; use ruff_python_formatter::formatted_file; use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; use ruff_text_size::{Ranged, TextSize}; +use ty_ide::{goto_type_definition, hover, inlay_hints, MarkupKind}; +use ty_project::metadata::options::Options; +use ty_project::metadata::value::ValueSource; +use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; +use ty_project::ProjectMetadata; +use ty_project::{Db, ProjectDatabase}; +use ty_python_semantic::Program; use wasm_bindgen::prelude::*; #[wasm_bindgen(start)] diff --git a/crates/red_knot_wasm/tests/api.rs b/crates/ty_wasm/tests/api.rs similarity index 92% rename from crates/red_knot_wasm/tests/api.rs rename to crates/ty_wasm/tests/api.rs index 6b6802680baf9d..e58d0871c8a992 100644 --- a/crates/red_knot_wasm/tests/api.rs +++ b/crates/ty_wasm/tests/api.rs @@ -1,6 +1,6 @@ #![cfg(target_arch = "wasm32")] -use red_knot_wasm::{Position, PositionEncoding, Workspace}; +use ty_wasm::{Position, PositionEncoding, Workspace}; use wasm_bindgen_test::wasm_bindgen_test; #[wasm_bindgen_test] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index d9c16cf9ded4fc..df9c82f6046096 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -17,8 +17,6 @@ libfuzzer = ["libfuzzer-sys/link_libfuzzer"] cargo-fuzz = true [dependencies] -red_knot_python_semantic = { path = "../crates/red_knot_python_semantic" } -red_knot_vendored = { path = "../crates/red_knot_vendored" } ruff_db = { path = "../crates/ruff_db" } ruff_linter = { path = "../crates/ruff_linter" } ruff_python_ast = { path = "../crates/ruff_python_ast" } @@ -28,6 +26,9 @@ ruff_source_file = { path = "../crates/ruff_source_file" } ruff_python_formatter = { path = "../crates/ruff_python_formatter" } ruff_text_size = { path = "../crates/ruff_text_size" } +ty_python_semantic = { path = "../crates/ty_python_semantic" } +ty_vendored = { path = "../crates/ty_vendored" } + libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7" } similar = { version = "2.5.0" } @@ -38,8 +39,8 @@ tracing = { version = "0.1.40" } members = ["."] [[bin]] -name = "red_knot_check_invalid_syntax" -path = "fuzz_targets/red_knot_check_invalid_syntax.rs" +name = "ty_check_invalid_syntax" +path = "fuzz_targets/ty_check_invalid_syntax.rs" [[bin]] name = "ruff_parse_simple" diff --git a/fuzz/README.md b/fuzz/README.md index 5265149b8d8bfd..14f7692a89c68c 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -74,9 +74,9 @@ Each fuzzer harness in [`fuzz_targets`](fuzz_targets) targets a different aspect them in different ways. While there is implementation-specific documentation in the source code itself, each harness is briefly described below. -### `red_knot_check_invalid_syntax` +### `ty_check_invalid_syntax` -This fuzz harness checks that the type checker (Red Knot) does not panic when checking a source +This fuzz harness checks that the type checker (ty) does not panic when checking a source file with invalid syntax. This rejects any corpus entries that is already valid Python code. Currently, this is limited to syntax errors that's produced by Ruff's Python parser which means that it does not cover all possible syntax errors (). diff --git a/fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs similarity index 95% rename from fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs rename to fuzz/fuzz_targets/ty_check_invalid_syntax.rs index 225bd10089a183..902f11ab8fca56 100644 --- a/fuzz/fuzz_targets/red_knot_check_invalid_syntax.rs +++ b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs @@ -7,12 +7,6 @@ use std::sync::{Arc, Mutex, OnceLock}; use libfuzzer_sys::{fuzz_target, Corpus}; -use red_knot_python_semantic::lint::LintRegistry; -use red_knot_python_semantic::types::check_types; -use red_knot_python_semantic::{ - default_lint_registry, lint::RuleSelection, Db as SemanticDb, Program, ProgramSettings, - PythonPlatform, SearchPathSettings, -}; use ruff_db::files::{system_path_to_file, File, Files}; use ruff_db::system::{ DbWithTestSystem, DbWithWritableSystem as _, System, SystemPathBuf, TestSystem, @@ -21,6 +15,12 @@ use ruff_db::vendored::VendoredFileSystem; use ruff_db::{Db as SourceDb, Upcast}; use ruff_python_ast::PythonVersion; use ruff_python_parser::{parse_unchecked, Mode, ParseOptions}; +use ty_python_semantic::lint::LintRegistry; +use ty_python_semantic::types::check_types; +use ty_python_semantic::{ + default_lint_registry, lint::RuleSelection, Db as SemanticDb, Program, ProgramSettings, + PythonPlatform, SearchPathSettings, +}; /// Database that can be used for testing. /// @@ -41,7 +41,7 @@ impl TestDb { Self { storage: salsa::Storage::default(), system: TestSystem::default(), - vendored: red_knot_vendored::file_system().clone(), + vendored: ty_vendored::file_system().clone(), events: Arc::default(), files: Files::default(), rule_selection: RuleSelection::from_registry(default_lint_registry()).into(), diff --git a/fuzz/init-fuzzer.sh b/fuzz/init-fuzzer.sh index 22f81f0378358e..7de7087dc5e5a2 100755 --- a/fuzz/init-fuzzer.sh +++ b/fuzz/init-fuzzer.sh @@ -23,7 +23,7 @@ if [ ! -d corpus/ruff_fix_validity ]; then # Build a smaller corpus in addition to the (optional) larger corpus curl -L 'https://github.com/python/cpython/archive/refs/tags/v3.13.0.tar.gz' | tar xz - cp -r "../../../crates/red_knot_project/resources/test/corpus" "red_knot_project" + cp -r "../../../crates/ty_project/resources/test/corpus" "ty_project" cp -r "../../../crates/ruff_linter/resources/test/fixtures" "ruff_linter" cp -r "../../../crates/ruff_python_formatter/resources/test/fixtures" "ruff_python_formatter" cp -r "../../../crates/ruff_python_parser/resources" "ruff_python_parser" diff --git a/playground/.prettierignore b/playground/.prettierignore index ac9e43b7a3b325..8c7763fecfb741 100644 --- a/playground/.prettierignore +++ b/playground/.prettierignore @@ -1,5 +1,5 @@ **.md ruff/dist ruff/ruff_wasm -knot/dist -knot/red_knot_wasm \ No newline at end of file +ty/dist +ty/ty_wasm \ No newline at end of file diff --git a/playground/README.md b/playground/README.md index efe28b75577bf8..22ba019a2dbf17 100644 --- a/playground/README.md +++ b/playground/README.md @@ -5,8 +5,8 @@ In-browser playground for Ruff. Available [https://play.ruff.rs/](https://play.r ## Getting started Install the NPM dependencies with `npm install`, and run, and run the development server with -`npm start --workspace ruff-playground` or `npm start --workspace knot-playground`. -You may need to restart the server after making changes to Ruff or Red Knot to re-build the WASM +`npm start --workspace ruff-playground` or `npm start --workspace ty-playground`. +You may need to restart the server after making changes to Ruff or ty to re-build the WASM module. To run the datastore, which is based @@ -37,4 +37,4 @@ additional inspiration from the [Biome Playground](https://biomejs.dev/playgroun ### Stack overflows If you see stack overflows in the playground, build the WASM module in release mode: -`npm run --workspace knot-playground build:wasm`. +`npm run --workspace ty-playground build:wasm`. diff --git a/playground/package-lock.json b/playground/package-lock.json index 61791af3fd90e8..7d1e4579aa43b3 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -8,7 +8,7 @@ "name": "playground", "version": "0.0.0", "workspaces": [ - "knot", + "ty", "ruff", "shared" ], @@ -30,9 +30,10 @@ "wasm-pack": "^0.13.1" } }, - "knot": { - "name": "knot-playground", + "ty": { + "name": "ty-playground", "version": "0.0.0", + "extraneous": true, "dependencies": { "@monaco-editor/react": "^4.7.0", "classnames": "^2.5.1", @@ -42,7 +43,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^2.1.7", - "red_knot_wasm": "file:red_knot_wasm", + "ty_wasm": "file:ty_wasm", "shared": "0.0.0", "smol-toml": "^1.3.1" }, @@ -50,8 +51,9 @@ "vite-plugin-static-copy": "^2.3.0" } }, - "knot/red_knot_wasm": { + "ty/ty_wasm": { "version": "0.0.0", + "extraneous": true, "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { @@ -4078,10 +4080,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/knot-playground": { - "resolved": "knot", - "link": true - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4991,10 +4989,6 @@ "node": ">=8.10.0" } }, - "node_modules/red_knot_wasm": { - "resolved": "knot/red_knot_wasm", - "link": true - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5641,6 +5635,14 @@ "strip-bom": "^3.0.0" } }, + "node_modules/ty_wasm": { + "resolved": "ty/ty_wasm", + "link": true + }, + "node_modules/ty-playground": { + "resolved": "ty", + "link": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5881,9 +5883,9 @@ } }, "node_modules/vite-plugin-static-copy": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.0.tgz", - "integrity": "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.1.tgz", + "integrity": "sha512-EfsPcBm3ewg3UMG8RJaC0ADq6/qnUZnokXx4By4+2cAcipjT9i0Y0owIJGqmZI7d6nxk4qB1q5aXOwNuSyPdyA==", "dev": true, "license": "MIT", "dependencies": { @@ -6037,9 +6039,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6094,7 +6096,7 @@ } }, "ruff/ruff_wasm": { - "version": "0.11.4", + "version": "0.11.8", "license": "MIT" }, "shared": { @@ -6105,6 +6107,31 @@ "react": "^19.0.0", "react-resizable-panels": "^2.1.7" } + }, + "ty": { + "name": "ty-playground", + "version": "0.0.0", + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "classnames": "^2.5.1", + "lz-string": "^1.5.0", + "monaco-editor": "^0.52.2", + "pyodide": "^0.27.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-resizable-panels": "^2.1.7", + "shared": "0.0.0", + "smol-toml": "^1.3.1", + "ty_wasm": "file:ty_wasm" + }, + "devDependencies": { + "vite-plugin-static-copy": "^2.3.0" + } + }, + "ty/ty_wasm": { + "name": "ty_wasm", + "version": "0.0.0", + "license": "MIT" } } } diff --git a/playground/package.json b/playground/package.json index 82a74c906cbcd9..9a3255ad9c4921 100644 --- a/playground/package.json +++ b/playground/package.json @@ -5,15 +5,15 @@ "type": "module", "scripts": { "check": "npm run dev:wasm && npm run lint && npm run tsc", - "dev:wasm": "npm run dev:wasm --workspace knot-playground && npm run dev:wasm --workspace ruff-playground", - "dev:build": "npm run dev:build --workspace knot-playground && npm run dev:build --workspace ruff-playground", + "dev:wasm": "npm run dev:wasm --workspace ty-playground && npm run dev:wasm --workspace ruff-playground", + "dev:build": "npm run dev:build --workspace ty-playground && npm run dev:build --workspace ruff-playground", "fmt": "prettier --cache -w .", "fmt:check": "prettier --cache --check .", - "lint": "eslint --cache --ext .ts,.tsx ruff/src knot/src", + "lint": "eslint --cache --ext .ts,.tsx ruff/src ty/src", "tsc": "tsc" }, "workspaces": [ - "knot", + "ty", "ruff", "shared" ], diff --git a/playground/tsconfig.json b/playground/tsconfig.json index d539e17861e5c5..408d95de6a45fb 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -16,6 +16,10 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": ["ruff/src", "knot/src"], - "references": [{ "path": "./tsconfig.node.json" }] + "include": ["ruff/src", "ty/src"], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] } diff --git a/playground/tsconfig.node.json b/playground/tsconfig.node.json index c4f5d4847bbd4c..56ea1c6b3753a4 100644 --- a/playground/tsconfig.node.json +++ b/playground/tsconfig.node.json @@ -5,5 +5,5 @@ "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, - "include": ["ruff/vite.config.ts", "knot/vite.config.ts"] + "include": ["ruff/vite.config.ts", "ty/vite.config.ts"] } diff --git a/playground/knot/index.html b/playground/ty/index.html similarity index 80% rename from playground/knot/index.html rename to playground/ty/index.html index 368ba81eb4935d..cd3fc70d91c57a 100644 --- a/playground/knot/index.html +++ b/playground/ty/index.html @@ -9,18 +9,18 @@ - Playground | Red Knot + Playground | ty - + diff --git a/playground/knot/package.json b/playground/ty/package.json similarity index 69% rename from playground/knot/package.json rename to playground/ty/package.json index 1e0001187d378d..a153371f0233b1 100644 --- a/playground/knot/package.json +++ b/playground/ty/package.json @@ -1,13 +1,13 @@ { - "name": "knot-playground", + "name": "ty-playground", "private": true, "version": "0.0.0", "type": "module", "scripts": { "prebuild": "npm run build:wasm", "build": "vite build", - "build:wasm": "wasm-pack build ../../crates/red_knot_wasm --target web --out-dir ../../playground/knot/red_knot_wasm", - "dev:wasm": "wasm-pack build ../../crates/red_knot_wasm --dev --target web --out-dir ../../playground/knot/red_knot_wasm", + "build:wasm": "wasm-pack build ../../crates/ty_wasm --target web --out-dir ../../playground/ty/ty_wasm", + "dev:wasm": "wasm-pack build ../../crates/ty_wasm --dev --target web --out-dir ../../playground/ty/ty_wasm", "predev:build": "npm run dev:wasm", "dev:build": "vite build", "prestart": "npm run dev:wasm", @@ -23,9 +23,9 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^2.1.7", - "red_knot_wasm": "file:red_knot_wasm", "shared": "0.0.0", - "smol-toml": "^1.3.1" + "smol-toml": "^1.3.1", + "ty_wasm": "file:ty_wasm" }, "overrides": { "@monaco-editor/react": { diff --git a/playground/knot/public/Astral.png b/playground/ty/public/Astral.png similarity index 100% rename from playground/knot/public/Astral.png rename to playground/ty/public/Astral.png diff --git a/playground/knot/public/apple-touch-icon.png b/playground/ty/public/apple-touch-icon.png similarity index 100% rename from playground/knot/public/apple-touch-icon.png rename to playground/ty/public/apple-touch-icon.png diff --git a/playground/knot/public/favicon-16x16.png b/playground/ty/public/favicon-16x16.png similarity index 100% rename from playground/knot/public/favicon-16x16.png rename to playground/ty/public/favicon-16x16.png diff --git a/playground/knot/public/favicon-32x32.png b/playground/ty/public/favicon-32x32.png similarity index 100% rename from playground/knot/public/favicon-32x32.png rename to playground/ty/public/favicon-32x32.png diff --git a/playground/knot/public/favicon.ico b/playground/ty/public/favicon.ico similarity index 100% rename from playground/knot/public/favicon.ico rename to playground/ty/public/favicon.ico diff --git a/playground/knot/src/Editor/Chrome.tsx b/playground/ty/src/Editor/Chrome.tsx similarity index 99% rename from playground/knot/src/Editor/Chrome.tsx rename to playground/ty/src/Editor/Chrome.tsx index 4e055015d6d915..334f47a2826103 100644 --- a/playground/knot/src/Editor/Chrome.tsx +++ b/playground/ty/src/Editor/Chrome.tsx @@ -13,7 +13,7 @@ import { Theme, VerticalResizeHandle, } from "shared"; -import type { Workspace } from "red_knot_wasm"; +import type { Workspace } from "ty_wasm"; import { Panel, PanelGroup } from "react-resizable-panels"; import { Files, isPythonFile } from "./Files"; import SecondarySideBar from "./SecondarySideBar"; diff --git a/playground/knot/src/Editor/Diagnostics.tsx b/playground/ty/src/Editor/Diagnostics.tsx similarity index 97% rename from playground/knot/src/Editor/Diagnostics.tsx rename to playground/ty/src/Editor/Diagnostics.tsx index a8616c9bb00efb..c1a395d2a12ffd 100644 --- a/playground/knot/src/Editor/Diagnostics.tsx +++ b/playground/ty/src/Editor/Diagnostics.tsx @@ -1,4 +1,4 @@ -import type { Severity, Range, TextRange } from "red_knot_wasm"; +import type { Severity, Range, TextRange } from "ty_wasm"; import classNames from "classnames"; import { Theme } from "shared"; import { useMemo } from "react"; diff --git a/playground/knot/src/Editor/Editor.tsx b/playground/ty/src/Editor/Editor.tsx similarity index 93% rename from playground/knot/src/Editor/Editor.tsx rename to playground/ty/src/Editor/Editor.tsx index 73d8f7527b2114..7936ce5ac89aa6 100644 --- a/playground/knot/src/Editor/Editor.tsx +++ b/playground/ty/src/Editor/Editor.tsx @@ -18,11 +18,11 @@ import { import { useCallback, useEffect, useRef } from "react"; import { Theme } from "shared"; import { - Range as KnotRange, + Range as TyRange, Severity, type Workspace, - Position as KnotPosition, -} from "red_knot_wasm"; + Position as TyPosition, +} from "ty_wasm"; import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; import { FileId, ReadonlyFiles } from "../Playground"; @@ -194,7 +194,7 @@ class PlaygroundServer const inlayHints = workspace.inlayHints( selectedHandle, - iRangeToKnotRange(range), + MonacoRangeToTyRange(range), ); if (inlayHints.length === 0) { @@ -300,7 +300,7 @@ class PlaygroundServer const hover = workspace.hover( selectedHandle, - new KnotPosition(position.lineNumber, position.column), + new TyPosition(position.lineNumber, position.column), ); if (hover == null) { @@ -308,7 +308,7 @@ class PlaygroundServer } return { - range: knotRangeToIRange(hover.range), + range: tyRangeToMonacoRange(hover.range), contents: [{ value: hover.markdown, isTrusted: true }], }; } @@ -334,7 +334,7 @@ class PlaygroundServer const links = workspace.gotoTypeDefinition( selectedHandle, - new KnotPosition(position.lineNumber, position.column), + new TyPosition(position.lineNumber, position.column), ); return ( @@ -343,16 +343,16 @@ class PlaygroundServer const targetSelection = link.selection_range == null ? undefined - : knotRangeToIRange(link.selection_range); + : tyRangeToMonacoRange(link.selection_range); const originSelection = link.origin_selection_range == null ? undefined - : knotRangeToIRange(link.origin_selection_range); + : tyRangeToMonacoRange(link.origin_selection_range); return { uri: Uri.parse(link.path), - range: knotRangeToIRange(link.full_range), + range: tyRangeToMonacoRange(link.full_range), targetSelectionRange: targetSelection, originSelectionRange: originSelection, } as languages.LocationLink; @@ -450,7 +450,7 @@ class PlaygroundServer } } -function knotRangeToIRange(range: KnotRange): IRange { +function tyRangeToMonacoRange(range: TyRange): IRange { return { startLineNumber: range.start.line, startColumn: range.start.column, @@ -459,9 +459,9 @@ function knotRangeToIRange(range: KnotRange): IRange { }; } -function iRangeToKnotRange(range: IRange): KnotRange { - return new KnotRange( - new KnotPosition(range.startLineNumber, range.startColumn), - new KnotPosition(range.endLineNumber, range.endColumn), +function MonacoRangeToTyRange(range: IRange): TyRange { + return new TyRange( + new TyPosition(range.startLineNumber, range.startColumn), + new TyPosition(range.endLineNumber, range.endColumn), ); } diff --git a/playground/knot/src/Editor/Files.tsx b/playground/ty/src/Editor/Files.tsx similarity index 99% rename from playground/knot/src/Editor/Files.tsx rename to playground/ty/src/Editor/Files.tsx index 215baa2476446d..d3457549013313 100644 --- a/playground/knot/src/Editor/Files.tsx +++ b/playground/ty/src/Editor/Files.tsx @@ -2,7 +2,7 @@ import { Icons, Theme } from "shared"; import classNames from "classnames"; import { useState } from "react"; import { FileId } from "../Playground"; -import { type FileHandle } from "red_knot_wasm"; +import { type FileHandle } from "ty_wasm"; export interface Props { // The file names diff --git a/playground/knot/src/Editor/SecondaryPanel.tsx b/playground/ty/src/Editor/SecondaryPanel.tsx similarity index 100% rename from playground/knot/src/Editor/SecondaryPanel.tsx rename to playground/ty/src/Editor/SecondaryPanel.tsx diff --git a/playground/knot/src/Editor/SecondarySideBar.tsx b/playground/ty/src/Editor/SecondarySideBar.tsx similarity index 100% rename from playground/knot/src/Editor/SecondarySideBar.tsx rename to playground/ty/src/Editor/SecondarySideBar.tsx diff --git a/playground/knot/src/Editor/api.ts b/playground/ty/src/Editor/api.ts similarity index 100% rename from playground/knot/src/Editor/api.ts rename to playground/ty/src/Editor/api.ts diff --git a/playground/knot/src/Editor/persist.ts b/playground/ty/src/Editor/persist.ts similarity index 100% rename from playground/knot/src/Editor/persist.ts rename to playground/ty/src/Editor/persist.ts diff --git a/playground/knot/src/Playground.tsx b/playground/ty/src/Playground.tsx similarity index 93% rename from playground/knot/src/Playground.tsx rename to playground/ty/src/Playground.tsx index 971d5f9eac7344..c6a171f6231037 100644 --- a/playground/knot/src/Playground.tsx +++ b/playground/ty/src/Playground.tsx @@ -10,13 +10,13 @@ import { useState, } from "react"; import { ErrorMessage, Header, setupMonaco, useTheme } from "shared"; -import { FileHandle, PositionEncoding, Workspace } from "red_knot_wasm"; +import { FileHandle, PositionEncoding, Workspace } from "ty_wasm"; import { persist, persistLocal, restore } from "./Editor/persist"; import { loader } from "@monaco-editor/react"; -import knotSchema from "../../../knot.schema.json"; +import tySchema from "../../../ty.schema.json"; import Chrome, { formatError } from "./Editor/Chrome"; -export const SETTINGS_FILE_NAME = "knot.json"; +export const SETTINGS_FILE_NAME = "ty.json"; export default function Playground() { const [theme, setTheme] = useTheme(); @@ -224,13 +224,13 @@ def with_style(line, word, style): output += "-" * len(word) -print(with_style("Red Knot is a fast type checker for Python.", "fast", "underlined")) +print(with_style("ty is a fast type checker for Python.", "fast", "underlined")) `; const DEFAULT_WORKSPACE = { files: { "main.py": DEFAULT_PROGRAM, - "knot.json": DEFAULT_SETTINGS, + "ty.json": DEFAULT_SETTINGS, }, current: "main.py", }; @@ -268,7 +268,7 @@ interface FilesState { * The database file handles by file id. * * Files without a file handle are well-known files that are only handled by the - * playground (e.g. knot.json) + * playground (e.g. ty.json) */ handles: Readonly<{ [id: FileId]: FileHandle | null }>; @@ -451,14 +451,14 @@ export interface InitializedPlayground { // Run once during startup. Initializes monaco, loads the wasm file, and restores the previous editor state. async function startPlayground(): Promise { - const red_knot = await import("../red_knot_wasm"); - await red_knot.default(); + const ty = await import("../ty_wasm"); + await ty.default(); const monaco = await loader.init(); setupMonaco(monaco, { - uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/knot.schema.json", - fileMatch: ["knot.json"], - schema: knotSchema, + uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/ty.schema.json", + fileMatch: ["ty.json"], + schema: tySchema, }); const restored = await restore(); @@ -483,7 +483,7 @@ function updateOptions( workspace?.updateOptions(settings); setError(null); } catch (error) { - setError(`Failed to update 'knot.json' options: ${formatError(error)}`); + setError(`Failed to update 'ty.json' options: ${formatError(error)}`); } } @@ -520,8 +520,17 @@ function restoreWorkspace( ) { let hasSettings = false; - for (const [name, content] of Object.entries(state.files)) { + // eslint-disable-next-line prefer-const + for (let [name, content] of Object.entries(state.files)) { let handle = null; + + if ( + name === "knot.json" && + !Object.keys(state.files).includes(SETTINGS_FILE_NAME) + ) { + name = SETTINGS_FILE_NAME; + } + if (name === SETTINGS_FILE_NAME) { updateOptions(workspace, content, setError); hasSettings = true; @@ -536,8 +545,11 @@ function restoreWorkspace( updateOptions(workspace, null, setError); } + const selected = + state.current === "knot.json" ? SETTINGS_FILE_NAME : state.current; + dispatchFiles({ type: "selectFileByName", - name: state.current, + name: selected, }); } diff --git a/playground/knot/src/index.css b/playground/ty/src/index.css similarity index 100% rename from playground/knot/src/index.css rename to playground/ty/src/index.css diff --git a/playground/knot/src/main.tsx b/playground/ty/src/main.tsx similarity index 100% rename from playground/knot/src/main.tsx rename to playground/ty/src/main.tsx diff --git a/playground/knot/src/third-party.d.ts b/playground/ty/src/third-party.d.ts similarity index 100% rename from playground/knot/src/third-party.d.ts rename to playground/ty/src/third-party.d.ts diff --git a/playground/knot/src/vite-env.d.ts b/playground/ty/src/vite-env.d.ts similarity index 100% rename from playground/knot/src/vite-env.d.ts rename to playground/ty/src/vite-env.d.ts diff --git a/playground/knot/vite.config.ts b/playground/ty/vite.config.ts similarity index 100% rename from playground/knot/vite.config.ts rename to playground/ty/vite.config.ts diff --git a/pyproject.toml b/pyproject.toml index a76aa557042ded..cf19e1306b12ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ include = [ [tool.ruff] target-version = "py38" extend-exclude = [ - "crates/red_knot_vendored/vendor/", + "crates/ty_vendored/vendor/", "crates/ruff/resources/", "crates/ruff_linter/resources/", "crates/ruff_python_formatter/resources/", @@ -99,7 +99,7 @@ required-imports = ["from __future__ import annotations"] [tool.black] force-exclude = ''' /( - | crates/red_knot_vendored/vendor + | crates/ty_vendored/vendor | crates/ruff_linter/resources | crates/ruff_python_formatter/resources | crates/ruff_python_parser/resources diff --git a/python/py-fuzzer/fuzz.py b/python/py-fuzzer/fuzz.py index ff084e38ab60b5..4e6881dd840c3a 100644 --- a/python/py-fuzzer/fuzz.py +++ b/python/py-fuzzer/fuzz.py @@ -48,13 +48,13 @@ ExitCode = NewType("ExitCode", int) -def redknot_contains_bug(code: str, *, red_knot_executable: Path) -> bool: +def ty_contains_bug(code: str, *, ty_executable: Path) -> bool: """Return `True` if the code triggers a panic in type-checking code.""" with tempfile.TemporaryDirectory() as tempdir: input_file = Path(tempdir, "input.py") input_file.write_text(code) completed_process = subprocess.run( - [red_knot_executable, "check", input_file], capture_output=True, text=True + [ty_executable, "check", input_file], capture_output=True, text=True ) return completed_process.returncode not in {0, 1, 2} @@ -85,8 +85,8 @@ def contains_bug(code: str, *, executable: Executable, executable_path: Path) -> match executable: case Executable.RUFF: return ruff_contains_bug(code, ruff_executable=executable_path) - case Executable.RED_KNOT: - return redknot_contains_bug(code, red_knot_executable=executable_path) + case Executable.TY: + return ty_contains_bug(code, ty_executable=executable_path) case _ as unreachable: assert_never(unreachable) @@ -139,10 +139,8 @@ def print_description(self, index: int, num_seeds: int) -> None: match self.executable: case Executable.RUFF: panic_message = f"The following code triggers a {new}parser bug:" - case Executable.RED_KNOT: - panic_message = ( - f"The following code triggers a {new}red-knot panic:" - ) + case Executable.TY: + panic_message = f"The following code triggers a {new}ty panic:" case _ as unreachable: assert_never(unreachable) @@ -292,7 +290,7 @@ def parse_seed_argument(arg: str) -> int | range: class Executable(enum.StrEnum): RUFF = "ruff" - RED_KNOT = "red_knot" + TY = "ty" @dataclass(slots=True) diff --git a/scripts/knot_benchmark/README.md b/scripts/ty_benchmark/README.md similarity index 72% rename from scripts/knot_benchmark/README.md rename to scripts/ty_benchmark/README.md index 622d6da7478738..e55150d4f1a14e 100644 --- a/scripts/knot_benchmark/README.md +++ b/scripts/ty_benchmark/README.md @@ -5,15 +5,15 @@ - Unix: `curl -LsSf https://astral.sh/uv/install.sh | sh` - Windows: `powershell -c "irm https://astral.sh/uv/install.ps1 | iex"` -1. Build red_knot: `cargo build --bin red_knot --release` -1. `cd` into the benchmark directory: `cd scripts/knot_benchmark` +1. Build ty: `cargo build --bin ty --release` +1. `cd` into the benchmark directory: `cd scripts/ty_benchmark` 1. Run benchmarks: `uv run benchmark` ## Known limitations -Red Knot only implements a tiny fraction of Mypy's and Pyright's functionality, +ty only implements a tiny fraction of Mypy's and Pyright's functionality, so the benchmarks aren't in any way a fair comparison today. However, -they'll become more meaningful as we build out more type checking features in Red Knot. +they'll become more meaningful as we build out more type checking features in ty. ### Windows support diff --git a/scripts/knot_benchmark/pyproject.toml b/scripts/ty_benchmark/pyproject.toml similarity index 79% rename from scripts/knot_benchmark/pyproject.toml rename to scripts/ty_benchmark/pyproject.toml index e1c5191232cf77..99040466fb1a96 100644 --- a/scripts/knot_benchmark/pyproject.toml +++ b/scripts/ty_benchmark/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "knot_benchmark" +name = "ty_benchmark" version = "0.0.1" -description = "Package for running end-to-end Red Knot benchmarks" +description = "Package for running end-to-end ty benchmarks" requires-python = ">=3.12" dependencies = ["mypy", "pyright"] diff --git a/scripts/knot_benchmark/src/benchmark/__init__.py b/scripts/ty_benchmark/src/benchmark/__init__.py similarity index 100% rename from scripts/knot_benchmark/src/benchmark/__init__.py rename to scripts/ty_benchmark/src/benchmark/__init__.py diff --git a/scripts/knot_benchmark/src/benchmark/cases.py b/scripts/ty_benchmark/src/benchmark/cases.py similarity index 95% rename from scripts/knot_benchmark/src/benchmark/cases.py rename to scripts/ty_benchmark/src/benchmark/cases.py index c95350dba59a6b..786b4b373ff506 100644 --- a/scripts/knot_benchmark/src/benchmark/cases.py +++ b/scripts/ty_benchmark/src/benchmark/cases.py @@ -53,18 +53,18 @@ def warm_command(self, project: Project, venv: Venv) -> Command | None: return None -class Knot(Tool): +class Ty(Tool): path: Path name: str def __init__(self, *, path: Path | None = None): - self.name = str(path) or "knot" + self.name = str(path) or "ty" self.path = path or ( - (Path(__file__) / "../../../../../target/release/red_knot").resolve() + (Path(__file__) / "../../../../../target/release/ty").resolve() ) assert self.path.is_file(), ( - f"Red Knot not found at '{self.path}'. Run `cargo build --release --bin red_knot`." + f"ty not found at '{self.path}'. Run `cargo build --release --bin ty`." ) def cold_command(self, project: Project, venv: Venv) -> Command: @@ -73,7 +73,7 @@ def cold_command(self, project: Project, venv: Venv) -> Command: command.extend(["--python", str(venv.path)]) return Command( - name="knot", + name="ty", command=command, ) diff --git a/scripts/knot_benchmark/src/benchmark/projects.py b/scripts/ty_benchmark/src/benchmark/projects.py similarity index 100% rename from scripts/knot_benchmark/src/benchmark/projects.py rename to scripts/ty_benchmark/src/benchmark/projects.py diff --git a/scripts/knot_benchmark/src/benchmark/run.py b/scripts/ty_benchmark/src/benchmark/run.py similarity index 88% rename from scripts/knot_benchmark/src/benchmark/run.py rename to scripts/ty_benchmark/src/benchmark/run.py index eab14496517331..2e8d0cdcfdbe70 100644 --- a/scripts/knot_benchmark/src/benchmark/run.py +++ b/scripts/ty_benchmark/src/benchmark/run.py @@ -8,7 +8,7 @@ from pathlib import Path from benchmark import Hyperfine -from benchmark.cases import Benchmark, Knot, Mypy, Pyright, Tool, Venv +from benchmark.cases import Benchmark, Mypy, Pyright, Tool, Ty, Venv from benchmark.projects import ALL as all_projects from benchmark.projects import DEFAULT as default_projects @@ -19,7 +19,7 @@ def main() -> None: """Run the benchmark.""" parser = argparse.ArgumentParser( - description="Benchmark knot against other packaging tools." + description="Benchmark ty against other packaging tools." ) parser.add_argument( "--verbose", "-v", action="store_true", help="Print verbose output." @@ -63,14 +63,14 @@ def main() -> None: action="store_true", ) parser.add_argument( - "--knot", - help="Whether to benchmark knot (assumes a red_knot binary exists at `./target/release/red_knot`).", + "--ty", + help="Whether to benchmark ty (assumes a ty binary exists at `./target/release/ty`).", action="store_true", ) parser.add_argument( - "--knot-path", + "--ty-path", type=Path, - help="Path(s) to the red_knot binary to benchmark.", + help="Path(s) to the ty binary to benchmark.", action="append", ) @@ -90,17 +90,17 @@ def main() -> None: if args.pyright: suites.append(Pyright()) - if args.knot: - suites.append(Knot()) + if args.ty: + suites.append(Ty()) - for path in args.knot_path or []: - suites.append(Knot(path=path)) + for path in args.ty_path or []: + suites.append(Ty(path=path)) if args.mypy: suites.append(Mypy()) # If no tools were specified, default to benchmarking all tools. - suites = suites or [Knot(), Pyright(), Mypy()] + suites = suites or [Ty(), Pyright(), Mypy()] # Determine the benchmarks to run, based on user input. benchmarks = ( diff --git a/scripts/knot_benchmark/uv.lock b/scripts/ty_benchmark/uv.lock similarity index 76% rename from scripts/knot_benchmark/uv.lock rename to scripts/ty_benchmark/uv.lock index 0fcb11197a1e9a..1091d51dd904ee 100644 --- a/scripts/knot_benchmark/uv.lock +++ b/scripts/ty_benchmark/uv.lock @@ -1,21 +1,7 @@ version = 1 +revision = 2 requires-python = ">=3.12" -[[package]] -name = "knot-benchmark" -version = "0.0.1" -source = { editable = "." } -dependencies = [ - { name = "mypy" }, - { name = "pyright" }, -] - -[package.metadata] -requires-dist = [ - { name = "mypy" }, - { name = "pyright" }, -] - [[package]] name = "mypy" version = "1.11.1" @@ -24,32 +10,32 @@ dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/9c/a4b3bda53823439cf395db8ecdda6229a83f9bf201714a68a15190bb2919/mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08", size = 3078369 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/9c/a4b3bda53823439cf395db8ecdda6229a83f9bf201714a68a15190bb2919/mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08", size = 3078369, upload_time = "2024-07-30T22:38:50.835Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/34/69638cee2e87303f19a0c35e80d42757e14d9aba328f272fdcdc0bf3c9b8/mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8", size = 10995789 }, - { url = "https://files.pythonhosted.org/packages/c4/3c/3e0611348fc53a4a7c80485959478b4f6eae706baf3b7c03cafa22639216/mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a", size = 10002696 }, - { url = "https://files.pythonhosted.org/packages/1c/21/a6b46c91b4c9d1918ee59c305f46850cde7cbea748635a352e7c3c8ed204/mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417", size = 12505772 }, - { url = "https://files.pythonhosted.org/packages/c4/55/07904d4c8f408e70308015edcbff067eaa77514475938a9dd81b063de2a8/mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e", size = 12954190 }, - { url = "https://files.pythonhosted.org/packages/1e/b7/3a50f318979c8c541428c2f1ee973cda813bcc89614de982dafdd0df2b3e/mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525", size = 9663138 }, - { url = "https://files.pythonhosted.org/packages/f8/d4/4960d0df55f30a7625d9c3c9414dfd42f779caabae137ef73ffaed0c97b9/mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54", size = 2619257 }, + { url = "https://files.pythonhosted.org/packages/3a/34/69638cee2e87303f19a0c35e80d42757e14d9aba328f272fdcdc0bf3c9b8/mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8", size = 10995789, upload_time = "2024-07-30T22:37:57.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3c/3e0611348fc53a4a7c80485959478b4f6eae706baf3b7c03cafa22639216/mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a", size = 10002696, upload_time = "2024-07-30T22:38:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/1c/21/a6b46c91b4c9d1918ee59c305f46850cde7cbea748635a352e7c3c8ed204/mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417", size = 12505772, upload_time = "2024-07-30T22:37:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/c4/55/07904d4c8f408e70308015edcbff067eaa77514475938a9dd81b063de2a8/mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e", size = 12954190, upload_time = "2024-07-30T22:37:31.244Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b7/3a50f318979c8c541428c2f1ee973cda813bcc89614de982dafdd0df2b3e/mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525", size = 9663138, upload_time = "2024-07-30T22:37:19.849Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/4960d0df55f30a7625d9c3c9414dfd42f779caabae137ef73ffaed0c97b9/mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54", size = 2619257, upload_time = "2024-07-30T22:37:40.567Z" }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload_time = "2023-02-04T12:11:27.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload_time = "2023-02-04T12:11:25.002Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload_time = "2024-06-04T18:44:11.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload_time = "2024-06-04T18:44:08.352Z" }, ] [[package]] @@ -59,16 +45,31 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/f0/25b0db363d6888164adb7c828b877bbf2c30936955fb9513922ae03e70e4/pyright-1.1.377.tar.gz", hash = "sha256:aabc30fedce0ded34baa0c49b24f10e68f4bfc8f68ae7f3d175c4b0f256b4fcf", size = 17484 } +sdist = { url = "https://files.pythonhosted.org/packages/49/f0/25b0db363d6888164adb7c828b877bbf2c30936955fb9513922ae03e70e4/pyright-1.1.377.tar.gz", hash = "sha256:aabc30fedce0ded34baa0c49b24f10e68f4bfc8f68ae7f3d175c4b0f256b4fcf", size = 17484, upload_time = "2024-08-21T02:25:15.74Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/c9/89c40c4de44fe9463e77dddd0c4e2d2dd7a93e8ddc6858dfe7d5f75d263d/pyright-1.1.377-py3-none-any.whl", hash = "sha256:af0dd2b6b636c383a6569a083f8c5a8748ae4dcde5df7914b3f3f267e14dd162", size = 18223 }, + { url = "https://files.pythonhosted.org/packages/34/c9/89c40c4de44fe9463e77dddd0c4e2d2dd7a93e8ddc6858dfe7d5f75d263d/pyright-1.1.377-py3-none-any.whl", hash = "sha256:af0dd2b6b636c383a6569a083f8c5a8748ae4dcde5df7914b3f3f267e14dd162", size = 18223, upload_time = "2024-08-21T02:25:14.585Z" }, +] + +[[package]] +name = "ty-benchmark" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "mypy" }, + { name = "pyright" }, +] + +[package.metadata] +requires-dist = [ + { name = "mypy" }, + { name = "pyright" }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload_time = "2024-06-07T18:52:15.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload_time = "2024-06-07T18:52:13.582Z" }, ] diff --git a/knot.schema.json b/ty.schema.json similarity index 92% rename from knot.schema.json rename to ty.schema.json index 49bc35855bb6da..a928b06343daa3 100644 --- a/knot.schema.json +++ b/ty.schema.json @@ -88,14 +88,14 @@ } }, "python": { - "description": "Path to the Python installation from which Red Knot resolves type information and third-party dependencies.\n\nRed Knot will search in the path's `site-packages` directories for type information and third-party imports.\n\nThis option is commonly used to specify the path to a virtual environment.", + "description": "Path to the Python installation from which ty resolves type information and third-party dependencies.\n\nty will search in the path's `site-packages` directories for type information and third-party imports.\n\nThis option is commonly used to specify the path to a virtual environment.", "type": [ "string", "null" ] }, "python-platform": { - "description": "Specifies the target platform that will be used to analyze the source code. If specified, Red Knot will tailor its use of type stub files, which conditionalize type definitions based on the platform.\n\nIf no platform is specified, knot will use the current platform: - `win32` for Windows - `darwin` for macOS - `android` for Android - `ios` for iOS - `linux` for everything else", + "description": "Specifies the target platform that will be used to analyze the source code. If specified, ty will tailor its use of type stub files, which conditionalize type definitions based on the platform.\n\nIf no platform is specified, ty will use the current platform: - `win32` for Windows - `darwin` for macOS - `android` for Android - `ios` for iOS - `linux` for everything else", "anyOf": [ { "$ref": "#/definitions/PythonPlatform" @@ -106,7 +106,7 @@ ] }, "python-version": { - "description": "Specifies the version of Python that will be used to analyze the source code. The version should be specified as a string in the format `M.m` where `M` is the major version and `m` is the minor (e.g. \"3.0\" or \"3.6\"). If a version is provided, knot will generate errors if the source code makes use of language features that are not supported in that version. It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.", + "description": "Specifies the version of Python that will be used to analyze the source code. The version should be specified as a string in the format `M.m` where `M` is the major version and `m` is the minor (e.g. \"3.0\" or \"3.6\"). If a version is provided, ty will generate errors if the source code makes use of language features that are not supported in that version. It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.", "anyOf": [ { "$ref": "#/definitions/PythonVersion" @@ -222,7 +222,7 @@ "properties": { "byte-string-type-annotation": { "title": "detects byte strings in type annotation positions", - "description": "## What it does\nChecks for byte-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use byte-string notation.\n\n## Examples\n```python\ndef test(): -> b\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", + "description": "## What it does\nChecks for byte-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyse type annotations that use byte-string notation.\n\n## Examples\n```python\ndef test(): -> b\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", "default": "error", "oneOf": [ { @@ -322,7 +322,7 @@ }, "fstring-type-annotation": { "title": "detects F-strings in type annotation positions", - "description": "## What it does\nChecks for f-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use f-string notation.\n\n## Examples\n```python\ndef test(): -> f\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", + "description": "## What it does\nChecks for f-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyse type annotations that use f-string notation.\n\n## Examples\n```python\ndef test(): -> f\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", "default": "error", "oneOf": [ { @@ -332,7 +332,7 @@ }, "implicit-concatenated-string-type-annotation": { "title": "detects implicit concatenated strings in type annotations", - "description": "## What it does\nChecks for implicit concatenated strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use implicit concatenated strings.\n\n## Examples\n```python\ndef test(): -> \"Literal[\" \"5\" \"]\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"Literal[5]\":\n ...\n```", + "description": "## What it does\nChecks for implicit concatenated strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyse type annotations that use implicit concatenated strings.\n\n## Examples\n```python\ndef test(): -> \"Literal[\" \"5\" \"]\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"Literal[5]\":\n ...\n```", "default": "error", "oneOf": [ { @@ -452,7 +452,7 @@ }, "invalid-ignore-comment": { "title": "detects ignore comments that use invalid syntax", - "description": "## What it does\nChecks for `type: ignore` and `knot: ignore` comments that are syntactically incorrect.\n\n## Why is this bad?\nA syntactically incorrect ignore comment is probably a mistake and is useless.\n\n## Examples\n```py\na = 20 / 0 # type: ignoree\n```\n\nUse instead:\n\n```py\na = 20 / 0 # type: ignore\n```", + "description": "## What it does\nChecks for `type: ignore` and `ty: ignore` comments that are syntactically incorrect.\n\n## Why is this bad?\nA syntactically incorrect ignore comment is probably a mistake and is useless.\n\n## Examples\n```py\na = 20 / 0 # type: ignoree\n```\n\nUse instead:\n\n```py\na = 20 / 0 # type: ignore\n```", "default": "warn", "oneOf": [ { @@ -662,7 +662,7 @@ }, "raw-string-type-annotation": { "title": "detects raw strings in type annotation positions", - "description": "## What it does\nChecks for raw-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use raw-string notation.\n\n## Examples\n```python\ndef test(): -> r\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", + "description": "## What it does\nChecks for raw-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like ty can't analyse type annotations that use raw-string notation.\n\n## Examples\n```python\ndef test(): -> r\"int\":\n ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n ...\n```", "default": "error", "oneOf": [ { @@ -682,7 +682,7 @@ }, "static-assert-error": { "title": "Failed static assertion", - "description": "## What it does\nMakes sure that the argument of `static_assert` is statically known to be true.\n\n## Examples\n```python\nfrom knot_extensions import static_assert\n\nstatic_assert(1 + 1 == 3) # error: evaluates to `False`\n\nstatic_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness\n```", + "description": "## What it does\nMakes sure that the argument of `static_assert` is statically known to be true.\n\n## Examples\n```python\nfrom ty_extensions import static_assert\n\nstatic_assert(1 + 1 == 3) # error: evaluates to `False`\n\nstatic_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness\n```", "default": "error", "oneOf": [ { @@ -751,8 +751,8 @@ ] }, "unknown-rule": { - "title": "detects `knot: ignore` comments that reference unknown rules", - "description": "## What it does\nChecks for `knot: ignore[code]` where `code` isn't a known lint rule.\n\n## Why is this bad?\nA `knot: ignore[code]` directive with a `code` that doesn't match\nany known rule will not suppress any type errors, and is probably a mistake.\n\n## Examples\n```py\na = 20 / 0 # knot: ignore[division-by-zer]\n```\n\nUse instead:\n\n```py\na = 20 / 0 # knot: ignore[division-by-zero]\n```", + "title": "detects `ty: ignore` comments that reference unknown rules", + "description": "## What it does\nChecks for `ty: ignore[code]` where `code` isn't a known lint rule.\n\n## Why is this bad?\nA `ty: ignore[code]` directive with a `code` that doesn't match\nany known rule will not suppress any type errors, and is probably a mistake.\n\n## Examples\n```py\na = 20 / 0 # ty: ignore[division-by-zer]\n```\n\nUse instead:\n\n```py\na = 20 / 0 # ty: ignore[division-by-zero]\n```", "default": "warn", "oneOf": [ { @@ -812,7 +812,7 @@ }, "unused-ignore-comment": { "title": "detects unused `type: ignore` comments", - "description": "## What it does\nChecks for `type: ignore` or `knot: ignore` directives that are no longer applicable.\n\n## Why is this bad?\nA `type: ignore` directive that no longer matches any diagnostic violations is likely\nincluded by mistake, and should be removed to avoid confusion.\n\n## Examples\n```py\na = 20 / 2 # knot: ignore[division-by-zero]\n```\n\nUse instead:\n\n```py\na = 20 / 2\n```", + "description": "## What it does\nChecks for `type: ignore` or `ty: ignore` directives that are no longer applicable.\n\n## Why is this bad?\nA `type: ignore` directive that no longer matches any diagnostic violations is likely\nincluded by mistake, and should be removed to avoid confusion.\n\n## Examples\n```py\na = 20 / 2 # ty: ignore[division-by-zero]\n```\n\nUse instead:\n\n```py\na = 20 / 2\n```", "default": "warn", "oneOf": [ { From 8535af8516f4ea4e3bf861bfe9f62dfd81f04941 Mon Sep 17 00:00:00 2001 From: Eric Botti <104158401+ercbot@users.noreply.github.com> Date: Sat, 3 May 2025 14:35:03 -0400 Subject: [PATCH 0234/1161] [red-knot] Add support for the LSP diagnostic tag (#17657) Co-authored-by: Micha Reiser --- crates/ruff_db/src/diagnostic/mod.rs | 39 +++++++++++++++++++ .../ty_python_semantic/src/types/context.rs | 16 ++++++++ .../src/server/api/requests/diagnostic.rs | 20 ++++++++-- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 6807f67888e872..023fc593681f3e 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -227,6 +227,11 @@ impl Diagnostic { pub fn primary_span(&self) -> Option { self.primary_annotation().map(|ann| ann.span.clone()) } + + /// Returns the tags from the primary annotation of this diagnostic if it exists. + pub fn primary_tags(&self) -> Option<&[DiagnosticTag]> { + self.primary_annotation().map(|ann| ann.tags.as_slice()) + } } #[derive(Debug, Clone, Eq, PartialEq)] @@ -338,6 +343,8 @@ pub struct Annotation { /// Whether this annotation is "primary" or not. When it isn't primary, an /// annotation is said to be "secondary." is_primary: bool, + /// The diagnostic tags associated with this annotation. + tags: Vec, } impl Annotation { @@ -355,6 +362,7 @@ impl Annotation { span, message: None, is_primary: true, + tags: Vec::new(), } } @@ -370,6 +378,7 @@ impl Annotation { span, message: None, is_primary: false, + tags: Vec::new(), } } @@ -412,6 +421,36 @@ impl Annotation { pub fn get_span(&self) -> &Span { &self.span } + + /// Returns the tags associated with this annotation. + pub fn get_tags(&self) -> &[DiagnosticTag] { + &self.tags + } + + /// Attaches this tag to this annotation. + /// + /// It will not replace any existing tags. + pub fn tag(mut self, tag: DiagnosticTag) -> Annotation { + self.tags.push(tag); + self + } + + /// Attaches an additional tag to this annotation. + pub fn push_tag(&mut self, tag: DiagnosticTag) { + self.tags.push(tag); + } +} + +/// Tags that can be associated with an annotation. +/// +/// These tags are used to provide additional information about the annotation. +/// and are passed through to the language server protocol. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum DiagnosticTag { + /// Unused or unnecessary code. Used for unused parameters, unreachable code, etc. + Unnecessary, + /// Deprecated or obsolete code. + Deprecated, } /// A string identifier for a lint rule. diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 37d16c9f227f68..6c63b529cdff3e 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -1,6 +1,7 @@ use std::fmt; use drop_bomb::DebugDropBomb; +use ruff_db::diagnostic::DiagnosticTag; use ruff_db::{ diagnostic::{Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Severity, Span}, files::File, @@ -259,6 +260,21 @@ impl LintDiagnosticGuard<'_, '_> { let ann = self.primary_annotation_mut().unwrap(); ann.set_message(message); } + + /// Adds a tag on the primary annotation for this diagnostic. + /// + /// This tag is associated with the primary annotation created + /// for every `Diagnostic` that uses the `LintDiagnosticGuard` API. + /// Specifically, the annotation is derived from the `TextRange` given to + /// the `InferContext::report_lint` API. + /// + /// Callers can add additional primary or secondary annotations via the + /// `DerefMut` trait implementation to a `Diagnostic`. + #[expect(dead_code)] + pub(super) fn add_primary_tag(&mut self, tag: DiagnosticTag) { + let ann = self.primary_annotation_mut().unwrap(); + ann.push_tag(tag); + } } impl std::ops::Deref for LintDiagnosticGuard<'_, '_> { diff --git a/crates/ty_server/src/server/api/requests/diagnostic.rs b/crates/ty_server/src/server/api/requests/diagnostic.rs index e128e22a4426e9..201d5db91b141c 100644 --- a/crates/ty_server/src/server/api/requests/diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/diagnostic.rs @@ -2,9 +2,9 @@ use std::borrow::Cow; use lsp_types::request::DocumentDiagnosticRequest; use lsp_types::{ - Diagnostic, DiagnosticSeverity, DocumentDiagnosticParams, DocumentDiagnosticReport, - DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, NumberOrString, Range, - RelatedFullDocumentDiagnosticReport, Url, + Diagnostic, DiagnosticSeverity, DiagnosticTag, DocumentDiagnosticParams, + DocumentDiagnosticReport, DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, + NumberOrString, Range, RelatedFullDocumentDiagnosticReport, Url, }; use crate::document::ToRangeExt; @@ -92,10 +92,22 @@ fn to_lsp_diagnostic( Severity::Error | Severity::Fatal => DiagnosticSeverity::ERROR, }; + let tags = diagnostic + .primary_tags() + .map(|tags| { + tags.iter() + .map(|tag| match tag { + ruff_db::diagnostic::DiagnosticTag::Unnecessary => DiagnosticTag::UNNECESSARY, + ruff_db::diagnostic::DiagnosticTag::Deprecated => DiagnosticTag::DEPRECATED, + }) + .collect::>() + }) + .filter(|mapped_tags| !mapped_tags.is_empty()); + Diagnostic { range, severity: Some(severity), - tags: None, + tags, code: Some(NumberOrString::String(diagnostic.id().to_string())), code_description: None, source: Some("ty".into()), From fa628018b28fa018d7a0e109223186172ef91275 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 3 May 2025 21:20:31 +0200 Subject: [PATCH 0235/1161] Use `#[expect(lint)]` over `#[allow(lint)]` where possible (#17822) --- crates/ruff/build.rs | 1 - crates/ruff/src/args.rs | 13 ++++++------ crates/ruff/src/cache.rs | 8 +++----- crates/ruff/src/commands/check.rs | 2 -- crates/ruff/src/commands/config.rs | 2 +- crates/ruff/src/commands/format.rs | 2 +- crates/ruff/src/commands/rule.rs | 2 +- crates/ruff/src/lib.rs | 4 ++-- crates/ruff/src/printer.rs | 1 - crates/ruff_benchmark/benches/linter.rs | 4 ++-- crates/ruff_cache/src/cache_key.rs | 2 +- crates/ruff_cache/tests/cache_key.rs | 2 +- crates/ruff_db/src/diagnostic/render.rs | 2 +- crates/ruff_db/src/system/os.rs | 1 - crates/ruff_db/src/system/walk_directory.rs | 2 +- crates/ruff_db/src/vendored.rs | 2 +- crates/ruff_dev/src/format_dev.rs | 9 ++++----- crates/ruff_dev/src/generate_rules_table.rs | 3 +-- crates/ruff_dev/src/main.rs | 4 ++-- crates/ruff_formatter/src/format_element.rs | 2 +- .../src/format_element/document.rs | 2 +- .../ruff_formatter/src/format_element/tag.rs | 2 +- crates/ruff_formatter/src/lib.rs | 2 +- crates/ruff_formatter/src/printer/mod.rs | 10 +++++----- crates/ruff_index/src/slice.rs | 6 +++--- crates/ruff_index/src/vec.rs | 6 +++--- .../src/checkers/ast/analyze/definitions.rs | 2 +- crates/ruff_linter/src/checkers/ast/mod.rs | 8 ++++---- crates/ruff_linter/src/checkers/imports.rs | 2 +- crates/ruff_linter/src/checkers/noqa.rs | 2 +- crates/ruff_linter/src/checkers/tokens.rs | 2 +- crates/ruff_linter/src/codes.rs | 2 +- crates/ruff_linter/src/docstrings/sections.rs | 2 +- crates/ruff_linter/src/fix/mod.rs | 1 - crates/ruff_linter/src/linter.rs | 7 +++---- crates/ruff_linter/src/logging.rs | 2 +- crates/ruff_linter/src/message/sarif.rs | 2 +- crates/ruff_linter/src/noqa.rs | 12 +++++------ crates/ruff_linter/src/registry/rule_set.rs | 6 +++--- crates/ruff_linter/src/rule_selector.rs | 2 +- .../rules/fastapi_unused_path_parameter.rs | 6 +++--- .../flake8_annotations/rules/definition.rs | 4 ++-- .../src/rules/flake8_annotations/settings.rs | 2 +- .../rules/async_function_with_timeout.rs | 2 +- .../rules/long_sleep_not_forever.rs | 2 +- .../rules/mutable_argument_default.rs | 2 +- .../flake8_commas/rules/trailing_commas.rs | 2 +- .../flake8_pytest_style/rules/fixture.rs | 4 ++-- .../ruff_linter/src/rules/isort/categorize.rs | 8 ++++---- crates/ruff_linter/src/rules/isort/format.rs | 2 +- crates/ruff_linter/src/rules/isort/mod.rs | 4 ++-- .../rules/isort/rules/add_required_imports.rs | 1 - .../src/rules/isort/rules/organize_imports.rs | 2 +- .../ruff_linter/src/rules/isort/settings.rs | 2 +- .../rules/invalid_first_argument_name.rs | 2 +- .../rules/pycodestyle/rules/blank_lines.rs | 2 +- .../src/rules/pycodestyle/rules/errors.rs | 2 +- .../pycodestyle/rules/logical_lines/mod.rs | 3 +-- .../rules/pydoclint/rules/check_docstring.rs | 2 +- .../src/rules/pydocstyle/rules/indent.rs | 2 +- .../rules/pylint/rules/import_private_name.rs | 2 +- .../pylint/rules/magic_value_comparison.rs | 2 +- .../src/rules/pylint/rules/unreachable.rs | 2 +- .../pylint/rules/useless_import_alias.rs | 2 +- .../src/rules/refurb/rules/math_constant.rs | 2 +- .../rules/refurb/rules/redundant_log_base.rs | 2 +- crates/ruff_linter/src/settings/types.rs | 4 ++-- crates/ruff_macros/src/combine_options.rs | 2 +- crates/ruff_macros/src/newtype_index.rs | 4 ++-- crates/ruff_macros/src/violation_metadata.rs | 2 +- crates/ruff_python_ast/src/comparable.rs | 1 - crates/ruff_python_ast/src/nodes.rs | 2 +- crates/ruff_python_codegen/src/generator.rs | 2 +- crates/ruff_python_formatter/src/builders.rs | 1 - crates/ruff_python_formatter/src/cli.rs | 2 +- .../ruff_python_formatter/src/comments/map.rs | 3 +-- crates/ruff_python_formatter/src/context.rs | 1 - .../src/expression/binary_like.rs | 2 +- .../src/expression/mod.rs | 5 ++--- crates/ruff_python_formatter/src/main.rs | 1 - crates/ruff_python_formatter/src/prelude.rs | 2 -- crates/ruff_python_formatter/src/range.rs | 2 +- .../src/statement/suite.rs | 1 - .../src/string/normalize.rs | 1 - crates/ruff_python_formatter/src/verbatim.rs | 2 +- crates/ruff_python_index/src/indexer.rs | 2 +- crates/ruff_python_literal/src/cformat.rs | 2 +- crates/ruff_python_literal/src/escape.rs | 12 ++--------- crates/ruff_python_parser/src/lexer.rs | 2 +- crates/ruff_python_parser/src/lexer/cursor.rs | 2 +- crates/ruff_python_parser/src/token_source.rs | 2 +- crates/ruff_python_parser/tests/fixtures.rs | 2 +- .../ruff_python_resolver/src/import_result.rs | 4 ++-- crates/ruff_python_resolver/src/resolver.rs | 4 ++-- .../src/analyze/typing.rs | 2 +- .../src/sys/builtin_modules.rs | 2 +- crates/ruff_python_trivia/src/tokenizer.rs | 4 ++-- crates/ruff_server/src/edit/notebook.rs | 2 +- crates/ruff_server/src/logging.rs | 2 +- crates/ruff_server/src/server.rs | 5 ++--- crates/ruff_server/src/server/api.rs | 2 +- crates/ruff_server/src/server/client.rs | 2 +- .../src/server/schedule/thread/pool.rs | 2 +- .../src/server/schedule/thread/priority.rs | 10 +++++----- .../ruff_server/src/session/capabilities.rs | 2 +- crates/ruff_server/src/session/index.rs | 1 - crates/ruff_server/src/session/settings.rs | 2 +- crates/ruff_source_file/src/line_index.rs | 2 +- crates/ruff_source_file/src/newlines.rs | 2 +- crates/ruff_text_size/src/serde_impls.rs | 2 +- crates/ruff_text_size/src/traits.rs | 2 +- crates/ruff_workspace/src/configuration.rs | 14 ++++--------- crates/ruff_workspace/src/options.rs | 8 ++++---- crates/ruff_workspace/src/pyproject.rs | 2 +- crates/ruff_workspace/src/settings.rs | 1 - crates/ty/build.rs | 1 - crates/ty/src/logging.rs | 2 +- crates/ty/src/main.rs | 1 - crates/ty_ide/src/db.rs | 2 +- crates/ty_project/src/metadata/value.rs | 4 ++-- crates/ty_project/src/watch/watcher.rs | 2 +- crates/ty_project/tests/check.rs | 2 +- crates/ty_python_semantic/src/ast_node_ref.rs | 14 ++++++------- crates/ty_python_semantic/src/lint.rs | 2 +- crates/ty_python_semantic/src/list.rs | 16 +++++++-------- .../ty_python_semantic/src/semantic_index.rs | 2 +- .../src/semantic_index/builder.rs | 20 +++++++++---------- .../src/semantic_index/definition.rs | 2 +- .../src/semantic_index/symbol.rs | 4 ++-- .../semantic_index/visibility_constraints.rs | 2 +- crates/ty_python_semantic/src/types.rs | 6 ++---- crates/ty_python_semantic/src/types/class.rs | 4 ++-- crates/ty_python_semantic/src/types/infer.rs | 2 +- crates/ty_python_semantic/src/types/narrow.rs | 2 +- .../ty_python_semantic/src/util/subscript.rs | 4 ++-- crates/ty_python_semantic/tests/mdtest.rs | 2 +- crates/ty_server/src/document/notebook.rs | 2 +- crates/ty_server/src/logging.rs | 2 +- crates/ty_server/src/server.rs | 5 ++--- crates/ty_server/src/server/api.rs | 2 +- crates/ty_server/src/server/client.rs | 2 +- .../src/server/schedule/thread/pool.rs | 2 +- .../src/server/schedule/thread/priority.rs | 10 +++++----- crates/ty_server/src/session/capabilities.rs | 2 +- crates/ty_server/src/session/index.rs | 1 - crates/ty_server/src/system.rs | 2 +- crates/ty_test/src/lib.rs | 2 +- scripts/generate_builtin_modules.py | 2 +- 148 files changed, 221 insertions(+), 268 deletions(-) diff --git a/crates/ruff/build.rs b/crates/ruff/build.rs index db70699e7e7553..befb62375c3745 100644 --- a/crates/ruff/build.rs +++ b/crates/ruff/build.rs @@ -13,7 +13,6 @@ fn main() { commit_info(&workspace_root); - #[allow(clippy::disallowed_methods)] let target = std::env::var("TARGET").unwrap(); println!("cargo::rustc-env=RUST_HOST_TARGET={target}"); } diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index bd95e59816e23a..6fd5425a5a4e9f 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -93,7 +93,7 @@ pub struct Args { pub(crate) global_options: GlobalConfigArgs, } -#[allow(clippy::large_enum_variant)] +#[expect(clippy::large_enum_variant)] #[derive(Debug, clap::Subcommand)] pub enum Command { /// Run Ruff on the given files or directories. @@ -184,7 +184,7 @@ pub struct AnalyzeGraphCommand { // The `Parser` derive is for ruff_dev, for ruff `Args` would be sufficient #[derive(Clone, Debug, clap::Parser)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct CheckCommand { /// List of files or directories to check. #[clap(help = "List of files or directories to check [default: .]")] @@ -446,7 +446,7 @@ pub struct CheckCommand { } #[derive(Clone, Debug, clap::Parser)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct FormatCommand { /// List of files or directories to format. #[clap(help = "List of files or directories to format [default: .]")] @@ -560,7 +560,7 @@ pub enum HelpFormat { Json, } -#[allow(clippy::module_name_repetitions)] +#[expect(clippy::module_name_repetitions)] #[derive(Debug, Default, Clone, clap::Args)] pub struct LogLevelArgs { /// Enable verbose logging. @@ -1031,7 +1031,7 @@ Possible choices: /// CLI settings that are distinct from configuration (commands, lists of files, /// etc.). -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct CheckArguments { pub add_noqa: bool, pub diff: bool, @@ -1050,7 +1050,7 @@ pub struct CheckArguments { /// CLI settings that are distinct from configuration (commands, lists of files, /// etc.). -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct FormatArguments { pub check: bool, pub no_cache: bool, @@ -1271,7 +1271,6 @@ pub struct AnalyzeGraphArgs { /// Configuration overrides provided via dedicated CLI flags: /// `--line-length`, `--respect-gitignore`, etc. #[derive(Clone, Default)] -#[allow(clippy::struct_excessive_bools)] struct ExplicitConfigOverrides { dummy_variable_rgx: Option, exclude: Option>, diff --git a/crates/ruff/src/cache.rs b/crates/ruff/src/cache.rs index 14bb1463b4cf3d..88fa67b063e2f4 100644 --- a/crates/ruff/src/cache.rs +++ b/crates/ruff/src/cache.rs @@ -86,7 +86,7 @@ pub(crate) struct Cache { changes: Mutex>, /// The "current" timestamp used as cache for the updates of /// [`FileCache::last_seen`] - #[allow(clippy::struct_field_names)] + #[expect(clippy::struct_field_names)] last_seen_cache: u64, } @@ -146,7 +146,7 @@ impl Cache { Cache::new(path, package) } - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] fn new(path: PathBuf, package: PackageCache) -> Self { Cache { path, @@ -204,7 +204,7 @@ impl Cache { } /// Applies the pending changes without storing the cache to disk. - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] pub(crate) fn save(&mut self) -> bool { /// Maximum duration for which we keep a file in cache that hasn't been seen. const MAX_LAST_SEEN: Duration = Duration::from_secs(30 * 24 * 60 * 60); // 30 days. @@ -834,7 +834,6 @@ mod tests { // Regression test for issue #3086. #[cfg(unix)] - #[allow(clippy::items_after_statements)] fn flip_execute_permission_bit(path: &Path) -> io::Result<()> { use std::os::unix::fs::PermissionsExt; let file = fs::OpenOptions::new().write(true).open(path)?; @@ -843,7 +842,6 @@ mod tests { } #[cfg(windows)] - #[allow(clippy::items_after_statements)] fn flip_read_only_permission(path: &Path) -> io::Result<()> { let file = fs::OpenOptions::new().write(true).open(path)?; let mut perms = file.metadata()?.permissions(); diff --git a/crates/ruff/src/commands/check.rs b/crates/ruff/src/commands/check.rs index 98cf0b52814156..9b7104e4dc95d1 100644 --- a/crates/ruff/src/commands/check.rs +++ b/crates/ruff/src/commands/check.rs @@ -30,7 +30,6 @@ use crate::cache::{Cache, PackageCacheMap, PackageCaches}; use crate::diagnostics::Diagnostics; /// Run the linter over a collection of files. -#[allow(clippy::too_many_arguments)] pub(crate) fn check( files: &[PathBuf], pyproject_config: &PyprojectConfig, @@ -181,7 +180,6 @@ pub(crate) fn check( /// Wraps [`lint_path`](crate::diagnostics::lint_path) in a [`catch_unwind`](std::panic::catch_unwind) and emits /// a diagnostic if the linting the file panics. -#[allow(clippy::too_many_arguments)] fn lint_path( path: &Path, package: Option>, diff --git a/crates/ruff/src/commands/config.rs b/crates/ruff/src/commands/config.rs index 4f83d7525a932a..956c041e10445a 100644 --- a/crates/ruff/src/commands/config.rs +++ b/crates/ruff/src/commands/config.rs @@ -5,7 +5,7 @@ use crate::args::HelpFormat; use ruff_workspace::options::Options; use ruff_workspace::options_base::OptionsMetadata; -#[allow(clippy::print_stdout)] +#[expect(clippy::print_stdout)] pub(crate) fn config(key: Option<&str>, format: HelpFormat) -> Result<()> { match key { None => { diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 6b1f5206f47c86..57414c249d0423 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -362,7 +362,7 @@ pub(crate) fn format_source( }) } else { // Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless. - #[allow(clippy::redundant_closure_for_method_calls)] + #[expect(clippy::redundant_closure_for_method_calls)] format_module_source(unformatted, options).map(|formatted| formatted.into_code()) }; diff --git a/crates/ruff/src/commands/rule.rs b/crates/ruff/src/commands/rule.rs index fd2e682b0fbdd5..271a0c4b1c81fb 100644 --- a/crates/ruff/src/commands/rule.rs +++ b/crates/ruff/src/commands/rule.rs @@ -19,7 +19,7 @@ struct Explanation<'a> { summary: &'a str, message_formats: &'a [&'a str], fix: String, - #[allow(clippy::struct_field_names)] + #[expect(clippy::struct_field_names)] explanation: Option<&'a str>, preview: bool, } diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 6446c51b980fa8..11c7ba3e70242e 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -134,7 +134,7 @@ pub fn run( { let default_panic_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { - #[allow(clippy::print_stderr)] + #[expect(clippy::print_stderr)] { eprintln!( r#" @@ -326,7 +326,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result 0 && config_arguments.log_level >= LogLevel::Default { let s = if modifications == 1 { "" } else { "s" }; - #[allow(clippy::print_stderr)] + #[expect(clippy::print_stderr)] { eprintln!("Added {modifications} noqa directive{s}."); } diff --git a/crates/ruff/src/printer.rs b/crates/ruff/src/printer.rs index bc0db8a2a7e730..bf8505c7c9fb17 100644 --- a/crates/ruff/src/printer.rs +++ b/crates/ruff/src/printer.rs @@ -241,7 +241,6 @@ impl Printer { } if !self.flags.intersects(Flags::SHOW_VIOLATIONS) { - #[allow(deprecated)] if matches!( self.format, OutputFormat::Full | OutputFormat::Concise | OutputFormat::Grouped diff --git a/crates/ruff_benchmark/benches/linter.rs b/crates/ruff_benchmark/benches/linter.rs index 712bd73b6ca883..556df1f8c995c4 100644 --- a/crates/ruff_benchmark/benches/linter.rs +++ b/crates/ruff_benchmark/benches/linter.rs @@ -45,9 +45,9 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; target_arch = "powerpc64" ) ))] -#[allow(non_upper_case_globals)] +#[expect(non_upper_case_globals)] #[export_name = "_rjem_malloc_conf"] -#[allow(unsafe_code)] +#[expect(unsafe_code)] pub static _rjem_malloc_conf: &[u8] = b"dirty_decay_ms:-1,muzzy_decay_ms:-1\0"; fn create_test_cases() -> Vec { diff --git a/crates/ruff_cache/src/cache_key.rs b/crates/ruff_cache/src/cache_key.rs index de66961dce44f2..0599f384c22852 100644 --- a/crates/ruff_cache/src/cache_key.rs +++ b/crates/ruff_cache/src/cache_key.rs @@ -213,7 +213,7 @@ macro_rules! impl_cache_key_tuple { ( $($name:ident)+) => ( impl<$($name: CacheKey),+> CacheKey for ($($name,)+) where last_type!($($name,)+): ?Sized { - #[allow(non_snake_case)] + #[expect(non_snake_case)] #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { let ($(ref $name,)+) = *self; diff --git a/crates/ruff_cache/tests/cache_key.rs b/crates/ruff_cache/tests/cache_key.rs index f78697880661ce..0027e32953624c 100644 --- a/crates/ruff_cache/tests/cache_key.rs +++ b/crates/ruff_cache/tests/cache_key.rs @@ -47,7 +47,7 @@ fn struct_ignored_fields() { struct NamedFieldsStruct { a: String, #[cache_key(ignore)] - #[allow(unused)] + #[expect(unused)] b: String, } diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 084b66d7692c50..1676347f331fae 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -376,7 +376,7 @@ struct Renderable<'r> { // (At time of writing, 2025-03-13, we currently render the diagnostic // ID into the main message of the parent diagnostic. We don't use this // specific field to do that though.) - #[allow(dead_code)] + #[expect(dead_code)] id: &'r str, diagnostics: Vec>, } diff --git a/crates/ruff_db/src/system/os.rs b/crates/ruff_db/src/system/os.rs index 91f97a63aa4aa8..ecadd077fb8abd 100644 --- a/crates/ruff_db/src/system/os.rs +++ b/crates/ruff_db/src/system/os.rs @@ -50,7 +50,6 @@ impl OsSystem { Self { // Spreading `..Default` because it isn't possible to feature gate the initializer of a single field. - #[allow(clippy::needless_update)] inner: Arc::new(OsSystemInner { cwd: cwd.to_path_buf(), case_sensitivity, diff --git a/crates/ruff_db/src/system/walk_directory.rs b/crates/ruff_db/src/system/walk_directory.rs index 7932ccfae7114e..752d4dae78857e 100644 --- a/crates/ruff_db/src/system/walk_directory.rs +++ b/crates/ruff_db/src/system/walk_directory.rs @@ -35,7 +35,7 @@ impl WalkDirectoryBuilder { /// Each additional path is traversed recursively. /// This should be preferred over building multiple /// walkers since it enables reusing resources. - #[allow(clippy::should_implement_trait)] + #[expect(clippy::should_implement_trait)] pub fn add(mut self, path: impl AsRef) -> Self { self.paths.push(path.as_ref().to_path_buf()); self diff --git a/crates/ruff_db/src/vendored.rs b/crates/ruff_db/src/vendored.rs index 923824d1e95a37..3203cfbf6b58e6 100644 --- a/crates/ruff_db/src/vendored.rs +++ b/crates/ruff_db/src/vendored.rs @@ -172,7 +172,7 @@ impl Default for VendoredFileSystem { /// that users of the `VendoredFileSystem` could realistically need. /// For debugging purposes, however, we want to have all information /// available. -#[allow(unused)] +#[expect(unused)] #[derive(Debug)] struct ZipFileDebugInfo { crc32_hash: u32, diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index f9be6b0bf591a0..7650149ec61386 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -63,7 +63,6 @@ fn find_pyproject_config( } /// Find files that ruff would check so we can format them. Adapted from `ruff`. -#[allow(clippy::type_complexity)] fn ruff_check_paths<'a>( pyproject_config: &'a PyprojectConfig, cli: &FormatArguments, @@ -135,12 +134,12 @@ impl Statistics { } /// We currently prefer the similarity index, but i'd like to keep this around - #[allow(clippy::cast_precision_loss, unused)] + #[expect(clippy::cast_precision_loss, unused)] pub(crate) fn jaccard_index(&self) -> f32 { self.intersection as f32 / (self.black_input + self.ruff_output + self.intersection) as f32 } - #[allow(clippy::cast_precision_loss)] + #[expect(clippy::cast_precision_loss)] pub(crate) fn similarity_index(&self) -> f32 { self.intersection as f32 / (self.black_input + self.intersection) as f32 } @@ -177,7 +176,7 @@ pub(crate) enum Format { Full, } -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] #[derive(clap::Args)] pub(crate) struct Args { /// Like `ruff check`'s files. See `--multi-project` if you want to format an ecosystem @@ -222,7 +221,7 @@ pub(crate) struct Args { #[arg(long)] pub(crate) files_with_errors: Option, #[clap(flatten)] - #[allow(clippy::struct_field_names)] + #[expect(clippy::struct_field_names)] pub(crate) log_level_args: LogLevelArgs, } diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 35723bae076bfa..16a7a48bf4680d 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -34,7 +34,6 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator, RuleGroup::Deprecated => { format!("{WARNING_SYMBOL}") } - #[allow(deprecated)] RuleGroup::Preview => { format!("{PREVIEW_SYMBOL}") } @@ -78,7 +77,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator, se = ""; } - #[allow(clippy::or_fun_call)] + #[expect(clippy::or_fun_call)] let _ = write!( table_out, "| {ss}{0}{1}{se} {{ #{0}{1} }} | {ss}{2}{se} | {ss}{3}{se} | {ss}{4}{se} |", diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index c2dd13d0f88402..f562d1d0ea0869 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -34,7 +34,7 @@ struct Args { } #[derive(Subcommand)] -#[allow(clippy::large_enum_variant)] +#[expect(clippy::large_enum_variant)] enum Command { /// Run all code and documentation generation steps. GenerateAll(generate_all::Args), @@ -82,7 +82,7 @@ fn main() -> Result { command, global_options, } = Args::parse(); - #[allow(clippy::print_stdout)] + #[expect(clippy::print_stdout)] match command { Command::GenerateAll(args) => generate_all::main(&args)?, Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?, diff --git a/crates/ruff_formatter/src/format_element.rs b/crates/ruff_formatter/src/format_element.rs index 8623f9850f4877..a08b25a4e4b60d 100644 --- a/crates/ruff_formatter/src/format_element.rs +++ b/crates/ruff_formatter/src/format_element.rs @@ -519,7 +519,7 @@ impl TextWidth { let char_width = match c { '\t' => indent_width.value(), '\n' => return TextWidth::Multiline, - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] c => c.width().unwrap_or(0) as u32, }; width += char_width; diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index 8236d92ee0eadd..70c95f0e25336f 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -280,7 +280,7 @@ impl Format> for &[FormatElement] { | FormatElement::SourceCodeSlice { .. }) => { fn write_escaped(element: &FormatElement, f: &mut Formatter) { let (text, text_width) = match element { - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] FormatElement::Token { text } => { (*text, TextWidth::Width(Width::new(text.len() as u32))) } diff --git a/crates/ruff_formatter/src/format_element/tag.rs b/crates/ruff_formatter/src/format_element/tag.rs index bb3b7854641f96..19eab684905f5f 100644 --- a/crates/ruff_formatter/src/format_element/tag.rs +++ b/crates/ruff_formatter/src/format_element/tag.rs @@ -379,7 +379,7 @@ impl PartialEq for LabelId { } impl LabelId { - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub fn of(label: T) -> Self { Self { value: label.value(), diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index f27ee19bec64af..b675813a579b25 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -925,7 +925,7 @@ pub struct FormatState { group_id_builder: UniqueGroupIdBuilder, } -#[allow(clippy::missing_fields_in_debug)] +#[expect(clippy::missing_fields_in_debug)] impl std::fmt::Debug for FormatState where Context: std::fmt::Debug, diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index efdef79be46fcd..071432500e83d3 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -331,7 +331,7 @@ impl<'a> Printer<'a> { FormatElement::Tag(StartVerbatim(kind)) => { if let VerbatimKind::Verbatim { length } = kind { // SAFETY: Ruff only supports formatting files <= 4GB - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] self.state.verbatim_markers.push(TextRange::at( TextSize::from(self.state.buffer.len() as u32), *length, @@ -464,7 +464,7 @@ impl<'a> Printer<'a> { self.push_marker(); match text { - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Text::Token(token) => { self.state.buffer.push_str(token); self.state.line_width += token.len() as u32; @@ -831,7 +831,7 @@ impl<'a> Printer<'a> { } else { self.state.buffer.push(char); - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] let char_width = if char == '\t' { self.options.indent_width.value() } else { @@ -1480,7 +1480,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { u32::from(indent.level()) * self.options().indent_width() + u32::from(indent.align()); match text { - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Text::Token(token) => { self.state.line_width += token.len() as u32; } @@ -1511,7 +1511,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { } } // SAFETY: A u32 is sufficient to format files <= 4GB - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] c => c.width().unwrap_or(0) as u32, }; self.state.line_width += char_width; diff --git a/crates/ruff_index/src/slice.rs b/crates/ruff_index/src/slice.rs index 9b3f9523f7a9cd..86675c4025d4fe 100644 --- a/crates/ruff_index/src/slice.rs +++ b/crates/ruff_index/src/slice.rs @@ -22,7 +22,7 @@ impl IndexSlice { pub const fn from_raw(raw: &[T]) -> &Self { let ptr: *const [T] = raw; - #[allow(unsafe_code)] + #[expect(unsafe_code)] // SAFETY: `IndexSlice` is `repr(transparent)` over a normal slice unsafe { &*(ptr as *const Self) @@ -33,7 +33,7 @@ impl IndexSlice { pub fn from_raw_mut(raw: &mut [T]) -> &mut Self { let ptr: *mut [T] = raw; - #[allow(unsafe_code)] + #[expect(unsafe_code)] // SAFETY: `IndexSlice` is `repr(transparent)` over a normal slice unsafe { &mut *(ptr as *mut Self) @@ -209,5 +209,5 @@ impl Default for &mut IndexSlice { // Whether `IndexSlice` is `Send` depends only on the data, // not the phantom data. -#[allow(unsafe_code)] +#[expect(unsafe_code)] unsafe impl Send for IndexSlice where T: Send {} diff --git a/crates/ruff_index/src/vec.rs b/crates/ruff_index/src/vec.rs index 4e9bc479229af0..6a9d03cc78c1c1 100644 --- a/crates/ruff_index/src/vec.rs +++ b/crates/ruff_index/src/vec.rs @@ -179,16 +179,16 @@ impl From<[T; N]> for IndexVec { // Whether `IndexVec` is `Send` depends only on the data, // not the phantom data. -#[allow(unsafe_code)] +#[expect(unsafe_code)] unsafe impl Send for IndexVec where T: Send {} -#[allow(unsafe_code)] +#[expect(unsafe_code)] #[cfg(feature = "salsa")] unsafe impl salsa::Update for IndexVec where T: salsa::Update, { - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { let old_vec: &mut IndexVec = unsafe { &mut *old_pointer }; salsa::Update::maybe_update(&mut old_vec.raw, new_value.raw) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index 392094ce56b2e5..ba5b9759ef564f 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -184,7 +184,7 @@ pub(crate) fn definitions(checker: &mut Checker) { // We don't recognise implicitly concatenated strings as valid docstrings in our model currently. let Some(sole_string_part) = string_literal.as_single_part_string() else { - #[allow(deprecated)] + #[expect(deprecated)] let location = checker .locator .compute_source_location(string_literal.start()); diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index b647d9ac00bf91..2a5a8f3a2b358e 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -234,14 +234,14 @@ pub(crate) struct Checker<'a> { /// The target [`PythonVersion`] for version-dependent checks. target_version: PythonVersion, /// Helper visitor for detecting semantic syntax errors. - #[allow(clippy::struct_field_names)] + #[expect(clippy::struct_field_names)] semantic_checker: SemanticSyntaxChecker, /// Errors collected by the `semantic_checker`. semantic_errors: RefCell>, } impl<'a> Checker<'a> { - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] pub(crate) fn new( parsed: &'a Parsed, parsed_annotations_arena: &'a typed_arena::Arena>, @@ -362,7 +362,7 @@ impl<'a> Checker<'a> { /// Returns the [`SourceRow`] for the given offset. pub(crate) fn compute_source_row(&self, offset: TextSize) -> SourceRow { - #[allow(deprecated)] + #[expect(deprecated)] let line = self.locator.compute_line_index(offset); if let Some(notebook_index) = self.notebook_index { @@ -2909,7 +2909,7 @@ impl<'a> ParsedAnnotationsCache<'a> { } } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn check_ast( parsed: &Parsed, locator: &Locator, diff --git a/crates/ruff_linter/src/checkers/imports.rs b/crates/ruff_linter/src/checkers/imports.rs index 2dd7a33a29999a..8199aaee7a1b23 100644 --- a/crates/ruff_linter/src/checkers/imports.rs +++ b/crates/ruff_linter/src/checkers/imports.rs @@ -16,7 +16,7 @@ use crate::rules::isort::block::{Block, BlockBuilder}; use crate::settings::LinterSettings; use crate::Locator; -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn check_imports( parsed: &Parsed, locator: &Locator, diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index da7c4b1e6ff881..f765da9c91111c 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -22,7 +22,7 @@ use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA}; use crate::settings::LinterSettings; use crate::Locator; -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn check_noqa( diagnostics: &mut Vec, path: &Path, diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index dc157ba3e0ec71..7564d498e287b4 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -19,7 +19,7 @@ use crate::rules::{ use crate::settings::LinterSettings; use crate::Locator; -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn check_tokens( tokens: &Tokens, path: &Path, diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index dce86a56a82f88..4f58c6a1167539 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -61,7 +61,7 @@ pub enum RuleGroup { #[ruff_macros::map_codes] pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { - #[allow(clippy::enum_glob_use)] + #[expect(clippy::enum_glob_use)] use Linter::*; #[rustfmt::skip] diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index aa42af30ef50fe..4f3671764b5416 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -418,7 +418,7 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option } /// Check if the suspected context is really a section header. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn is_docstring_section( line: &Line, indent_size: TextSize, diff --git a/crates/ruff_linter/src/fix/mod.rs b/crates/ruff_linter/src/fix/mod.rs index 77f78fbddb360b..c0441c58e4760f 100644 --- a/crates/ruff_linter/src/fix/mod.rs +++ b/crates/ruff_linter/src/fix/mod.rs @@ -172,7 +172,6 @@ mod tests { use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile; use crate::Locator; - #[allow(deprecated)] fn create_diagnostics( filename: &str, source: &str, diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 2886ea528c7349..4b23e99c89a24a 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -98,7 +98,7 @@ pub struct FixerResult<'a> { } /// Generate [`Message`]s from the source code contents at the given `Path`. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub fn check_path( path: &Path, package: Option>, @@ -538,7 +538,6 @@ fn diagnostics_to_messages( /// Generate `Diagnostic`s from source code content, iteratively fixing /// until stable. -#[allow(clippy::too_many_arguments)] pub fn lint_fix<'a>( path: &Path, package: Option>, @@ -672,7 +671,7 @@ fn collect_rule_codes(rules: impl IntoIterator) -> String { .join(", ") } -#[allow(clippy::print_stderr)] +#[expect(clippy::print_stderr)] fn report_failed_to_converge_error(path: &Path, transformed: &str, messages: &[Message]) { let codes = collect_rule_codes(messages.iter().filter_map(Message::rule)); if cfg!(debug_assertions) { @@ -705,7 +704,7 @@ This indicates a bug in Ruff. If you could open an issue at: } } -#[allow(clippy::print_stderr)] +#[expect(clippy::print_stderr)] fn report_fix_syntax_error( path: &Path, transformed: &str, diff --git a/crates/ruff_linter/src/logging.rs b/crates/ruff_linter/src/logging.rs index 5cf75d509b971b..dadf6ce25138c8 100644 --- a/crates/ruff_linter/src/logging.rs +++ b/crates/ruff_linter/src/logging.rs @@ -109,7 +109,7 @@ pub enum LogLevel { } impl LogLevel { - #[allow(clippy::trivially_copy_pass_by_ref)] + #[expect(clippy::trivially_copy_pass_by_ref)] const fn level_filter(&self) -> log::LevelFilter { match self { LogLevel::Default => log::LevelFilter::Info, diff --git a/crates/ruff_linter/src/message/sarif.rs b/crates/ruff_linter/src/message/sarif.rs index b74e293fd4cfd1..12dfd3a1a507e3 100644 --- a/crates/ruff_linter/src/message/sarif.rs +++ b/crates/ruff_linter/src/message/sarif.rs @@ -137,7 +137,7 @@ impl SarifResult { } #[cfg(target_arch = "wasm32")] - #[allow(clippy::unnecessary_wraps)] + #[expect(clippy::unnecessary_wraps)] fn from_message(message: &Message) -> Result { let start_location = message.compute_start_location(); let end_location = message.compute_end_location(); diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index fc00468992a582..e4875c0f87e6ae 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -238,7 +238,7 @@ impl<'a> FileNoqaDirectives<'a> { let no_indentation_at_offset = indentation_at_offset(range.start(), locator.contents()).is_none(); if !warnings.is_empty() || no_indentation_at_offset { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); @@ -268,7 +268,7 @@ impl<'a> FileNoqaDirectives<'a> { { Some(rule.noqa_code()) } else { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Invalid rule code provided to `# ruff: noqa` at {path_display}:{line}: {code}"); @@ -285,7 +285,7 @@ impl<'a> FileNoqaDirectives<'a> { }); } Err(err) => { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Invalid `# ruff: noqa` directive at {path_display}:{line}: {err}"); @@ -1053,7 +1053,7 @@ impl<'a> NoqaDirectives<'a> { directive, })) => { if !warnings.is_empty() { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); for warning in warnings { @@ -1073,7 +1073,7 @@ impl<'a> NoqaDirectives<'a> { ) .is_err() { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Invalid rule code provided to `# noqa` at {path_display}:{line}: {code}"); @@ -1091,7 +1091,7 @@ impl<'a> NoqaDirectives<'a> { }); } Err(err) => { - #[allow(deprecated)] + #[expect(deprecated)] let line = locator.compute_line_index(range.start()); let path_display = relativize_path(path); warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}"); diff --git a/crates/ruff_linter/src/registry/rule_set.rs b/crates/ruff_linter/src/registry/rule_set.rs index 241cb4020d4b37..d83ff07712c0cc 100644 --- a/crates/ruff_linter/src/registry/rule_set.rs +++ b/crates/ruff_linter/src/registry/rule_set.rs @@ -16,7 +16,7 @@ pub struct RuleSet([u64; RULESET_SIZE]); impl RuleSet { const EMPTY: [u64; RULESET_SIZE] = [0; RULESET_SIZE]; // 64 fits into a u16 without truncation - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] const SLICE_BITS: u16 = u64::BITS as u16; /// Returns an empty rule set. @@ -361,14 +361,14 @@ impl Iterator for RuleSetIterator { loop { let slice = self.set.0.get_mut(self.index as usize)?; // `trailing_zeros` is guaranteed to return a value in [0;64] - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] let bit = slice.trailing_zeros() as u16; if bit < RuleSet::SLICE_BITS { *slice ^= 1 << bit; let rule_value = self.index * RuleSet::SLICE_BITS + bit; // SAFETY: RuleSet guarantees that only valid rules are stored in the set. - #[allow(unsafe_code)] + #[expect(unsafe_code)] return Some(unsafe { std::mem::transmute::(rule_value) }); } diff --git a/crates/ruff_linter/src/rule_selector.rs b/crates/ruff_linter/src/rule_selector.rs index b433cad267bf41..ad385b219f9d36 100644 --- a/crates/ruff_linter/src/rule_selector.rs +++ b/crates/ruff_linter/src/rule_selector.rs @@ -314,7 +314,7 @@ mod schema { .filter(|_rule| { // Filter out all test-only rules #[cfg(any(feature = "test-rules", test))] - #[allow(clippy::used_underscore_binding)] + #[expect(clippy::used_underscore_binding)] if _rule.starts_with("RUF9") || _rule == "PLW0101" { return false; } diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs index 7cf69ee8c40b95..5a0f4facbb174e 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs @@ -79,7 +79,7 @@ impl Violation for FastApiUnusedPathParameter { function_name, is_positional, } = self; - #[allow(clippy::if_not_else)] + #[expect(clippy::if_not_else)] if !is_positional { format!("Parameter `{arg_name}` appears in route path, but not in `{function_name}` signature") } else { @@ -190,7 +190,7 @@ pub(crate) fn fastapi_unused_path_parameter( function_name: function_def.name.to_string(), is_positional, }, - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] diagnostic_range .add_start(TextSize::from(range.start as u32 + 1)) .sub_end(TextSize::from((path.len() - range.end + 1) as u32)), @@ -424,7 +424,7 @@ impl<'a> Iterator for PathParamIterator<'a> { let param_name_end = param_content.find(':').unwrap_or(param_content.len()); let param_name = ¶m_content[..param_name_end].trim(); - #[allow(clippy::range_plus_one)] + #[expect(clippy::range_plus_one)] return Some((param_name, start..end + 1)); } } diff --git a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs index e9560cfd93d5ff..13a0631f92c49d 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs @@ -150,7 +150,7 @@ impl Violation for MissingTypeKwargs { #[deprecated(note = "ANN101 has been removed")] pub(crate) struct MissingTypeSelf; -#[allow(deprecated)] +#[expect(deprecated)] impl Violation for MissingTypeSelf { fn message(&self) -> String { unreachable!("ANN101 has been removed"); @@ -194,7 +194,7 @@ impl Violation for MissingTypeSelf { #[deprecated(note = "ANN102 has been removed")] pub(crate) struct MissingTypeCls; -#[allow(deprecated)] +#[expect(deprecated)] impl Violation for MissingTypeCls { fn message(&self) -> String { unreachable!("ANN102 has been removed") diff --git a/crates/ruff_linter/src/rules/flake8_annotations/settings.rs b/crates/ruff_linter/src/rules/flake8_annotations/settings.rs index 342a56023a0e1a..c2b1875100e5d6 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/settings.rs @@ -5,7 +5,7 @@ use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; #[derive(Debug, Clone, Default, CacheKey)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct Settings { pub mypy_init_return: bool, pub suppress_dummy_args: bool, diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs b/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs index 53cbf952b160d5..019df9f61f4f88 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/async_function_with_timeout.rs @@ -8,7 +8,7 @@ use crate::checkers::ast::Checker; use crate::rules::flake8_async::helpers::AsyncModule; use ruff_python_ast::PythonVersion; -#[allow(clippy::doc_link_with_quotes)] +#[expect(clippy::doc_link_with_quotes)] /// ## What it does /// Checks for `async` function definitions with `timeout` parameters. /// diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs b/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs index 42dfdc30ba5f98..0be298deb5a850 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/long_sleep_not_forever.rs @@ -92,7 +92,7 @@ pub(crate) fn long_sleep_not_forever(checker: &Checker, call: &ExprCall) { } Number::Float(float_value) => { - #[allow(clippy::cast_precision_loss)] + #[expect(clippy::cast_precision_loss)] if *float_value <= one_day_in_secs as f64 { return; } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs index a828364f9c1041..4a65f76ebb851f 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs @@ -133,7 +133,7 @@ pub(crate) fn mutable_argument_default(checker: &Checker, function_def: &ast::St /// Generate a [`Fix`] to move a mutable argument default initialization /// into the function body. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn move_initialization( function_def: &ast::StmtFunctionDef, parameter: &Parameter, diff --git a/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs b/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs index bc20c966a0f101..0245a05b8d7976 100644 --- a/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs +++ b/crates/ruff_linter/src/rules/flake8_commas/rules/trailing_commas.rs @@ -74,7 +74,7 @@ impl From<(TokenKind, TextRange)> for SimpleToken { TokenKind::Import => TokenType::Named, _ => TokenType::Irrelevant, }; - #[allow(clippy::inconsistent_struct_constructor)] + #[expect(clippy::inconsistent_struct_constructor)] Self { range, ty } } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs index 4b3f038f3e8c92..ff34a8a6c00e06 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs @@ -218,7 +218,7 @@ impl AlwaysFixableViolation for PytestExtraneousScopeFunction { #[deprecated(note = "PT004 has been removed")] pub(crate) struct PytestMissingFixtureNameUnderscore; -#[allow(deprecated)] +#[expect(deprecated)] impl Violation for PytestMissingFixtureNameUnderscore { fn message(&self) -> String { unreachable!("PT004 has been removed"); @@ -283,7 +283,7 @@ impl Violation for PytestMissingFixtureNameUnderscore { #[deprecated(note = "PT005 has been removed")] pub(crate) struct PytestIncorrectFixtureNameUnderscore; -#[allow(deprecated)] +#[expect(deprecated)] impl Violation for PytestIncorrectFixtureNameUnderscore { fn message(&self) -> String { unreachable!("PT005 has been removed"); diff --git a/crates/ruff_linter/src/rules/isort/categorize.rs b/crates/ruff_linter/src/rules/isort/categorize.rs index 1e5d39b3fcf42a..0ed97f9a6744d5 100644 --- a/crates/ruff_linter/src/rules/isort/categorize.rs +++ b/crates/ruff_linter/src/rules/isort/categorize.rs @@ -79,16 +79,16 @@ enum Reason<'a> { Future, KnownStandardLibrary, SamePackage, - #[allow(dead_code)] + #[expect(dead_code)] SourceMatch(&'a Path), NoMatch, UserDefinedSection, NoSections, - #[allow(dead_code)] + #[expect(dead_code)] DisabledSection(&'a ImportSection), } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn categorize<'a>( module_name: &str, is_relative: bool, @@ -172,7 +172,7 @@ fn match_sources<'a>(paths: &'a [PathBuf], base: &str) -> Option<&'a Path> { None } -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn categorize_imports<'a>( block: ImportBlock<'a>, src: &[PathBuf], diff --git a/crates/ruff_linter/src/rules/isort/format.rs b/crates/ruff_linter/src/rules/isort/format.rs index 29639d27d5600d..e7210cd8dabee0 100644 --- a/crates/ruff_linter/src/rules/isort/format.rs +++ b/crates/ruff_linter/src/rules/isort/format.rs @@ -40,7 +40,7 @@ pub(crate) fn format_import( } /// Add an import-from statement to the [`RopeBuilder`]. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] pub(crate) fn format_import_from( import_from: &ImportFromData, comments: &ImportFromCommentSet, diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 40e0d6f9da112e..05d516f663fe35 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -63,7 +63,7 @@ pub(crate) enum AnnotatedImport<'a> { }, } -#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] +#[expect(clippy::too_many_arguments)] pub(crate) fn format_imports( block: &Block, comments: Vec, @@ -149,7 +149,7 @@ pub(crate) fn format_imports( output } -#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] +#[expect(clippy::too_many_arguments)] fn format_import_block( block: ImportBlock, line_length: LineLength, diff --git a/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs b/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs index b3c8d6cd1c04ca..68fdccc28b1aa3 100644 --- a/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs @@ -85,7 +85,6 @@ fn includes_import(stmt: &Stmt, target: &NameImport) -> bool { } } -#[allow(clippy::too_many_arguments)] fn add_required_import( required_import: &NameImport, parsed: &Parsed, diff --git a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs index 4ea44ee823924d..b982eae70b20a7 100644 --- a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs @@ -77,7 +77,7 @@ fn matches_ignoring_indentation(val1: &str, val2: &str) -> bool { }) } -#[allow(clippy::cast_sign_loss, clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] /// I001 pub(crate) fn organize_imports( block: &Block, diff --git a/crates/ruff_linter/src/rules/isort/settings.rs b/crates/ruff_linter/src/rules/isort/settings.rs index efed45bce84bbb..7c25b71e033413 100644 --- a/crates/ruff_linter/src/rules/isort/settings.rs +++ b/crates/ruff_linter/src/rules/isort/settings.rs @@ -45,7 +45,7 @@ impl Display for RelativeImportsOrder { } #[derive(Debug, Clone, CacheKey)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub struct Settings { pub required_imports: BTreeSet, pub combine_as_imports: bool, diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs index 21bd9172a7134a..f09e0b476a6e50 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/invalid_first_argument_name.rs @@ -143,7 +143,7 @@ impl Violation for InvalidFirstArgumentNameForClassMethod { #[derive_message_formats] // The first string below is what shows up in the documentation // in the rule table, and it is the more common case. - #[allow(clippy::if_not_else)] + #[expect(clippy::if_not_else)] fn message(&self) -> String { if !self.is_new { "First argument of a class method should be named `cls`".to_string() diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs index 87400d64bd3663..d2ccde22853fac 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs @@ -818,7 +818,7 @@ impl<'a> BlankLinesChecker<'a> { } } - #[allow(clippy::nonminimal_bool)] + #[expect(clippy::nonminimal_bool)] fn check_line( &self, line: &LogicalLineInfo, diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs index 25eefaeb673cd1..7ab939f4fa1fa9 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs @@ -66,7 +66,7 @@ impl Violation for IOError { #[deprecated(note = "E999 has been removed")] pub(crate) struct SyntaxError; -#[allow(deprecated)] +#[expect(deprecated)] impl Violation for SyntaxError { fn message(&self) -> String { unreachable!("E999 has been removed") diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs index 167c3456df6aad..4ee7f7d39e4439 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/mod.rs @@ -392,7 +392,6 @@ impl LogicalLinesBuilder { } // SAFETY: `LogicalLines::from_tokens` asserts that the file has less than `u32::MAX` tokens and each tokens is at least one character long - #[allow(clippy::cast_possible_truncation)] fn push_token(&mut self, kind: TokenKind, range: TextRange) { let line = &mut self.current_line; @@ -428,7 +427,7 @@ impl LogicalLinesBuilder { } // SAFETY: `LogicalLines::from_tokens` asserts that the file has less than `u32::MAX` tokens and each tokens is at least one character long - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] fn finish_line(&mut self) { let end = self.tokens.len() as u32; if self.current_line.tokens_start < end { diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index 826d71761501dc..5ca161a153d5ab 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -523,7 +523,7 @@ impl Ranged for YieldEntry { } } -#[allow(clippy::enum_variant_names)] +#[expect(clippy::enum_variant_names)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ReturnEntryKind { NotNone, diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs index 597380a369e142..648928fa2a0b96 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs @@ -10,7 +10,7 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::Rule; -#[allow(clippy::tabs_in_doc_comments)] +#[expect(clippy::tabs_in_doc_comments)] /// ## What it does /// Checks for docstrings that are indented with tabs. /// diff --git a/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs b/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs index 78beec5e92c964..096fc868457d8e 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/import_private_name.rs @@ -151,7 +151,7 @@ fn is_typing(reference: &ResolvedReference) -> bool { || reference.in_runtime_evaluated_annotation() } -#[allow(clippy::struct_field_names)] +#[expect(clippy::struct_field_names)] struct ImportInfo<'a> { module_name: &'a [&'a str], member_name: Cow<'a, str>, diff --git a/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs index ff34e25dfcbcf7..5c0ac91fcb765a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/magic_value_comparison.rs @@ -89,7 +89,7 @@ fn is_magic_value(literal_expr: LiteralExpressionRef, allowed_types: &[ConstantT !matches!(value.to_str(), "" | "__main__") } LiteralExpressionRef::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value { - #[allow(clippy::float_cmp)] + #[expect(clippy::float_cmp)] ast::Number::Float(value) => !(*value == 0.0 || *value == 1.0), ast::Number::Int(value) => !matches!(*value, Int::ZERO | Int::ONE), ast::Number::Complex { .. } => true, diff --git a/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs b/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs index b7541f55214305..c9db3f0836b03c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unreachable.rs @@ -98,7 +98,7 @@ fn reachable(cfg: &ControlFlowGraph) -> HashSet { /// Returns `Some(true)` if the condition is always true, e.g. `if True`, same /// with `Some(false)` if it's never taken. If it can't be determined it returns /// `None`, e.g. `if i == 100`. -#[allow(clippy::unnecessary_wraps)] +#[expect(clippy::unnecessary_wraps)] fn taken(condition: &Condition) -> Option { match condition { Condition::Always => Some(true), diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs index 292f26f8f6ba3b..8b26ad96d37028 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_import_alias.rs @@ -37,7 +37,7 @@ impl Violation for UselessImportAlias { #[derive_message_formats] fn message(&self) -> String { - #[allow(clippy::if_not_else)] + #[expect(clippy::if_not_else)] if !self.required_import_conflict { "Import alias does not rename original package".to_string() } else { diff --git a/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs b/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs index af468f75368ba7..66e4fcdbd59456 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/math_constant.rs @@ -105,7 +105,7 @@ enum Constant { } impl Constant { - #[allow(clippy::approx_constant)] + #[expect(clippy::approx_constant)] fn from_value(value: f64) -> Option { if (3.14..3.15).contains(&value) { matches_constant(std::f64::consts::PI, value).then_some(Self::Pi) diff --git a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs index a506aa17b834ca..6ba702d1d885bf 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs @@ -129,7 +129,7 @@ fn is_number_literal(expr: &Expr, value: i8) -> bool { if let Number::Int(number) = &number_literal.value { return number.as_i8().is_some_and(|number| number == value); } else if let Number::Float(number) = number_literal.value { - #[allow(clippy::float_cmp)] + #[expect(clippy::float_cmp)] return number == f64::from(value); } } diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 9a6fd859e09ebc..bfd20f1ec0c09e 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -249,12 +249,12 @@ pub struct FilePatternSet { cache_key: u64, // This field is only for displaying the internals // of `set`. - #[allow(clippy::used_underscore_binding)] + #[expect(clippy::used_underscore_binding)] _set_internals: Vec, } impl FilePatternSet { - #[allow(clippy::used_underscore_binding)] + #[expect(clippy::used_underscore_binding)] pub fn try_from_iter(patterns: I) -> Result where I: IntoIterator, diff --git a/crates/ruff_macros/src/combine_options.rs b/crates/ruff_macros/src/combine_options.rs index 05b2395e072bf2..46d99965f6003c 100644 --- a/crates/ruff_macros/src/combine_options.rs +++ b/crates/ruff_macros/src/combine_options.rs @@ -19,7 +19,7 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result Self { - #[allow(deprecated)] + #[expect(deprecated)] Self { #( #output diff --git a/crates/ruff_macros/src/newtype_index.rs b/crates/ruff_macros/src/newtype_index.rs index ad3638d21f7b68..99663542d7a7e3 100644 --- a/crates/ruff_macros/src/newtype_index.rs +++ b/crates/ruff_macros/src/newtype_index.rs @@ -48,7 +48,7 @@ pub(super) fn generate_newtype_index(item: ItemStruct) -> syn::Result syn::Result syn::Result Ok(quote! { #[automatically_derived] - #[allow(deprecated)] + #[expect(deprecated)] impl ruff_diagnostics::ViolationMetadata for #name { fn rule_name() -> &'static str { stringify!(#name) diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index 8dd4c0dd85a9c3..5197bfd38ac7bb 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -226,7 +226,6 @@ pub struct PatternMatchOr<'a> { patterns: Vec>, } -#[allow(clippy::enum_variant_names)] #[derive(Debug, PartialEq, Eq, Hash)] pub enum ComparablePattern<'a> { MatchValue(PatternMatchValue<'a>), diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index be28519a0f6fe6..282fa2a78f062e 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -352,7 +352,7 @@ impl Deref for FStringLiteralElement { /// Transforms a value prior to formatting it. #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, is_macro::Is)] #[repr(i8)] -#[allow(clippy::cast_possible_wrap)] +#[expect(clippy::cast_possible_wrap)] pub enum ConversionFlag { /// No conversion None = -1, // CPython uses -1 diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index 11217c7ff35288..ed9412caf51e9c 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -1365,7 +1365,7 @@ impl<'a> Generator<'a> { if !conversion.is_none() { self.p("!"); - #[allow(clippy::cast_possible_truncation)] + self.p(&format!("{}", conversion as u8 as char)); } diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 86cabaea091e25..8a3b81baf7606e 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -178,7 +178,6 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { self } - #[allow(unused)] pub(crate) fn entries(&mut self, entries: I) -> &mut Self where T: Ranged, diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs index b88d8e20bbef4e..a2b5afb9c2012c 100644 --- a/crates/ruff_python_formatter/src/cli.rs +++ b/crates/ruff_python_formatter/src/cli.rs @@ -24,7 +24,7 @@ pub enum Emit { #[derive(Parser)] #[command(author, version, about, long_about = None)] -#[allow(clippy::struct_excessive_bools)] // It's only the dev cli anyways +#[expect(clippy::struct_excessive_bools)] // It's only the dev cli anyways pub struct Cli { /// Python files to format. If there are none, stdin will be used. `-` as stdin is not supported pub files: Vec, diff --git a/crates/ruff_python_formatter/src/comments/map.rs b/crates/ruff_python_formatter/src/comments/map.rs index e7be3d7a5f66f7..0d727fa2120677 100644 --- a/crates/ruff_python_formatter/src/comments/map.rs +++ b/crates/ruff_python_formatter/src/comments/map.rs @@ -244,7 +244,6 @@ impl MultiMap { } /// Returns `true` if `key` has any *leading*, *dangling*, or *trailing* parts. - #[allow(unused)] pub(super) fn has(&self, key: &K) -> bool { self.index.contains_key(key) } @@ -542,7 +541,7 @@ impl PartIndex { // OK because: // * The `value < u32::MAX` guarantees that the add doesn't overflow. // * The `+ 1` guarantees that the index is not zero - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Self(std::num::NonZeroU32::new((value as u32) + 1).expect("valid value")) } diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index c1293758019cd2..33b882abf1dc82 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -106,7 +106,6 @@ impl<'a> PyFormatContext<'a> { } /// Returns `true` if preview mode is enabled. - #[allow(unused)] pub(crate) const fn is_preview(&self) -> bool { self.options.preview().is_enabled() } diff --git a/crates/ruff_python_formatter/src/expression/binary_like.rs b/crates/ruff_python_formatter/src/expression/binary_like.rs index 429740c0b327a7..95edb19feb947f 100644 --- a/crates/ruff_python_formatter/src/expression/binary_like.rs +++ b/crates/ruff_python_formatter/src/expression/binary_like.rs @@ -571,7 +571,7 @@ impl<'a> FlatBinaryExpressionSlice<'a> { "Operand slice must contain at least one operand" ); - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe { // SAFETY: `BinaryChainSlice` has the same layout as a slice because it uses `repr(transparent)` &*(std::ptr::from_ref::<[OperandOrOperator<'a>]>(slice) diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index d1a1777362f291..9329d2ecfb38ea 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -523,7 +523,6 @@ impl<'ast> IntoFormat> for Expr { /// * The expression contains at least one parenthesized sub expression (optimization to avoid unnecessary work) /// /// This mimics Black's [`_maybe_split_omitting_optional_parens`](https://github.com/psf/black/blob/d1248ca9beaf0ba526d265f4108836d89cf551b7/src/black/linegen.py#L746-L820) -#[allow(clippy::if_same_then_else)] pub(crate) fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool { let mut visitor = CanOmitOptionalParenthesesVisitor::new(context); visitor.visit_subexpression(expr); @@ -679,7 +678,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { // It's impossible for a file smaller or equal to 4GB to contain more than 2^32 comparisons // because each comparison requires a left operand, and `n` `operands` and right sides. - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Expr::BoolOp(ast::ExprBoolOp { range: _, op: _, @@ -702,7 +701,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { // It's impossible for a file smaller or equal to 4GB to contain more than 2^32 comparisons // because each comparison requires a left operand, and `n` `operands` and right sides. - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Expr::Compare(ast::ExprCompare { range: _, left: _, diff --git a/crates/ruff_python_formatter/src/main.rs b/crates/ruff_python_formatter/src/main.rs index 248ee5fa54a7b3..9436c52a63de56 100644 --- a/crates/ruff_python_formatter/src/main.rs +++ b/crates/ruff_python_formatter/src/main.rs @@ -14,7 +14,6 @@ pub(crate) fn read_from_stdin() -> Result { Ok(buffer) } -#[allow(clippy::print_stdout)] fn main() -> Result<()> { let cli: Cli = Cli::parse(); diff --git a/crates/ruff_python_formatter/src/prelude.rs b/crates/ruff_python_formatter/src/prelude.rs index f3f88f6145a8de..e9c50579541c9c 100644 --- a/crates/ruff_python_formatter/src/prelude.rs +++ b/crates/ruff_python_formatter/src/prelude.rs @@ -1,7 +1,5 @@ -#[allow(unused_imports)] pub(crate) use crate::{ builders::PyFormatterExtensions, AsFormat, FormatNodeRule, FormattedIterExt as _, IntoFormat, PyFormatContext, PyFormatter, }; -#[allow(unused_imports)] pub(crate) use ruff_formatter::prelude::*; diff --git a/crates/ruff_python_formatter/src/range.rs b/crates/ruff_python_formatter/src/range.rs index 03b16b4467f522..052c7cff74271e 100644 --- a/crates/ruff_python_formatter/src/range.rs +++ b/crates/ruff_python_formatter/src/range.rs @@ -546,7 +546,7 @@ impl NarrowRange<'_> { Some(SavedLevel { level: saved_level }) } - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn leave_level(&mut self, saved_state: SavedLevel) { self.level = saved_state.level; } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 7b275677ef288b..1b80f92b3baa0d 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -170,7 +170,6 @@ impl FormatRule> for FormatSuite { } else { first.fmt(f)?; - #[allow(clippy::if_same_then_else)] let empty_line_after_docstring = if matches!(first, SuiteChildStatement::Docstring(_)) && self.kind == SuiteKind::Class { diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index 7ebc3929053dd0..413345ad677d10 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -691,7 +691,6 @@ pub(crate) fn normalize_string( } if !new_flags.is_triple_quoted() { - #[allow(clippy::if_same_then_else)] if next == opposite_quote { // Remove the escape by ending before the backslash and starting again with the quote chars.next(); diff --git a/crates/ruff_python_formatter/src/verbatim.rs b/crates/ruff_python_formatter/src/verbatim.rs index 2f60a7abc81b53..33536f85920ba6 100644 --- a/crates/ruff_python_formatter/src/verbatim.rs +++ b/crates/ruff_python_formatter/src/verbatim.rs @@ -487,7 +487,7 @@ enum SuppressionComments<'a> { /// Comments that all fall into the formatted range. Formatted { - #[allow(unused)] + #[expect(unused)] comments: &'a [SourceComment], }, } diff --git a/crates/ruff_python_index/src/indexer.rs b/crates/ruff_python_index/src/indexer.rs index bf1d53c64d9f3f..e358fda6c52dc1 100644 --- a/crates/ruff_python_index/src/indexer.rs +++ b/crates/ruff_python_index/src/indexer.rs @@ -53,7 +53,7 @@ impl Indexer { continuation_lines.push(line_start); // SAFETY: Safe because of the len assertion at the top of the function. - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] { line_start = prev_end + TextSize::new((index + 1) as u32); } diff --git a/crates/ruff_python_literal/src/cformat.rs b/crates/ruff_python_literal/src/cformat.rs index cb724fa82c8405..5427e7a854c57c 100644 --- a/crates/ruff_python_literal/src/cformat.rs +++ b/crates/ruff_python_literal/src/cformat.rs @@ -241,7 +241,7 @@ where Ok((format_type, c)) } -#[allow(clippy::cast_possible_wrap)] +#[expect(clippy::cast_possible_wrap)] fn parse_quantity(iter: &mut ParseIter) -> Result, ParsingError> where T: Into + Copy, diff --git a/crates/ruff_python_literal/src/escape.rs b/crates/ruff_python_literal/src/escape.rs index 5a218dbcfd0912..10adf74a4d5f74 100644 --- a/crates/ruff_python_literal/src/escape.rs +++ b/crates/ruff_python_literal/src/escape.rs @@ -103,11 +103,7 @@ impl std::fmt::Display for StrRepr<'_, '_> { impl UnicodeEscape<'_> { const REPR_RESERVED_LEN: usize = 2; // for quotes - #[allow( - clippy::cast_possible_wrap, - clippy::cast_possible_truncation, - clippy::cast_sign_loss - )] + #[expect(clippy::cast_possible_wrap, clippy::cast_sign_loss)] pub fn repr_layout(source: &str, preferred_quote: Quote) -> EscapeLayout { Self::output_layout_with_checker(source, preferred_quote, |a, b| { Some((a as isize).checked_add(b as isize)? as usize) @@ -265,11 +261,7 @@ impl<'a> AsciiEscape<'a> { } impl AsciiEscape<'_> { - #[allow( - clippy::cast_possible_wrap, - clippy::cast_possible_truncation, - clippy::cast_sign_loss - )] + #[expect(clippy::cast_possible_wrap, clippy::cast_sign_loss)] pub fn repr_layout(source: &[u8], preferred_quote: Quote) -> EscapeLayout { Self::output_layout_with_checker(source, preferred_quote, 3, |a, b| { Some((a as isize).checked_add(b as isize)? as usize) diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index c3e52f844c82dd..f465efd715988b 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -1459,7 +1459,7 @@ impl<'src> Lexer<'src> { /// Retrieves the current offset of the cursor within the source code. // SAFETY: Lexer doesn't allow files larger than 4GB - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] #[inline] fn offset(&self) -> TextSize { TextSize::new(self.source.len() as u32) - self.cursor.text_len() diff --git a/crates/ruff_python_parser/src/lexer/cursor.rs b/crates/ruff_python_parser/src/lexer/cursor.rs index d1107f18ef2b13..413d648ca00b27 100644 --- a/crates/ruff_python_parser/src/lexer/cursor.rs +++ b/crates/ruff_python_parser/src/lexer/cursor.rs @@ -62,7 +62,7 @@ impl<'src> Cursor<'src> { /// /// Use [`Cursor::rest`] to get the remaining text. // SAFETY: The `source.text_len` call in `new` would panic if the string length is larger than a `u32`. - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] pub(super) fn text_len(&self) -> TextSize { TextSize::new(self.chars.as_str().len() as u32) } diff --git a/crates/ruff_python_parser/src/token_source.rs b/crates/ruff_python_parser/src/token_source.rs index 8b379af4c26f7f..3fef2cf99d9935 100644 --- a/crates/ruff_python_parser/src/token_source.rs +++ b/crates/ruff_python_parser/src/token_source.rs @@ -210,7 +210,7 @@ pub(crate) struct TokenSourceCheckpoint { /// of `contents`. /// /// See [#9546](https://github.com/astral-sh/ruff/pull/9546) for a more detailed explanation. -#[allow(dead_code)] +#[expect(dead_code)] fn allocate_tokens_vec(contents: &str) -> Vec { let lower_bound = contents.len().saturating_mul(15) / 100; Vec::with_capacity(lower_bound) diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index f81cbcd41e0941..dc2d7a8d328e40 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -274,7 +274,7 @@ fn extract_options(source: &str) -> Option { // Use it for quickly debugging a parser issue. #[test] #[ignore] -#[allow(clippy::print_stdout)] +#[expect(clippy::print_stdout)] fn parser_quick_test() { let source = "\ f'{' diff --git a/crates/ruff_python_resolver/src/import_result.rs b/crates/ruff_python_resolver/src/import_result.rs index ddeab06c85aa56..b0556d06fce849 100644 --- a/crates/ruff_python_resolver/src/import_result.rs +++ b/crates/ruff_python_resolver/src/import_result.rs @@ -6,7 +6,7 @@ use crate::implicit_imports::ImplicitImports; use crate::py_typed::PyTypedInfo; #[derive(Debug, Clone, PartialEq, Eq)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub(crate) struct ImportResult { /// Whether the import name was relative (e.g., ".foo"). pub(crate) is_relative: bool, @@ -76,7 +76,7 @@ pub(crate) struct ImportResult { /// If the import resolved to a type hint (i.e., a `.pyi` file), then /// a non-type-hint resolution will be stored here. - #[allow(clippy::struct_field_names)] + #[expect(clippy::struct_field_names)] pub(crate) non_stub_import_result: Option>, /// Information extracted from the `py.typed` in the package used to diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs index 22228d349620eb..0be466a9298dca 100644 --- a/crates/ruff_python_resolver/src/resolver.rs +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -12,7 +12,7 @@ use crate::import_result::{ImportResult, ImportType}; use crate::module_descriptor::ImportModuleDescriptor; use crate::{host, native_module, py_typed, search}; -#[allow(clippy::fn_params_excessive_bools)] +#[expect(clippy::fn_params_excessive_bools)] fn resolve_module_descriptor( root: &Path, module_descriptor: &ImportModuleDescriptor, @@ -206,7 +206,7 @@ fn resolve_module_descriptor( /// defined in [PEP 420]. /// /// [PEP 420]: https://peps.python.org/pep-0420/ -#[allow(clippy::fn_params_excessive_bools)] +#[expect(clippy::fn_params_excessive_bools)] fn resolve_absolute_import( root: &Path, module_descriptor: &ImportModuleDescriptor, diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index c588da272aff83..be1d529ce9e5f0 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -1147,7 +1147,7 @@ pub fn find_assigned_value<'a>(symbol: &str, semantic: &'a SemanticModel<'a>) -> /// /// This function will return a `NumberLiteral` with value `Int(42)` when called with `foo` and a /// `StringLiteral` with value `"str"` when called with `bla`. -#[allow(clippy::single_match)] +#[expect(clippy::single_match)] pub fn find_binding_value<'a>(binding: &Binding, semantic: &'a SemanticModel) -> Option<&'a Expr> { match binding.kind { // Ex) `x := 1` diff --git a/crates/ruff_python_stdlib/src/sys/builtin_modules.rs b/crates/ruff_python_stdlib/src/sys/builtin_modules.rs index 6000857a2f821c..9fce83d0ef8c89 100644 --- a/crates/ruff_python_stdlib/src/sys/builtin_modules.rs +++ b/crates/ruff_python_stdlib/src/sys/builtin_modules.rs @@ -9,7 +9,7 @@ /// modules. /// /// [builtin module]: https://docs.python.org/3/library/sys.html#sys.builtin_module_names -#[allow(clippy::unnested_or_patterns)] +#[expect(clippy::unnested_or_patterns)] pub fn is_builtin_module(minor_version: u8, module: &str) -> bool { matches!( (minor_version, module), diff --git a/crates/ruff_python_trivia/src/tokenizer.rs b/crates/ruff_python_trivia/src/tokenizer.rs index 4b0e1860f5d775..6b366b39991f7a 100644 --- a/crates/ruff_python_trivia/src/tokenizer.rs +++ b/crates/ruff_python_trivia/src/tokenizer.rs @@ -30,7 +30,7 @@ pub fn find_only_token_in_range( let token = tokens.next().expect("Expected a token"); debug_assert_eq!(token.kind(), token_kind); let mut tokens = tokens.skip_while(|token| token.kind == SimpleTokenKind::LParen); - #[allow(clippy::debug_assert_with_mut_call)] + #[expect(clippy::debug_assert_with_mut_call)] { debug_assert_eq!(tokens.next(), None); } @@ -114,7 +114,7 @@ pub fn lines_after_ignoring_trivia(offset: TextSize, code: &str) -> u32 { /// Counts the empty lines after `offset`, ignoring any trailing trivia on the same line as /// `offset`. -#[allow(clippy::cast_possible_truncation)] +#[expect(clippy::cast_possible_truncation)] pub fn lines_after_ignoring_end_of_line_trivia(offset: TextSize, code: &str) -> u32 { // SAFETY: We don't support files greater than 4GB, so casting to u32 is safe. SimpleTokenizer::starts_at(offset, code) diff --git a/crates/ruff_server/src/edit/notebook.rs b/crates/ruff_server/src/edit/notebook.rs index d71ada7808d455..154628d8626392 100644 --- a/crates/ruff_server/src/edit/notebook.rs +++ b/crates/ruff_server/src/edit/notebook.rs @@ -247,7 +247,7 @@ mod tests { use super::NotebookDocument; enum TestCellContent { - #[allow(dead_code)] + #[expect(dead_code)] Markup(String), Code(String), } diff --git a/crates/ruff_server/src/logging.rs b/crates/ruff_server/src/logging.rs index c402c3a869fa6b..dd9b0b01d0ac77 100644 --- a/crates/ruff_server/src/logging.rs +++ b/crates/ruff_server/src/logging.rs @@ -34,7 +34,7 @@ pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&std::path::Pat .append(true) .open(&path) .map_err(|err| { - #[allow(clippy::print_stderr)] + #[expect(clippy::print_stderr)] { eprintln!( "Failed to open file at {} for logging: {err}", diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index b515147ca23009..66ca342211b0db 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -5,7 +5,7 @@ use lsp_types as types; use lsp_types::InitializeParams; use std::num::NonZeroUsize; // The new PanicInfoHook name requires MSRV >= 1.82 -#[allow(deprecated)] +#[expect(deprecated)] use std::panic::PanicInfo; use std::str::FromStr; use types::ClientCapabilities; @@ -116,7 +116,7 @@ impl Server { pub fn run(self) -> crate::Result<()> { // The new PanicInfoHook name requires MSRV >= 1.82 - #[allow(deprecated)] + #[expect(deprecated)] type PanicHook = Box) + 'static + Sync + Send>; struct RestorePanicHook { hook: Option, @@ -170,7 +170,6 @@ impl Server { .join() } - #[allow(clippy::needless_pass_by_value)] // this is because we aren't using `next_request_id` yet. fn event_loop( connection: &Connection, client_capabilities: &ClientCapabilities, diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index 7b95ea68e3eac1..0493eee6bdf618 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -143,7 +143,7 @@ fn local_notification_task<'a, N: traits::SyncNotificationHandler>( })) } -#[allow(dead_code)] +#[expect(dead_code)] fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationHandler>( req: server::Notification, schedule: BackgroundSchedule, diff --git a/crates/ruff_server/src/server/client.rs b/crates/ruff_server/src/server/client.rs index c5a502e213c5e6..953aba3a8cf60e 100644 --- a/crates/ruff_server/src/server/client.rs +++ b/crates/ruff_server/src/server/client.rs @@ -48,7 +48,7 @@ impl Client<'_> { } } -#[allow(dead_code)] // we'll need to use `Notifier` in the future +#[expect(dead_code)] // we'll need to use `Notifier` in the future impl Notifier { pub(crate) fn notify(&self, params: N::Params) -> crate::Result<()> where diff --git a/crates/ruff_server/src/server/schedule/thread/pool.rs b/crates/ruff_server/src/server/schedule/thread/pool.rs index ea654a11d2af46..9ed3dad2a66070 100644 --- a/crates/ruff_server/src/server/schedule/thread/pool.rs +++ b/crates/ruff_server/src/server/schedule/thread/pool.rs @@ -106,7 +106,7 @@ impl Pool { self.job_sender.send(job).unwrap(); } - #[allow(dead_code)] + #[expect(dead_code)] pub(super) fn len(&self) -> usize { self.extant_tasks.load(Ordering::SeqCst) } diff --git a/crates/ruff_server/src/server/schedule/thread/priority.rs b/crates/ruff_server/src/server/schedule/thread/priority.rs index e6a555242fcb72..65b6e3d984908b 100644 --- a/crates/ruff_server/src/server/schedule/thread/priority.rs +++ b/crates/ruff_server/src/server/schedule/thread/priority.rs @@ -183,14 +183,14 @@ mod imp { QoSClass::Background => libc::qos_class_t::QOS_CLASS_BACKGROUND, }; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let code = unsafe { libc::pthread_set_qos_class_self_np(c, 0) }; if code == 0 { return; } - #[allow(unsafe_code)] + #[expect(unsafe_code)] let errno = unsafe { *libc::__error() }; match errno { @@ -223,10 +223,10 @@ mod imp { } pub(super) fn get_current_thread_qos_class() -> Option { - #[allow(unsafe_code)] + #[expect(unsafe_code)] let current_thread = unsafe { libc::pthread_self() }; let mut qos_class_raw = libc::qos_class_t::QOS_CLASS_UNSPECIFIED; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let code = unsafe { libc::pthread_get_qos_class_np(current_thread, &mut qos_class_raw, std::ptr::null_mut()) }; @@ -241,7 +241,7 @@ mod imp { // ones which we cannot handle anyway // // 0: https://github.com/apple-oss-distributions/libpthread/blob/67e155c94093be9a204b69637d198eceff2c7c46/src/qos.c#L171-L177 - #[allow(unsafe_code)] + #[expect(unsafe_code)] let errno = unsafe { *libc::__error() }; unreachable!("`pthread_get_qos_class_np` failed unexpectedly (os error {errno})"); } diff --git a/crates/ruff_server/src/session/capabilities.rs b/crates/ruff_server/src/session/capabilities.rs index 911457236d9de9..83ab1dba63b65a 100644 --- a/crates/ruff_server/src/session/capabilities.rs +++ b/crates/ruff_server/src/session/capabilities.rs @@ -2,7 +2,7 @@ use lsp_types::ClientCapabilities; use ruff_linter::display_settings; #[derive(Debug, Clone, PartialEq, Eq, Default)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub(crate) struct ResolvedClientCapabilities { pub(crate) code_action_deferred_edit_resolution: bool, pub(crate) apply_edit: bool, diff --git a/crates/ruff_server/src/session/index.rs b/crates/ruff_server/src/session/index.rs index 4f75a35fe6b0ce..868b91a0cb489b 100644 --- a/crates/ruff_server/src/session/index.rs +++ b/crates/ruff_server/src/session/index.rs @@ -517,7 +517,6 @@ impl DocumentController { } } - #[allow(dead_code)] pub(crate) fn as_text(&self) -> Option<&TextDocument> { match self { Self::Text(document) => Some(document), diff --git a/crates/ruff_server/src/session/settings.rs b/crates/ruff_server/src/session/settings.rs index b7b24a2ffabd34..d10b493737fcf3 100644 --- a/crates/ruff_server/src/session/settings.rs +++ b/crates/ruff_server/src/session/settings.rs @@ -19,7 +19,7 @@ pub(crate) type WorkspaceSettingsMap = FxHashMap; /// sends them. #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub(crate) struct ResolvedClientSettings { fix_all: bool, organize_imports: bool, diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/ruff_source_file/src/line_index.rs index 73807034c1f5eb..54d82b1ab94d2b 100644 --- a/crates/ruff_source_file/src/line_index.rs +++ b/crates/ruff_source_file/src/line_index.rs @@ -43,7 +43,7 @@ impl LineIndex { b'\r' if bytes.get(i + 1) == Some(&b'\n') => continue, b'\n' | b'\r' => { // SAFETY: Assertion above guarantees `i <= u32::MAX` - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] line_starts.push(TextSize::from(i as u32) + TextSize::from(1)); } _ => {} diff --git a/crates/ruff_source_file/src/newlines.rs b/crates/ruff_source_file/src/newlines.rs index deb6d8469031a7..1078750b35806b 100644 --- a/crates/ruff_source_file/src/newlines.rs +++ b/crates/ruff_source_file/src/newlines.rs @@ -322,7 +322,7 @@ impl LineEnding { } } - #[allow(clippy::len_without_is_empty)] + #[expect(clippy::len_without_is_empty)] pub const fn len(&self) -> usize { match self { LineEnding::Lf | LineEnding::Cr => 1, diff --git a/crates/ruff_text_size/src/serde_impls.rs b/crates/ruff_text_size/src/serde_impls.rs index b6885d674ac2c3..42e0017ec0278a 100644 --- a/crates/ruff_text_size/src/serde_impls.rs +++ b/crates/ruff_text_size/src/serde_impls.rs @@ -31,7 +31,7 @@ impl Serialize for TextRange { } impl<'de> Deserialize<'de> for TextRange { - #[allow(clippy::nonminimal_bool)] + #[expect(clippy::nonminimal_bool)] fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/crates/ruff_text_size/src/traits.rs b/crates/ruff_text_size/src/traits.rs index 0ea015135a3feb..69b0f647b5ddee 100644 --- a/crates/ruff_text_size/src/traits.rs +++ b/crates/ruff_text_size/src/traits.rs @@ -31,7 +31,7 @@ impl TextLen for &'_ String { impl Sealed for char {} impl TextLen for char { #[inline] - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] fn text_len(self) -> TextSize { (self.len_utf8() as u32).into() } diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index e311a0bf85fc24..38551a6bee8259 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -273,7 +273,6 @@ impl Configuration { project_root: project_root.to_path_buf(), }, - #[allow(deprecated)] linter: LinterSettings { rules, exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?, @@ -668,7 +667,7 @@ pub struct LintConfiguration { impl LintConfiguration { fn from_options(options: LintOptions, project_root: &Path) -> Result { - #[allow(deprecated)] + #[expect(deprecated)] let ignore = options .common .ignore @@ -676,7 +675,7 @@ impl LintConfiguration { .flatten() .chain(options.common.extend_ignore.into_iter().flatten()) .collect(); - #[allow(deprecated)] + #[expect(deprecated)] let unfixable = options .common .unfixable @@ -685,7 +684,7 @@ impl LintConfiguration { .chain(options.common.extend_unfixable.into_iter().flatten()) .collect(); - #[allow(deprecated)] + #[expect(deprecated)] let ignore_init_module_imports = { if options.common.ignore_init_module_imports.is_some() { warn_user_once!("The `ignore-init-module-imports` option is deprecated and will be removed in a future release. Ruff's handling of imports in `__init__.py` files has been improved (in preview) and unused imports will always be flagged."); @@ -1193,7 +1192,6 @@ pub struct FormatConfiguration { } impl FormatConfiguration { - #[allow(clippy::needless_pass_by_value)] pub fn from_options(options: FormatOptions, project_root: &Path) -> Result { Ok(Self { // `--extension` is a hidden command-line argument that isn't supported in configuration @@ -1231,7 +1229,6 @@ impl FormatConfiguration { } #[must_use] - #[allow(clippy::needless_pass_by_value)] pub fn combine(self, config: Self) -> Self { Self { exclude: self.exclude.or(config.exclude), @@ -1260,7 +1257,6 @@ pub struct AnalyzeConfiguration { } impl AnalyzeConfiguration { - #[allow(clippy::needless_pass_by_value)] pub fn from_options(options: AnalyzeOptions, project_root: &Path) -> Result { Ok(Self { exclude: options.exclude.map(|paths| { @@ -1287,7 +1283,6 @@ impl AnalyzeConfiguration { } #[must_use] - #[allow(clippy::needless_pass_by_value)] pub fn combine(self, config: Self) -> Self { Self { exclude: self.exclude.or(config.exclude), @@ -1339,7 +1334,7 @@ fn warn_about_deprecated_top_level_lint_options( top_level_options: &LintCommonOptions, path: Option<&Path>, ) { - #[allow(deprecated)] + #[expect(deprecated)] let LintCommonOptions { allowed_confusables, dummy_variable_rgx, @@ -1659,7 +1654,6 @@ mod tests { Rule::BlankLinesBeforeNestedDefinition, ]; - #[allow(clippy::needless_pass_by_value)] fn resolve_rules( selections: impl IntoIterator, preview: Option, diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 320c7a4187b0c2..eb4a49c93a072b 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -1150,14 +1150,14 @@ impl Flake8BanditOptions { extend_markup_names: self .extend_markup_names .or_else(|| { - #[allow(deprecated)] + #[expect(deprecated)] ruff_options.and_then(|options| options.extend_markup_names.clone()) }) .unwrap_or_default(), allowed_markup_calls: self .allowed_markup_calls .or_else(|| { - #[allow(deprecated)] + #[expect(deprecated)] ruff_options.and_then(|options| options.allowed_markup_calls.clone()) }) .unwrap_or_default(), @@ -1308,7 +1308,7 @@ pub struct Flake8BuiltinsOptions { impl Flake8BuiltinsOptions { pub fn into_settings(self) -> ruff_linter::rules::flake8_builtins::settings::Settings { - #[allow(deprecated)] + #[expect(deprecated)] ruff_linter::rules::flake8_builtins::settings::Settings { ignorelist: self .ignorelist @@ -3951,7 +3951,7 @@ impl From for LintOptions { } = value; LintOptions { - #[allow(deprecated)] + #[expect(deprecated)] common: LintCommonOptions { allowed_confusables, dummy_variable_rgx, diff --git a/crates/ruff_workspace/src/pyproject.rs b/crates/ruff_workspace/src/pyproject.rs index cbee2032f9fcbb..e1a021ff1ad5f2 100644 --- a/crates/ruff_workspace/src/pyproject.rs +++ b/crates/ruff_workspace/src/pyproject.rs @@ -399,7 +399,7 @@ strict-checking = false "#, )?; - #[allow(deprecated)] + #[expect(deprecated)] let expected = Flake8BuiltinsOptions { builtins_allowed_modules: Some(vec!["asyncio".to_string()]), allowed_modules: Some(vec!["sys".to_string()]), diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 9ca5ef91682052..1e875bc2a8dd82 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -19,7 +19,6 @@ use std::fmt; use std::path::{Path, PathBuf}; #[derive(Debug, CacheKey)] -#[allow(clippy::struct_excessive_bools)] pub struct Settings { #[cache_key(ignore)] pub cache_dir: PathBuf, diff --git a/crates/ty/build.rs b/crates/ty/build.rs index bcfcf6331d2589..a66db0e7ad826c 100644 --- a/crates/ty/build.rs +++ b/crates/ty/build.rs @@ -13,7 +13,6 @@ fn main() { commit_info(&workspace_root); - #[allow(clippy::disallowed_methods)] let target = std::env::var("TARGET").unwrap(); println!("cargo::rustc-env=RUST_HOST_TARGET={target}"); } diff --git a/crates/ty/src/logging.rs b/crates/ty/src/logging.rs index 010dfda494ab4c..5119af652bd040 100644 --- a/crates/ty/src/logging.rs +++ b/crates/ty/src/logging.rs @@ -147,7 +147,7 @@ pub(crate) fn setup_tracing(level: VerbosityLevel) -> anyhow::Result() -> ( Option>>, Option>>, diff --git a/crates/ty/src/main.rs b/crates/ty/src/main.rs index 18a4deebae003f..20e132fd876ab1 100644 --- a/crates/ty/src/main.rs +++ b/crates/ty/src/main.rs @@ -24,7 +24,6 @@ mod logging; mod python_version; mod version; -#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)] pub fn main() -> ExitStatus { run().unwrap_or_else(|error| { use std::io::Write; diff --git a/crates/ty_ide/src/db.rs b/crates/ty_ide/src/db.rs index 39ff4b0c043be6..7f97c68a042995 100644 --- a/crates/ty_ide/src/db.rs +++ b/crates/ty_ide/src/db.rs @@ -27,7 +27,7 @@ pub(crate) mod tests { rule_selection: Arc, } - #[allow(dead_code)] + #[expect(dead_code)] impl TestDb { pub(crate) fn new() -> Self { Self { diff --git a/crates/ty_project/src/metadata/value.rs b/crates/ty_project/src/metadata/value.rs index e4defe98ed23e0..b2bb8eb4f18d78 100644 --- a/crates/ty_project/src/metadata/value.rs +++ b/crates/ty_project/src/metadata/value.rs @@ -155,7 +155,7 @@ where } // The type already has an `iter` method thanks to `Deref`. -#[allow(clippy::into_iter_without_iter)] +#[expect(clippy::into_iter_without_iter)] impl<'a, T> IntoIterator for &'a RangedValue where &'a T: IntoIterator, @@ -168,7 +168,7 @@ where } // The type already has a `into_iter_mut` method thanks to `DerefMut`. -#[allow(clippy::into_iter_without_iter)] +#[expect(clippy::into_iter_without_iter)] impl<'a, T> IntoIterator for &'a mut RangedValue where &'a mut T: IntoIterator, diff --git a/crates/ty_project/src/watch/watcher.rs b/crates/ty_project/src/watch/watcher.rs index 8cee2dd7b55782..28fed1cb8dbd1c 100644 --- a/crates/ty_project/src/watch/watcher.rs +++ b/crates/ty_project/src/watch/watcher.rs @@ -186,7 +186,7 @@ impl Debouncer { } } - #[allow(clippy::unused_self, clippy::needless_pass_by_value)] + #[expect(clippy::unused_self, clippy::needless_pass_by_value)] fn add_error(&mut self, error: notify::Error) { // Micha: I skimmed through some of notify's source code and it seems the most common errors // are IO errors. All other errors should really only happen when adding or removing a watched folders. diff --git a/crates/ty_project/tests/check.rs b/crates/ty_project/tests/check.rs index 8a56d3099a6c31..0d6bbfc2ddef13 100644 --- a/crates/ty_project/tests/check.rs +++ b/crates/ty_project/tests/check.rs @@ -76,7 +76,7 @@ fn typeshed_no_panic() -> anyhow::Result<()> { )) } -#[allow(clippy::print_stdout)] +#[expect(clippy::print_stdout)] fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { let root = SystemPathBuf::from("/src"); diff --git a/crates/ty_python_semantic/src/ast_node_ref.rs b/crates/ty_python_semantic/src/ast_node_ref.rs index 692191b5b715bb..994c7b2cf52e36 100644 --- a/crates/ty_python_semantic/src/ast_node_ref.rs +++ b/crates/ty_python_semantic/src/ast_node_ref.rs @@ -43,7 +43,7 @@ pub struct AstNodeRef { node: std::ptr::NonNull, } -#[allow(unsafe_code)] +#[expect(unsafe_code)] impl AstNodeRef { /// Creates a new `AstNodeRef` that references `node`. The `parsed` is the [`ParsedModule`] to /// which the `AstNodeRef` belongs. @@ -112,7 +112,7 @@ where } } -#[allow(unsafe_code)] +#[expect(unsafe_code)] unsafe impl salsa::Update for AstNodeRef { unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool { let old_ref = &mut (*old_pointer); @@ -126,9 +126,9 @@ unsafe impl salsa::Update for AstNodeRef { } } -#[allow(unsafe_code)] +#[expect(unsafe_code)] unsafe impl Send for AstNodeRef where T: Send {} -#[allow(unsafe_code)] +#[expect(unsafe_code)] unsafe impl Sync for AstNodeRef where T: Sync {} #[cfg(test)] @@ -139,7 +139,7 @@ mod tests { use ruff_python_parser::parse_unchecked_source; #[test] - #[allow(unsafe_code)] + #[expect(unsafe_code)] fn equality() { let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python); let parsed = ParsedModule::new(parsed_raw.clone()); @@ -167,7 +167,7 @@ mod tests { assert_ne!(node1, other_node); } - #[allow(unsafe_code)] + #[expect(unsafe_code)] #[test] fn inequality() { let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python); @@ -186,7 +186,7 @@ mod tests { } #[test] - #[allow(unsafe_code)] + #[expect(unsafe_code)] fn debug() { let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python); let parsed = ParsedModule::new(parsed_raw); diff --git a/crates/ty_python_semantic/src/lint.rs b/crates/ty_python_semantic/src/lint.rs index 17a778696a68fe..e30231662b23d8 100644 --- a/crates/ty_python_semantic/src/lint.rs +++ b/crates/ty_python_semantic/src/lint.rs @@ -244,7 +244,7 @@ macro_rules! declare_lint { } ) => { $( #[doc = $doc] )+ - #[allow(clippy::needless_update)] + #[expect(clippy::needless_update)] $vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata { name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)), summary: $summary, diff --git a/crates/ty_python_semantic/src/list.rs b/crates/ty_python_semantic/src/list.rs index fe6e55b2f0c26b..b6cd82686cf17f 100644 --- a/crates/ty_python_semantic/src/list.rs +++ b/crates/ty_python_semantic/src/list.rs @@ -180,7 +180,7 @@ impl ListBuilder { /// as our return type, since we never return `None`. However, for consistency with our other /// methods, we always use `Option` as the return type for any method that can return a /// list. - #[allow(clippy::unnecessary_wraps)] + #[expect(clippy::unnecessary_wraps)] fn add_cell(&mut self, rest: Option, key: K, value: V) -> Option { Some(self.storage.cells.push(ListCell { rest, key, value })) } @@ -319,7 +319,7 @@ impl ListBuilder { /// Returns the intersection of two lists. The result will contain an entry for any key that /// appears in both lists. The corresponding values will be combined using the `combine` /// function that you provide. - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub(crate) fn intersect_with( &mut self, a: List, @@ -372,7 +372,7 @@ impl ListBuilder { impl ListStorage { /// Iterates through the elements in a set _in reverse order_. - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub(crate) fn iter_set_reverse(&self, set: List) -> ListSetReverseIterator { ListSetReverseIterator { storage: self, @@ -513,7 +513,7 @@ mod tests { impl ListStorage { /// Iterates through the entries in a list _in reverse order by key_. - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] pub(crate) fn iter_reverse(&self, list: List) -> ListReverseIterator<'_, K, V> { ListReverseIterator { storage: self, @@ -649,7 +649,7 @@ mod property_tests { #[quickcheck_macros::quickcheck] #[ignore] - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn roundtrip_set_from_vec(elements: Vec) -> bool { let mut builder = ListBuilder::default(); let set = builder.set_from_elements(&elements); @@ -660,7 +660,7 @@ mod property_tests { #[quickcheck_macros::quickcheck] #[ignore] - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn roundtrip_set_intersection(a_elements: Vec, b_elements: Vec) -> bool { let mut builder = ListBuilder::default(); let a = builder.set_from_elements(&a_elements); @@ -712,7 +712,7 @@ mod property_tests { #[quickcheck_macros::quickcheck] #[ignore] - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn roundtrip_list_from_vec(pairs: Vec<(u16, u16)>) -> bool { let mut builder = ListBuilder::default(); let list = builder.set_from_pairs(&pairs); @@ -723,7 +723,7 @@ mod property_tests { #[quickcheck_macros::quickcheck] #[ignore] - #[allow(clippy::needless_pass_by_value)] + #[expect(clippy::needless_pass_by_value)] fn roundtrip_list_intersection( a_elements: Vec<(u16, u16)>, b_elements: Vec<(u16, u16)>, diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index 11137f3eebd2e1..baff5384f9e0b7 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -257,7 +257,7 @@ impl<'db> SemanticIndex<'db> { } /// Returns the parent scope of `scope_id`. - #[allow(unused)] + #[expect(unused)] #[track_caller] pub(crate) fn parent_scope(&self, scope_id: FileScopeId) -> Option<&Scope> { Some(&self.scopes[self.parent_scope_id(scope_id)?]) diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 6426e5cc0f89e7..27ae67b297e4a3 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -239,7 +239,7 @@ impl<'db> SemanticIndexBuilder<'db> { let children_start = self.scopes.next_index() + 1; // SAFETY: `node` is guaranteed to be a child of `self.module` - #[allow(unsafe_code)] + #[expect(unsafe_code)] let node_with_kind = unsafe { node.to_kind(self.module.clone()) }; let scope = Scope::new( @@ -457,7 +457,7 @@ impl<'db> SemanticIndexBuilder<'db> { definition_node: impl Into>, ) -> (Definition<'db>, usize) { let definition_node: DefinitionNodeRef<'_> = definition_node.into(); - #[allow(unsafe_code)] + #[expect(unsafe_code)] // SAFETY: `definition_node` is guaranteed to be a child of `self.module` let kind = unsafe { definition_node.into_owned(self.module.clone()) }; let category = kind.category(self.source_type.is_stub()); @@ -801,11 +801,11 @@ impl<'db> SemanticIndexBuilder<'db> { self.db, self.file, self.current_scope(), - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe { AstNodeRef::new(self.module.clone(), expression_node) }, - #[allow(unsafe_code)] + #[expect(unsafe_code)] assigned_to .map(|assigned_to| unsafe { AstNodeRef::new(self.module.clone(), assigned_to) }), expression_kind, @@ -1009,7 +1009,7 @@ impl<'db> SemanticIndexBuilder<'db> { value_file_scope, self.current_scope(), // SAFETY: `target` belongs to the `self.module` tree - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe { AstNodeRef::new(self.module.clone(), target) }, @@ -2188,7 +2188,7 @@ where match self.current_assignment() { Some(CurrentAssignment::Assign { node, unpack, .. }) => { // SAFETY: `value` and `expr` belong to the `self.module` tree - #[allow(unsafe_code)] + #[expect(unsafe_code)] let assignment = AssignmentDefinitionKind::new( TargetKind::from(unpack), unsafe { AstNodeRef::new(self.module.clone(), &node.value) }, @@ -2203,7 +2203,7 @@ where Some(CurrentAssignment::AnnAssign(ann_assign)) => { self.add_standalone_type_expression(&ann_assign.annotation); // SAFETY: `annotation`, `value` and `expr` belong to the `self.module` tree - #[allow(unsafe_code)] + #[expect(unsafe_code)] let assignment = AnnotatedAssignmentDefinitionKind::new( unsafe { AstNodeRef::new(self.module.clone(), &ann_assign.annotation) @@ -2221,7 +2221,7 @@ where } Some(CurrentAssignment::For { node, unpack, .. }) => { // // SAFETY: `iter` and `expr` belong to the `self.module` tree - #[allow(unsafe_code)] + #[expect(unsafe_code)] let assignment = ForStmtDefinitionKind::new( TargetKind::from(unpack), unsafe { AstNodeRef::new(self.module.clone(), &node.iter) }, @@ -2241,7 +2241,7 @@ where .. }) => { // SAFETY: `context_expr` and `expr` belong to the `self.module` tree - #[allow(unsafe_code)] + #[expect(unsafe_code)] let assignment = WithItemDefinitionKind::new( TargetKind::from(unpack), unsafe { AstNodeRef::new(self.module.clone(), &item.context_expr) }, @@ -2260,7 +2260,7 @@ where first, }) => { // SAFETY: `iter` and `expr` belong to the `self.module` tree - #[allow(unsafe_code)] + #[expect(unsafe_code)] let assignment = ComprehensionDefinitionKind { target_kind: TargetKind::from(unpack), iterable: unsafe { diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 145e4b205ad446..e0a1a6c571f323 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -300,7 +300,7 @@ pub(crate) struct MatchPatternDefinitionNodeRef<'a> { } impl<'db> DefinitionNodeRef<'db> { - #[allow(unsafe_code)] + #[expect(unsafe_code)] pub(super) unsafe fn into_owned(self, parsed: ParsedModule) -> DefinitionKind<'db> { match self { DefinitionNodeRef::Import(ImportDefinitionNodeRef { diff --git a/crates/ty_python_semantic/src/semantic_index/symbol.rs b/crates/ty_python_semantic/src/semantic_index/symbol.rs index ec9ef3886eff3c..d8f8fd026ce373 100644 --- a/crates/ty_python_semantic/src/semantic_index/symbol.rs +++ b/crates/ty_python_semantic/src/semantic_index/symbol.rs @@ -287,7 +287,7 @@ impl SymbolTable { &self.symbols[symbol_id.into()] } - #[allow(unused)] + #[expect(unused)] pub(crate) fn symbol_ids(&self) -> impl Iterator { self.symbols.indices() } @@ -420,7 +420,7 @@ impl NodeWithScopeRef<'_> { /// /// # Safety /// The node wrapped by `self` must be a child of `module`. - #[allow(unsafe_code)] + #[expect(unsafe_code)] pub(super) unsafe fn to_kind(self, module: ParsedModule) -> NodeWithScopeKind { match self { NodeWithScopeRef::Module => NodeWithScopeKind::Module, diff --git a/crates/ty_python_semantic/src/semantic_index/visibility_constraints.rs b/crates/ty_python_semantic/src/semantic_index/visibility_constraints.rs index 84337680ab4be4..98175af6bfa9fa 100644 --- a/crates/ty_python_semantic/src/semantic_index/visibility_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/visibility_constraints.rs @@ -267,7 +267,7 @@ impl Idx for ScopedVisibilityConstraintId { #[inline] fn new(value: usize) -> Self { assert!(value <= (SMALLEST_TERMINAL.0 as usize)); - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] Self(value as u32) } diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cd4d4b0fae89af..1e4ca637d905c3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -5617,7 +5617,6 @@ impl<'db> TypeVarInstance<'db> { matches!(self.kind(db), TypeVarKind::Legacy) } - #[allow(unused)] pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { Some(ty) @@ -5626,7 +5625,6 @@ impl<'db> TypeVarInstance<'db> { } } - #[allow(unused)] pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&'db [Type<'db>]> { if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) { Some(tuple.elements(db)) @@ -5856,7 +5854,7 @@ impl<'db> IterationError<'db> { /// Emit a diagnostic that is certain that `iterable_type` is not iterable. /// /// `because` should explain why `iterable_type` is not iterable. - #[allow(clippy::wrong_self_convention)] + #[expect(clippy::wrong_self_convention)] fn is_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { let mut diag = self.builder.into_diagnostic(format_args!( "Object of type `{iterable_type}` is not iterable", @@ -6787,7 +6785,7 @@ impl<'db> FunctionType<'db> { /// 3. third `foo` definition, it would contain both overloads and the implementation which is /// itself fn to_overloaded(self, db: &'db dyn Db) -> Option<&'db OverloadedFunction<'db>> { - #[allow(clippy::ref_option)] // TODO: Remove once salsa supports deref (https://github.com/salsa-rs/salsa/pull/772) + #[allow(clippy::ref_option)] #[salsa::tracked(return_ref)] fn to_overloaded_impl<'db>( db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 315809a08616de..039e818191a87b 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -70,7 +70,7 @@ fn try_mro_cycle_recover<'db>( salsa::CycleRecoveryAction::Iterate } -#[allow(clippy::unnecessary_wraps)] +#[expect(clippy::unnecessary_wraps)] fn try_mro_cycle_initial<'db>( db: &'db dyn Db, self_: ClassLiteral<'db>, @@ -82,7 +82,7 @@ fn try_mro_cycle_initial<'db>( )) } -#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)] +#[expect(clippy::ref_option, clippy::trivially_copy_pass_by_ref)] fn inheritance_cycle_recover<'db>( _db: &'db dyn Db, _value: &Option, diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 0734bcf7f82347..6698ffa23ab9fe 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -4078,7 +4078,7 @@ impl<'db> TypeInferenceBuilder<'db> { } } - #[allow(clippy::unused_self)] + #[expect(clippy::unused_self)] fn infer_boolean_literal_expression(&mut self, literal: &ast::ExprBooleanLiteral) -> Type<'db> { let ast::ExprBooleanLiteral { range: _, value } = literal; diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index a63d9d55620488..71ed52f6650f0b 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -127,7 +127,7 @@ fn constraints_for_expression_cycle_initial<'db>( None } -#[allow(clippy::ref_option)] +#[expect(clippy::ref_option)] fn negative_constraints_for_expression_cycle_recover<'db>( _db: &'db dyn Db, _value: &Option>, diff --git a/crates/ty_python_semantic/src/util/subscript.rs b/crates/ty_python_semantic/src/util/subscript.rs index 6dca232046e3e6..bf0cdaec84c9bf 100644 --- a/crates/ty_python_semantic/src/util/subscript.rs +++ b/crates/ty_python_semantic/src/util/subscript.rs @@ -127,7 +127,7 @@ impl PySlice for [T] { if len == 0 { // The iterator needs to have the same type as the step>0 case below, // so we need to use `.skip(0)`. - #[allow(clippy::iter_skip_zero)] + #[expect(clippy::iter_skip_zero)] return Ok(Either::Left(self.iter().skip(0).take(0).step_by(1))); } @@ -192,7 +192,7 @@ impl PySlice for [T] { } #[cfg(test)] -#[allow(clippy::redundant_clone)] +#[expect(clippy::redundant_clone)] mod tests { use crate::util::subscript::{OutOfBoundsError, StepSizeZeroError}; diff --git a/crates/ty_python_semantic/tests/mdtest.rs b/crates/ty_python_semantic/tests/mdtest.rs index 0c651167af3f2c..f989465bfa2029 100644 --- a/crates/ty_python_semantic/tests/mdtest.rs +++ b/crates/ty_python_semantic/tests/mdtest.rs @@ -7,7 +7,7 @@ use ty_test::OutputFormat; dir: "$CARGO_MANIFEST_DIR/resources/mdtest", glob: "**/*.md" )] -#[allow(clippy::needless_pass_by_value)] +#[expect(clippy::needless_pass_by_value)] fn mdtest(fixture: Fixture<&str>) { let absolute_fixture_path = Utf8Path::new(fixture.path()); let crate_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")); diff --git a/crates/ty_server/src/document/notebook.rs b/crates/ty_server/src/document/notebook.rs index b01cc5dac0c3fa..813456bb982b89 100644 --- a/crates/ty_server/src/document/notebook.rs +++ b/crates/ty_server/src/document/notebook.rs @@ -248,7 +248,7 @@ mod tests { use super::NotebookDocument; enum TestCellContent { - #[allow(dead_code)] + #[expect(dead_code)] Markup(String), Code(String), } diff --git a/crates/ty_server/src/logging.rs b/crates/ty_server/src/logging.rs index a322ef249975a1..7b2e3169b22c98 100644 --- a/crates/ty_server/src/logging.rs +++ b/crates/ty_server/src/logging.rs @@ -34,7 +34,7 @@ pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&std::path::Pat .append(true) .open(&path) .map_err(|err| { - #[allow(clippy::print_stderr)] + #[expect(clippy::print_stderr)] { eprintln!( "Failed to open file at {} for logging: {err}", diff --git a/crates/ty_server/src/server.rs b/crates/ty_server/src/server.rs index 3556fec9d42c50..7f88d9418f7469 100644 --- a/crates/ty_server/src/server.rs +++ b/crates/ty_server/src/server.rs @@ -2,7 +2,7 @@ use std::num::NonZeroUsize; // The new PanicInfoHook name requires MSRV >= 1.82 -#[allow(deprecated)] +#[expect(deprecated)] use std::panic::PanicInfo; use lsp_server::Message; @@ -114,7 +114,7 @@ impl Server { pub(crate) fn run(self) -> crate::Result<()> { // The new PanicInfoHook name requires MSRV >= 1.82 - #[allow(deprecated)] + #[expect(deprecated)] type PanicHook = Box) + 'static + Sync + Send>; struct RestorePanicHook { hook: Option, @@ -168,7 +168,6 @@ impl Server { .join() } - #[allow(clippy::needless_pass_by_value)] // this is because we aren't using `next_request_id` yet. fn event_loop( connection: &Connection, _client_capabilities: &ClientCapabilities, diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index 5bfc835cc55835..6b4794e5823f6a 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -144,7 +144,7 @@ fn local_notification_task<'a, N: traits::SyncNotificationHandler>( })) } -#[allow(dead_code)] +#[expect(dead_code)] fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationHandler>( req: server::Notification, schedule: BackgroundSchedule, diff --git a/crates/ty_server/src/server/client.rs b/crates/ty_server/src/server/client.rs index c5a502e213c5e6..953aba3a8cf60e 100644 --- a/crates/ty_server/src/server/client.rs +++ b/crates/ty_server/src/server/client.rs @@ -48,7 +48,7 @@ impl Client<'_> { } } -#[allow(dead_code)] // we'll need to use `Notifier` in the future +#[expect(dead_code)] // we'll need to use `Notifier` in the future impl Notifier { pub(crate) fn notify(&self, params: N::Params) -> crate::Result<()> where diff --git a/crates/ty_server/src/server/schedule/thread/pool.rs b/crates/ty_server/src/server/schedule/thread/pool.rs index ea654a11d2af46..9ed3dad2a66070 100644 --- a/crates/ty_server/src/server/schedule/thread/pool.rs +++ b/crates/ty_server/src/server/schedule/thread/pool.rs @@ -106,7 +106,7 @@ impl Pool { self.job_sender.send(job).unwrap(); } - #[allow(dead_code)] + #[expect(dead_code)] pub(super) fn len(&self) -> usize { self.extant_tasks.load(Ordering::SeqCst) } diff --git a/crates/ty_server/src/server/schedule/thread/priority.rs b/crates/ty_server/src/server/schedule/thread/priority.rs index e6a555242fcb72..65b6e3d984908b 100644 --- a/crates/ty_server/src/server/schedule/thread/priority.rs +++ b/crates/ty_server/src/server/schedule/thread/priority.rs @@ -183,14 +183,14 @@ mod imp { QoSClass::Background => libc::qos_class_t::QOS_CLASS_BACKGROUND, }; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let code = unsafe { libc::pthread_set_qos_class_self_np(c, 0) }; if code == 0 { return; } - #[allow(unsafe_code)] + #[expect(unsafe_code)] let errno = unsafe { *libc::__error() }; match errno { @@ -223,10 +223,10 @@ mod imp { } pub(super) fn get_current_thread_qos_class() -> Option { - #[allow(unsafe_code)] + #[expect(unsafe_code)] let current_thread = unsafe { libc::pthread_self() }; let mut qos_class_raw = libc::qos_class_t::QOS_CLASS_UNSPECIFIED; - #[allow(unsafe_code)] + #[expect(unsafe_code)] let code = unsafe { libc::pthread_get_qos_class_np(current_thread, &mut qos_class_raw, std::ptr::null_mut()) }; @@ -241,7 +241,7 @@ mod imp { // ones which we cannot handle anyway // // 0: https://github.com/apple-oss-distributions/libpthread/blob/67e155c94093be9a204b69637d198eceff2c7c46/src/qos.c#L171-L177 - #[allow(unsafe_code)] + #[expect(unsafe_code)] let errno = unsafe { *libc::__error() }; unreachable!("`pthread_get_qos_class_np` failed unexpectedly (os error {errno})"); } diff --git a/crates/ty_server/src/session/capabilities.rs b/crates/ty_server/src/session/capabilities.rs index 5ba4c3e0a59638..6b1e51c53ad3eb 100644 --- a/crates/ty_server/src/session/capabilities.rs +++ b/crates/ty_server/src/session/capabilities.rs @@ -1,7 +1,7 @@ use lsp_types::{ClientCapabilities, MarkupKind}; #[derive(Debug, Clone, PartialEq, Eq, Default)] -#[allow(clippy::struct_excessive_bools)] +#[expect(clippy::struct_excessive_bools)] pub(crate) struct ResolvedClientCapabilities { pub(crate) code_action_deferred_edit_resolution: bool, pub(crate) apply_edit: bool, diff --git a/crates/ty_server/src/session/index.rs b/crates/ty_server/src/session/index.rs index f5e12e768d1a4c..3e7110434513c0 100644 --- a/crates/ty_server/src/session/index.rs +++ b/crates/ty_server/src/session/index.rs @@ -230,7 +230,6 @@ impl DocumentController { } } - #[allow(dead_code)] pub(crate) fn as_text(&self) -> Option<&TextDocument> { match self { Self::Text(document) => Some(document), diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs index 918e5c81bc03ab..088f124c51a421 100644 --- a/crates/ty_server/src/system.rs +++ b/crates/ty_server/src/system.rs @@ -258,6 +258,6 @@ fn virtual_path_not_found(path: impl Display) -> std::io::Error { fn document_revision(document: &DocumentQuery) -> FileRevision { // The file revision is just an opaque number which doesn't have any significant meaning other // than that the file has changed if the revisions are different. - #[allow(clippy::cast_sign_loss)] + #[expect(clippy::cast_sign_loss)] FileRevision::new(document.version() as u128) } diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index b6dab4fb024e12..d6f26b579b7bb3 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -33,7 +33,7 @@ const MDTEST_TEST_FILTER: &str = "MDTEST_TEST_FILTER"; /// Run `path` as a markdown test suite with given `title`. /// /// Panic on test failure, and print failure details. -#[allow(clippy::print_stdout)] +#[expect(clippy::print_stdout)] pub fn run( absolute_fixture_path: &Utf8Path, relative_fixture_path: &Utf8Path, diff --git a/scripts/generate_builtin_modules.py b/scripts/generate_builtin_modules.py index b1a193773153ac..8f1171498fccea 100644 --- a/scripts/generate_builtin_modules.py +++ b/scripts/generate_builtin_modules.py @@ -70,7 +70,7 @@ def generate_module( /// modules. /// /// [builtin module]: https://docs.python.org/3/library/sys.html#sys.builtin_module_names - #[allow(clippy::unnested_or_patterns)] + #[expect(clippy::unnested_or_patterns)] pub fn is_builtin_module(minor_version: u8, module: &str) -> bool { matches!((minor_version, module), """, From fe4051b2e6bf74a33f67f60fdbd775a6568e96a3 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Sat, 3 May 2025 18:19:19 -0400 Subject: [PATCH 0236/1161] Fix missing `combine` call for `lint.typing-extensions` setting (#17823) ## Summary Fixes #17821. ## Test Plan New CLI test. This might be overkill for such a simple fix, but it made me feel better to add a test. --- crates/ruff/tests/lint.rs | 31 ++++++++++++++++++++++ crates/ruff_workspace/src/configuration.rs | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index cb701bf1402b81..6e53c124789d82 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -5654,3 +5654,34 @@ fn semantic_syntax_errors() -> Result<()> { Ok(()) } + +/// Regression test for . +/// +/// `lint.typing-extensions = false` with Python 3.9 should disable the PYI019 lint because it would +/// try to import `Self` from `typing_extensions` +#[test] +fn combine_typing_extensions_config() { + let contents = " +from typing import TypeVar +T = TypeVar('T') +class Foo: + def f(self: T) -> T: ... +"; + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--config", "lint.typing-extensions = false"]) + .arg("--select=PYI019") + .arg("--target-version=py39") + .arg("-") + .pass_stdin(contents), + @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + " + ); +} diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 38551a6bee8259..072ecbcee5c96a 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -1172,7 +1172,7 @@ impl LintConfiguration { pylint: self.pylint.combine(config.pylint), pyupgrade: self.pyupgrade.combine(config.pyupgrade), ruff: self.ruff.combine(config.ruff), - typing_extensions: self.typing_extensions, + typing_extensions: self.typing_extensions.or(config.typing_extensions), } } } From 68e32c103fc25c59344417a46285548dbba4c79c Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sun, 4 May 2025 15:42:10 +0200 Subject: [PATCH 0237/1161] Ignore PRs labeled with `ty` for Ruff changelog (#17831) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cf19e1306b12ee..a58b912265ec0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,7 +110,7 @@ force-exclude = ''' major_labels = [] # Ruff never uses the major version number minor_labels = ["breaking"] # Bump the minor version on breaking changes -changelog_ignore_labels = ["internal", "ci", "red-knot", "testing"] +changelog_ignore_labels = ["internal", "ci", "testing", "ty"] changelog_sections.breaking = "Breaking changes" changelog_sections.preview = "Preview features" From e95130ad8062f64a11247d51a72f62bb735ddd19 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sun, 4 May 2025 16:27:15 +0200 Subject: [PATCH 0238/1161] Introduce `TY_MAX_PARALLELISM` environment variable (#17830) --- crates/ruff_db/src/lib.rs | 32 +++++++++++++++++++++---- crates/ruff_db/src/system/os.rs | 26 +++++++++----------- crates/ty/docs/tracing.md | 42 +++++++++++++++++++++------------ crates/ty/src/main.rs | 12 ++++++++++ 4 files changed, 77 insertions(+), 35 deletions(-) diff --git a/crates/ruff_db/src/lib.rs b/crates/ruff_db/src/lib.rs index 85beefc1d89353..bb30c63371669e 100644 --- a/crates/ruff_db/src/lib.rs +++ b/crates/ruff_db/src/lib.rs @@ -1,11 +1,10 @@ -use std::hash::BuildHasherDefault; - -use ruff_python_ast::PythonVersion; -use rustc_hash::FxHasher; - use crate::files::Files; use crate::system::System; use crate::vendored::VendoredFileSystem; +use ruff_python_ast::PythonVersion; +use rustc_hash::FxHasher; +use std::hash::BuildHasherDefault; +use std::num::NonZeroUsize; pub mod diagnostic; pub mod display; @@ -37,6 +36,29 @@ pub trait Upcast { fn upcast_mut(&mut self) -> &mut T; } +/// Returns the maximum number of tasks that ty is allowed +/// to process in parallel. +/// +/// Returns [`std::thread::available_parallelism`], unless the environment +/// variable `TY_MAX_PARALLELISM` or `RAYON_NUM_THREADS` is set. `TY_MAX_PARALLELISM` takes +/// precedence over `RAYON_NUM_THREADS`. +/// +/// Falls back to `1` if `available_parallelism` is not available. +/// +/// Setting `TY_MAX_PARALLELISM` to `2` only restricts the number of threads that ty spawns +/// to process work in parallel. For example, to index a directory or checking the files of a project. +/// ty can still spawn more threads for other tasks, e.g. to wait for a Ctrl+C signal or +/// watching the files for changes. +pub fn max_parallelism() -> NonZeroUsize { + std::env::var("TY_MAX_PARALLELISM") + .or_else(|_| std::env::var("RAYON_NUM_THREADS")) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or_else(|| { + std::thread::available_parallelism().unwrap_or_else(|_| NonZeroUsize::new(1).unwrap()) + }) +} + #[cfg(test)] mod tests { use std::sync::Arc; diff --git a/crates/ruff_db/src/system/os.rs b/crates/ruff_db/src/system/os.rs index ecadd077fb8abd..0145a4e8780bf8 100644 --- a/crates/ruff_db/src/system/os.rs +++ b/crates/ruff_db/src/system/os.rs @@ -1,20 +1,20 @@ +use super::walk_directory::{ + self, DirectoryWalker, WalkDirectoryBuilder, WalkDirectoryConfiguration, + WalkDirectoryVisitorBuilder, WalkState, +}; +use crate::max_parallelism; +use crate::system::{ + CaseSensitivity, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result, System, + SystemPath, SystemPathBuf, SystemVirtualPath, WritableSystem, +}; use filetime::FileTime; use ruff_notebook::{Notebook, NotebookError}; use rustc_hash::FxHashSet; +use std::num::NonZeroUsize; use std::panic::RefUnwindSafe; use std::sync::Arc; use std::{any::Any, path::PathBuf}; -use crate::system::{ - CaseSensitivity, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result, System, - SystemPath, SystemPathBuf, SystemVirtualPath, WritableSystem, -}; - -use super::walk_directory::{ - self, DirectoryWalker, WalkDirectoryBuilder, WalkDirectoryConfiguration, - WalkDirectoryVisitorBuilder, WalkState, -}; - /// A system implementation that uses the OS file system. #[derive(Debug, Clone)] pub struct OsSystem { @@ -426,11 +426,7 @@ impl DirectoryWalker for OsDirectoryWalker { builder.add(additional_path.as_std_path()); } - builder.threads( - std::thread::available_parallelism() - .map_or(1, std::num::NonZeroUsize::get) - .min(12), - ); + builder.threads(max_parallelism().min(NonZeroUsize::new(12).unwrap()).get()); builder.build_parallel().run(|| { let mut visitor = visitor_builder.build(); diff --git a/crates/ty/docs/tracing.md b/crates/ty/docs/tracing.md index 7bbf7f5806af0b..75536bcf85c4b0 100644 --- a/crates/ty/docs/tracing.md +++ b/crates/ty/docs/tracing.md @@ -1,6 +1,7 @@ # Tracing -Traces are a useful tool to narrow down the location of a bug or, at least, to understand why the compiler is doing a particular thing. +Traces are a useful tool to narrow down the location of a bug or, at least, to understand why the compiler is doing a +particular thing. Note, tracing messages with severity `debug` or greater are user-facing. They should be phrased accordingly. Tracing spans are only shown when using `-vvv`. @@ -9,20 +10,28 @@ Tracing spans are only shown when using `-vvv`. The CLI supports different verbosity levels. - default: Only show errors and warnings. -- `-v` activates `info!`: Show generally useful information such as paths of configuration files, detected platform, etc., but it's not a lot of messages, it's something you'll activate in CI by default. cargo build e.g. shows you which packages are fresh. -- `-vv` activates `debug!` and timestamps: This should be enough information to get to the bottom of bug reports. When you're processing many packages or files, you'll get pages and pages of output, but each line is link to a specific action or state change. -- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you almost certainly want to use `TY_LOG` to filter it down to the area your investigating. - -## Better logging with `TY_LOG` and `RAYON_NUM_THREADS` +- `-v` activates `info!`: Show generally useful information such as paths of configuration files, detected platform, + etc., but it's not a lot of messages, it's something you'll activate in CI by default. cargo build e.g. shows you + which packages are fresh. +- `-vv` activates `debug!` and timestamps: This should be enough information to get to the bottom of bug reports. When + you're processing many packages or files, you'll get pages and pages of output, but each line is link to a specific + action or state change. +- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. + Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you + almost certainly want to use `TY_LOG` to filter it down to the area your investigating. + +## Better logging with `TY_LOG` and `TY_MAX_PARALLELISM` By default, the CLI shows messages from the `ruff` and `ty` crates. Tracing messages from other crates are not shown. The `TY_LOG` environment variable allows you to customize which messages are shown by specifying one -or more [filter directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives). +or +more [filter directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives). -The `RAYON_NUM_THREADS` environment variable, meanwhile, can be used to control the level of concurrency ty uses. +The `TY_MAX_PARALLELISM` environment variable, meanwhile, can be used to control the level of parallelism ty uses. By default, ty will attempt to parallelize its work so that multiple files are checked simultaneously, -but this can result in a confused logging output where messages from different threads are intertwined. -To switch off concurrency entirely and have more readable logs, use `RAYON_NUM_THREADS=1`. +but this can result in a confused logging output where messages from different threads are intertwined and non +determinism. +To switch off parallelism entirely and have more readable logs, use `TY_MAX_PARALLELSIM=1` (or `RAYON_NUM_THREADS=1`). ### Examples @@ -79,22 +88,24 @@ query to return the failure as part of the query's result or use a Salsa accumul ## Tracing in tests -You can use `ruff_db::testing::setup_logging` or `ruff_db::testing::setup_logging_with_filter` to set up logging in tests. +You can use `ruff_db::testing::setup_logging` or `ruff_db::testing::setup_logging_with_filter` to set up logging in +tests. ```rust use ruff_db::testing::setup_logging; #[test] fn test() { - let _logging = setup_logging(); + let _logging = setup_logging(); - tracing::info!("This message will be printed to stderr"); + tracing::info!("This message will be printed to stderr"); } ``` Note: Most test runners capture stderr and only show its output when a test fails. -Note also that `setup_logging` only sets up logging for the current thread because [`set_global_default`](https://docs.rs/tracing/latest/tracing/subscriber/fn.set_global_default.html) can only be +Note also that `setup_logging` only sets up logging for the current thread because +[`set_global_default`](https://docs.rs/tracing/latest/tracing/subscriber/fn.set_global_default.html) can only be called **once**. ## Release builds @@ -103,7 +114,8 @@ called **once**. ## Profiling -ty generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable `TY_LOG_PROFILE` to `1` or `true`. +ty generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable +`TY_LOG_PROFILE` to `1` or `true`. ```bash TY_LOG_PROFILE=1 ty -- --current-directory=../test -vvv diff --git a/crates/ty/src/main.rs b/crates/ty/src/main.rs index 20e132fd876ab1..1f3e712f6943c8 100644 --- a/crates/ty/src/main.rs +++ b/crates/ty/src/main.rs @@ -10,7 +10,9 @@ use anyhow::{anyhow, Context}; use clap::Parser; use colored::Colorize; use crossbeam::channel as crossbeam_channel; +use rayon::ThreadPoolBuilder; use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity}; +use ruff_db::max_parallelism; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use salsa::plumbing::ZalsaDatabase; use ty_project::metadata::options::Options; @@ -25,6 +27,8 @@ mod python_version; mod version; pub fn main() -> ExitStatus { + setup_rayon(); + run().unwrap_or_else(|error| { use std::io::Write; @@ -392,3 +396,11 @@ fn set_colored_override(color: Option) { } } } + +/// Initializes the global rayon thread pool to never use more than `TY_MAX_PARALLELISM` threads. +fn setup_rayon() { + ThreadPoolBuilder::default() + .num_threads(max_parallelism().get()) + .build_global() + .unwrap(); +} From 3b15af6d4ff48e6964ebcbce1670dc842008d71c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 07:29:36 +0200 Subject: [PATCH 0239/1161] Update dependency ruff to v0.11.8 (#17839) --- docs/requirements-insiders.txt | 2 +- docs/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements-insiders.txt b/docs/requirements-insiders.txt index 02123797b27e25..569e0034bae13e 100644 --- a/docs/requirements-insiders.txt +++ b/docs/requirements-insiders.txt @@ -1,5 +1,5 @@ PyYAML==6.0.2 -ruff==0.11.7 +ruff==0.11.8 mkdocs==1.6.1 mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@39da7a5e761410349e9a1b8abf593b0cdd5453ff mkdocs-redirects==1.2.2 diff --git a/docs/requirements.txt b/docs/requirements.txt index 8b818105f1f329..2e0c1ad9dac261 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.2 -ruff==0.11.7 +ruff==0.11.8 mkdocs==1.6.1 mkdocs-material==9.5.38 mkdocs-redirects==1.2.2 From 6a36cd6f02901af2121268edf69affc588d3794d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 07:30:46 +0200 Subject: [PATCH 0240/1161] Update Rust crate hashbrown to v0.15.3 (#17842) --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c99ff7b35bdf49..b3db4e411c2e35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1117,9 +1117,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "allocator-api2", "equivalent", @@ -1132,7 +1132,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -1361,7 +1361,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -1381,7 +1381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "serde", ] @@ -3242,7 +3242,7 @@ dependencies = [ "compact_str", "crossbeam-queue", "dashmap 6.1.0", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "hashlink", "indexmap", "parking_lot", @@ -4030,7 +4030,7 @@ dependencies = [ "countme", "dir-test", "drop_bomb", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "indexmap", "insta", "itertools 0.14.0", From 073b993ab0e5586918e58beb4067c38b68c9f86e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 07:31:02 +0200 Subject: [PATCH 0241/1161] Update Rust crate assert_fs to v1.1.3 (#17841) --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3db4e411c2e35..f2f2c16a3536ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,9 +150,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "assert_fs" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674" +checksum = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9" dependencies = [ "anstyle", "doc-comment", @@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -487,7 +487,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -904,7 +904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1485,7 +1485,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi 0.5.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1549,7 +1549,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3205,7 +3205,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3218,7 +3218,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.3", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3604,7 +3604,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.0.2", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4586,7 +4586,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] From d1c6dd9ac1d944c1b1c9d5dc53705f5e2342a61e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 07:32:50 +0200 Subject: [PATCH 0242/1161] Update Rust crate toml to v0.8.22 (#17844) --- Cargo.lock | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2f2c16a3536ca..2adeb1fafd35c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3799,9 +3799,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.8.20" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", @@ -3811,26 +3811,33 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "tracing" version = "0.1.41" @@ -4824,9 +4831,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3" dependencies = [ "memchr", ] From a10606dda2a8bf46b0e9a9475b10db47b1d8a2d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 07:34:09 +0200 Subject: [PATCH 0243/1161] Update Rust crate jiff to v0.2.12 (#17843) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2adeb1fafd35c7..8f3269e73a45a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1539,9 +1539,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" +checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1554,9 +1554,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" +checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300" dependencies = [ "proc-macro2", "quote", From 87c64c9eabd2a55b4751abe03460439323a1c2b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 07:43:01 +0200 Subject: [PATCH 0244/1161] Update actions/setup-python action to v5.6.0 (#17846) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-binaries.yml | 16 ++++++++-------- .github/workflows/ci.yaml | 8 ++++---- .github/workflows/publish-docs.yml | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 52f930a31a55e1..50ba9509504f60 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -43,7 +43,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" @@ -72,7 +72,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -114,7 +114,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: arm64 @@ -170,7 +170,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: ${{ matrix.platform.arch }} @@ -223,7 +223,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -298,7 +298,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" @@ -363,7 +363,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -429,7 +429,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b56b2431242c5d..3d3c294f4bc581 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -525,7 +525,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} @@ -705,7 +705,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -759,7 +759,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 @@ -830,7 +830,7 @@ jobs: persist-credentials: false repository: "astral-sh/ruff-lsp" - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: # installation fails on 3.13 and newer python-version: "3.12" diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index f0a2520fef4a6b..b1c819f5dc2442 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -28,7 +28,7 @@ jobs: ref: ${{ inputs.ref }} persist-credentials: true - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: 3.12 From 108c470348e38f401cec4a5842babd662fcb36ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 07:43:33 +0200 Subject: [PATCH 0245/1161] Update actions/download-artifact action to v4.3.0 (#17845) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-docker.yml | 4 ++-- .github/workflows/ci.yaml | 8 ++++---- .github/workflows/publish-pypi.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 7c70d3360e0fab..98ceac751a73d5 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -113,7 +113,7 @@ jobs: if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} steps: - name: Download digests - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: /tmp/digests pattern: digests-* @@ -256,7 +256,7 @@ jobs: if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} steps: - name: Download digests - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: /tmp/digests pattern: digests-* diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3d3c294f4bc581..45c86b1fc9fd6f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -460,7 +460,7 @@ jobs: with: persist-credentials: false - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 name: Download Ruff binary to test id: download-cached-binary with: @@ -529,7 +529,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 name: Download comparison Ruff binary id: ruff-target with: @@ -649,7 +649,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 name: Download new ty binary id: ty-new with: @@ -835,7 +835,7 @@ jobs: # installation fails on 3.13 and newer python-version: "3.12" - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 name: Download development ruff binary id: ruff-target with: diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 6070d9f29b859b..567dd74fa1cf24 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -23,7 +23,7 @@ jobs: steps: - name: "Install uv" uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: wheels-* path: wheels From b8ed729f59f8356f5b5191e0dd5a3a2e6893e010 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 07:43:54 +0200 Subject: [PATCH 0246/1161] Update taiki-e/install-action digest to 86c23ee (#17838) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 45c86b1fc9fd6f..da384604f379ba 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -239,11 +239,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 + uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 + uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2 with: tool: cargo-insta - name: ty mdtests (GitHub annotations) @@ -297,11 +297,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 + uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 + uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2 with: tool: cargo-insta - name: "Run tests" @@ -324,7 +324,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 + uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2 with: tool: cargo-nextest - name: "Run tests" @@ -407,11 +407,11 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 + uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2 with: tool: cargo-nextest - name: "Install cargo insta" - uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 + uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2 with: tool: cargo-insta - name: "Run tests" @@ -908,7 +908,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2 + uses: taiki-e/install-action@86c23eed46c17b80677df6d8151545ce3e236c61 # v2 with: tool: cargo-codspeed From 2485afe640a3d6c294244c6e11bd7bb2398d3593 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 07:36:09 +0000 Subject: [PATCH 0247/1161] Update pre-commit dependencies (#17840) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Micha Reiser --- .pre-commit-config.yaml | 4 ++-- crates/ty_python_semantic/src/semantic_index/builder.rs | 4 ++-- crates/ty_python_semantic/src/semantic_index/use_def.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7e4a79932db85..d84a1c6bf62925 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,7 +65,7 @@ repos: - black==25.1.0 - repo: https://github.com/crate-ci/typos - rev: v1.31.1 + rev: v1.32.0 hooks: - id: typos @@ -79,7 +79,7 @@ repos: pass_filenames: false # This makes it a lot faster - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.7 + rev: v0.11.8 hooks: - id: ruff-format - id: ruff diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 27ae67b297e4a3..4178d4d09ed58f 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -226,8 +226,8 @@ impl<'db> SemanticIndexBuilder<'db> { fn push_scope(&mut self, node: NodeWithScopeRef) { let parent = self.current_scope(); - let reachabililty = self.current_use_def_map().reachability; - self.push_scope_with_parent(node, Some(parent), reachabililty); + let reachability = self.current_use_def_map().reachability; + self.push_scope_with_parent(node, Some(parent), reachability); } fn push_scope_with_parent( diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index fe7f4ed0db9a11..c9e62422dbeb38 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -644,7 +644,7 @@ pub(super) struct UseDefMapBuilder<'db> { /// Tracks whether or not the scope start is visible at the current point in control flow. /// This is subtly different from `scope_start_visibility`, as we apply these constraints - /// at the beginnging of a branch. Visibility constraints, on the other hand, need to be + /// at the beginning of a branch. Visibility constraints, on the other hand, need to be /// applied at the end of a branch, as we apply them retroactively to all live bindings: /// ```py /// y = 1 From 78b4c3ccf1d6cb10613671ccec09cafba0d1de72 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 5 May 2025 12:25:48 +0200 Subject: [PATCH 0248/1161] [ty] Minor typo in environment variable name (#17848) --- crates/ty/docs/tracing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty/docs/tracing.md b/crates/ty/docs/tracing.md index 75536bcf85c4b0..898b8271d1f234 100644 --- a/crates/ty/docs/tracing.md +++ b/crates/ty/docs/tracing.md @@ -31,7 +31,7 @@ The `TY_MAX_PARALLELISM` environment variable, meanwhile, can be used to control By default, ty will attempt to parallelize its work so that multiple files are checked simultaneously, but this can result in a confused logging output where messages from different threads are intertwined and non determinism. -To switch off parallelism entirely and have more readable logs, use `TY_MAX_PARALLELSIM=1` (or `RAYON_NUM_THREADS=1`). +To switch off parallelism entirely and have more readable logs, use `TY_MAX_PARALLELISM=1` (or `RAYON_NUM_THREADS=1`). ### Examples From a95c73d5d0d26d16736dca6e94c2e648d9d94e3c Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 5 May 2025 06:40:36 -0500 Subject: [PATCH 0249/1161] Implement deferred annotations for Python 3.14 (#17658) This PR updates the semantic model for Python 3.14 by essentially equating "run using Python 3.14" with "uses `from __future__ import annotations`". While this is not technically correct under the hood, it appears to be correct for the purposes of our semantic model. That is: from the point of view of deciding when to parse, bind, etc. annotations, these two contexts behave the same. More generally these contexts behave the same unless you are performing some kind of introspection like the following: Without future import: ```pycon >>> from annotationlib import get_annotations,Format >>> def foo()->Bar:... ... >>> get_annotations(foo,format=Format.FORWARDREF) {'return': ForwardRef('Bar')} >>> get_annotations(foo,format=Format.STRING) {'return': 'Bar'} >>> get_annotations(foo,format=Format.VALUE) Traceback (most recent call last): [...] NameError: name 'Bar' is not defined >>> get_annotations(foo) Traceback (most recent call last): [...] NameError: name 'Bar' is not defined ``` With future import: ``` >>> from __future__ import annotations >>> from annotationlib import get_annotations,Format >>> def foo()->Bar:... ... >>> get_annotations(foo,format=Format.FORWARDREF) {'return': 'Bar'} >>> get_annotations(foo,format=Format.STRING) {'return': 'Bar'} >>> get_annotations(foo,format=Format.VALUE) {'return': 'Bar'} >>> get_annotations(foo) {'return': 'Bar'} ``` (Note: the result of the last call to `get_annotations` in these examples relies on the fact that, as of this writing, the default value for `format` is `Format.VALUE`). If one day we support lint rules targeting code that introspects using the new `annotationlib`, then it is possible we will need to revisit our approximation. Closes #15100 --- .../src/checkers/ast/annotation.rs | 13 ++++++++---- crates/ruff_linter/src/checkers/ast/mod.rs | 21 +++++++++++++------ crates/ruff_python_ast/src/python_version.rs | 4 ++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/annotation.rs b/crates/ruff_linter/src/checkers/ast/annotation.rs index 86d1ba50f8d81e..f545ba27eb1168 100644 --- a/crates/ruff_linter/src/checkers/ast/annotation.rs +++ b/crates/ruff_linter/src/checkers/ast/annotation.rs @@ -1,4 +1,4 @@ -use ruff_python_ast::StmtFunctionDef; +use ruff_python_ast::{PythonVersion, StmtFunctionDef}; use ruff_python_semantic::{ScopeKind, SemanticModel}; use crate::rules::flake8_type_checking; @@ -29,7 +29,11 @@ pub(super) enum AnnotationContext { impl AnnotationContext { /// Determine the [`AnnotationContext`] for an annotation based on the current scope of the /// semantic model. - pub(super) fn from_model(semantic: &SemanticModel, settings: &LinterSettings) -> Self { + pub(super) fn from_model( + semantic: &SemanticModel, + settings: &LinterSettings, + version: PythonVersion, + ) -> Self { // If the annotation is in a class scope (e.g., an annotated assignment for a // class field) or a function scope, and that class or function is marked as // runtime-required, treat the annotation as runtime-required. @@ -59,7 +63,7 @@ impl AnnotationContext { // If `__future__` annotations are enabled or it's a stub file, // then annotations are never evaluated at runtime, // so we can treat them as typing-only. - if semantic.future_annotations_or_stub() { + if semantic.future_annotations_or_stub() || version.defers_annotations() { return Self::TypingOnly; } @@ -81,6 +85,7 @@ impl AnnotationContext { function_def: &StmtFunctionDef, semantic: &SemanticModel, settings: &LinterSettings, + version: PythonVersion, ) -> Self { if flake8_type_checking::helpers::runtime_required_function( function_def, @@ -88,7 +93,7 @@ impl AnnotationContext { semantic, ) { Self::RuntimeRequired - } else if semantic.future_annotations_or_stub() { + } else if semantic.future_annotations_or_stub() || version.defers_annotations() { Self::TypingOnly } else { Self::RuntimeEvaluated diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 2a5a8f3a2b358e..819e28fdc04e2b 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -1004,9 +1004,13 @@ impl<'a> Visitor<'a> for Checker<'a> { } // Function annotations are always evaluated at runtime, unless future annotations - // are enabled. - let annotation = - AnnotationContext::from_function(function_def, &self.semantic, self.settings); + // are enabled or the Python version is at least 3.14. + let annotation = AnnotationContext::from_function( + function_def, + &self.semantic, + self.settings, + self.target_version(), + ); // The first parameter may be a single dispatch. let singledispatch = @@ -1203,7 +1207,11 @@ impl<'a> Visitor<'a> for Checker<'a> { value, .. }) => { - match AnnotationContext::from_model(&self.semantic, self.settings) { + match AnnotationContext::from_model( + &self.semantic, + self.settings, + self.target_version(), + ) { AnnotationContext::RuntimeRequired => { self.visit_runtime_required_annotation(annotation); } @@ -1358,7 +1366,7 @@ impl<'a> Visitor<'a> for Checker<'a> { // we can't defer again, or we'll infinitely recurse! && !self.semantic.in_deferred_type_definition() && self.semantic.in_type_definition() - && self.semantic.future_annotations_or_stub() + && (self.semantic.future_annotations_or_stub()||self.target_version.defers_annotations()) && (self.semantic.in_annotation() || self.source_type.is_stub()) { if let Expr::StringLiteral(string_literal) = expr { @@ -2585,7 +2593,8 @@ impl<'a> Checker<'a> { // if they are annotations in a module where `from __future__ import // annotations` is active, or they are type definitions in a stub file. debug_assert!( - self.semantic.future_annotations_or_stub() + (self.semantic.future_annotations_or_stub() + || self.target_version.defers_annotations()) && (self.source_type.is_stub() || self.semantic.in_annotation()) ); diff --git a/crates/ruff_python_ast/src/python_version.rs b/crates/ruff_python_ast/src/python_version.rs index 20906a25100269..a9add9d5a4e396 100644 --- a/crates/ruff_python_ast/src/python_version.rs +++ b/crates/ruff_python_ast/src/python_version.rs @@ -73,6 +73,10 @@ impl PythonVersion { pub fn supports_pep_701(self) -> bool { self >= Self::PY312 } + + pub fn defers_annotations(self) -> bool { + self >= Self::PY314 + } } impl Default for PythonVersion { From 1945bfdb84b9268a998fd947d3ce9f80c4d55304 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 5 May 2025 13:52:08 +0200 Subject: [PATCH 0250/1161] [ty] Fix standalone expression type retrieval in presence of cycles (#17849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary When entering an `infer_expression_types` cycle from `TypeInferenceBuilder::infer_standalone_expression`, we might get back a `TypeInference::cycle_fallback(…)` that doesn't actually contain any new types, but instead it contains a `cycle_fallback_type` which is set to `Some(Type::Never)`. When calling `self.extend(…)`, we therefore don't really pull in a type for the expression we're interested in. This caused us to panic if we tried to call `self.expression_type(…)` after `self.extend(…)`. The proposed fix here is to retrieve that type from the nested `TypeInferenceBuilder` directly, which will correctly fall back to `cycle_fallback_type`. ## Details I minimized the second example from #17792 a bit further and used this example for debugging: ```py from __future__ import annotations class C: ... def f(arg: C): pass x, _ = f(1) assert x ``` This is self-referential because when we check the assignment statement `x, _ = f(1)`, we need to look up the signature of `f`. Since evaluation of annotations is deferred, we look up the public type of `C` for the `arg` parameter. The public use of `C` is visibility-constraint by "`x`" via the `assert` statement. While evaluating this constraint, we need to look up the type of `x`, which in turn leads us back to the `x, _ = f(1)` definition. The reason why this only showed up in the relatively peculiar case with unpack assignments is the code here: https://github.com/astral-sh/ruff/blob/78b4c3ccf1d6cb10613671ccec09cafba0d1de72/crates/ty_python_semantic/src/types/infer.rs#L2709-L2718 For a non-unpack assignment like `x = f(1)`, we would not try to infer the right-hand side eagerly. Instead, we would enter a `infer_definition_types` cycle that handles the situation correctly. For unpack assignments, however, we try to infer the type of `value` (`f(1)`) and therefore enter the cycle via `standalone_expression_type => infer_expression_type`. closes #17792 ## Test Plan * New regression test * Made sure that we can now run successfully on scipy => see #17850 --- .../test/corpus/88_regression_issue_17792.py | 15 +++++++++++++++ crates/ty_python_semantic/src/types/infer.rs | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 crates/ty_project/resources/test/corpus/88_regression_issue_17792.py diff --git a/crates/ty_project/resources/test/corpus/88_regression_issue_17792.py b/crates/ty_project/resources/test/corpus/88_regression_issue_17792.py new file mode 100644 index 00000000000000..c9977a8397bb24 --- /dev/null +++ b/crates/ty_project/resources/test/corpus/88_regression_issue_17792.py @@ -0,0 +1,15 @@ +# Regression test for https://github.com/astral-sh/ruff/issues/17792 + +from __future__ import annotations + + +class C: ... + + +def f(arg: C): + pass + + +x, _ = f(1) + +assert x diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 6698ffa23ab9fe..ba08e405454fe6 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -4002,7 +4002,12 @@ impl<'db> TypeInferenceBuilder<'db> { let standalone_expression = self.index.expression(expression); let types = infer_expression_types(self.db(), standalone_expression); self.extend(types); - self.expression_type(expression) + + // Instead of calling `self.expression_type(expr)` after extending here, we get + // the result from `types` directly because we might be in cycle recovery where + // `types.cycle_fallback_type` is `Some(fallback_ty)`, which we can retrieve by + // using `expression_type` on `types`: + types.expression_type(expression.scoped_expression_id(self.db(), self.scope())) } fn infer_expression_impl(&mut self, expression: &ast::Expr) -> Type<'db> { From 5a91badb8b499876db38d0ce591d6fa7b9e295a6 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 5 May 2025 13:52:57 +0200 Subject: [PATCH 0251/1161] [ty] Move 'scipy' to list of 'good' projects (#17850) ## Summary Adds `scipy` as a new project to `good.txt` as a follow-up to #17849. --- crates/ty_python_semantic/resources/primer/bad.txt | 1 - crates/ty_python_semantic/resources/primer/good.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/primer/bad.txt b/crates/ty_python_semantic/resources/primer/bad.txt index 4d2f648ab280e9..f09d5589f34c4b 100644 --- a/crates/ty_python_semantic/resources/primer/bad.txt +++ b/crates/ty_python_semantic/resources/primer/bad.txt @@ -22,7 +22,6 @@ pytest # cycle panics (signature_) pywin32 # bad use-def map (binding with definitely-visible unbound) schemathesis # cycle panics (signature_) scikit-learn # success, but mypy-primer hangs processing the output -scipy # missing expression type ("expression should belong to this TypeInference region") spack # success, but mypy-primer hangs processing the output spark # cycle panics (try_metaclass_) steam.py # cycle panics (try_metaclass_), often hangs when multi-threaded diff --git a/crates/ty_python_semantic/resources/primer/good.txt b/crates/ty_python_semantic/resources/primer/good.txt index 219abfbf11503d..9841ff13d76f08 100644 --- a/crates/ty_python_semantic/resources/primer/good.txt +++ b/crates/ty_python_semantic/resources/primer/good.txt @@ -90,6 +90,7 @@ rclip rich rotki schema_salad +scipy scrapy setuptools sockeye From a507c1b8b3ba496225947c2fbac9cd4b6770be55 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Mon, 5 May 2025 21:31:57 +0800 Subject: [PATCH 0252/1161] [`airflow`] Remove `airflow.utils.dag_parsing_context.get_parsing_context` (`AIR301`) (#17852) ## Summary Remove `airflow.utils.dag_parsing_context.get_parsing_context` from AIR301 as it has been moved to AIR311 ## Test Plan the test fixture was updated in the previous PR --- crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs index 7db3e1ebbd944b..ecc30466f3f36d 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs @@ -710,11 +710,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) { // airflow.utils.dag_cycle_tester ["dag_cycle_tester", "test_cycle"] => Replacement::None, - // airflow.utils.dag_parsing_context - ["dag_parsing_context", "get_parsing_context"] => { - Replacement::Name("airflow.sdk.get_parsing_context") - } - // airflow.utils.db ["db", "create_session"] => Replacement::None, From 6e9fb9af38fbc148dde21eff3c89c80eef43d330 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Mon, 5 May 2025 22:01:05 +0800 Subject: [PATCH 0253/1161] [`airflow`] Skip attribute check in try catch block (`AIR301`) (#17790) ## Summary Skip attribute check in try catch block (`AIR301`) ## Test Plan update `crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_try.py` --- .../test/fixtures/airflow/AIR301_names_try.py | 9 +++ .../ruff_linter/src/rules/airflow/helpers.rs | 46 ++++++++++++++- crates/ruff_linter/src/rules/numpy/helpers.rs | 59 +++++++++++++++++++ .../numpy/rules/numpy_2_0_deprecation.rs | 55 +---------------- 4 files changed, 114 insertions(+), 55 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_try.py b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_try.py index 5d4c3da974b80b..113fe88831f74a 100644 --- a/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_try.py +++ b/crates/ruff_linter/resources/test/fixtures/airflow/AIR301_names_try.py @@ -6,3 +6,12 @@ from airflow.datasets import Dataset as Asset Asset + +try: + from airflow.sdk import Asset +except ModuleNotFoundError: + from airflow import datasets + + Asset = datasets.Dataset + +asset = Asset() diff --git a/crates/ruff_linter/src/rules/airflow/helpers.rs b/crates/ruff_linter/src/rules/airflow/helpers.rs index 9cd5818b8feb7d..c5468943be2657 100644 --- a/crates/ruff_linter/src/rules/airflow/helpers.rs +++ b/crates/ruff_linter/src/rules/airflow/helpers.rs @@ -1,5 +1,7 @@ -use crate::rules::numpy::helpers::ImportSearcher; +use crate::rules::numpy::helpers::{AttributeSearcher, ImportSearcher}; +use ruff_python_ast::name::QualifiedNameBuilder; use ruff_python_ast::statement_visitor::StatementVisitor; +use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{Expr, ExprName, StmtTry}; use ruff_python_semantic::Exceptions; use ruff_python_semantic::SemanticModel; @@ -47,6 +49,22 @@ pub(crate) fn is_guarded_by_try_except( semantic: &SemanticModel, ) -> bool { match expr { + Expr::Attribute(_) => { + if !semantic.in_exception_handler() { + return false; + } + let Some(try_node) = semantic + .current_statements() + .find_map(|stmt| stmt.as_try_stmt()) + else { + return false; + }; + let suspended_exceptions = Exceptions::from_try_stmt(try_node, semantic); + if !suspended_exceptions.contains(Exceptions::ATTRIBUTE_ERROR) { + return false; + } + try_block_contains_undeprecated_attribute(try_node, replacement, semantic) + } Expr::Name(ExprName { id, .. }) => { let Some(binding_id) = semantic.lookup_symbol(id.as_str()) else { return false; @@ -78,7 +96,31 @@ pub(crate) fn is_guarded_by_try_except( } /// Given an [`ast::StmtTry`] node, does the `try` branch of that node -/// contain any [`ast::StmtImportFrom`] nodes that indicate the numpy +/// contain any [`ast::ExprAttribute`] nodes that indicate the airflow +/// member is being accessed from the non-deprecated location? +fn try_block_contains_undeprecated_attribute( + try_node: &StmtTry, + replacement: &Replacement, + semantic: &SemanticModel, +) -> bool { + let Replacement::AutoImport { module, name } = replacement else { + return false; + }; + let undeprecated_qualified_name = { + let mut builder = QualifiedNameBuilder::default(); + for part in module.split('.') { + builder.push(part); + } + builder.push(name); + builder.build() + }; + let mut attribute_searcher = AttributeSearcher::new(undeprecated_qualified_name, semantic); + attribute_searcher.visit_body(&try_node.body); + attribute_searcher.found_attribute +} + +/// Given an [`ast::StmtTry`] node, does the `try` branch of that node +/// contain any [`ast::StmtImportFrom`] nodes that indicate the airflow /// member is being imported from the non-deprecated location? fn try_block_contains_undeprecated_import(try_node: &StmtTry, replacement: &Replacement) -> bool { let Replacement::AutoImport { module, name } = replacement else { diff --git a/crates/ruff_linter/src/rules/numpy/helpers.rs b/crates/ruff_linter/src/rules/numpy/helpers.rs index 39ee1c1767a4b3..e5cef6205388b5 100644 --- a/crates/ruff_linter/src/rules/numpy/helpers.rs +++ b/crates/ruff_linter/src/rules/numpy/helpers.rs @@ -1,5 +1,10 @@ +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::statement_visitor::StatementVisitor; +use ruff_python_ast::visitor::Visitor; +use ruff_python_ast::visitor::{walk_expr, walk_stmt}; +use ruff_python_ast::Expr; use ruff_python_ast::{statement_visitor, Alias, Stmt, StmtImportFrom}; +use ruff_python_semantic::SemanticModel; /// AST visitor that searches an AST tree for [`ast::StmtImportFrom`] nodes /// that match a certain [`QualifiedName`]. @@ -43,3 +48,57 @@ impl StatementVisitor<'_> for ImportSearcher<'_> { } } } + +/// AST visitor that searches an AST tree for [`ast::ExprAttribute`] nodes +/// that match a certain [`QualifiedName`]. +pub(crate) struct AttributeSearcher<'a> { + attribute_to_find: QualifiedName<'a>, + semantic: &'a SemanticModel<'a>, + pub found_attribute: bool, +} + +impl<'a> AttributeSearcher<'a> { + pub(crate) fn new( + attribute_to_find: QualifiedName<'a>, + semantic: &'a SemanticModel<'a>, + ) -> Self { + Self { + attribute_to_find, + semantic, + found_attribute: false, + } + } +} + +impl Visitor<'_> for AttributeSearcher<'_> { + fn visit_expr(&mut self, expr: &'_ Expr) { + if self.found_attribute { + return; + } + if expr.is_attribute_expr() + && self + .semantic + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| qualified_name == self.attribute_to_find) + { + self.found_attribute = true; + return; + } + walk_expr(self, expr); + } + + fn visit_stmt(&mut self, stmt: &ruff_python_ast::Stmt) { + if !self.found_attribute { + walk_stmt(self, stmt); + } + } + + fn visit_body(&mut self, body: &[ruff_python_ast::Stmt]) { + for stmt in body { + self.visit_stmt(stmt); + if self.found_attribute { + return; + } + } + } +} diff --git a/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs b/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs index 5fca95a4f77008..38f69c46738074 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs @@ -1,7 +1,7 @@ -use crate::rules::numpy::helpers::ImportSearcher; +use crate::rules::numpy::helpers::{AttributeSearcher, ImportSearcher}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::name::{QualifiedName, QualifiedNameBuilder}; +use ruff_python_ast::name::QualifiedNameBuilder; use ruff_python_ast::statement_visitor::StatementVisitor; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr}; @@ -823,57 +823,6 @@ fn try_block_contains_undeprecated_attribute( attribute_searcher.found_attribute } -/// AST visitor that searches an AST tree for [`ast::ExprAttribute`] nodes -/// that match a certain [`QualifiedName`]. -struct AttributeSearcher<'a> { - attribute_to_find: QualifiedName<'a>, - semantic: &'a SemanticModel<'a>, - found_attribute: bool, -} - -impl<'a> AttributeSearcher<'a> { - fn new(attribute_to_find: QualifiedName<'a>, semantic: &'a SemanticModel<'a>) -> Self { - Self { - attribute_to_find, - semantic, - found_attribute: false, - } - } -} - -impl Visitor<'_> for AttributeSearcher<'_> { - fn visit_expr(&mut self, expr: &'_ Expr) { - if self.found_attribute { - return; - } - if expr.is_attribute_expr() - && self - .semantic - .resolve_qualified_name(expr) - .is_some_and(|qualified_name| qualified_name == self.attribute_to_find) - { - self.found_attribute = true; - return; - } - ast::visitor::walk_expr(self, expr); - } - - fn visit_stmt(&mut self, stmt: &ruff_python_ast::Stmt) { - if !self.found_attribute { - ast::visitor::walk_stmt(self, stmt); - } - } - - fn visit_body(&mut self, body: &[ruff_python_ast::Stmt]) { - for stmt in body { - self.visit_stmt(stmt); - if self.found_attribute { - return; - } - } - } -} - /// Given an [`ast::StmtTry`] node, does the `try` branch of that node /// contain any [`ast::StmtImportFrom`] nodes that indicate the numpy /// member is being imported from the non-deprecated location? From 90c12f417714128405b0ca376403d87efef2483e Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 5 May 2025 16:34:23 +0200 Subject: [PATCH 0254/1161] [ty] ecosystem: Activate running on 'materialize' (#17862) --- crates/ty_python_semantic/resources/primer/bad.txt | 1 - crates/ty_python_semantic/resources/primer/good.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/primer/bad.txt b/crates/ty_python_semantic/resources/primer/bad.txt index f09d5589f34c4b..85b70a1ef9327a 100644 --- a/crates/ty_python_semantic/resources/primer/bad.txt +++ b/crates/ty_python_semantic/resources/primer/bad.txt @@ -11,7 +11,6 @@ freqtrade # cycle panics (try_metaclass_) hydpy # cycle panics (try_metaclass_) ibis # cycle panics (try_metaclass_) manticore # stack overflow -materialize # stack overflow mypy # cycle panic (signature_) pandas # slow pandas-stubs # cycle panics (try_metaclass_) diff --git a/crates/ty_python_semantic/resources/primer/good.txt b/crates/ty_python_semantic/resources/primer/good.txt index 9841ff13d76f08..ebadfce7e5c817 100644 --- a/crates/ty_python_semantic/resources/primer/good.txt +++ b/crates/ty_python_semantic/resources/primer/good.txt @@ -49,6 +49,7 @@ jinja koda-validate kopf kornia +materialize meson mitmproxy mkdocs From 5e2c81841782a5330d09073887bcf6199f9eac1f Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Mon, 5 May 2025 11:50:44 -0300 Subject: [PATCH 0255/1161] [`flake8-bandit`] Mark tuples of string literals as trusted input in `S603` (#17801) ## Summary Fixes #17798 ## Test Plan Snapshot tests --- .../test/fixtures/flake8_bandit/S603.py | 6 ++++++ .../flake8_bandit/rules/shell_injection.rs | 4 ++-- ...les__flake8_bandit__tests__S603_S603.py.snap | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S603.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S603.py index 09f336fd9535c1..f46d3d487243c0 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S603.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S603.py @@ -39,3 +39,9 @@ # But non-instant are not. (e := "echo") run(e) + + +# https://github.com/astral-sh/ruff/issues/17798 +# Tuple literals are trusted +check_output(("literal", "cmd", "using", "tuple"), text=True) +Popen(("literal", "cmd", "using", "tuple")) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs index fef19910e597ee..3bedfba07f7ec5 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs @@ -289,11 +289,11 @@ impl Violation for UnixCommandWildcardInjection { } /// Check if an expression is a trusted input for subprocess.run. -/// We assume that any str or list[str] literal can be trusted. +/// We assume that any str, list[str] or tuple[str] literal can be trusted. fn is_trusted_input(arg: &Expr) -> bool { match arg { Expr::StringLiteral(_) => true, - Expr::List(ast::ExprList { elts, .. }) => { + Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { elts.iter().all(|elt| matches!(elt, Expr::StringLiteral(_))) } Expr::Named(named) => is_trusted_input(&named.value), diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S603_S603.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S603_S603.py.snap index e0576d382b0dfc..13003f8def1c37 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S603_S603.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S603_S603.py.snap @@ -200,3 +200,20 @@ S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input 41 | run(e) | ^^^ S603 | + +S603.py:46:1: S603 `subprocess` call: check for execution of untrusted input + | +44 | # https://github.com/astral-sh/ruff/issues/17798 +45 | # Tuple literals are trusted +46 | check_output(("literal", "cmd", "using", "tuple"), text=True) + | ^^^^^^^^^^^^ S603 +47 | Popen(("literal", "cmd", "using", "tuple")) + | + +S603.py:47:1: S603 `subprocess` call: check for execution of untrusted input + | +45 | # Tuple literals are trusted +46 | check_output(("literal", "cmd", "using", "tuple"), text=True) +47 | Popen(("literal", "cmd", "using", "tuple")) + | ^^^^^ S603 + | From 965a4dd731c2f62998e60d09707b83312b6a46d6 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 5 May 2025 11:40:01 -0500 Subject: [PATCH 0256/1161] [`isort`] Check full module path against project root(s) when categorizing first-party (#16565) When attempting to determine whether `import foo.bar.baz` is a known first-party import relative to [user-provided source paths](https://docs.astral.sh/ruff/settings/#src), when `preview` is enabled we now check that `SRC/foo/bar/baz` is a directory or `SRC/foo/bar/baz.py` or `SRC/foo/bar/baz.pyi` exist. Previously, we just checked the analogous thing for `SRC/foo`, but this can be misleading in situations with disjoint namespace packages that share a common base name (e.g. we may be working inside the namespace package `foo.buzz` and importing `foo.bar` from elsewhere). Supersedes #12987 Closes #12984 --- Cargo.lock | 1 + .../integration_test__rule_f401.snap | 6 +- crates/ruff_linter/Cargo.toml | 1 + ...inter__message__sarif__tests__results.snap | 2 +- crates/ruff_linter/src/preview.rs | 5 + .../rules/typing_only_runtime_import.rs | 25 +- .../ruff_linter/src/rules/isort/categorize.rs | 533 +++++++++++++++++- crates/ruff_linter/src/rules/isort/mod.rs | 7 +- .../src/rules/isort/rules/organize_imports.rs | 16 + .../src/rules/pyflakes/rules/unused_import.rs | 22 +- crates/ruff_python_semantic/src/binding.rs | 29 + docs/faq.md | 26 +- 12 files changed, 652 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f3269e73a45a3..893ea46197981c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2834,6 +2834,7 @@ dependencies = [ "smallvec", "strum", "strum_macros", + "tempfile", "test-case", "thiserror 2.0.12", "toml", diff --git a/crates/ruff/tests/snapshots/integration_test__rule_f401.snap b/crates/ruff/tests/snapshots/integration_test__rule_f401.snap index 3e9270c33e3694..a861fe395ce50a 100644 --- a/crates/ruff/tests/snapshots/integration_test__rule_f401.snap +++ b/crates/ruff/tests/snapshots/integration_test__rule_f401.snap @@ -5,7 +5,6 @@ info: args: - rule - F401 -snapshot_kind: text --- success: true exit_code: 0 @@ -84,6 +83,11 @@ else: print("numpy is not installed") ``` +## Preview +When [preview](https://docs.astral.sh/ruff/preview/) is enabled, +the criterion for determining whether an import is first-party +is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details. + ## Options - `lint.ignore-init-module-imports` - `lint.pyflakes.allowed-unused-imports` diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 37be59af5e5073..2e48fbde19be38 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -76,6 +76,7 @@ insta = { workspace = true, features = ["filters", "json", "redactions"] } test-case = { workspace = true } # Disable colored output in tests colored = { workspace = true, features = ["no-color"] } +tempfile = { workspace = true } [features] default = [] diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap index 287d91e6f18643..224cdf75ae6d90 100644 --- a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__sarif__tests__results.snap @@ -81,7 +81,7 @@ expression: value "rules": [ { "fullDescription": { - "text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n" + "text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n" }, "help": { "text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability" diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 0edf643f123683..425088baea5a74 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -22,6 +22,11 @@ pub(crate) const fn is_py314_support_enabled(settings: &LinterSettings) -> bool settings.preview.is_enabled() } +// https://github.com/astral-sh/ruff/pull/16565 +pub(crate) const fn is_full_path_match_source_strategy_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + // Rule-specific behavior // https://github.com/astral-sh/ruff/pull/17136 diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 777a7b123713b7..2ddafcc71e6d79 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -12,10 +12,12 @@ use crate::checkers::ast::Checker; use crate::codes::Rule; use crate::fix; use crate::importer::ImportedMembers; +use crate::preview::is_full_path_match_source_strategy_enabled; use crate::rules::flake8_type_checking::helpers::{ filter_contained, is_typing_reference, quote_annotation, }; use crate::rules::flake8_type_checking::imports::ImportBinding; +use crate::rules::isort::categorize::MatchSourceStrategy; use crate::rules::isort::{categorize, ImportSection, ImportType}; /// ## What it does @@ -63,6 +65,12 @@ use crate::rules::isort::{categorize, ImportSection, ImportType}; /// return len(sized) /// ``` /// +/// +/// ## Preview +/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled, +/// the criterion for determining whether an import is first-party +/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-third-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details. +/// /// ## Options /// - `lint.flake8-type-checking.quote-annotations` /// - `lint.flake8-type-checking.runtime-evaluated-base-classes` @@ -138,6 +146,11 @@ impl Violation for TypingOnlyFirstPartyImport { /// return len(df) /// ``` /// +/// ## Preview +/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled, +/// the criterion for determining whether an import is first-party +/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-first-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details. +/// /// ## Options /// - `lint.flake8-type-checking.quote-annotations` /// - `lint.flake8-type-checking.runtime-evaluated-base-classes` @@ -299,9 +312,18 @@ pub(crate) fn typing_only_runtime_import( continue; } + let source_name = import.source_name().join("."); + // Categorize the import, using coarse-grained categorization. + let match_source_strategy = + if is_full_path_match_source_strategy_enabled(checker.settings) { + MatchSourceStrategy::FullPath + } else { + MatchSourceStrategy::Root + }; + let import_type = match categorize( - &qualified_name.to_string(), + &source_name, qualified_name.is_unresolved_import(), &checker.settings.src, checker.package(), @@ -311,6 +333,7 @@ pub(crate) fn typing_only_runtime_import( checker.settings.isort.no_sections, &checker.settings.isort.section_order, &checker.settings.isort.default_section, + match_source_strategy, ) { ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => { ImportType::FirstParty diff --git a/crates/ruff_linter/src/rules/isort/categorize.rs b/crates/ruff_linter/src/rules/isort/categorize.rs index 0ed97f9a6744d5..2e1e5800a2a43e 100644 --- a/crates/ruff_linter/src/rules/isort/categorize.rs +++ b/crates/ruff_linter/src/rules/isort/categorize.rs @@ -1,7 +1,8 @@ use std::collections::BTreeMap; use std::fmt; +use std::fs; +use std::iter; use std::path::{Path, PathBuf}; -use std::{fs, iter}; use log::debug; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; @@ -100,6 +101,7 @@ pub(crate) fn categorize<'a>( no_sections: bool, section_order: &'a [ImportSection], default_section: &'a ImportSection, + match_source_strategy: MatchSourceStrategy, ) -> &'a ImportSection { let module_base = module_name.split('.').next().unwrap(); let (mut import_type, mut reason) = { @@ -127,7 +129,7 @@ pub(crate) fn categorize<'a>( &ImportSection::Known(ImportType::FirstParty), Reason::SamePackage, ) - } else if let Some(src) = match_sources(src, module_base) { + } else if let Some(src) = match_sources(src, module_name, match_source_strategy) { ( &ImportSection::Known(ImportType::FirstParty), Reason::SourceMatch(src), @@ -156,20 +158,64 @@ fn same_package(package: Option>, module_base: &str) -> bool { .is_some_and(|package| package.ends_with(module_base)) } -fn match_sources<'a>(paths: &'a [PathBuf], base: &str) -> Option<&'a Path> { - for path in paths { - if let Ok(metadata) = fs::metadata(path.join(base)) { - if metadata.is_dir() { - return Some(path); +/// Returns the source path with respect to which the module `name` +/// should be considered first party, or `None` if no path is found. +/// +/// The [`MatchSourceStrategy`] is the criterion used to decide whether +/// the module path matches a given source directory. +/// +/// # Examples +/// +/// - The module named `foo` will match `[SRC]` if `[SRC]/foo` is a directory, +/// no matter the strategy. +/// +/// - With `match_source_strategy == MatchSourceStrategy::Root`, the module +/// named `foo.baz` will match `[SRC]` if `[SRC]/foo` is a +/// directory or `[SRC]/foo.py` exists. +/// +/// - With `match_source_stratgy == MatchSourceStrategy::FullPath`, the module +/// named `foo.baz` will match `[SRC]` only if `[SRC]/foo/baz` is a directory, +/// or `[SRC]/foo/baz.py` exists or `[SRC]/foo/baz.pyi` exists. +fn match_sources<'a>( + paths: &'a [PathBuf], + name: &str, + match_source_strategy: MatchSourceStrategy, +) -> Option<&'a Path> { + match match_source_strategy { + MatchSourceStrategy::Root => { + let base = name.split('.').next()?; + for path in paths { + if let Ok(metadata) = fs::metadata(path.join(base)) { + if metadata.is_dir() { + return Some(path); + } + } + if let Ok(metadata) = fs::metadata(path.join(format!("{base}.py"))) { + if metadata.is_file() { + return Some(path); + } + } } + None } - if let Ok(metadata) = fs::metadata(path.join(format!("{base}.py"))) { - if metadata.is_file() { - return Some(path); + MatchSourceStrategy::FullPath => { + let relative_path: PathBuf = name.split('.').collect(); + relative_path.components().next()?; + for root in paths { + let candidate = root.join(&relative_path); + if candidate.is_dir() { + return Some(root); + } + if ["py", "pyi"] + .into_iter() + .any(|extension| candidate.with_extension(extension).is_file()) + { + return Some(root); + } } + None } } - None } #[expect(clippy::too_many_arguments)] @@ -183,6 +229,7 @@ pub(crate) fn categorize_imports<'a>( no_sections: bool, section_order: &'a [ImportSection], default_section: &'a ImportSection, + match_source_strategy: MatchSourceStrategy, ) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> { let mut block_by_type: BTreeMap<&ImportSection, ImportBlock> = BTreeMap::default(); // Categorize `Stmt::Import`. @@ -198,6 +245,7 @@ pub(crate) fn categorize_imports<'a>( no_sections, section_order, default_section, + match_source_strategy, ); block_by_type .entry(import_type) @@ -218,6 +266,7 @@ pub(crate) fn categorize_imports<'a>( no_sections, section_order, default_section, + match_source_strategy, ); block_by_type .entry(classification) @@ -238,6 +287,7 @@ pub(crate) fn categorize_imports<'a>( no_sections, section_order, default_section, + match_source_strategy, ); block_by_type .entry(classification) @@ -258,6 +308,7 @@ pub(crate) fn categorize_imports<'a>( no_sections, section_order, default_section, + match_source_strategy, ); block_by_type .entry(classification) @@ -409,3 +460,463 @@ impl fmt::Display for KnownModules { Ok(()) } } + +/// Rule to determine whether a module path matches +/// a relative path from a source directory. +#[derive(Debug, Clone, Copy)] +pub(crate) enum MatchSourceStrategy { + /// Matches if first term in module path is found in file system + /// + /// # Example + /// Module is `foo.bar.baz` and `[SRC]/foo` exists + Root, + /// Matches only if full module path is reflected in file system + /// + /// # Example + /// Module is `foo.bar.baz` and `[SRC]/foo/bar/baz` exists + FullPath, +} + +#[cfg(test)] +mod tests { + use crate::rules::isort::categorize::{match_sources, MatchSourceStrategy}; + + use std::fs; + use std::path::{Path, PathBuf}; + use tempfile::tempdir; + + /// Helper function to create a file with parent directories + fn create_file>(path: P) { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, "").unwrap(); + } + + /// Helper function to create a directory and all parent directories + fn create_dir>(path: P) { + fs::create_dir_all(path).unwrap(); + } + + /// Tests a traditional Python package layout: + /// ``` + /// project/ + /// └── mypackage/ + /// ├── __init__.py + /// ├── module1.py + /// └── module2.py + /// ``` + #[test] + fn test_traditional_layout() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + // Create traditional layout + create_dir(project_dir.join("mypackage")); + create_file(project_dir.join("mypackage/__init__.py")); + create_file(project_dir.join("mypackage/module1.py")); + create_file(project_dir.join("mypackage/module2.py")); + + let paths = vec![project_dir.clone()]; + + // Test with Root strategy + + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.module1", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.nonexistent", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "nonexistent", MatchSourceStrategy::Root), + None + ); + + // Test with FullPath strategy + + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::FullPath), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath), + Some(project_dir.as_path()) + ); + + // Differs in behavior from [`MatchSourceStrategy::Root`] + assert_eq!( + match_sources( + &paths, + "mypackage.nonexistent", + MatchSourceStrategy::FullPath + ), + None + ); + } + + /// Tests a src-based Python package layout: + /// ``` + /// project/ + /// └── src/ + /// └── mypackage/ + /// ├── __init__.py + /// └── module1.py + /// ``` + #[test] + fn test_src_layout() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + let src_dir = project_dir.join("src"); + + // Create src layout + create_dir(src_dir.join("mypackage")); + create_file(src_dir.join("mypackage/__init__.py")); + create_file(src_dir.join("mypackage/module1.py")); + + let paths = vec![src_dir.clone()]; + + // Test with Root strategy + + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::Root), + Some(src_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.module1", MatchSourceStrategy::Root), + Some(src_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.nonexistent", MatchSourceStrategy::Root), + Some(src_dir.as_path()) + ); + + // Test with FullPath strategy + + assert_eq!( + match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath), + Some(src_dir.as_path()) + ); + + // Differs in behavior from [`MatchSourceStrategy::Root`] + assert_eq!( + match_sources( + &paths, + "mypackage.nonexistent", + MatchSourceStrategy::FullPath + ), + None + ); + } + + /// Tests a nested package layout: + /// ``` + /// project/ + /// └── mypackage/ + /// ├── __init__.py + /// ├── module1.py + /// └── subpackage/ + /// ├── __init__.py + /// └── module2.py + /// ``` + #[test] + fn test_nested_packages() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + // Create nested package layout + create_dir(project_dir.join("mypackage/subpackage")); + create_file(project_dir.join("mypackage/__init__.py")); + create_file(project_dir.join("mypackage/module1.py")); + create_file(project_dir.join("mypackage/subpackage/__init__.py")); + create_file(project_dir.join("mypackage/subpackage/module2.py")); + + let paths = vec![project_dir.clone()]; + + // Test with Root strategy + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "mypackage.subpackage", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + // Test with FullPath strategy + + assert_eq!( + match_sources( + &paths, + "mypackage.subpackage.module2", + MatchSourceStrategy::FullPath + ), + Some(project_dir.as_path()) + ); + + // Differs in behavior from [`MatchSourceStrategy::Root`] + assert_eq!( + match_sources( + &paths, + "mypackage.subpackage.nonexistent", + MatchSourceStrategy::FullPath + ), + None + ); + } + + /// Tests a namespace package layout (PEP 420): + /// ``` + /// project/ + /// └── namespace/ # No __init__.py (namespace package) + /// └── package1/ + /// ├── __init__.py + /// └── module1.py + /// ``` + #[test] + fn test_namespace_packages() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + // Create namespace package layout + create_dir(project_dir.join("namespace/package1")); + create_file(project_dir.join("namespace/package1/__init__.py")); + create_file(project_dir.join("namespace/package1/module1.py")); + + let paths = vec![project_dir.clone()]; + // Test with Root strategy + + assert_eq!( + match_sources(&paths, "namespace", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources(&paths, "namespace.package1", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources( + &paths, + "namespace.package2.module1", + MatchSourceStrategy::Root + ), + Some(project_dir.as_path()) + ); + + // Test with FullPath strategy + + assert_eq!( + match_sources(&paths, "namespace.package1", MatchSourceStrategy::FullPath), + Some(project_dir.as_path()) + ); + + assert_eq!( + match_sources( + &paths, + "namespace.package1.module1", + MatchSourceStrategy::FullPath + ), + Some(project_dir.as_path()) + ); + + // Differs in behavior from [`MatchSourceStrategy::Root`] + assert_eq!( + match_sources( + &paths, + "namespace.package2.module1", + MatchSourceStrategy::FullPath + ), + None + ); + } + + /// Tests a package with type stubs (.pyi files): + /// ``` + /// project/ + /// └── mypackage/ + /// ├── __init__.py + /// └── module1.pyi # Only .pyi file, no .py + /// ``` + #[test] + fn test_type_stubs() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + // Create package with type stub + create_dir(project_dir.join("mypackage")); + create_file(project_dir.join("mypackage/__init__.py")); + create_file(project_dir.join("mypackage/module1.pyi")); // Only create .pyi file, not .py + + // Test with FullPath strategy + let paths = vec![project_dir.clone()]; + + // Module "mypackage.module1" should match project_dir using .pyi file + assert_eq!( + match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath), + Some(project_dir.as_path()) + ); + } + + /// Tests a package with both a module and a directory having the same name: + /// ``` + /// project/ + /// └── mypackage/ + /// ├── __init__.py + /// ├── feature.py # Module with same name as directory + /// └── feature/ # Directory with same name as module + /// ├── __init__.py + /// └── submodule.py + /// ``` + #[test] + fn test_same_name_module_and_directory() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + // Create package with module and directory of the same name + create_dir(project_dir.join("mypackage/feature")); + create_file(project_dir.join("mypackage/__init__.py")); + create_file(project_dir.join("mypackage/feature.py")); // Module with same name as directory + create_file(project_dir.join("mypackage/feature/__init__.py")); + create_file(project_dir.join("mypackage/feature/submodule.py")); + + // Test with Root strategy + let paths = vec![project_dir.clone()]; + + // Module "mypackage.feature" should match project_dir (matches the file first) + assert_eq!( + match_sources(&paths, "mypackage.feature", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + + // Test with FullPath strategy + + // Module "mypackage.feature" should match project_dir + assert_eq!( + match_sources(&paths, "mypackage.feature", MatchSourceStrategy::FullPath), + Some(project_dir.as_path()) + ); + + // Module "mypackage.feature.submodule" should match project_dir + assert_eq!( + match_sources( + &paths, + "mypackage.feature.submodule", + MatchSourceStrategy::FullPath + ), + Some(project_dir.as_path()) + ); + } + + /// Tests multiple source directories with different packages: + /// ``` + /// project1/ + /// └── package1/ + /// ├── __init__.py + /// └── module1.py + /// + /// project2/ + /// └── package2/ + /// ├── __init__.py + /// └── module2.py + /// ``` + #[test] + fn test_multiple_source_paths() { + let temp_dir = tempdir().unwrap(); + let project1_dir = temp_dir.path().join("project1"); + let project2_dir = temp_dir.path().join("project2"); + + // Create files in project1 + create_dir(project1_dir.join("package1")); + create_file(project1_dir.join("package1/__init__.py")); + create_file(project1_dir.join("package1/module1.py")); + + // Create files in project2 + create_dir(project2_dir.join("package2")); + create_file(project2_dir.join("package2/__init__.py")); + create_file(project2_dir.join("package2/module2.py")); + + // Test with multiple paths in search order + let paths = vec![project1_dir.clone(), project2_dir.clone()]; + + // Module "package1" should match project1_dir + assert_eq!( + match_sources(&paths, "package1", MatchSourceStrategy::Root), + Some(project1_dir.as_path()) + ); + + // Module "package2" should match project2_dir + assert_eq!( + match_sources(&paths, "package2", MatchSourceStrategy::Root), + Some(project2_dir.as_path()) + ); + + // Try with reversed order to check search order + let paths_reversed = vec![project2_dir, project1_dir.clone()]; + + // Module "package1" should still match project1_dir + assert_eq!( + match_sources(&paths_reversed, "package1", MatchSourceStrategy::Root), + Some(project1_dir.as_path()) + ); + } + + /// Tests behavior with an empty module name + /// ``` + /// project/ + /// └── mypackage/ + /// ``` + /// + /// In theory this should never happen since we expect + /// module names to have been normalized by the time we + /// call `match_sources`. But it is worth noting that the + /// behavior is different depending on the [`MatchSourceStrategy`] + #[test] + fn test_empty_module_name() { + let temp_dir = tempdir().unwrap(); + let project_dir = temp_dir.path().join("project"); + + create_dir(project_dir.join("mypackage")); + + let paths = vec![project_dir.clone()]; + + assert_eq!( + match_sources(&paths, "", MatchSourceStrategy::Root), + Some(project_dir.as_path()) + ); + assert_eq!( + match_sources(&paths, "", MatchSourceStrategy::FullPath), + None + ); + } + + /// Tests behavior with an empty list of source paths + #[test] + fn test_empty_paths() { + let paths: Vec = vec![]; + + // Empty paths should return None + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::Root), + None + ); + assert_eq!( + match_sources(&paths, "mypackage", MatchSourceStrategy::FullPath), + None + ); + } +} diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 05d516f663fe35..782b2ec1ea3281 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use annotate::annotate_imports; use block::{Block, Trailer}; pub(crate) use categorize::categorize; -use categorize::categorize_imports; +use categorize::{categorize_imports, MatchSourceStrategy}; pub use categorize::{ImportSection, ImportType}; use comments::Comment; use normalize::normalize_imports; @@ -76,6 +76,7 @@ pub(crate) fn format_imports( source_type: PySourceType, target_version: PythonVersion, settings: &Settings, + match_source_strategy: MatchSourceStrategy, tokens: &Tokens, ) -> String { let trailer = &block.trailer; @@ -103,6 +104,7 @@ pub(crate) fn format_imports( package, target_version, settings, + match_source_strategy, ); if !block_output.is_empty() && !output.is_empty() { @@ -159,6 +161,7 @@ fn format_import_block( package: Option>, target_version: PythonVersion, settings: &Settings, + match_source_strategy: MatchSourceStrategy, ) -> String { #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum LineInsertion { @@ -169,7 +172,6 @@ fn format_import_block( Inserted, } - // Categorize by type (e.g., first-party vs. third-party). let mut block_by_type = categorize_imports( block, src, @@ -180,6 +182,7 @@ fn format_import_block( settings.no_sections, &settings.section_order, &settings.default_section, + match_source_strategy, ); let mut output = String::new(); diff --git a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs index b982eae70b20a7..30ec8be7d152f4 100644 --- a/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/organize_imports.rs @@ -15,6 +15,8 @@ use super::super::block::Block; use super::super::{comments, format_imports}; use crate::line_width::LineWidthBuilder; use crate::package::PackageRoot; +use crate::preview::is_full_path_match_source_strategy_enabled; +use crate::rules::isort::categorize::MatchSourceStrategy; use crate::settings::LinterSettings; use crate::Locator; @@ -36,6 +38,13 @@ use crate::Locator; /// import numpy as np /// import pandas /// ``` +/// +/// ## Preview +/// When [`preview`](https://docs.astral.sh/ruff/preview/) mode is enabled, Ruff applies a stricter criterion +/// for determining whether an import should be classified as first-party. +/// Specifically, for an import of the form `import foo.bar.baz`, Ruff will +/// check that `foo/bar`, relative to a [user-specified `src`](https://docs.astral.sh/ruff/settings/#src) directory, contains either +/// the directory `baz` or else a file with the name `baz.py` or `baz.pyi`. #[derive(ViolationMetadata)] pub(crate) struct UnsortedImports; @@ -117,6 +126,12 @@ pub(crate) fn organize_imports( trailing_lines_end(block.imports.last().unwrap(), locator.contents()) }; + let match_source_strategy = if is_full_path_match_source_strategy_enabled(settings) { + MatchSourceStrategy::FullPath + } else { + MatchSourceStrategy::Root + }; + // Generate the sorted import block. let expected = format_imports( block, @@ -130,6 +145,7 @@ pub(crate) fn organize_imports( source_type, target_version, &settings.isort, + match_source_strategy, tokens, ); diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs index 91fcb878a2cd41..2cd22c8510c3a1 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs @@ -16,8 +16,11 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix; -use crate::preview::is_dunder_init_fix_unused_import_enabled; +use crate::preview::{ + is_dunder_init_fix_unused_import_enabled, is_full_path_match_source_strategy_enabled, +}; use crate::registry::Rule; +use crate::rules::isort::categorize::MatchSourceStrategy; use crate::rules::{isort, isort::ImportSection, isort::ImportType}; /// ## What it does @@ -88,6 +91,11 @@ use crate::rules::{isort, isort::ImportSection, isort::ImportType}; /// print("numpy is not installed") /// ``` /// +/// ## Preview +/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled, +/// the criterion for determining whether an import is first-party +/// is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details. +/// /// ## Options /// - `lint.ignore-init-module-imports` /// - `lint.pyflakes.allowed-unused-imports` @@ -222,10 +230,15 @@ enum UnusedImportContext { } fn is_first_party(import: &AnyImport, checker: &Checker) -> bool { - let qualified_name = import.qualified_name(); + let source_name = import.source_name().join("."); + let match_source_strategy = if is_full_path_match_source_strategy_enabled(checker.settings) { + MatchSourceStrategy::FullPath + } else { + MatchSourceStrategy::Root + }; let category = isort::categorize( - &qualified_name.to_string(), - qualified_name.is_unresolved_import(), + &source_name, + import.qualified_name().is_unresolved_import(), &checker.settings.src, checker.package(), checker.settings.isort.detect_same_package, @@ -234,6 +247,7 @@ fn is_first_party(import: &AnyImport, checker: &Checker) -> bool { checker.settings.isort.no_sections, &checker.settings.isort.section_order, &checker.settings.isort.default_section, + match_source_strategy, ); matches! { category, diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index a2f9c8ee346682..73614a7a16dbf2 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -714,6 +714,15 @@ pub trait Imported<'a> { /// Returns the member name of the imported symbol. For a straight import, this is equivalent /// to the qualified name; for a `from` import, this is the name of the imported symbol. fn member_name(&self) -> Cow<'a, str>; + + /// Returns the source module of the imported symbol. + /// + /// For example: + /// + /// - `import foo` returns `["foo"]` + /// - `import foo.bar` returns `["foo","bar"]` + /// - `from foo import bar` returns `["foo"]` + fn source_name(&self) -> &[&'a str]; } impl<'a> Imported<'a> for Import<'a> { @@ -731,6 +740,10 @@ impl<'a> Imported<'a> for Import<'a> { fn member_name(&self) -> Cow<'a, str> { Cow::Owned(self.qualified_name().to_string()) } + + fn source_name(&self) -> &[&'a str] { + self.qualified_name.segments() + } } impl<'a> Imported<'a> for SubmoduleImport<'a> { @@ -748,6 +761,10 @@ impl<'a> Imported<'a> for SubmoduleImport<'a> { fn member_name(&self) -> Cow<'a, str> { Cow::Owned(self.qualified_name().to_string()) } + + fn source_name(&self) -> &[&'a str] { + self.qualified_name.segments() + } } impl<'a> Imported<'a> for FromImport<'a> { @@ -765,6 +782,10 @@ impl<'a> Imported<'a> for FromImport<'a> { fn member_name(&self) -> Cow<'a, str> { Cow::Borrowed(self.qualified_name.segments()[self.qualified_name.segments().len() - 1]) } + + fn source_name(&self) -> &[&'a str] { + self.module_name() + } } /// A wrapper around an import [`BindingKind`] that can be any of the three types of imports. @@ -799,6 +820,14 @@ impl<'ast> Imported<'ast> for AnyImport<'_, 'ast> { Self::FromImport(import) => import.member_name(), } } + + fn source_name(&self) -> &[&'ast str] { + match self { + Self::Import(import) => import.source_name(), + Self::SubmoduleImport(import) => import.source_name(), + Self::FromImport(import) => import.source_name(), + } + } } #[cfg(test)] diff --git a/docs/faq.md b/docs/faq.md index d2dde465305d79..6ed942e1f3b849 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -309,7 +309,31 @@ my_project When Ruff sees an import like `import foo`, it will then iterate over the `src` directories, looking for a corresponding Python module (in reality, a directory named `foo` or a file named -`foo.py`). +`foo.py`). For module paths with multiple components like `import foo.bar`, +the default behavior is to search only for a directory named `foo` or a file +named `foo.py`. However, if `preview` is enabled, Ruff will require that the full relative path `foo/bar` exists as a directory, or that `foo/bar.py` or `foo/bar.pyi` exist as files. Finally, imports of the form `from foo import bar`, Ruff will only use `foo` when determining whether a module is first-party or third-party. + +If there is a directory +whose name matches a third-party package, but does not contain Python code, +it could happen that the above algorithm incorrectly infers an import to be first-party. +To prevent this, you can modify the [`known-third-party`](settings.md#lint_isort_known-third-party) setting. For example, if you import +the package `wandb` but also have a subdirectory of your `src` with +the same name, you can add the following: + +=== "pyproject.toml" + + ```toml + [tool.ruff.lint.isort] + known-third-party = ["wandb"] + ``` + +=== "ruff.toml" + + ```toml + [lint.isort] + known-third-party = ["wandb"] + ``` + If the `src` field is omitted, Ruff will default to using the "project root", along with a `"src"` subdirectory, as the first-party sources, to support both flat and nested project layouts. From 101e1a5ddd0eacd5bbfc1cb6a7ee18cfd685d7fa Mon Sep 17 00:00:00 2001 From: Max Mynter <32773644+maxmynter@users.noreply.github.com> Date: Mon, 5 May 2025 19:30:16 +0200 Subject: [PATCH 0257/1161] [semantic-syntax-tests] for for `InvalidStarExpression`, `DuplicateMatchKey`, and `DuplicateMatchClassAttribute` (#17754) Re: #17526 ## Summary Add integration tests for Python Semantic Syntax for `InvalidStarExpression`, `DuplicateMatchKey`, and `DuplicateMatchClassAttribute`. ## Note - Red knot integration tests for `DuplicateMatchKey` exist already in line 89-101. ## Test Plan This is a test. --- crates/ruff_linter/src/linter.rs | 47 +++++++++++++++++++ ..._duplicate_match_class_attribute_3.10.snap | 10 ++++ ...cateMatchKey_duplicate_match_key_3.10.snap | 10 ++++ ...pression_invalid_star_expression_3.10.snap | 9 ++++ ...sion_invalid_star_expression_for_3.10.snap | 9 ++++ ...on_invalid_star_expression_yield_3.10.snap | 9 ++++ .../diagnostics/semantic_syntax_errors.md | 42 +++++++++++++++++ 7 files changed, 136 insertions(+) create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 4b23e99c89a24a..f2445b128caf8d 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1063,6 +1063,53 @@ mod tests { PythonVersion::PY310, "MultipleCaseAssignment" )] + #[test_case( + "duplicate_match_key", + " + match x: + case {'key': 1, 'key': 2}: + pass + ", + PythonVersion::PY310, + "DuplicateMatchKey" + )] + #[test_case( + "duplicate_match_class_attribute", + " + match x: + case Point(x=1, x=2): + pass + ", + PythonVersion::PY310, + "DuplicateMatchClassAttribute" + )] + #[test_case( + "invalid_star_expression", + " + def func(): + return *x + ", + PythonVersion::PY310, + "InvalidStarExpression" + )] + #[test_case( + "invalid_star_expression_for", + " + for *x in range(10): + pass + ", + PythonVersion::PY310, + "InvalidStarExpression" + )] + #[test_case( + "invalid_star_expression_yield", + " + def func(): + yield *x + ", + PythonVersion::PY310, + "InvalidStarExpression" + )] fn test_semantic_errors( name: &str, contents: &str, diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap new file mode 100644 index 00000000000000..bac3111de0219d --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchClassAttribute_duplicate_match_class_attribute_3.10.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:21: SyntaxError: attribute name `x` repeated in class pattern + | +2 | match x: +3 | case Point(x=1, x=2): + | ^ +4 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap new file mode 100644 index 00000000000000..f877e86e31013d --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_DuplicateMatchKey_duplicate_match_key_3.10.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:21: SyntaxError: mapping pattern checks duplicate key `'key'` + | +2 | match x: +3 | case {'key': 1, 'key': 2}: + | ^^^^^ +4 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap new file mode 100644 index 00000000000000..81bc057071a741 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_3.10.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:12: SyntaxError: Starred expression cannot be used here + | +2 | def func(): +3 | return *x + | ^^ + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap new file mode 100644 index 00000000000000..811caf59bb7086 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_for_3.10.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:2:5: SyntaxError: Starred expression cannot be used here + | +2 | for *x in range(10): + | ^^ +3 | pass + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap new file mode 100644 index 00000000000000..ee1549b0cddd94 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_InvalidStarExpression_invalid_star_expression_yield_3.10.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:3:11: SyntaxError: Starred expression cannot be used here + | +2 | def func(): +3 | yield *x + | ^^ + | diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md index e380e10018c672..7063c58dda1cd3 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -100,6 +100,26 @@ match 2: ... ``` +## Duplicate `match` class attribute + +Attribute names in class patterns must be unique: + +```toml +[environment] +python-version = "3.10" +``` + +```py +class Point: + pass + +obj = Point() +match obj: + # error: [invalid-syntax] "attribute name `x` repeated in class pattern" + case Point(x=1, x=2): + pass +``` + ## `return`, `yield`, `yield from`, and `await` outside function ```py @@ -186,6 +206,28 @@ def f[X, Y, X](): pass ``` +## Invalid star expression + +Star expressions can't be used in certain contexts: + +```py +def func(): + # error: [invalid-syntax] "Starred expression cannot be used here" + return *[1, 2, 3] + +def gen(): + # error: [invalid-syntax] "Starred expression cannot be used here" + yield * [1, 2, 3] + +# error: [invalid-syntax] "Starred expression cannot be used here" +for *x in range(10): + pass + +# error: [invalid-syntax] "Starred expression cannot be used here" +for x in *range(10): + pass +``` + ## `await` outside async function This error includes `await`, `async for`, `async with`, and `async` comprehensions. From 178c882740b734860de401fbc240e07966d9d53a Mon Sep 17 00:00:00 2001 From: Max Mynter <32773644+maxmynter@users.noreply.github.com> Date: Mon, 5 May 2025 20:02:06 +0200 Subject: [PATCH 0258/1161] [semantic-syntax-tests] Add test fixtures for `AwaitOutsideAsyncFunction` (#17785) Re: #17526 ## Summary Add test fixtures for `AwaitOutsideAsync` and `AsyncComprehensionOutsideAsyncFunction` errors. ## Test Plan This is a test. --- .../syntax_errors/async_comprehension.py | 11 +++++++ .../await_outside_async_function.py | 5 +++ crates/ruff_linter/src/linter.rs | 2 ++ ...linter__tests__async_comprehension.py.snap | 31 +++++++++++++++++++ ...ests__await_outside_async_function.py.snap | 18 +++++++++++ 5 files changed, 67 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/syntax_errors/async_comprehension.py create mode 100644 crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension.py.snap create mode 100644 crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/syntax_errors/async_comprehension.py b/crates/ruff_linter/resources/test/fixtures/syntax_errors/async_comprehension.py new file mode 100644 index 00000000000000..c2000f141aca4a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/syntax_errors/async_comprehension.py @@ -0,0 +1,11 @@ +async def elements(n): + yield n + +def regular_function(): + [x async for x in elements(1)] + + async with elements(1) as x: + pass + + async for _ in elements(1): + pass diff --git a/crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py b/crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py new file mode 100644 index 00000000000000..dd49cb05f9f6e0 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/syntax_errors/await_outside_async_function.py @@ -0,0 +1,5 @@ +def func(): + await 1 + +# Top-level await +await 1 diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index f2445b128caf8d..b41ce44a6b075c 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1157,6 +1157,8 @@ mod tests { Rule::LoadBeforeGlobalDeclaration, Path::new("load_before_global_declaration.py") )] + #[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async_function.py"))] + #[test_case(Rule::AwaitOutsideAsync, Path::new("async_comprehension.py"))] fn test_syntax_errors(rule: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); let path = Path::new("resources/test/fixtures/syntax_errors").join(path); diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension.py.snap new file mode 100644 index 00000000000000..9e570b889cc0a2 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__async_comprehension.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +resources/test/fixtures/syntax_errors/async_comprehension.py:5:8: PLE1142 `await` should be used within an async function + | +4 | def regular_function(): +5 | [x async for x in elements(1)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE1142 +6 | +7 | async with elements(1) as x: + | + +resources/test/fixtures/syntax_errors/async_comprehension.py:7:5: PLE1142 `await` should be used within an async function + | + 5 | [x async for x in elements(1)] + 6 | + 7 | / async with elements(1) as x: + 8 | | pass + | |____________^ PLE1142 + 9 | +10 | async for _ in elements(1): + | + +resources/test/fixtures/syntax_errors/async_comprehension.py:10:5: PLE1142 `await` should be used within an async function + | + 8 | pass + 9 | +10 | / async for _ in elements(1): +11 | | pass + | |____________^ PLE1142 + | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap new file mode 100644 index 00000000000000..fb45718a9ab9e3 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_outside_async_function.py.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +resources/test/fixtures/syntax_errors/await_outside_async_function.py:2:5: PLE1142 `await` should be used within an async function + | +1 | def func(): +2 | await 1 + | ^^^^^^^ PLE1142 +3 | +4 | # Top-level await + | + +resources/test/fixtures/syntax_errors/await_outside_async_function.py:5:1: PLE1142 `await` should be used within an async function + | +4 | # Top-level await +5 | await 1 + | ^^^^^^^ PLE1142 + | From 784daae49784ad192549add1a902ef12fcd03cdb Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Mon, 5 May 2025 14:07:46 -0400 Subject: [PATCH 0259/1161] migrate to dist-workspace.toml, use new workspace.packages config (#17864) --- .github/workflows/release.yml | 2 +- Cargo.toml | 72 --------------------------------- dist-workspace.toml | 75 +++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 73 deletions(-) create mode 100644 dist-workspace.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dad8faf3193eae..847160c25b3683 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,7 +69,7 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.5-prerelease.1/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: diff --git a/Cargo.toml b/Cargo.toml index 3dd064483e5cbc..30f8c0aba646fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -268,75 +268,3 @@ debug = 1 # The profile that 'cargo dist' will build with. [profile.dist] inherits = "release" - -# Config for 'dist' -[workspace.metadata.dist] -# The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.28.4" -# Make distability of apps opt-in instead of opt-out -dist = false -# CI backends to support -ci = "github" -# The installers to generate for each app -installers = ["shell", "powershell"] -# The archive format to use for windows builds (defaults .zip) -windows-archive = ".zip" -# The archive format to use for non-windows builds (defaults .tar.xz) -unix-archive = ".tar.gz" -# Target platforms to build apps for (Rust target-triple syntax) -targets = [ - "aarch64-apple-darwin", - "aarch64-pc-windows-msvc", - "aarch64-unknown-linux-gnu", - "aarch64-unknown-linux-musl", - "arm-unknown-linux-musleabihf", - "armv7-unknown-linux-gnueabihf", - "armv7-unknown-linux-musleabihf", - "i686-pc-windows-msvc", - "i686-unknown-linux-gnu", - "i686-unknown-linux-musl", - "powerpc64-unknown-linux-gnu", - "powerpc64le-unknown-linux-gnu", - "s390x-unknown-linux-gnu", - "x86_64-apple-darwin", - "x86_64-pc-windows-msvc", - "x86_64-unknown-linux-gnu", - "x86_64-unknown-linux-musl", -] -# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true) -auto-includes = false -# Whether dist should create a Github Release or use an existing draft -create-release = true -# Which actions to run on pull requests -pr-run-mode = "plan" -# Whether CI should trigger releases with dispatches instead of tag pushes -dispatch-releases = true -# Which phase dist should use to create the GitHub release -github-release = "announce" -# Whether CI should include auto-generated code to build local artifacts -build-local-artifacts = false -# Local artifacts jobs to run in CI -local-artifacts-jobs = ["./build-binaries", "./build-docker"] -# Publish jobs to run in CI -publish-jobs = ["./publish-pypi", "./publish-wasm"] -# Post-announce jobs to run in CI -post-announce-jobs = [ - "./notify-dependents", - "./publish-docs", - "./publish-playground", -] -# Custom permissions for GitHub Jobs -github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } } -# Whether to install an updater program -install-updater = false -# Path that installers should place binaries in -install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"] - -[workspace.metadata.dist.github-custom-runners] -global = "depot-ubuntu-latest-4" - -[workspace.metadata.dist.github-action-commits] -"actions/checkout" = "85e6279cec87321a52edac9c87bce653a07cf6c2" # v4 -"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2 -"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0 -"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3 diff --git a/dist-workspace.toml b/dist-workspace.toml new file mode 100644 index 00000000000000..e3968dfbe7c452 --- /dev/null +++ b/dist-workspace.toml @@ -0,0 +1,75 @@ +[workspace] +members = ["cargo:."] +packages = ["ruff"] + +# Config for 'dist' +[dist] +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.28.5-prerelease.1" +# Whether to consider the binaries in a package for distribution (defaults true) +dist = false +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = ["shell", "powershell"] +# The archive format to use for windows builds (defaults .zip) +windows-archive = ".zip" +# The archive format to use for non-windows builds (defaults .tar.xz) +unix-archive = ".tar.gz" +# Target platforms to build apps for (Rust target-triple syntax) +targets = [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "aarch64-pc-windows-msvc", + "arm-unknown-linux-musleabihf", + "armv7-unknown-linux-gnueabihf", + "armv7-unknown-linux-musleabihf", + "x86_64-apple-darwin", + "powerpc64-unknown-linux-gnu", + "powerpc64le-unknown-linux-gnu", + "s390x-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "x86_64-pc-windows-msvc", + "i686-unknown-linux-gnu", + "i686-unknown-linux-musl", + "i686-pc-windows-msvc" +] +# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true) +auto-includes = false +# Whether dist should create a Github Release or use an existing draft +create-release = true +# Which actions to run on pull requests +pr-run-mode = "plan" +# Whether CI should trigger releases with dispatches instead of tag pushes +dispatch-releases = true +# Which phase dist should use to create the GitHub release +github-release = "announce" +# Whether CI should include auto-generated code to build local artifacts +build-local-artifacts = false +# Local artifacts jobs to run in CI +local-artifacts-jobs = ["./build-binaries", "./build-docker"] +# Publish jobs to run in CI +publish-jobs = ["./publish-pypi", "./publish-wasm"] +# Post-announce jobs to run in CI +post-announce-jobs = [ + "./notify-dependents", + "./publish-docs", + "./publish-playground" +] +# Custom permissions for GitHub Jobs +github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } } +# Whether to install an updater program +install-updater = false +# Path that installers should place binaries in +install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"] + +[dist.github-custom-runners] +global = "depot-ubuntu-latest-4" + +[dist.github-action-commits] +"actions/checkout" = "85e6279cec87321a52edac9c87bce653a07cf6c2" # v4 +"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2 +"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0 +"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3 From 3f32446e161de7b8444cae54174543f12a812816 Mon Sep 17 00:00:00 2001 From: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com> Date: Mon, 5 May 2025 20:47:56 +0200 Subject: [PATCH 0260/1161] [`ruff`] add fix safety section (`RUF013`) (#17759) The PR add the fix safety section for rule `RUF013` (https://github.com/astral-sh/ruff/issues/15584 ) The fix was introduced here #4831 The rule as a lot of False Negative (as it is explained in the docs of the rule). The main reason because the fix is unsafe is that it could change code generation tools behaviour, as in the example here: ```python def generate_api_docs(func): hints = get_type_hints(func) for param, hint in hints.items(): if is_optional_type(hint): print(f"Parameter '{param}' is optional") else: print(f"Parameter '{param}' is required") # Before fix def create_user(name: str, roles: list[str] = None): pass # After fix def create_user(name: str, roles: Optional[list[str]] = None): pass # Generated docs would change from "roles is required" to "roles is optional" ``` --- crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs index e1a9d6f7bd9203..e2a5ba91a3a05b 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs @@ -72,6 +72,11 @@ use super::super::typing::type_hint_explicitly_allows_none; /// ## Options /// - `target-version` /// +/// ## Fix safety +/// +/// This fix is always marked as unsafe because it can change the behavior of code that relies on +/// type hints, and it assumes the default value is always appropriate—which might not be the case. +/// /// [PEP 484]: https://peps.python.org/pep-0484/#union-types #[derive(ViolationMetadata)] pub(crate) struct ImplicitOptional { From 4850c187ea4c9d013d50593907297c46b85f9850 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 5 May 2025 12:12:38 -0700 Subject: [PATCH 0261/1161] [ty] add cycle handling for FunctionType::signature query (#17833) This fixes cycle panics in several ecosystem projects (moved to `good.txt` in a following PR https://github.com/astral-sh/ruff/pull/17834 because our mypy-primer job doesn't handle it well if we move projects to `good.txt` in the same PR that fixes `ty` to handle them), as well as in the minimal case in the added mdtest. It also fixes a number of panicking fuzzer seeds. It doesn't appear to cause any regression in any ecosystem project or any fuzzer seed. --- .../resources/mdtest/cycle.md | 15 ++++++++++ crates/ty_python_semantic/src/types.rs | 28 +++++++++++++++---- .../src/types/signatures.rs | 6 +++- 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/cycle.md diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md new file mode 100644 index 00000000000000..dea0a124e5bb3a --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -0,0 +1,15 @@ +# Cycles + +## Function signature + +Deferred annotations can result in cycles in resolving a function signature: + +```py +from __future__ import annotations + +# error: [invalid-type-form] +def f(x: f): + pass + +reveal_type(f) # revealed: def f(x: Unknown) -> Unknown +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 1e4ca637d905c3..ed8f2e5496a548 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6495,6 +6495,11 @@ impl<'db> FunctionSignature<'db> { pub(crate) fn iter(&self) -> Iter> { self.as_slice().iter() } + + /// Returns the "bottom" signature (subtype of all fully-static signatures.) + pub(crate) fn bottom(db: &'db dyn Db) -> Self { + Self::Single(Signature::bottom(db)) + } } impl<'db> IntoIterator for &'db FunctionSignature<'db> { @@ -6639,7 +6644,7 @@ impl<'db> FunctionType<'db> { /// /// Were this not a salsa query, then the calling query /// would depend on the function's AST and rerun for every change in that file. - #[salsa::tracked(return_ref)] + #[salsa::tracked(return_ref, cycle_fn=signature_cycle_recover, cycle_initial=signature_cycle_initial)] pub(crate) fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> { if let Some(overloaded) = self.to_overloaded(db) { FunctionSignature::Overloaded( @@ -6849,6 +6854,22 @@ impl<'db> FunctionType<'db> { } } +fn signature_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &FunctionSignature<'db>, + _count: u32, + _function: FunctionType<'db>, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn signature_cycle_initial<'db>( + db: &'db dyn Db, + _function: FunctionType<'db>, +) -> FunctionSignature<'db> { + FunctionSignature::bottom(db) +} + /// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might /// have special behavior. #[derive( @@ -7060,10 +7081,7 @@ impl<'db> CallableType<'db> { /// `(*args: object, **kwargs: object) -> Never`. #[cfg(test)] pub(crate) fn bottom(db: &'db dyn Db) -> Type<'db> { - Type::Callable(CallableType::single( - db, - Signature::new(Parameters::object(db), Some(Type::Never)), - )) + Type::Callable(CallableType::single(db, Signature::bottom(db))) } /// Return a "normalized" version of this `Callable` type. diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index a44720d38c3945..d480a34a70f7d0 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -291,6 +291,11 @@ impl<'db> Signature<'db> { } } + /// Return the "bottom" signature, subtype of all other fully-static signatures. + pub(crate) fn bottom(db: &'db dyn Db) -> Self { + Self::new(Parameters::object(db), Some(Type::Never)) + } + pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self { Self { generic_context: self.generic_context, @@ -957,7 +962,6 @@ impl<'db> Parameters<'db> { } /// Return parameters that represents `(*args: object, **kwargs: object)`. - #[cfg(test)] pub(crate) fn object(db: &'db dyn Db) -> Self { Self { value: vec![ From 20d64b9c85592d7a02f2633e6335c02b0b98a7b4 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 5 May 2025 12:21:06 -0700 Subject: [PATCH 0262/1161] [ty] add passing projects to primer (#17834) Add projects to primer that now pass, with https://github.com/astral-sh/ruff/pull/17833 --- crates/ty_python_semantic/resources/primer/bad.txt | 9 ++------- crates/ty_python_semantic/resources/primer/good.txt | 5 +++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/primer/bad.txt b/crates/ty_python_semantic/resources/primer/bad.txt index 85b70a1ef9327a..41343cf57bd3c5 100644 --- a/crates/ty_python_semantic/resources/primer/bad.txt +++ b/crates/ty_python_semantic/resources/primer/bad.txt @@ -1,5 +1,4 @@ -Expression # cycle panic (signature_) -Tanjun # cycle panic (signature_) +Tanjun # hangs altair # cycle panics (try_metaclass_) antidote # hangs / slow artigraph # cycle panics (value_type_) @@ -11,20 +10,16 @@ freqtrade # cycle panics (try_metaclass_) hydpy # cycle panics (try_metaclass_) ibis # cycle panics (try_metaclass_) manticore # stack overflow -mypy # cycle panic (signature_) pandas # slow pandas-stubs # cycle panics (try_metaclass_) pandera # cycle panics (try_metaclass_) prefect # slow pylint # cycle panics (self-recursive type alias) -pytest # cycle panics (signature_) pywin32 # bad use-def map (binding with definitely-visible unbound) -schemathesis # cycle panics (signature_) +schemathesis # salsa cycle.rs:164 assertion fails, only in CI scikit-learn # success, but mypy-primer hangs processing the output spack # success, but mypy-primer hangs processing the output spark # cycle panics (try_metaclass_) steam.py # cycle panics (try_metaclass_), often hangs when multi-threaded -streamlit # cycle panic (signature_) sympy # stack overflow -trio # cycle panics (deferred annotatation resolving in wrong scope) xarray # cycle panics (try_metaclass_) diff --git a/crates/ty_python_semantic/resources/primer/good.txt b/crates/ty_python_semantic/resources/primer/good.txt index ebadfce7e5c817..0f6b92129bbc11 100644 --- a/crates/ty_python_semantic/resources/primer/good.txt +++ b/crates/ty_python_semantic/resources/primer/good.txt @@ -1,4 +1,5 @@ AutoSplit +Expression PyGithub PyWinCtl SinbadCogs @@ -56,6 +57,7 @@ mkdocs mkosi mongo-python-driver more-itertools +mypy mypy-protobuf mypy_primer nionutils @@ -83,6 +85,7 @@ pylox pyodide pyp pyppeteer +pytest pytest-robotframework python-chess python-htmlgen @@ -101,7 +104,9 @@ starlette static-frame stone strawberry +streamlit tornado +trio twine typeshed-stats urllib3 From de78da5ee6e0b91da1bc37c3139a15f6906d42f8 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 5 May 2025 21:31:55 +0200 Subject: [PATCH 0263/1161] [ty] Increase worker-thread stack size (#17869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary closes #17472 This is obviously just a band-aid solution to this problem (in that you can always make your [pathological inputs](https://github.com/sympy/sympy/blob/28994edd82a18c22a98a52245c173f3c836dcad3/sympy/polys/numberfields/resolvent_lookup.py) bigger and it will still crash), but I think this is not an unreasonable change — even if we add more sophisticated solutions later. I tried using `stacker` as suggested by @MichaReiser, and it works. But it's unclear where exactly would be the right place to put it, and even for the `sympy` problem, we would need to add it both in the semantic index builder AST traversal and in type inference. Increasing the default stack size for worker threads, as proposed here, doesn't solve the underlying problem (that there is a hard limit), but it is more universal in the sense that it is not specific to large binary-operator expression chains. To determine a reasonable stack size, I created files that look like *right associative*: ```py from typing import reveal_type total = (1 + (1 + (1 + (1 + (… + 1))))) reveal_type(total) ``` *left associative* ```py from typing import reveal_type total = 1 + 1 + 1 + 1 + … + 1 reveal_type(total) ``` with a variable amount of operands (`N`). I then chose the stack size large enough to still be able to handle cases that existing type checkers can not: ``` right N = 20: mypy takes ~ 1min N = 350: pyright crashes with a stack overflow (mypy fails with "too many nested parentheses") N = 800: ty(main) infers Literal[800] instantly N = 1000: ty(main) crashes with "thread '' has overflowed its stack" N = 7000: ty(this branch) infers Literal[7000] instantly N = 8000+: ty(this branch) crashes left N = 300: pyright emits "Maximum parse depth exceeded; break expression into smaller sub-expressions" total is inferred as Unknown N = 5500: mypy crashes with "INTERNAL ERROR" N = 2500: ty(main) infers Literal[2500] instantly N = 3000: ty(main) crashes with "thread '' has overflowed its stack" N = 22000: ty(this branch) infers Literal[22000] instantly N = 23000+: ty(this branch) crashes ``` ## Test Plan New regression test. --- crates/ty/src/main.rs | 6 ++++++ crates/ty/tests/cli.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/crates/ty/src/main.rs b/crates/ty/src/main.rs index 1f3e712f6943c8..3167a24ad4a435 100644 --- a/crates/ty/src/main.rs +++ b/crates/ty/src/main.rs @@ -401,6 +401,12 @@ fn set_colored_override(color: Option) { fn setup_rayon() { ThreadPoolBuilder::default() .num_threads(max_parallelism().get()) + // Use a reasonably large stack size to avoid running into stack overflows too easily. The + // size was chosen in such a way as to still be able to handle large expressions involving + // binary operators (x + x + … + x) both during the AST walk in semantic index building as + // well as during type checking. Using this stack size, we can handle handle expressions + // that are several times larger than the corresponding limits in existing type checkers. + .stack_size(16 * 1024 * 1024) .build_global() .unwrap(); } diff --git a/crates/ty/tests/cli.rs b/crates/ty/tests/cli.rs index fd46ca323c8934..51cf009ea95e96 100644 --- a/crates/ty/tests/cli.rs +++ b/crates/ty/tests/cli.rs @@ -1,6 +1,7 @@ use anyhow::Context; use insta::internals::SettingsBindDropGuard; use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; +use std::fmt::Write; use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::TempDir; @@ -1195,6 +1196,42 @@ fn concise_revealed_type() -> anyhow::Result<()> { Ok(()) } +#[test] +fn can_handle_large_binop_expressions() -> anyhow::Result<()> { + let mut content = String::new(); + writeln!( + &mut content, + " + from typing_extensions import reveal_type + total = 1{plus_one_repeated} + reveal_type(total) + ", + plus_one_repeated = " + 1".repeat(2000 - 1) + )?; + + let case = TestCase::with_file("test.py", &ruff_python_trivia::textwrap::dedent(&content))?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + info: revealed-type: Revealed type + --> test.py:4:1 + | + 2 | from typing_extensions import reveal_type + 3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1... + 4 | reveal_type(total) + | ^^^^^^^^^^^^^^^^^^ `Literal[2000]` + | + + Found 1 diagnostic + + ----- stderr ----- + "); + + Ok(()) +} + struct TestCase { _temp_dir: TempDir, _settings_scope: SettingsBindDropGuard, From 9a6633da0b4cb84ab6359a811adf91a47b8c5963 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 5 May 2025 21:50:13 +0200 Subject: [PATCH 0264/1161] [ty] ecosystem: activate running on 'sympy' (#17870) ## Summary Following #17869, we can now run `ty` on `sympy`. --- crates/ty_python_semantic/resources/primer/bad.txt | 3 +-- crates/ty_python_semantic/resources/primer/good.txt | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/primer/bad.txt b/crates/ty_python_semantic/resources/primer/bad.txt index 41343cf57bd3c5..5876cdff472e7c 100644 --- a/crates/ty_python_semantic/resources/primer/bad.txt +++ b/crates/ty_python_semantic/resources/primer/bad.txt @@ -9,7 +9,7 @@ discord.py # some kind of hang, only when multi-threaded? freqtrade # cycle panics (try_metaclass_) hydpy # cycle panics (try_metaclass_) ibis # cycle panics (try_metaclass_) -manticore # stack overflow +manticore # stack overflow, see https://github.com/astral-sh/ruff/issues/17863 pandas # slow pandas-stubs # cycle panics (try_metaclass_) pandera # cycle panics (try_metaclass_) @@ -21,5 +21,4 @@ scikit-learn # success, but mypy-primer hangs processing the output spack # success, but mypy-primer hangs processing the output spark # cycle panics (try_metaclass_) steam.py # cycle panics (try_metaclass_), often hangs when multi-threaded -sympy # stack overflow xarray # cycle panics (try_metaclass_) diff --git a/crates/ty_python_semantic/resources/primer/good.txt b/crates/ty_python_semantic/resources/primer/good.txt index 0f6b92129bbc11..7597d403fc3531 100644 --- a/crates/ty_python_semantic/resources/primer/good.txt +++ b/crates/ty_python_semantic/resources/primer/good.txt @@ -105,6 +105,7 @@ static-frame stone strawberry streamlit +sympy tornado trio twine From 47e3aa40b3377b8cae107b53241ecb348429c2d7 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 5 May 2025 17:17:36 -0400 Subject: [PATCH 0265/1161] [ty] Specialize bound methods and nominal instances (#17865) Fixes https://github.com/astral-sh/ruff/pull/17832#issuecomment-2851224968. We had a comment that we did not need to apply specializations to generic aliases, or to the bound `self` of a bound method, because they were already specialized. But they might be specialized with a type variable, which _does_ need to be specialized, in the case of a "multi-step" specialization, such as: ```py class LinkedList[T]: ... class C[U]: def method(self) -> LinkedList[U]: return LinkedList[U]() ``` --------- Co-authored-by: Alex Waygood --- .../resources/mdtest/decorators.md | 2 +- .../mdtest/generics/legacy/classes.md | 34 +++++++++++++++++++ .../mdtest/generics/pep695/classes.md | 29 ++++++++++++++++ crates/ty_python_semantic/src/types.rs | 17 +++------- crates/ty_python_semantic/src/types/class.rs | 26 ++++++++++++++ .../ty_python_semantic/src/types/instance.rs | 11 ++++++ 6 files changed, 106 insertions(+), 13 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md index 923bf2e3eb8233..2c947bd734be17 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators.md @@ -145,7 +145,7 @@ def f(x: int) -> int: return x**2 # TODO: Should be `_lru_cache_wrapper[int]` -reveal_type(f) # revealed: _lru_cache_wrapper[_T] +reveal_type(f) # revealed: _lru_cache_wrapper[Unknown] # TODO: Should be `int` reveal_type(f(1)) # revealed: Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index cecf23de0a24f7..03e281d03c4070 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -361,6 +361,40 @@ c: C[int] = C[int]() reveal_type(c.method("string")) # revealed: Literal["string"] ``` +## Specializations propagate + +In a specialized generic alias, the specialization is applied to the attributes and methods of the +class. + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class LinkedList(Generic[T]): ... + +class C(Generic[T, U]): + x: T + y: U + + def method1(self) -> T: + return self.x + + def method2(self) -> U: + return self.y + + def method3(self) -> LinkedList[T]: + return LinkedList[T]() + +c = C[int, str]() +reveal_type(c.x) # revealed: int +reveal_type(c.y) # revealed: str +reveal_type(c.method1()) # revealed: int +reveal_type(c.method2()) # revealed: str +reveal_type(c.method3()) # revealed: LinkedList[int] +``` + ## Cyclic class definitions ### F-bounded quantification diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 948ec47f4496e9..736687d4207d30 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -305,6 +305,35 @@ c: C[int] = C[int]() reveal_type(c.method("string")) # revealed: Literal["string"] ``` +## Specializations propagate + +In a specialized generic alias, the specialization is applied to the attributes and methods of the +class. + +```py +class LinkedList[T]: ... + +class C[T, U]: + x: T + y: U + + def method1(self) -> T: + return self.x + + def method2(self) -> U: + return self.y + + def method3(self) -> LinkedList[T]: + return LinkedList[T]() + +c = C[int, str]() +reveal_type(c.x) # revealed: int +reveal_type(c.y) # revealed: str +reveal_type(c.method1()) # revealed: int +reveal_type(c.method2()) # revealed: str +reveal_type(c.method3()) # revealed: LinkedList[int] +``` + ## Cyclic class definitions ### F-bounded quantification diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index ed8f2e5496a548..cf991b7d03c541 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4962,20 +4962,16 @@ impl<'db> Type<'db> { Type::FunctionLiteral(function.apply_specialization(db, specialization)) } - // Note that we don't need to apply the specialization to `self_instance`, since it - // must either be a non-generic class literal (which cannot have any typevars to - // specialize) or a generic alias (which has already been fully specialized). For a - // generic alias, the specialization being applied here must be for some _other_ - // generic context nested within the generic alias's class literal, which the generic - // alias's context cannot refer to. (The _method_ does need to be specialized, since it - // might be a nested generic method, whose generic context is what is now being - // specialized.) Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new( db, method.function(db).apply_specialization(db, specialization), - method.self_instance(db), + method.self_instance(db).apply_specialization(db, specialization), )), + Type::NominalInstance(instance) => Type::NominalInstance( + instance.apply_specialization(db, specialization), + ), + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet( function.apply_specialization(db, specialization), @@ -5060,9 +5056,6 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::SliceLiteral(_) | Type::BoundSuper(_) - // `NominalInstance` contains a ClassType, which has already been specialized if needed, - // like above with BoundMethod's self_instance. - | Type::NominalInstance(_) // Same for `ProtocolInstance` | Type::ProtocolInstance(_) | Type::KnownInstance(_) => self, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 039e818191a87b..41303cce94ad61 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -146,6 +146,19 @@ impl<'db> GenericAlias<'db> { pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { self.origin(db).definition(db) } + + pub(super) fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { + Self::new( + db, + self.origin(db), + self.specialization(db) + .apply_specialization(db, specialization), + ) + } } impl<'db> From> for Type<'db> { @@ -210,6 +223,19 @@ impl<'db> ClassType<'db> { self.is_known(db, KnownClass::Object) } + pub(super) fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { + match self { + Self::NonGeneric(_) => self, + Self::Generic(generic) => { + Self::Generic(generic.apply_specialization(db, specialization)) + } + } + } + /// Iterate over the [method resolution order] ("MRO") of the class. /// /// If the MRO could not be accurately resolved, this method falls back to iterating diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 762dc9a34149f7..e09ad686a5ef23 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -3,6 +3,7 @@ use super::protocol_class::ProtocolInterface; use super::{ClassType, KnownClass, SubclassOfType, Type}; use crate::symbol::{Symbol, SymbolAndQualifiers}; +use crate::types::generics::Specialization; use crate::Db; pub(super) use synthesized_protocol::SynthesizedProtocolType; @@ -111,6 +112,16 @@ impl<'db> NominalInstanceType<'db> { pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { SubclassOfType::from(db, self.class) } + + pub(super) fn apply_specialization( + self, + db: &'db dyn Db, + specialization: Specialization<'db>, + ) -> Self { + Self { + class: self.class.apply_specialization(db, specialization), + } + } } impl<'db> From> for Type<'db> { From bb6c7cad0731905dd9a11dd1e22b1a85089ab2d8 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 5 May 2025 22:44:59 +0100 Subject: [PATCH 0266/1161] [ty] Fix false-positive `[invalid-return-type]` diagnostics on generator functions (#17871) --- .../resources/mdtest/function/return_type.md | 54 ++++++++++++ ...ion_return_type_-_Generator_functions.snap | 82 +++++++++++++++++++ .../ty_python_semantic/src/semantic_index.rs | 3 + .../src/semantic_index/builder.rs | 14 ++++ .../src/semantic_index/symbol.rs | 6 +- crates/ty_python_semantic/src/types/class.rs | 20 ++++- .../src/types/diagnostic.rs | 39 ++++++++- crates/ty_python_semantic/src/types/infer.rs | 57 +++++++++---- 8 files changed, 253 insertions(+), 22 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions.snap diff --git a/crates/ty_python_semantic/resources/mdtest/function/return_type.md b/crates/ty_python_semantic/resources/mdtest/function/return_type.md index 8e83d036543e5e..c5d8e77c63ed6f 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/return_type.md +++ b/crates/ty_python_semantic/resources/mdtest/function/return_type.md @@ -340,3 +340,57 @@ def f() -> int: def f(cond: bool) -> str: return "hello" if cond else NotImplemented ``` + +## Generator functions + + + +A function with a `yield` statement anywhere in its body is a +[generator function](https://docs.python.org/3/glossary.html#term-generator). A generator function +implicitly returns an instance of `types.GeneratorType` even if it does not contain any `return` +statements. + +```py +import types +import typing + +def f() -> types.GeneratorType: + yield 42 + +def g() -> typing.Generator: + yield 42 + +def h() -> typing.Iterator: + yield 42 + +def i() -> typing.Iterable: + yield 42 + +def j() -> str: # error: [invalid-return-type] + yield 42 +``` + +If it is an `async` function with a `yield` statement in its body, it is an +[asynchronous generator function](https://docs.python.org/3/glossary.html#term-asynchronous-generator). +An asynchronous generator function implicitly returns an instance of `types.AsyncGeneratorType` even +if it does not contain any `return` statements. + +```py +import types +import typing + +async def f() -> types.AsyncGeneratorType: + yield 42 + +async def g() -> typing.AsyncGenerator: + yield 42 + +async def h() -> typing.AsyncIterator: + yield 42 + +async def i() -> typing.AsyncIterable: + yield 42 + +async def j() -> str: # error: [invalid-return-type] + yield 42 +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions.snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions.snap new file mode 100644 index 00000000000000..5e4ead93ed7d99 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions.snap @@ -0,0 +1,82 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: return_type.md - Function return type - Generator functions +mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | import types + 2 | import typing + 3 | + 4 | def f() -> types.GeneratorType: + 5 | yield 42 + 6 | + 7 | def g() -> typing.Generator: + 8 | yield 42 + 9 | +10 | def h() -> typing.Iterator: +11 | yield 42 +12 | +13 | def i() -> typing.Iterable: +14 | yield 42 +15 | +16 | def j() -> str: # error: [invalid-return-type] +17 | yield 42 +18 | import types +19 | import typing +20 | +21 | async def f() -> types.AsyncGeneratorType: +22 | yield 42 +23 | +24 | async def g() -> typing.AsyncGenerator: +25 | yield 42 +26 | +27 | async def h() -> typing.AsyncIterator: +28 | yield 42 +29 | +30 | async def i() -> typing.AsyncIterable: +31 | yield 42 +32 | +33 | async def j() -> str: # error: [invalid-return-type] +34 | yield 42 +``` + +# Diagnostics + +``` +error: lint:invalid-return-type: Return type does not match returned value + --> src/mdtest_snippet.py:16:12 + | +14 | yield 42 +15 | +16 | def j() -> str: # error: [invalid-return-type] + | ^^^ Expected `str`, found `types.GeneratorType` +17 | yield 42 +18 | import types + | +info: Function is inferred as returning `types.GeneratorType` because it is a generator function +info: See https://docs.python.org/3/glossary.html#term-generator for more details + +``` + +``` +error: lint:invalid-return-type: Return type does not match returned value + --> src/mdtest_snippet.py:33:18 + | +31 | yield 42 +32 | +33 | async def j() -> str: # error: [invalid-return-type] + | ^^^ Expected `str`, found `types.AsyncGeneratorType` +34 | yield 42 + | +info: Function is inferred as returning `types.AsyncGeneratorType` because it is an async generator function +info: See https://docs.python.org/3/glossary.html#term-asynchronous-generator for more details + +``` diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index baff5384f9e0b7..67f98453260b17 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -194,6 +194,9 @@ pub(crate) struct SemanticIndex<'db> { /// List of all semantic syntax errors in this file. semantic_syntax_errors: Vec, + + /// Set of all generator functions in this file. + generator_functions: FxHashSet, } impl<'db> SemanticIndex<'db> { diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 4178d4d09ed58f..215e3fbb42d0d0 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -109,6 +109,10 @@ pub(super) struct SemanticIndexBuilder<'db> { definitions_by_node: FxHashMap>, expressions_by_node: FxHashMap>, imported_modules: FxHashSet, + /// Hashset of all [`FileScopeId`]s that correspond to [generator functions]. + /// + /// [generator functions]: https://docs.python.org/3/glossary.html#term-generator + generator_functions: FxHashSet, eager_bindings: FxHashMap, /// Errors collected by the `semantic_checker`. semantic_syntax_errors: RefCell>, @@ -142,6 +146,7 @@ impl<'db> SemanticIndexBuilder<'db> { expressions_by_node: FxHashMap::default(), imported_modules: FxHashSet::default(), + generator_functions: FxHashSet::default(), eager_bindings: FxHashMap::default(), @@ -1081,6 +1086,7 @@ impl<'db> SemanticIndexBuilder<'db> { self.scope_ids_by_scope.shrink_to_fit(); self.scopes_by_node.shrink_to_fit(); self.eager_bindings.shrink_to_fit(); + self.generator_functions.shrink_to_fit(); SemanticIndex { symbol_tables, @@ -1097,6 +1103,7 @@ impl<'db> SemanticIndexBuilder<'db> { has_future_annotations: self.has_future_annotations, eager_bindings: self.eager_bindings, semantic_syntax_errors: self.semantic_syntax_errors.into_inner(), + generator_functions: self.generator_functions, } } @@ -2305,6 +2312,13 @@ where walk_expr(self, expr); } + ast::Expr::Yield(_) => { + let scope = self.current_scope(); + if self.scopes[scope].kind() == ScopeKind::Function { + self.generator_functions.insert(scope); + } + walk_expr(self, expr); + } _ => { walk_expr(self, expr); } diff --git a/crates/ty_python_semantic/src/semantic_index/symbol.rs b/crates/ty_python_semantic/src/semantic_index/symbol.rs index d8f8fd026ce373..66d35ab363738e 100644 --- a/crates/ty_python_semantic/src/semantic_index/symbol.rs +++ b/crates/ty_python_semantic/src/semantic_index/symbol.rs @@ -13,7 +13,7 @@ use rustc_hash::FxHasher; use crate::ast_node_ref::AstNodeRef; use crate::node_key::NodeKey; use crate::semantic_index::visibility_constraints::ScopedVisibilityConstraintId; -use crate::semantic_index::{semantic_index, SymbolMap}; +use crate::semantic_index::{semantic_index, SemanticIndex, SymbolMap}; use crate::Db; #[derive(Eq, PartialEq, Debug)] @@ -170,6 +170,10 @@ impl FileScopeId { let index = semantic_index(db, file); index.scope_ids_by_scope[self] } + + pub(crate) fn is_generator_function(self, index: &SemanticIndex) -> bool { + index.generator_functions.contains(&self) + } } #[derive(Debug, salsa::Update)] diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 41303cce94ad61..30f24abe956f63 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1865,6 +1865,8 @@ pub enum KnownClass { MethodWrapperType, WrapperDescriptorType, UnionType, + GeneratorType, + AsyncGeneratorType, // Typeshed NoneType, // Part of `types` for Python >= 3.10 // Typing @@ -1929,6 +1931,8 @@ impl<'db> KnownClass { | Self::Super | Self::WrapperDescriptorType | Self::UnionType + | Self::GeneratorType + | Self::AsyncGeneratorType | Self::MethodWrapperType => Truthiness::AlwaysTrue, Self::NoneType => Truthiness::AlwaysFalse, @@ -2013,6 +2017,8 @@ impl<'db> KnownClass { | Self::BaseExceptionGroup | Self::Classmethod | Self::GenericAlias + | Self::GeneratorType + | Self::AsyncGeneratorType | Self::ModuleType | Self::FunctionType | Self::MethodType @@ -2075,6 +2081,8 @@ impl<'db> KnownClass { Self::UnionType => "UnionType", Self::MethodWrapperType => "MethodWrapperType", Self::WrapperDescriptorType => "WrapperDescriptorType", + Self::GeneratorType => "GeneratorType", + Self::AsyncGeneratorType => "AsyncGeneratorType", Self::NamedTuple => "NamedTuple", Self::NoneType => "NoneType", Self::SpecialForm => "_SpecialForm", @@ -2116,7 +2124,7 @@ impl<'db> KnownClass { } } - fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { + pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { struct KnownClassDisplay<'db> { db: &'db dyn Db, class: KnownClass, @@ -2293,6 +2301,8 @@ impl<'db> KnownClass { | Self::ModuleType | Self::FunctionType | Self::MethodType + | Self::GeneratorType + | Self::AsyncGeneratorType | Self::MethodWrapperType | Self::UnionType | Self::WrapperDescriptorType => KnownModule::Types, @@ -2374,6 +2384,8 @@ impl<'db> KnownClass { | Self::GenericAlias | Self::ModuleType | Self::FunctionType + | Self::GeneratorType + | Self::AsyncGeneratorType | Self::MethodType | Self::MethodWrapperType | Self::WrapperDescriptorType @@ -2434,6 +2446,8 @@ impl<'db> KnownClass { | Self::MethodType | Self::MethodWrapperType | Self::WrapperDescriptorType + | Self::GeneratorType + | Self::AsyncGeneratorType | Self::SpecialForm | Self::ChainMap | Self::Counter @@ -2491,6 +2505,8 @@ impl<'db> KnownClass { "GenericAlias" => Self::GenericAlias, "NoneType" => Self::NoneType, "ModuleType" => Self::ModuleType, + "GeneratorType" => Self::GeneratorType, + "AsyncGeneratorType" => Self::AsyncGeneratorType, "FunctionType" => Self::FunctionType, "MethodType" => Self::MethodType, "UnionType" => Self::UnionType, @@ -2574,6 +2590,8 @@ impl<'db> KnownClass { | Self::Super | Self::NotImplementedType | Self::UnionType + | Self::GeneratorType + | Self::AsyncGeneratorType | Self::WrapperDescriptorType => module == self.canonical_module(db), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), Self::SpecialForm diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 3ace0181d0e7f1..5e62790a808f87 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -1,5 +1,5 @@ use super::context::InferContext; -use super::ClassLiteral; +use super::{ClassLiteral, KnownClass}; use crate::db::Db; use crate::declare_lint; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; @@ -12,7 +12,7 @@ use crate::types::string_annotation::{ use crate::types::{protocol_class::ProtocolClassLiteral, KnownFunction, KnownInstanceType, Type}; use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic}; use ruff_python_ast::{self as ast, AnyNodeRef}; -use ruff_text_size::Ranged; +use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use std::fmt::Formatter; @@ -1307,6 +1307,41 @@ pub(super) fn report_invalid_return_type( ); } +pub(super) fn report_invalid_generator_function_return_type( + context: &InferContext, + return_type_range: TextRange, + inferred_return: KnownClass, + expected_ty: Type, +) { + let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, return_type_range) else { + return; + }; + + let mut diag = builder.into_diagnostic("Return type does not match returned value"); + let inferred_ty = inferred_return.display(context.db()); + diag.set_primary_message(format_args!( + "Expected `{expected_ty}`, found `{inferred_ty}`", + expected_ty = expected_ty.display(context.db()), + )); + + let (description, link) = if inferred_return == KnownClass::AsyncGeneratorType { + ( + "an async generator function", + "https://docs.python.org/3/glossary.html#term-asynchronous-generator", + ) + } else { + ( + "a generator function", + "https://docs.python.org/3/glossary.html#term-generator", + ) + }; + + diag.info(format_args!( + "Function is inferred as returning `{inferred_ty}` because it is {description}" + )); + diag.info(format_args!("See {link} for more details")); +} + pub(super) fn report_implicit_return_type( context: &InferContext, range: impl Ranged, diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index ba08e405454fe6..cc4745b768833a 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -68,12 +68,12 @@ use crate::types::class::MetaclassErrorKind; use crate::types::diagnostic::{ report_implicit_return_type, report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable, report_invalid_assignment, - report_invalid_attribute_assignment, report_invalid_return_type, - report_possibly_unbound_attribute, TypeCheckDiagnostics, CALL_NON_CALLABLE, - CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, - CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, INCONSISTENT_MRO, - INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, - INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE, + report_invalid_attribute_assignment, report_invalid_generator_function_return_type, + report_invalid_return_type, report_possibly_unbound_attribute, TypeCheckDiagnostics, + CALL_NON_CALLABLE, CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, + CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, + INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, + INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR, @@ -1611,11 +1611,7 @@ impl<'db> TypeInferenceBuilder<'db> { } self.infer_body(&function.body); - if let Some(declared_ty) = function - .returns - .as_deref() - .map(|ret| self.file_expression_type(ret)) - { + if let Some(returns) = function.returns.as_deref() { fn is_stub_suite(suite: &[ast::Stmt]) -> bool { match suite { [ast::Stmt::Expr(ast::StmtExpr { value: first, .. }), ast::Stmt::Expr(ast::StmtExpr { value: second, .. }), ..] => { @@ -1641,6 +1637,36 @@ impl<'db> TypeInferenceBuilder<'db> { return; } + let declared_ty = self.file_expression_type(returns); + + let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function)); + if scope_id.is_generator_function(self.index) { + // TODO: `AsyncGeneratorType` and `GeneratorType` are both generic classes. + // + // If type arguments are supplied to `(Async)Iterable`, `(Async)Iterator`, + // `(Async)Generator` or `(Async)GeneratorType` in the return annotation, + // we should iterate over the `yield` expressions and `return` statements in the function + // to check that they are consistent with the type arguments provided. + let inferred_return = if function.is_async { + KnownClass::AsyncGeneratorType + } else { + KnownClass::GeneratorType + }; + + if !inferred_return + .to_instance(self.db()) + .is_assignable_to(self.db(), declared_ty) + { + report_invalid_generator_function_return_type( + &self.context, + returns.range(), + inferred_return, + declared_ty, + ); + } + return; + } + for invalid in self .return_types_and_ranges .iter() @@ -1660,23 +1686,18 @@ impl<'db> TypeInferenceBuilder<'db> { report_invalid_return_type( &self.context, invalid.range, - function.returns.as_ref().unwrap().range(), + returns.range(), declared_ty, invalid.ty, ); } - let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function)); let use_def = self.index.use_def_map(scope_id); if use_def.can_implicit_return(self.db()) && !KnownClass::NoneType .to_instance(self.db()) .is_assignable_to(self.db(), declared_ty) { - report_implicit_return_type( - &self.context, - function.returns.as_ref().unwrap().range(), - declared_ty, - ); + report_implicit_return_type(&self.context, returns.range(), declared_ty); } } } From ada4c4cb1fd580ed15b51aea97002a897b0784c3 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 5 May 2025 18:29:30 -0400 Subject: [PATCH 0267/1161] [ty] Don't require default typevars when specializing (#17872) If a typevar is declared as having a default, we shouldn't require a type to be specified for that typevar when explicitly specializing a generic class: ```py class WithDefault[T, U = int]: ... reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] ``` --------- Co-authored-by: Alex Waygood --- .../mdtest/generics/legacy/classes.md | 11 +++++++++++ .../mdtest/generics/pep695/classes.md | 9 +++++++++ .../ty_python_semantic/src/types/call/bind.rs | 17 +++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 3 +++ crates/ty_python_semantic/src/types/infer.rs | 18 ++++++------------ 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 03e281d03c4070..b078d38d0ec937 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -150,6 +150,17 @@ reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str] reveal_type(Constrained[object]()) # revealed: Unknown ``` +If the type variable has a default, it can be omitted: + +```py +WithDefaultU = TypeVar("WithDefaultU", default=int) + +class WithDefault(Generic[T, WithDefaultU]): ... + +reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] +reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] +``` + ## Inferring generic class parameters We can infer the type parameter from a type context: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 736687d4207d30..39bb9cfb6294ea 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -133,6 +133,15 @@ reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str] reveal_type(Constrained[object]()) # revealed: Unknown ``` +If the type variable has a default, it can be omitted: + +```py +class WithDefault[T, U = int]: ... + +reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str] +reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] +``` + ## Inferring generic class parameters We can infer the type parameter from a type context: diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index d6068e8f55790b..3f93985a43d99f 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1316,10 +1316,27 @@ impl<'db> Binding<'db> { self.inherited_specialization } + /// Returns the bound types for each parameter, in parameter source order, or `None` if no + /// argument was matched to that parameter. pub(crate) fn parameter_types(&self) -> &[Option>] { &self.parameter_tys } + /// Returns the bound types for each parameter, in parameter source order, with default values + /// applied for arguments that weren't matched to a parameter. Returns `None` if there are any + /// non-default arguments that weren't matched to a parameter. + pub(crate) fn parameter_types_with_defaults( + &self, + signature: &Signature<'db>, + ) -> Option]>> { + signature + .parameters() + .iter() + .zip(&self.parameter_tys) + .map(|(parameter, parameter_ty)| parameter_ty.or(parameter.default_type())) + .collect() + } + pub(crate) fn arguments_for_parameter<'a>( &'a self, argument_types: &'a CallArgumentTypes<'a, 'db>, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 0210cd2062b9d8..a436f880afadf2 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -123,6 +123,9 @@ impl<'db> GenericContext<'db> { } None => {} } + if let Some(default_ty) = typevar.default_ty(db) { + parameter = parameter.with_default_type(default_ty); + } parameter } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index cc4745b768833a..87d9a44fb744c4 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -6711,10 +6711,8 @@ impl<'db> TypeInferenceBuilder<'db> { } _ => CallArgumentTypes::positional([self.infer_type_expression(slice_node)]), }; - let signatures = Signatures::single(CallableSignature::single( - value_ty, - generic_context.signature(self.db()), - )); + let signature = generic_context.signature(self.db()); + let signatures = Signatures::single(CallableSignature::single(value_ty, signature.clone())); let bindings = match Bindings::match_parameters(signatures, &call_argument_types) .check_types(self.db(), &call_argument_types) { @@ -6732,14 +6730,10 @@ impl<'db> TypeInferenceBuilder<'db> { .matching_overloads() .next() .expect("valid bindings should have matching overload"); - let specialization = generic_context.specialize( - self.db(), - overload - .parameter_types() - .iter() - .map(|ty| ty.unwrap_or(Type::unknown())) - .collect(), - ); + let parameters = overload + .parameter_types_with_defaults(&signature) + .expect("matching overload should not have missing arguments"); + let specialization = generic_context.specialize(self.db(), parameters); Type::from(GenericAlias::new(self.db(), generic_class, specialization)) } From a4c8e43c5fe008633985d491de9e8b34230fd795 Mon Sep 17 00:00:00 2001 From: yunchi Date: Mon, 5 May 2025 16:13:04 -0700 Subject: [PATCH 0268/1161] [`pylint`] Add fix safety section (`PLR1722`) (#17826) parent: #15584 fix introduced at: #816 --- .../src/rules/pylint/rules/sys_exit_alias.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs b/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs index be59c46c5b771d..1d56beeab6ebff 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/sys_exit_alias.rs @@ -19,6 +19,20 @@ use ruff_text_size::Ranged; /// Prefer `sys.exit()`, as the `sys` module is guaranteed to exist in all /// contexts. /// +/// ## Fix safety +/// This fix is always unsafe. When replacing `exit` or `quit` with `sys.exit`, +/// the behavior can change in the following ways: +/// +/// 1. If the code runs in an environment where the `site` module is not imported +/// (e.g., with `python -S`), the original code would raise a `NameError`, while +/// the fixed code would execute normally. +/// +/// 2. `site.exit` and `sys.exit` handle tuple arguments differently. `site.exit` +/// treats tuples as regular objects and always returns exit code 1, while `sys.exit` +/// interprets tuple contents to determine the exit code: an empty tuple () results in +/// exit code 0, and a single-element tuple like (2,) uses that element's value (2) as +/// the exit code. +/// /// ## Example /// ```python /// if __name__ == "__main__": From fd76d70a319f4ae75ed0eea59fe0ee9457827ca8 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Tue, 6 May 2025 08:28:42 +0900 Subject: [PATCH 0269/1161] [red-knot] fix narrowing in nested scopes (#17630) ## Summary This PR fixes #17595. ## Test Plan New test cases are added to `mdtest/narrow/conditionals/nested.md`. --------- Co-authored-by: Carl Meyer --- .../mdtest/narrow/conditionals/nested.md | 132 +++++++++++++++++ .../ty_python_semantic/src/semantic_index.rs | 36 ++--- .../src/semantic_index/builder.rs | 38 +++-- .../semantic_index/narrowing_constraints.rs | 7 + .../src/semantic_index/use_def.rs | 139 +++++++++++++----- .../semantic_index/use_def/symbol_state.rs | 56 ++++++- crates/ty_python_semantic/src/symbol.rs | 22 +-- crates/ty_python_semantic/src/types/infer.rs | 97 +++++++++--- crates/ty_python_semantic/src/types/narrow.rs | 11 +- 9 files changed, 414 insertions(+), 124 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md index ab026cef6733ee..6b001ea09411a0 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md @@ -42,3 +42,135 @@ def _(flag1: bool, flag2: bool): else: reveal_type(x) # revealed: Never ``` + +## Cross-scope narrowing + +Narrowing constraints are also valid in eager nested scopes (however, because class variables are +not visible from nested scopes, constraints on those variables are invalid). + +Currently they are assumed to be invalid in lazy nested scopes since there is a possibility that the +constraints may no longer be valid due to a "time lag". However, it may be possible to determine +that some of them are valid by performing a more detailed analysis (e.g. checking that the narrowing +target has not changed in all places where the function is called). + +### Narrowing constraints introduced in eager nested scopes + +```py +g: str | None = "a" + +def f(x: str | None): + def _(): + if x is not None: + reveal_type(x) # revealed: str + + if not isinstance(x, str): + reveal_type(x) # revealed: None + + if g is not None: + reveal_type(g) # revealed: str + + class C: + if x is not None: + reveal_type(x) # revealed: str + + if not isinstance(x, str): + reveal_type(x) # revealed: None + + if g is not None: + reveal_type(g) # revealed: str + + # TODO: should be str + # This could be fixed if we supported narrowing with if clauses in comprehensions. + [reveal_type(x) for _ in range(1) if x is not None] # revealed: str | None +``` + +### Narrowing constraints introduced in the outer scope + +```py +g: str | None = "a" + +def f(x: str | None): + if x is not None: + def _(): + # If there is a possibility that `x` may be rewritten after this function definition, + # the constraint `x is not None` outside the function is no longer be applicable for narrowing. + reveal_type(x) # revealed: str | None + + class C: + reveal_type(x) # revealed: str + + [reveal_type(x) for _ in range(1)] # revealed: str + + if g is not None: + def _(): + reveal_type(g) # revealed: str | None + + class D: + reveal_type(g) # revealed: str + + [reveal_type(g) for _ in range(1)] # revealed: str +``` + +### Narrowing constraints introduced in multiple scopes + +```py +from typing import Literal + +g: str | Literal[1] | None = "a" + +def f(x: str | Literal[1] | None): + class C: + if x is not None: + def _(): + if x != 1: + reveal_type(x) # revealed: str | None + + class D: + if x != 1: + reveal_type(x) # revealed: str + + # TODO: should be str + [reveal_type(x) for _ in range(1) if x != 1] # revealed: str | Literal[1] + + if g is not None: + def _(): + if g != 1: + reveal_type(g) # revealed: str | None + + class D: + if g != 1: + reveal_type(g) # revealed: str +``` + +### Narrowing constraints with bindings in class scope, and nested scopes + +```py +from typing import Literal + +g: str | Literal[1] | None = "a" + +def f(flag: bool): + class C: + (g := None) if flag else (g := None) + # `g` is always bound here, so narrowing checks don't apply to nested scopes + if g is not None: + class F: + reveal_type(g) # revealed: str | Literal[1] | None + + class C: + # this conditional binding leaves "unbound" visible, so following narrowing checks apply + None if flag else (g := None) + + if g is not None: + class F: + reveal_type(g) # revealed: str | Literal[1] + + # This class variable is not visible from the nested class scope. + g = None + + # This additional constraint is not relevant to nested scopes, since it only applies to + # a binding of `g` that they cannot see: + if g is None: + class E: + reveal_type(g) # revealed: str | Literal[1] +``` diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index 67f98453260b17..42956f0bfe7851 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -17,18 +17,19 @@ use crate::semantic_index::ast_ids::AstIds; use crate::semantic_index::builder::SemanticIndexBuilder; use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions}; use crate::semantic_index::expression::Expression; +use crate::semantic_index::narrowing_constraints::ScopedNarrowingConstraint; use crate::semantic_index::symbol::{ FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId, SymbolTable, }; -use crate::semantic_index::use_def::{EagerBindingsKey, ScopedEagerBindingsId, UseDefMap}; +use crate::semantic_index::use_def::{EagerSnapshotKey, ScopedEagerSnapshotId, UseDefMap}; use crate::Db; pub mod ast_ids; mod builder; pub mod definition; pub mod expression; -mod narrowing_constraints; +pub(crate) mod narrowing_constraints; pub(crate) mod predicate; mod re_exports; pub mod symbol; @@ -141,8 +142,9 @@ pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> { FileScopeId::global().to_scope_id(db, file) } -pub(crate) enum EagerBindingsResult<'map, 'db> { - Found(BindingWithConstraintsIterator<'map, 'db>), +pub(crate) enum EagerSnapshotResult<'map, 'db> { + FoundConstraint(ScopedNarrowingConstraint), + FoundBindings(BindingWithConstraintsIterator<'map, 'db>), NotFound, NoLongerInEagerContext, } @@ -189,8 +191,8 @@ pub(crate) struct SemanticIndex<'db> { /// Flags about the global scope (code usage impacting inference) has_future_annotations: bool, - /// Map of all of the eager bindings that appear in this file. - eager_bindings: FxHashMap, + /// Map of all of the eager snapshots that appear in this file. + eager_snapshots: FxHashMap, /// List of all semantic syntax errors in this file. semantic_syntax_errors: Vec, @@ -390,36 +392,34 @@ impl<'db> SemanticIndex<'db> { /// * `NoLongerInEagerContext` if the nested scope is no longer in an eager context /// (that is, not every scope that will be traversed is eager). /// * an iterator of bindings for a particular nested eager scope reference if the bindings exist. - /// * `NotFound` if the bindings do not exist in the nested eager scope. - pub(crate) fn eager_bindings( + /// * a narrowing constraint if there are no bindings, but there is a narrowing constraint for an outer scope symbol. + /// * `NotFound` if the narrowing constraint / bindings do not exist in the nested eager scope. + pub(crate) fn eager_snapshot( &self, enclosing_scope: FileScopeId, symbol: &str, nested_scope: FileScopeId, - ) -> EagerBindingsResult<'_, 'db> { + ) -> EagerSnapshotResult<'_, 'db> { for (ancestor_scope_id, ancestor_scope) in self.ancestor_scopes(nested_scope) { if ancestor_scope_id == enclosing_scope { break; } if !ancestor_scope.is_eager() { - return EagerBindingsResult::NoLongerInEagerContext; + return EagerSnapshotResult::NoLongerInEagerContext; } } let Some(symbol_id) = self.symbol_tables[enclosing_scope].symbol_id_by_name(symbol) else { - return EagerBindingsResult::NotFound; + return EagerSnapshotResult::NotFound; }; - let key = EagerBindingsKey { + let key = EagerSnapshotKey { enclosing_scope, enclosing_symbol: symbol_id, nested_scope, }; - let Some(id) = self.eager_bindings.get(&key) else { - return EagerBindingsResult::NotFound; + let Some(id) = self.eager_snapshots.get(&key) else { + return EagerSnapshotResult::NotFound; }; - match self.use_def_maps[enclosing_scope].eager_bindings(*id) { - Some(bindings) => EagerBindingsResult::Found(bindings), - None => EagerBindingsResult::NotFound, - } + self.use_def_maps[enclosing_scope].eager_snapshot(*id) } pub(crate) fn semantic_syntax_errors(&self) -> &[SemanticSyntaxError] { diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 215e3fbb42d0d0..34fed4fcfe1635 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -42,7 +42,7 @@ use crate::semantic_index::symbol::{ ScopedSymbolId, SymbolTableBuilder, }; use crate::semantic_index::use_def::{ - EagerBindingsKey, FlowSnapshot, ScopedEagerBindingsId, UseDefMapBuilder, + EagerSnapshotKey, FlowSnapshot, ScopedEagerSnapshotId, UseDefMapBuilder, }; use crate::semantic_index::visibility_constraints::{ ScopedVisibilityConstraintId, VisibilityConstraintsBuilder, @@ -113,7 +113,7 @@ pub(super) struct SemanticIndexBuilder<'db> { /// /// [generator functions]: https://docs.python.org/3/glossary.html#term-generator generator_functions: FxHashSet, - eager_bindings: FxHashMap, + eager_snapshots: FxHashMap, /// Errors collected by the `semantic_checker`. semantic_syntax_errors: RefCell>, } @@ -148,7 +148,7 @@ impl<'db> SemanticIndexBuilder<'db> { imported_modules: FxHashSet::default(), generator_functions: FxHashSet::default(), - eager_bindings: FxHashMap::default(), + eager_snapshots: FxHashMap::default(), python_version: Program::get(db).python_version(db), source_text: OnceCell::new(), @@ -253,13 +253,15 @@ impl<'db> SemanticIndexBuilder<'db> { children_start..children_start, reachability, ); + let is_class_scope = scope.kind().is_class(); self.try_node_context_stack_manager.enter_nested_scope(); let file_scope_id = self.scopes.push(scope); self.symbol_tables.push(SymbolTableBuilder::default()); self.instance_attribute_tables .push(SymbolTableBuilder::default()); - self.use_def_maps.push(UseDefMapBuilder::default()); + self.use_def_maps + .push(UseDefMapBuilder::new(is_class_scope)); let ast_id_scope = self.ast_ids.push(AstIdsBuilder::default()); let scope_id = ScopeId::new(self.db, self.file, file_scope_id, countme::Count::default()); @@ -303,12 +305,6 @@ impl<'db> SemanticIndexBuilder<'db> { let enclosing_scope_kind = self.scopes[enclosing_scope_id].kind(); let enclosing_symbol_table = &self.symbol_tables[enclosing_scope_id]; - // Names bound in class scopes are never visible to nested scopes, so we never need to - // save eager scope bindings in a class scope. - if enclosing_scope_kind.is_class() { - continue; - } - for nested_symbol in self.symbol_tables[popped_scope_id].symbols() { // Skip this symbol if this enclosing scope doesn't contain any bindings for it. // Note that even if this symbol is bound in the popped scope, @@ -321,24 +317,26 @@ impl<'db> SemanticIndexBuilder<'db> { continue; }; let enclosing_symbol = enclosing_symbol_table.symbol(enclosing_symbol_id); - if !enclosing_symbol.is_bound() { - continue; - } - // Snapshot the bindings of this symbol that are visible at this point in this + // Snapshot the state of this symbol that are visible at this point in this // enclosing scope. - let key = EagerBindingsKey { + let key = EagerSnapshotKey { enclosing_scope: enclosing_scope_id, enclosing_symbol: enclosing_symbol_id, nested_scope: popped_scope_id, }; - let eager_bindings = self.use_def_maps[enclosing_scope_id] - .snapshot_eager_bindings(enclosing_symbol_id); - self.eager_bindings.insert(key, eager_bindings); + let eager_snapshot = self.use_def_maps[enclosing_scope_id].snapshot_eager_state( + enclosing_symbol_id, + enclosing_scope_kind, + enclosing_symbol.is_bound(), + ); + self.eager_snapshots.insert(key, eager_snapshot); } // Lazy scopes are "sticky": once we see a lazy scope we stop doing lookups // eagerly, even if we would encounter another eager enclosing scope later on. + // Also, narrowing constraints outside a lazy scope are not applicable. + // TODO: If the symbol has never been rewritten, they are applicable. if !enclosing_scope_kind.is_eager() { break; } @@ -1085,8 +1083,8 @@ impl<'db> SemanticIndexBuilder<'db> { self.scope_ids_by_scope.shrink_to_fit(); self.scopes_by_node.shrink_to_fit(); - self.eager_bindings.shrink_to_fit(); self.generator_functions.shrink_to_fit(); + self.eager_snapshots.shrink_to_fit(); SemanticIndex { symbol_tables, @@ -1101,7 +1099,7 @@ impl<'db> SemanticIndexBuilder<'db> { use_def_maps, imported_modules: Arc::new(self.imported_modules), has_future_annotations: self.has_future_annotations, - eager_bindings: self.eager_bindings, + eager_snapshots: self.eager_snapshots, semantic_syntax_errors: self.semantic_syntax_errors.into_inner(), generator_functions: self.generator_functions, } diff --git a/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs b/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs index 6ed80e7ebf2b14..83bfb0d25dbf95 100644 --- a/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/narrowing_constraints.rs @@ -29,6 +29,7 @@ //! [`Predicate`]: crate::semantic_index::predicate::Predicate use crate::list::{List, ListBuilder, ListSetReverseIterator, ListStorage}; +use crate::semantic_index::ast_ids::ScopedUseId; use crate::semantic_index::predicate::ScopedPredicateId; /// A narrowing constraint associated with a live binding. @@ -38,6 +39,12 @@ use crate::semantic_index::predicate::ScopedPredicateId; /// [`Predicate`]: crate::semantic_index::predicate::Predicate pub(crate) type ScopedNarrowingConstraint = List; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ConstraintKey { + NarrowingConstraint(ScopedNarrowingConstraint), + UseId(ScopedUseId), +} + /// One of the [`Predicate`]s in a narrowing constraint, which constraints the type of the /// binding's symbol. /// diff --git a/crates/ty_python_semantic/src/semantic_index/use_def.rs b/crates/ty_python_semantic/src/semantic_index/use_def.rs index c9e62422dbeb38..c53c8eb4687db3 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def.rs @@ -259,25 +259,25 @@ use ruff_index::{newtype_index, IndexVec}; use rustc_hash::FxHashMap; -use self::symbol_state::ScopedDefinitionId; use self::symbol_state::{ - LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, SymbolBindings, - SymbolDeclarations, SymbolState, + EagerSnapshot, LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator, + ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState, }; use crate::node_key::NodeKey; use crate::semantic_index::ast_ids::ScopedUseId; use crate::semantic_index::definition::Definition; use crate::semantic_index::narrowing_constraints::{ - NarrowingConstraints, NarrowingConstraintsBuilder, NarrowingConstraintsIterator, + ConstraintKey, NarrowingConstraints, NarrowingConstraintsBuilder, NarrowingConstraintsIterator, }; use crate::semantic_index::predicate::{ Predicate, Predicates, PredicatesBuilder, ScopedPredicateId, StarImportPlaceholderPredicate, }; -use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId}; +use crate::semantic_index::symbol::{FileScopeId, ScopeKind, ScopedSymbolId}; use crate::semantic_index::visibility_constraints::{ ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder, }; -use crate::types::Truthiness; +use crate::semantic_index::EagerSnapshotResult; +use crate::types::{infer_narrowing_constraint, IntersectionBuilder, Truthiness, Type}; mod symbol_state; @@ -328,7 +328,7 @@ pub(crate) struct UseDefMap<'db> { /// Snapshot of bindings in this scope that can be used to resolve a reference in a nested /// eager scope. - eager_bindings: EagerBindings, + eager_snapshots: EagerSnapshots, /// Whether or not the start of the scope is visible. /// This is used to check if the function can implicitly return `None`. @@ -354,6 +354,22 @@ impl<'db> UseDefMap<'db> { self.bindings_iterator(&self.bindings_by_use[use_id]) } + pub(crate) fn narrowing_constraints_at_use( + &self, + constraint_key: ConstraintKey, + ) -> ConstraintsIterator<'_, 'db> { + let constraint = match constraint_key { + ConstraintKey::NarrowingConstraint(constraint) => constraint, + ConstraintKey::UseId(use_id) => { + self.bindings_by_use[use_id].unbound_narrowing_constraint() + } + }; + ConstraintsIterator { + predicates: &self.predicates, + constraint_ids: self.narrowing_constraints.iter_predicates(constraint), + } + } + pub(super) fn is_reachable( &self, db: &dyn crate::Db, @@ -398,13 +414,19 @@ impl<'db> UseDefMap<'db> { self.bindings_iterator(self.instance_attributes[symbol].bindings()) } - pub(crate) fn eager_bindings( + pub(crate) fn eager_snapshot( &self, - eager_bindings: ScopedEagerBindingsId, - ) -> Option> { - self.eager_bindings - .get(eager_bindings) - .map(|symbol_bindings| self.bindings_iterator(symbol_bindings)) + eager_bindings: ScopedEagerSnapshotId, + ) -> EagerSnapshotResult<'_, 'db> { + match self.eager_snapshots.get(eager_bindings) { + Some(EagerSnapshot::Constraint(constraint)) => { + EagerSnapshotResult::FoundConstraint(*constraint) + } + Some(EagerSnapshot::Bindings(symbol_bindings)) => { + EagerSnapshotResult::FoundBindings(self.bindings_iterator(symbol_bindings)) + } + None => EagerSnapshotResult::NotFound, + } } pub(crate) fn bindings_at_declaration( @@ -489,19 +511,19 @@ impl<'db> UseDefMap<'db> { } } -/// Uniquely identifies a snapshot of bindings that can be used to resolve a reference in a nested -/// eager scope. +/// Uniquely identifies a snapshot of a symbol state that can be used to resolve a reference in a +/// nested eager scope. /// /// An eager scope has its entire body executed immediately at the location where it is defined. /// For any free references in the nested scope, we use the bindings that are visible at the point /// where the nested scope is defined, instead of using the public type of the symbol. /// -/// There is a unique ID for each distinct [`EagerBindingsKey`] in the file. +/// There is a unique ID for each distinct [`EagerSnapshotKey`] in the file. #[newtype_index] -pub(crate) struct ScopedEagerBindingsId; +pub(crate) struct ScopedEagerSnapshotId; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub(crate) struct EagerBindingsKey { +pub(crate) struct EagerSnapshotKey { /// The enclosing scope containing the bindings pub(crate) enclosing_scope: FileScopeId, /// The referenced symbol (in the enclosing scope) @@ -510,8 +532,8 @@ pub(crate) struct EagerBindingsKey { pub(crate) nested_scope: FileScopeId, } -/// A snapshot of bindings that can be used to resolve a reference in a nested eager scope. -type EagerBindings = IndexVec; +/// A snapshot of symbol states that can be used to resolve a reference in a nested eager scope. +type EagerSnapshots = IndexVec; #[derive(Debug)] pub(crate) struct BindingWithConstraintsIterator<'map, 'db> { @@ -568,6 +590,33 @@ impl<'db> Iterator for ConstraintsIterator<'_, 'db> { impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {} +impl<'db> ConstraintsIterator<'_, 'db> { + pub(crate) fn narrow( + self, + db: &'db dyn crate::Db, + base_ty: Type<'db>, + symbol: ScopedSymbolId, + ) -> Type<'db> { + let constraint_tys: Vec<_> = self + .filter_map(|constraint| infer_narrowing_constraint(db, constraint, symbol)) + .collect(); + + if constraint_tys.is_empty() { + base_ty + } else { + let intersection_ty = constraint_tys + .into_iter() + .rev() + .fold( + IntersectionBuilder::new(db).add_positive(base_ty), + IntersectionBuilder::add_positive, + ) + .build(); + intersection_ty + } + } +} + #[derive(Clone)] pub(crate) struct DeclarationsIterator<'map, 'db> { all_definitions: &'map IndexVec>>, @@ -688,13 +737,16 @@ pub(super) struct UseDefMapBuilder<'db> { /// Currently live bindings for each instance attribute. instance_attribute_states: IndexVec, - /// Snapshot of bindings in this scope that can be used to resolve a reference in a nested - /// eager scope. - eager_bindings: EagerBindings, + /// Snapshots of symbol states in this scope that can be used to resolve a reference in a + /// nested eager scope. + eager_snapshots: EagerSnapshots, + + /// Is this a class scope? + is_class_scope: bool, } -impl Default for UseDefMapBuilder<'_> { - fn default() -> Self { +impl<'db> UseDefMapBuilder<'db> { + pub(super) fn new(is_class_scope: bool) -> Self { Self { all_definitions: IndexVec::from_iter([None]), predicates: PredicatesBuilder::default(), @@ -707,13 +759,11 @@ impl Default for UseDefMapBuilder<'_> { declarations_by_binding: FxHashMap::default(), bindings_by_declaration: FxHashMap::default(), symbol_states: IndexVec::new(), - eager_bindings: EagerBindings::default(), + eager_snapshots: EagerSnapshots::default(), instance_attribute_states: IndexVec::new(), + is_class_scope, } } -} - -impl<'db> UseDefMapBuilder<'db> { pub(super) fn mark_unreachable(&mut self) { self.record_visibility_constraint(ScopedVisibilityConstraintId::ALWAYS_FALSE); self.reachability = ScopedVisibilityConstraintId::ALWAYS_FALSE; @@ -738,7 +788,7 @@ impl<'db> UseDefMapBuilder<'db> { let symbol_state = &mut self.symbol_states[symbol]; self.declarations_by_binding .insert(binding, symbol_state.declarations().clone()); - symbol_state.record_binding(def_id, self.scope_start_visibility); + symbol_state.record_binding(def_id, self.scope_start_visibility, self.is_class_scope); } pub(super) fn record_attribute_binding( @@ -750,7 +800,7 @@ impl<'db> UseDefMapBuilder<'db> { let attribute_state = &mut self.instance_attribute_states[symbol]; self.declarations_by_binding .insert(binding, attribute_state.declarations().clone()); - attribute_state.record_binding(def_id, self.scope_start_visibility); + attribute_state.record_binding(def_id, self.scope_start_visibility, self.is_class_scope); } pub(super) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId { @@ -936,7 +986,7 @@ impl<'db> UseDefMapBuilder<'db> { let def_id = self.all_definitions.push(Some(definition)); let symbol_state = &mut self.symbol_states[symbol]; symbol_state.record_declaration(def_id); - symbol_state.record_binding(def_id, self.scope_start_visibility); + symbol_state.record_binding(def_id, self.scope_start_visibility, self.is_class_scope); } pub(super) fn record_use( @@ -961,12 +1011,25 @@ impl<'db> UseDefMapBuilder<'db> { self.node_reachability.insert(node_key, self.reachability); } - pub(super) fn snapshot_eager_bindings( + pub(super) fn snapshot_eager_state( &mut self, enclosing_symbol: ScopedSymbolId, - ) -> ScopedEagerBindingsId { - self.eager_bindings - .push(self.symbol_states[enclosing_symbol].bindings().clone()) + scope: ScopeKind, + is_bound: bool, + ) -> ScopedEagerSnapshotId { + // Names bound in class scopes are never visible to nested scopes, so we never need to + // save eager scope bindings in a class scope. + if scope.is_class() || !is_bound { + self.eager_snapshots.push(EagerSnapshot::Constraint( + self.symbol_states[enclosing_symbol] + .bindings() + .unbound_narrowing_constraint(), + )) + } else { + self.eager_snapshots.push(EagerSnapshot::Bindings( + self.symbol_states[enclosing_symbol].bindings().clone(), + )) + } } /// Take a snapshot of the current visible-symbols state. @@ -1086,7 +1149,7 @@ impl<'db> UseDefMapBuilder<'db> { self.node_reachability.shrink_to_fit(); self.declarations_by_binding.shrink_to_fit(); self.bindings_by_declaration.shrink_to_fit(); - self.eager_bindings.shrink_to_fit(); + self.eager_snapshots.shrink_to_fit(); UseDefMap { all_definitions: self.all_definitions, @@ -1099,7 +1162,7 @@ impl<'db> UseDefMapBuilder<'db> { instance_attributes: self.instance_attribute_states, declarations_by_binding: self.declarations_by_binding, bindings_by_declaration: self.bindings_by_declaration, - eager_bindings: self.eager_bindings, + eager_snapshots: self.eager_snapshots, scope_start_visibility: self.scope_start_visibility, } } diff --git a/crates/ty_python_semantic/src/semantic_index/use_def/symbol_state.rs b/crates/ty_python_semantic/src/semantic_index/use_def/symbol_state.rs index 6807baf8e0a057..02c4e3e6825357 100644 --- a/crates/ty_python_semantic/src/semantic_index/use_def/symbol_state.rs +++ b/crates/ty_python_semantic/src/semantic_index/use_def/symbol_state.rs @@ -65,6 +65,10 @@ impl ScopedDefinitionId { /// When creating a use-def-map builder, we always add an empty `None` definition /// at index 0, so this ID is always present. pub(super) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0); + + fn is_unbound(self) -> bool { + self == Self::UNBOUND + } } /// Can keep inline this many live bindings or declarations per symbol at a given time; more will @@ -177,14 +181,41 @@ impl SymbolDeclarations { } } +/// A snapshot of a symbol state that can be used to resolve a reference in a nested eager scope. +/// If there are bindings in a (non-class) scope , they are stored in `Bindings`. +/// Even if it's a class scope (class variables are not visible to nested scopes) or there are no +/// bindings, the current narrowing constraint is necessary for narrowing, so it's stored in +/// `Constraint`. +#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)] +pub(super) enum EagerSnapshot { + Constraint(ScopedNarrowingConstraint), + Bindings(SymbolBindings), +} + /// Live bindings for a single symbol at some point in control flow. Each live binding comes /// with a set of narrowing constraints and a visibility constraint. #[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)] pub(super) struct SymbolBindings { + /// The narrowing constraint applicable to the "unbound" binding, if we need access to it even + /// when it's not visible. This happens in class scopes, where local bindings are not visible + /// to nested scopes, but we still need to know what narrowing constraints were applied to the + /// "unbound" binding. + unbound_narrowing_constraint: Option, /// A list of live bindings for this symbol, sorted by their `ScopedDefinitionId` live_bindings: SmallVec<[LiveBinding; INLINE_DEFINITIONS_PER_SYMBOL]>, } +impl SymbolBindings { + pub(super) fn unbound_narrowing_constraint(&self) -> ScopedNarrowingConstraint { + debug_assert!( + self.unbound_narrowing_constraint.is_some() + || self.live_bindings[0].binding.is_unbound() + ); + self.unbound_narrowing_constraint + .unwrap_or(self.live_bindings[0].narrowing_constraint) + } +} + /// One of the live bindings for a single symbol at some point in control flow. #[derive(Clone, Debug, PartialEq, Eq)] pub(super) struct LiveBinding { @@ -203,6 +234,7 @@ impl SymbolBindings { visibility_constraint: scope_start_visibility, }; Self { + unbound_narrowing_constraint: None, live_bindings: smallvec![initial_binding], } } @@ -212,7 +244,13 @@ impl SymbolBindings { &mut self, binding: ScopedDefinitionId, visibility_constraint: ScopedVisibilityConstraintId, + is_class_scope: bool, ) { + // If we are in a class scope, and the unbound binding was previously visible, but we will + // now replace it, record the narrowing constraints on it: + if is_class_scope && self.live_bindings[0].binding.is_unbound() { + self.unbound_narrowing_constraint = Some(self.live_bindings[0].narrowing_constraint); + } // The new binding replaces all previous live bindings in this path, and has no // constraints. self.live_bindings.clear(); @@ -278,6 +316,14 @@ impl SymbolBindings { ) { let a = std::mem::take(self); + if let Some((a, b)) = a + .unbound_narrowing_constraint + .zip(b.unbound_narrowing_constraint) + { + self.unbound_narrowing_constraint = + Some(narrowing_constraints.intersect_constraints(a, b)); + } + // Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that // the merged `live_bindings` vec remains sorted. If a definition is found in both `a` and // `b`, we compose the constraints from the two paths in an appropriate way (intersection @@ -333,10 +379,11 @@ impl SymbolState { &mut self, binding_id: ScopedDefinitionId, visibility_constraint: ScopedVisibilityConstraintId, + is_class_scope: bool, ) { debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND); self.bindings - .record_binding(binding_id, visibility_constraint); + .record_binding(binding_id, visibility_constraint, is_class_scope); } /// Add given constraint to all live bindings. @@ -467,6 +514,7 @@ mod tests { sym.record_binding( ScopedDefinitionId::from_u32(1), ScopedVisibilityConstraintId::ALWAYS_TRUE, + false, ); assert_bindings(&narrowing_constraints, &sym, &["1<>"]); @@ -479,6 +527,7 @@ mod tests { sym.record_binding( ScopedDefinitionId::from_u32(1), ScopedVisibilityConstraintId::ALWAYS_TRUE, + false, ); let predicate = ScopedPredicateId::from_u32(0).into(); sym.record_narrowing_constraint(&mut narrowing_constraints, predicate); @@ -496,6 +545,7 @@ mod tests { sym1a.record_binding( ScopedDefinitionId::from_u32(1), ScopedVisibilityConstraintId::ALWAYS_TRUE, + false, ); let predicate = ScopedPredicateId::from_u32(0).into(); sym1a.record_narrowing_constraint(&mut narrowing_constraints, predicate); @@ -504,6 +554,7 @@ mod tests { sym1b.record_binding( ScopedDefinitionId::from_u32(1), ScopedVisibilityConstraintId::ALWAYS_TRUE, + false, ); let predicate = ScopedPredicateId::from_u32(0).into(); sym1b.record_narrowing_constraint(&mut narrowing_constraints, predicate); @@ -521,6 +572,7 @@ mod tests { sym2a.record_binding( ScopedDefinitionId::from_u32(2), ScopedVisibilityConstraintId::ALWAYS_TRUE, + false, ); let predicate = ScopedPredicateId::from_u32(1).into(); sym2a.record_narrowing_constraint(&mut narrowing_constraints, predicate); @@ -529,6 +581,7 @@ mod tests { sym1b.record_binding( ScopedDefinitionId::from_u32(2), ScopedVisibilityConstraintId::ALWAYS_TRUE, + false, ); let predicate = ScopedPredicateId::from_u32(2).into(); sym1b.record_narrowing_constraint(&mut narrowing_constraints, predicate); @@ -546,6 +599,7 @@ mod tests { sym3a.record_binding( ScopedDefinitionId::from_u32(3), ScopedVisibilityConstraintId::ALWAYS_TRUE, + false, ); let predicate = ScopedPredicateId::from_u32(3).into(); sym3a.record_narrowing_constraint(&mut narrowing_constraints, predicate); diff --git a/crates/ty_python_semantic/src/symbol.rs b/crates/ty_python_semantic/src/symbol.rs index 49b404eb711f56..767d4392f29750 100644 --- a/crates/ty_python_semantic/src/symbol.rs +++ b/crates/ty_python_semantic/src/symbol.rs @@ -8,8 +8,8 @@ use crate::semantic_index::{ symbol_table, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator, }; use crate::types::{ - binding_type, declaration_type, infer_narrowing_constraint, todo_type, IntersectionBuilder, - KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType, + binding_type, declaration_type, todo_type, KnownClass, Truthiness, Type, TypeAndQualifiers, + TypeQualifiers, UnionBuilder, UnionType, }; use crate::{resolve_module, Db, KnownModule, Program}; @@ -791,24 +791,8 @@ fn symbol_from_bindings_impl<'db>( return None; } - let constraint_tys: Vec<_> = narrowing_constraint - .filter_map(|constraint| infer_narrowing_constraint(db, constraint, binding)) - .collect(); - let binding_ty = binding_type(db, binding); - if constraint_tys.is_empty() { - Some(binding_ty) - } else { - let intersection_ty = constraint_tys - .into_iter() - .rev() - .fold( - IntersectionBuilder::new(db).add_positive(binding_ty), - IntersectionBuilder::add_positive, - ) - .build(); - Some(intersection_ty) - } + Some(narrowing_constraint.narrow(db, binding_ty, binding.symbol(db))) }, ); diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 87d9a44fb744c4..0719800bfca919 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -54,10 +54,11 @@ use crate::semantic_index::definition::{ ForStmtDefinitionKind, TargetKind, WithItemDefinitionKind, }; use crate::semantic_index::expression::{Expression, ExpressionKind}; +use crate::semantic_index::narrowing_constraints::ConstraintKey; use crate::semantic_index::symbol::{ FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind, }; -use crate::semantic_index::{semantic_index, EagerBindingsResult, SemanticIndex}; +use crate::semantic_index::{semantic_index, EagerSnapshotResult, SemanticIndex}; use crate::symbol::{ builtins_module_scope, builtins_symbol, explicit_global_symbol, module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations, @@ -5146,9 +5147,23 @@ impl<'db> TypeInferenceBuilder<'db> { let symbol_table = self.index.symbol_table(file_scope_id); let use_def = self.index.use_def_map(file_scope_id); + let mut constraint_keys = vec![]; + // Perform narrowing with applicable constraints between the current scope and the enclosing scope. + let narrow_with_applicable_constraints = |mut ty, constraint_keys: &[_]| { + for (enclosing_scope_file_id, constraint_key) in constraint_keys { + let use_def = self.index.use_def_map(*enclosing_scope_file_id); + let constraints = use_def.narrowing_constraints_at_use(*constraint_key); + let symbol_table = self.index.symbol_table(*enclosing_scope_file_id); + let symbol = symbol_table.symbol_id_by_name(symbol_name).unwrap(); + + ty = constraints.narrow(db, ty, symbol); + } + ty + }; + // If we're inferring types of deferred expressions, always treat them as public symbols - let local_scope_symbol = if self.is_deferred() { - if let Some(symbol_id) = symbol_table.symbol_id_by_name(symbol_name) { + let (local_scope_symbol, use_id) = if self.is_deferred() { + let symbol = if let Some(symbol_id) = symbol_table.symbol_id_by_name(symbol_name) { symbol_from_bindings(db, use_def.public_bindings(symbol_id)) } else { assert!( @@ -5156,10 +5171,12 @@ impl<'db> TypeInferenceBuilder<'db> { "Expected the symbol table to create a symbol for every Name node" ); Symbol::Unbound - } + }; + (symbol, None) } else { let use_id = name_node.scoped_use_id(db, scope); - symbol_from_bindings(db, use_def.bindings_at_use(use_id)) + let symbol = symbol_from_bindings(db, use_def.bindings_at_use(use_id)); + (symbol, Some(use_id)) }; let symbol = SymbolAndQualifiers::from(local_scope_symbol).or_fall_back_to(db, || { @@ -5187,6 +5204,10 @@ impl<'db> TypeInferenceBuilder<'db> { return Symbol::Unbound.into(); } + if let Some(use_id) = use_id { + constraint_keys.push((file_scope_id, ConstraintKey::UseId(use_id))); + } + let current_file = self.file(); // Walk up parent scopes looking for a possible enclosing scope that may have a @@ -5200,14 +5221,12 @@ impl<'db> TypeInferenceBuilder<'db> { // There is one exception to this rule: type parameter scopes can see // names defined in an immediately-enclosing class scope. let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file); + let is_immediately_enclosing_scope = scope.is_type_parameter(db) && scope .scope(db) .parent() .is_some_and(|parent| parent == enclosing_scope_file_id); - if !enclosing_scope_id.is_function_like(db) && !is_immediately_enclosing_scope { - continue; - } // If the reference is in a nested eager scope, we need to look for the symbol at // the point where the previous enclosing scope was defined, instead of at the end @@ -5216,23 +5235,42 @@ impl<'db> TypeInferenceBuilder<'db> { // enclosing scopes that actually contain bindings that we should use when // resolving the reference.) if !self.is_deferred() { - match self.index.eager_bindings( + match self.index.eager_snapshot( enclosing_scope_file_id, symbol_name, file_scope_id, ) { - EagerBindingsResult::Found(bindings) => { - return symbol_from_bindings(db, bindings).into(); + EagerSnapshotResult::FoundConstraint(constraint) => { + constraint_keys.push(( + enclosing_scope_file_id, + ConstraintKey::NarrowingConstraint(constraint), + )); + } + EagerSnapshotResult::FoundBindings(bindings) => { + if !enclosing_scope_id.is_function_like(db) + && !is_immediately_enclosing_scope + { + continue; + } + return symbol_from_bindings(db, bindings) + .map_type(|ty| { + narrow_with_applicable_constraints(ty, &constraint_keys) + }) + .into(); } - // There are no visible bindings here. + // There are no visible bindings / constraint here. // Don't fall back to non-eager symbol resolution. - EagerBindingsResult::NotFound => { + EagerSnapshotResult::NotFound => { continue; } - EagerBindingsResult::NoLongerInEagerContext => {} + EagerSnapshotResult::NoLongerInEagerContext => {} } } + if !enclosing_scope_id.is_function_like(db) && !is_immediately_enclosing_scope { + continue; + } + let enclosing_symbol_table = self.index.symbol_table(enclosing_scope_file_id); let Some(enclosing_symbol) = enclosing_symbol_table.symbol_by_name(symbol_name) else { @@ -5244,7 +5282,8 @@ impl<'db> TypeInferenceBuilder<'db> { // runtime, it is the scope that creates the cell for our closure.) If the name // isn't bound in that scope, we should get an unbound name, not continue // falling back to other scopes / globals / builtins. - return symbol(db, enclosing_scope_id, symbol_name); + return symbol(db, enclosing_scope_id, symbol_name) + .map_type(|ty| narrow_with_applicable_constraints(ty, &constraint_keys)); } } @@ -5257,28 +5296,42 @@ impl<'db> TypeInferenceBuilder<'db> { } if !self.is_deferred() { - match self.index.eager_bindings( + match self.index.eager_snapshot( FileScopeId::global(), symbol_name, file_scope_id, ) { - EagerBindingsResult::Found(bindings) => { - return symbol_from_bindings(db, bindings).into(); + EagerSnapshotResult::FoundConstraint(constraint) => { + constraint_keys.push(( + FileScopeId::global(), + ConstraintKey::NarrowingConstraint(constraint), + )); } - // There are no visible bindings here. - EagerBindingsResult::NotFound => { + EagerSnapshotResult::FoundBindings(bindings) => { + return symbol_from_bindings(db, bindings) + .map_type(|ty| { + narrow_with_applicable_constraints(ty, &constraint_keys) + }) + .into(); + } + // There are no visible bindings / constraint here. + EagerSnapshotResult::NotFound => { return Symbol::Unbound.into(); } - EagerBindingsResult::NoLongerInEagerContext => {} + EagerSnapshotResult::NoLongerInEagerContext => {} } } explicit_global_symbol(db, self.file(), symbol_name) + .map_type(|ty| narrow_with_applicable_constraints(ty, &constraint_keys)) }) // Not found in the module's explicitly declared global symbols? // Check the "implicit globals" such as `__doc__`, `__file__`, `__name__`, etc. // These are looked up as attributes on `types.ModuleType`. - .or_fall_back_to(db, || module_type_implicit_global_symbol(db, symbol_name)) + .or_fall_back_to(db, || { + module_type_implicit_global_symbol(db, symbol_name) + .map_type(|ty| narrow_with_applicable_constraints(ty, &constraint_keys)) + }) // Not found in globals? Fallback to builtins // (without infinite recursion if we're already in builtins.) .or_fall_back_to(db, || { diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 71ed52f6650f0b..b5a96c1a70dbd5 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -1,5 +1,4 @@ use crate::semantic_index::ast_ids::HasScopedExpressionId; -use crate::semantic_index::definition::Definition; use crate::semantic_index::expression::Expression; use crate::semantic_index::predicate::{ PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, @@ -21,7 +20,7 @@ use std::sync::Arc; use super::UnionType; -/// Return the type constraint that `test` (if true) would place on `definition`, if any. +/// Return the type constraint that `test` (if true) would place on `symbol`, if any. /// /// For example, if we have this code: /// @@ -35,12 +34,12 @@ use super::UnionType; /// The `test` expression `x is not None` places the constraint "not None" on the definition of /// `x`, so in that case we'd return `Some(Type::Intersection(negative=[Type::None]))`. /// -/// But if we called this with the same `test` expression, but the `definition` of `y`, no -/// constraint is applied to that definition, so we'd just return `None`. +/// But if we called this with the same `test` expression, but the `symbol` of `y`, no +/// constraint is applied to that symbol, so we'd just return `None`. pub(crate) fn infer_narrowing_constraint<'db>( db: &'db dyn Db, predicate: Predicate<'db>, - definition: Definition<'db>, + symbol: ScopedSymbolId, ) -> Option> { let constraints = match predicate.node { PredicateNode::Expression(expression) => { @@ -60,7 +59,7 @@ pub(crate) fn infer_narrowing_constraint<'db>( PredicateNode::StarImportPlaceholder(_) => return None, }; if let Some(constraints) = constraints { - constraints.get(&definition.symbol(db)).copied() + constraints.get(&symbol).copied() } else { None } From 89424cce5fe7d5560de2b4c4d5b430ec19c8bc2c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 6 May 2025 00:37:24 +0100 Subject: [PATCH 0270/1161] [ty] Do not emit errors if enums or NamedTuples constructed using functional syntax are used in type expressions (#17873) ## Summary This fixes some false positives that showed up in the primer diff for https://github.com/astral-sh/ruff/pull/17832 ## Test Plan new mdtests added that fail with false-positive diagnostics on `main` --- .../annotations/unsupported_special_types.md | 19 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 11 +++++++++++ 2 files changed, 30 insertions(+) create mode 100644 crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md new file mode 100644 index 00000000000000..7b0a0736625971 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md @@ -0,0 +1,19 @@ +# Unsupported special types + +We do not understand the functional syntax for creating `NamedTuple`s, `TypedDict`s or `Enum`s yet. +But we also do not emit false positives when these are used in type expressions. + +```py +import collections +import enum +import typing + +# TODO: should not error (requires understanding metaclass `__call__`) +MyEnum = enum.Enum("MyEnum", ["foo", "bar", "baz"]) # error: [too-many-positional-arguments] + +MyTypedDict = typing.TypedDict("MyTypedDict", {"foo": int}) +MyNamedTuple1 = typing.NamedTuple("MyNamedTuple1", [("foo", int)]) +MyNamedTuple2 = collections.namedtuple("MyNamedTuple2", ["foo"]) + +def f(a: MyEnum, b: MyTypedDict, c: MyNamedTuple1, d: MyNamedTuple2): ... +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cf991b7d03c541..833acc95ca135e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4833,6 +4833,17 @@ impl<'db> Type<'db> { Some(KnownClass::UnionType) => Ok(todo_type!( "Support for `types.UnionType` instances in type expressions" )), + Some(KnownClass::NamedTuple) => Ok(todo_type!( + "Support for functional `typing.NamedTuple` syntax" + )), + _ if instance + .class() + .iter_mro(db) + .filter_map(ClassBase::into_class) + .any(|class| class.is_known(db, KnownClass::Enum)) => + { + Ok(todo_type!("Support for functional `enum` syntax")) + } _ => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType( *self From d410d12bc5d495c44083b3451b7ac73c66fd27a2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 May 2025 21:34:45 -0400 Subject: [PATCH 0271/1161] Update code of conduct email address (#17875) ## Summary For consistency with the `pyproject.toml`, etc. --- CODE_OF_CONDUCT.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 350f7422a5dea0..5c8537b757c2e3 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -71,8 +71,7 @@ representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -. +reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the From 6f821ac8468013f16da0ab33991c27f76705669e Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 6 May 2025 09:14:21 +0200 Subject: [PATCH 0272/1161] Show a warning at the end of the diagnostic list if there are any fatal warnings (#17855) --- crates/ruff_db/src/diagnostic/mod.rs | 4 ++++ crates/ty/src/main.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 023fc593681f3e..b795911d6de54d 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -674,6 +674,10 @@ impl Severity { Severity::Fatal => AnnotateLevel::Error, } } + + pub const fn is_fatal(self) -> bool { + matches!(self, Severity::Fatal) + } } /// Configuration for rendering diagnostics. diff --git a/crates/ty/src/main.rs b/crates/ty/src/main.rs index 3167a24ad4a435..f0082f18249573 100644 --- a/crates/ty/src/main.rs +++ b/crates/ty/src/main.rs @@ -306,6 +306,10 @@ impl MainLoop { if diagnostics_count > 1 { "s" } else { "" } )?; + if max_severity.is_fatal() { + tracing::warn!("A fatal occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."); + } + if self.watcher.is_none() { return Ok(match max_severity { Severity::Info => ExitStatus::Success, From 24d3fc27fb919dc52d077cf1e424644d4d331e3a Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 6 May 2025 09:44:23 +0200 Subject: [PATCH 0273/1161] Fixup wording of fatal error warning (#17881) --- crates/ty/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty/src/main.rs b/crates/ty/src/main.rs index f0082f18249573..156ce39bd5a180 100644 --- a/crates/ty/src/main.rs +++ b/crates/ty/src/main.rs @@ -307,7 +307,7 @@ impl MainLoop { )?; if max_severity.is_fatal() { - tracing::warn!("A fatal occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."); + tracing::warn!("A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."); } if self.watcher.is_none() { From 7f50b503cfc6248ae5172aad0dc8bbb0ab33f34b Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 6 May 2025 10:16:17 +0200 Subject: [PATCH 0274/1161] Allowlist play.ty.dev (#17857) --- playground/api/src/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/playground/api/src/index.ts b/playground/api/src/index.ts index d75c8135f27395..9ac1ff5a97d6d8 100644 --- a/playground/api/src/index.ts +++ b/playground/api/src/index.ts @@ -31,7 +31,11 @@ const PRODUCTION_HEADERS = { "Access-Control-Allow-Origin": "https://play.ruff.rs", }; -const ALLOWED_DOMAINS = ["https://playknot.ruff.rs", "https://types.ruff.rs"]; +const ALLOWED_DOMAINS = new Set([ + "https://playknot.ruff.rs", + "https://types.ruff.rs", + "https://play.ty.dev", +]); export default { async fetch( @@ -45,7 +49,7 @@ export default { if (!DEV) { const origin = request.headers.get("origin"); - if (origin && ALLOWED_DOMAINS.includes(origin)) { + if (origin && ALLOWED_DOMAINS.has(origin)) { headers["Access-Control-Allow-Origin"] = origin; } } From 9000eb3bfd1a0d44e0c203e513e0da3d5f505703 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 6 May 2025 10:51:45 +0200 Subject: [PATCH 0275/1161] Update favicon and URL for playground (#17860) --- playground/shared/src/Header.tsx | 12 +++--------- playground/ty/index.html | 9 ++++++--- playground/ty/public/favicon-16x16.png | Bin 1184 -> 391 bytes playground/ty/public/favicon-32x32.png | Bin 2028 -> 624 bytes playground/ty/public/favicon.ico | Bin 2356 -> 3152 bytes playground/ty/src/Playground.tsx | 2 +- 6 files changed, 10 insertions(+), 13 deletions(-) diff --git a/playground/shared/src/Header.tsx b/playground/shared/src/Header.tsx index 3dfad3785270e9..0d83cc50582f5b 100644 --- a/playground/shared/src/Header.tsx +++ b/playground/shared/src/Header.tsx @@ -17,7 +17,7 @@ export default function Header({ }: { edit: number | null; theme: Theme; - logo: "ruff" | "astral"; + logo: "ruff" | "ty"; version: string | null; onChangeTheme: (theme: Theme) => void; onReset?(): void; @@ -77,13 +77,7 @@ function Divider() { ); } -function Logo({ - name, - className, -}: { - name: "ruff" | "astral"; - className: string; -}) { +function Logo({ name, className }: { name: "ruff" | "ty"; className: string }) { switch (name) { case "ruff": return ( @@ -100,7 +94,7 @@ function Logo({ /> ); - case "astral": + case "ty": return ( - + @@ -22,9 +25,9 @@ property="og:description" content="An in-browser playground for ty, an extremely fast Python type-checker written in Rust." /> - + - +